1
0
forked from Rowland/EG

first commit

This commit is contained in:
Rowland 2025-07-02 09:49:59 +08:00
parent c270dbbff4
commit 8e604545f4
100 changed files with 18875 additions and 0 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

12
core/__init__.py Normal file
View File

@ -0,0 +1,12 @@
"""
Core package - 核心功能模块
包含引擎的核心功能
- world.py: 基础世界功能相机光照地板等
- selection.py: 选择和变换系统
"""
from .world import CoreWorld
from .selection import SelectionSystem
__all__ = ['CoreWorld', 'SelectionSystem']

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

231
core/event_handler.py Normal file
View File

@ -0,0 +1,231 @@
from panda3d.core import (Point3, Point2, CollisionTraverser, CollisionHandlerQueue,
CollisionNode, CollisionRay, GeomNode)
class EventHandler:
"""事件处理器 - 处理鼠标和键盘输入事件"""
def __init__(self, world):
"""初始化事件处理器"""
self.world = world
def mousePressEventLeft(self, evt):
"""处理鼠标左键按下事件"""
print("\n=== 开始处理鼠标左键事件 ===")
print(f"当前工具: {self.world.currentTool}")
if not evt:
print("事件为空")
return
# 获取鼠标点击的位置
x = evt.get('x', 0)
y = evt.get('y', 0)
print(f"鼠标点击位置: ({x}, {y})")
# 获取准确的窗口尺寸
winWidth, winHeight = self.world.getWindowSize()
# 直接使用 x, y 创建鼠标位置
mx = 2.0 * x / float(winWidth) - 1.0
my = 1.0 - 2.0 * y / float(winHeight)
print(f"转换后的坐标: ({mx}, {my})")
# 创建射线
nearPoint = Point3()
farPoint = Point3()
self.world.cam.node().getLens().extrude(Point2(mx, my), nearPoint, farPoint)
print(f"射线起点: {nearPoint}")
print(f"射线终点: {farPoint}")
# 进行射线检测
picker = CollisionTraverser()
queue = CollisionHandlerQueue()
pickerNode = CollisionNode('mouseRay')
pickerNP = self.world.cam.attachNewNode(pickerNode)
pickerNode.setFromCollideMask(GeomNode.getDefaultCollideMask())
# 使用 nearPoint 和 farPoint 创建射线
direction = farPoint - nearPoint
direction.normalize()
pickerNode.addSolid(CollisionRay(nearPoint, direction))
picker.addCollider(pickerNP, queue)
picker.traverse(self.world.render)
print(f"碰撞检测结果数量: {queue.getNumEntries()}")
if queue.getNumEntries() > 0:
# 获取最近的碰撞点
entry = queue.getEntry(0)
hitPos = entry.getSurfacePoint(self.world.render)
hitNode = entry.getIntoNodePath()
print(f"碰撞到节点: {hitNode.getName()}")
# 优先检查是否点击了坐标轴
print(f"检查坐标轴点击: 坐标轴存在={bool(self.world.selection.gizmo)}")
if self.world.selection.gizmo:
print("准备检查坐标轴点击...")
gizmoAxis = self.world.selection.checkGizmoClick(x, y)
if gizmoAxis:
print(f"✓ 检测到坐标轴点击: {gizmoAxis}")
# 开始坐标轴拖拽
self.world.selection.startGizmoDrag(gizmoAxis, x, y)
return
else:
print("× 没有点击到坐标轴")
# 处理GUI编辑模式
if self.world.guiEditMode:
# 检查是否点击了GUI元素
clickedGUI = self.world.gui_manager.findClickedGUI(hitNode)
if clickedGUI:
# 选中GUI元素
self.world.selection.updateSelection(clickedGUI)
self.world.gui_manager.selectGUIInTree(clickedGUI)
print(f"选中GUI元素: {clickedGUI.getTag('gui_text')}")
elif hasattr(self.world, 'currentGUITool') and self.world.currentGUITool:
# 在点击位置创建新GUI元素
self.world.gui_manager.createGUIAtPosition(hitPos, self.world.currentGUITool)
return
# 根据当前工具处理点击事件
if self.world.currentTool == "选择":
print("使用选择工具处理点击")
self._handleSelectionClick(hitNode)
else:
print("没有检测到碰撞")
# 如果不在GUI编辑模式清除选择
if not self.world.guiEditMode:
self.world.selection.updateSelection(None)
# 在GUI编辑模式下即使没有碰撞也可以在空白区域创建GUI
if (self.world.guiEditMode and
hasattr(self.world, 'currentGUITool') and
self.world.currentGUITool):
# 使用默认的地面高度创建GUI
default_height = 0.0
world_pos = Point3(mx * 10, 0, my * 10) # 简单的屏幕到世界坐标转换
world_pos.setZ(default_height)
self.world.gui_manager.createGUIAtPosition(world_pos, self.world.currentGUITool)
pickerNP.removeNode()
print("=== 鼠标左键事件处理结束 ===\n")
def _handleSelectionClick(self, hitNode):
"""处理选择工具的点击事件"""
# 查找可选择的节点(模型或其子节点)
while hitNode != self.world.render:
# 检查是否是模型或模型的子节点
isModel = hitNode in self.world.models
isChildOfModel = False
for model in self.world.models:
# 检查是否是模型的子节点
current = hitNode
while current != self.world.render:
if current == model:
isChildOfModel = True
break
current = current.getParent()
if isChildOfModel:
break
print(f"检查节点 {hitNode.getName()}: isModel={isModel}, isChildOfModel={isChildOfModel}")
if isModel or isChildOfModel:
print(f"选中节点: {hitNode.getName()}")
# 在树形控件中查找并选中对应的项
if self.world.interface_manager.treeWidget:
root = self.world.interface_manager.treeWidget.invisibleRootItem()
for i in range(root.childCount()):
sceneItem = root.child(i)
if sceneItem.text(0) == "场景":
foundItem = self.world.interface_manager.findTreeItem(hitNode, sceneItem)
if foundItem:
self.world.interface_manager.treeWidget.setCurrentItem(foundItem)
self.world.property_panel.updatePropertyPanel(foundItem)
# 更新选择状态并显示选择框
self.world.selection.updateSelection(hitNode)
break
break
hitNode = hitNode.getParent()
def mouseReleaseEventLeft(self, evt):
"""处理鼠标左键释放事件"""
# 处理坐标轴拖拽结束
if self.world.selection.isDraggingGizmo:
self.world.selection.stopGizmoDrag()
return
def wheelForward(self, data=None):
"""处理滚轮向前滚动(前进)"""
# 调用CoreWorld的父类方法
super(type(self.world), self.world).wheelForward(data)
# 更新属性面板
if (self.world.interface_manager.treeWidget and
self.world.interface_manager.treeWidget.currentItem() and
self.world.interface_manager.treeWidget.currentItem().text(0) == "相机"):
self.world.property_panel.updatePropertyPanel(
self.world.interface_manager.treeWidget.currentItem())
def wheelBackward(self, data=None):
"""处理滚轮向后滚动(后退)"""
# 调用CoreWorld的父类方法
super(type(self.world), self.world).wheelBackward(data)
# 更新属性面板
if (self.world.interface_manager.treeWidget and
self.world.interface_manager.treeWidget.currentItem() and
self.world.interface_manager.treeWidget.currentItem().text(0) == "相机"):
self.world.property_panel.updatePropertyPanel(
self.world.interface_manager.treeWidget.currentItem())
def mousePressEventMiddle(self, evt):
"""处理鼠标中键按下事件"""
# 已移除原有的Z轴拖拽功能
pass
def mouseReleaseEventMiddle(self, evt):
"""处理鼠标中键释放事件"""
# 已移除原有的Z轴拖拽功能
pass
def mouseMoveEvent(self, evt):
"""处理鼠标移动事件"""
if not evt:
return
# 处理坐标轴拖拽
if self.world.selection.isDraggingGizmo:
x = evt.get('x', 0)
y = evt.get('y', 0)
# 获取准确的窗口尺寸
winWidth, winHeight = self.world.getWindowSize()
# 将屏幕坐标转换为世界坐标
mx = 2.0 * x / float(winWidth) - 1.0
my = 1.0 - 2.0 * y / float(winHeight)
# 更新坐标轴拖拽
self.world.selection.updateGizmoDrag(x, y)
return
# 更新坐标轴高亮(鼠标悬停效果)
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的父类方法处理基础的相机旋转
super(type(self.world), self.world).mouseMoveEvent(evt)

784
core/selection.py Normal file
View File

@ -0,0 +1,784 @@
"""
选择和变换系统模块
负责物体选择和变换相关功能
- 选择框的创建和更新
- 坐标轴(Gizmo)系统
- 拖拽变换逻辑
- 射线检测和碰撞检测
"""
from panda3d.core import (Vec3, Point3, Point2, LineSegs, ColorAttrib, RenderState,
DepthTestAttrib, CollisionTraverser, CollisionHandlerQueue,
CollisionNode, CollisionRay, GeomNode, BitMask32)
from direct.task.TaskManagerGlobal import taskMgr
class SelectionSystem:
"""选择和变换系统类"""
def __init__(self, world):
"""初始化选择系统
Args:
world: 核心世界对象引用
"""
self.world = world
# 选择相关状态
self.selectedNode = None
self.selectionBox = None # 选择框
self.selectionBoxTarget = None # 选择框跟踪的目标节点
# 坐标轴工具Gizmo相关
self.gizmo = None # 坐标轴
self.gizmoTarget = None # 坐标轴跟踪的目标节点
self.gizmoXAxis = None # X轴
self.gizmoYAxis = None # Y轴
self.gizmoZAxis = None # Z轴
self.axis_length = 3.0 # 坐标轴长度
# 拖拽相关状态
self.isDraggingGizmo = False # 是否正在拖拽坐标轴
self.dragGizmoAxis = None # 当前拖拽的轴("x", "y", "z"
self.gizmoStartPos = None # 拖拽开始时的位置
self.gizmoTargetStartPos = None # 拖拽开始时目标节点的位置
self.dragStartMousePos = None # 拖拽开始时的鼠标位置
# 高亮相关
self.gizmoHighlightAxis = None
self.gizmo_colors = {
"x": (1, 0, 0, 1), # 红色
"y": (0, 1, 0, 1), # 绿色
"z": (0, 0, 1, 1) # 蓝色
}
self.gizmo_highlight_colors = {
"x": (1, 1, 0, 1), # 黄色高亮
"y": (1, 1, 0, 1), # 黄色高亮
"z": (1, 1, 0, 1) # 黄色高亮
}
print("✓ 选择和变换系统初始化完成")
# ==================== 选择框系统 ====================
def createSelectionBox(self, nodePath):
"""为选中的节点创建选择框"""
try:
# 如果已有选择框,先移除
if self.selectionBox:
self.selectionBox.removeNode()
self.selectionBox = None
if not nodePath:
return
# 创建选择框作为render的子节点但会实时跟踪目标节点
self.selectionBox = self.world.render.attachNewNode("selectionBox")
self.selectionBoxTarget = nodePath # 保存目标节点引用
# 启动选择框更新任务
taskMgr.add(self.updateSelectionBoxTask, "updateSelectionBox")
# 初始更新选择框
self.updateSelectionBoxGeometry()
print(f"为节点 {nodePath.getName()} 创建了选择框")
except Exception as e:
print(f"创建选择框失败: {str(e)}")
def updateSelectionBoxGeometry(self):
"""更新选择框的几何形状和位置"""
try:
if not self.selectionBox or not self.selectionBoxTarget:
return
# 清除现有的几何体
self.selectionBox.removeNode()
self.selectionBox = self.world.render.attachNewNode("selectionBox")
# 获取目标节点的世界边界框
bounds = self.selectionBoxTarget.getBounds()
if not bounds or bounds.isEmpty():
return
# 获取边界框的最小和最大点(世界坐标)
minPoint = bounds.getMin()
maxPoint = bounds.getMax()
# 创建线段对象
lines = LineSegs()
lines.setThickness(2.0)
# 定义立方体的8个顶点
vertices = [
(minPoint.x, minPoint.y, minPoint.z), # 0: 前下左
(maxPoint.x, minPoint.y, minPoint.z), # 1: 前下右
(maxPoint.x, maxPoint.y, minPoint.z), # 2: 后下右
(minPoint.x, maxPoint.y, minPoint.z), # 3: 后下左
(minPoint.x, minPoint.y, maxPoint.z), # 4: 前上左
(maxPoint.x, minPoint.y, maxPoint.z), # 5: 前上右
(maxPoint.x, maxPoint.y, maxPoint.z), # 6: 后上右
(minPoint.x, maxPoint.y, maxPoint.z), # 7: 后上左
]
# 定义立方体的边(连接顶点的线段)
edges = [
# 底面
(0, 1), (1, 2), (2, 3), (3, 0),
# 顶面
(4, 5), (5, 6), (6, 7), (7, 4),
# 垂直边
(0, 4), (1, 5), (2, 6), (3, 7)
]
# 绘制所有边
for start, end in edges:
lines.moveTo(*vertices[start])
lines.drawTo(*vertices[end])
# 创建选择框几何体
geomNode = lines.create()
self.selectionBox.attachNewNode(geomNode)
# 设置选择框的颜色为亮橙色
self.selectionBox.setColor(1.0, 0.5, 0.0, 1.0)
# 设置渲染状态,确保线框总是在最前面显示
state = RenderState.make(
DepthTestAttrib.make(DepthTestAttrib.MLess),
ColorAttrib.makeFlat((1.0, 0.5, 0.0, 1.0))
)
self.selectionBox.setState(state)
# 确保选择框不被光照影响
self.selectionBox.setLightOff()
# 让选择框稍微大一点,避免与模型重叠
self.selectionBox.setScale(1.01)
except Exception as e:
print(f"更新选择框几何体失败: {str(e)}")
def updateSelectionBoxTask(self, task):
"""选择框更新任务"""
try:
if not self.selectionBox or not self.selectionBoxTarget:
return task.done # 结束任务
# 检查目标节点是否还存在
if self.selectionBoxTarget.isEmpty():
self.clearSelectionBox()
return task.done
# 获取目标节点的当前边界框
bounds = self.selectionBoxTarget.getBounds()
if not bounds or bounds.isEmpty():
return task.cont
# 获取当前边界框信息
currentMinPoint = bounds.getMin()
currentMaxPoint = bounds.getMax()
# 检查边界框是否发生变化(位置或大小)
if (not hasattr(self, '_lastMinPoint') or not hasattr(self, '_lastMaxPoint') or
self._lastMinPoint != currentMinPoint or self._lastMaxPoint != currentMaxPoint):
# 更新选择框几何体
self.updateSelectionBoxGeometry()
# 保存当前边界框信息
self._lastMinPoint = currentMinPoint
self._lastMaxPoint = currentMaxPoint
return task.cont # 继续任务
except Exception as e:
print(f"选择框更新任务出错: {str(e)}")
return task.done
def clearSelectionBox(self):
"""清除选择框"""
if self.selectionBox:
self.selectionBox.removeNode()
self.selectionBox = None
# 停止选择框更新任务
taskMgr.remove("updateSelectionBox")
# 清除目标节点引用
self.selectionBoxTarget = None
print("清除了选择框")
# ==================== 坐标轴(Gizmo)系统 ====================
def createGizmo(self, nodePath):
"""为选中的节点创建坐标轴工具"""
try:
# 如果已有坐标轴,先移除
if self.gizmo:
self.gizmo.removeNode()
self.gizmo = None
if not nodePath:
return
# 创建坐标轴主节点
self.gizmo = self.world.render.attachNewNode("gizmo")
self.gizmoTarget = nodePath
# 获取目标节点的边界框
bounds = nodePath.getBounds()
if bounds and not bounds.isEmpty():
center = bounds.getCenter()
maxPoint = bounds.getMax()
# 将坐标轴放在实体的上方
gizmo_pos = Point3(center.x, center.y, maxPoint.z + 2.0)
self.gizmo.setPos(gizmo_pos)
# 创建坐标轴的几何体
self.createGizmoGeometry()
# 启动坐标轴更新任务
taskMgr.add(self.updateGizmoTask, "updateGizmo")
print(f"为节点 {nodePath.getName()} 创建了坐标轴")
except Exception as e:
print(f"创建坐标轴失败: {str(e)}")
def createGizmoGeometry(self):
"""创建坐标轴的几何体"""
try:
if not self.gizmo:
return
# 创建X轴红色
x_lines = LineSegs("x_axis")
x_lines.setThickness(6.0)
x_lines.moveTo(0, 0, 0)
x_lines.drawTo(self.axis_length, 0, 0)
# 创建X轴箭头
x_lines.moveTo(self.axis_length - 0.5, -0.2, 0)
x_lines.drawTo(self.axis_length, 0, 0)
x_lines.drawTo(self.axis_length - 0.5, 0.2, 0)
x_geom = x_lines.create()
self.gizmoXAxis = self.gizmo.attachNewNode(x_geom)
self.gizmoXAxis.setName("gizmo_x_axis")
self.gizmoXAxis.setLightOff()
# 创建Y轴绿色
y_lines = LineSegs("y_axis")
y_lines.setThickness(6.0)
y_lines.moveTo(0, 0, 0)
y_lines.drawTo(0, self.axis_length, 0)
# 创建Y轴箭头
y_lines.moveTo(-0.2, self.axis_length - 0.5, 0)
y_lines.drawTo(0, self.axis_length, 0)
y_lines.drawTo(0.2, self.axis_length - 0.5, 0)
y_geom = y_lines.create()
self.gizmoYAxis = self.gizmo.attachNewNode(y_geom)
self.gizmoYAxis.setName("gizmo_y_axis")
self.gizmoYAxis.setLightOff()
# 创建Z轴蓝色
z_lines = LineSegs("z_axis")
z_lines.setThickness(6.0)
z_lines.moveTo(0, 0, 0)
z_lines.drawTo(0, 0, self.axis_length)
# 创建Z轴箭头
z_lines.moveTo(-0.2, 0, self.axis_length - 0.5)
z_lines.drawTo(0, 0, self.axis_length)
z_lines.drawTo(0.2, 0, self.axis_length - 0.5)
z_geom = z_lines.create()
self.gizmoZAxis = self.gizmo.attachNewNode(z_geom)
self.gizmoZAxis.setName("gizmo_z_axis")
self.gizmoZAxis.setLightOff()
# 确保坐标轴不被光照影响
self.gizmo.setLightOff()
# 改进渲染状态设置
self.gizmo.setBin("fixed", 100) # 提高优先级
self.gizmo.setDepthTest(True) # 启用深度测试,但设置高优先级
self.gizmo.setDepthWrite(False) # 不写入深度缓冲
# 确保坐标轴总是可见
state = RenderState.make(
DepthTestAttrib.make(DepthTestAttrib.MAlways), # 总是通过深度测试
)
self.gizmo.setState(state)
# 强制设置各轴的渲染状态,确保颜色可以变化
red_state = RenderState.make(ColorAttrib.makeFlat((1, 0, 0, 1)))
green_state = RenderState.make(ColorAttrib.makeFlat((0, 1, 0, 1)))
blue_state = RenderState.make(ColorAttrib.makeFlat((0, 0, 1, 1)))
self.gizmoXAxis.setState(red_state)
self.gizmoYAxis.setState(green_state)
self.gizmoZAxis.setState(blue_state)
# 初始化高亮状态
self.gizmoHighlightAxis = None
# 立即设置初始颜色,确保创建时就有正确的颜色
self.setGizmoAxisColor("x", self.gizmo_colors["x"])
self.setGizmoAxisColor("y", self.gizmo_colors["y"])
self.setGizmoAxisColor("z", self.gizmo_colors["z"])
print(f"✓ 坐标轴几何体创建完成,长度={self.axis_length}")
except Exception as e:
print(f"创建坐标轴几何体失败: {str(e)}")
def updateGizmoTask(self, task):
"""坐标轴更新任务"""
try:
if not self.gizmo or not self.gizmoTarget:
return task.done
# 检查目标节点是否还存在
if self.gizmoTarget.isEmpty():
self.clearGizmo()
return task.done
# 更新坐标轴位置,始终在目标节点上方
bounds = self.gizmoTarget.getBounds()
if bounds and not bounds.isEmpty():
center = bounds.getCenter()
maxPoint = bounds.getMax()
gizmo_pos = Point3(center.x, center.y, maxPoint.z + 2.0)
self.gizmo.setPos(gizmo_pos)
return task.cont
except Exception as e:
print(f"坐标轴更新任务出错: {str(e)}")
return task.done
def clearGizmo(self):
"""清除坐标轴"""
if self.gizmo:
self.gizmo.removeNode()
self.gizmo = None
# 停止坐标轴更新任务
taskMgr.remove("updateGizmo")
# 清除坐标轴相关引用
self.gizmoTarget = None
self.gizmoXAxis = None
self.gizmoYAxis = None
self.gizmoZAxis = None
self.isDraggingGizmo = False
self.dragGizmoAxis = None
self.dragStartMousePos = None
print("清除了坐标轴")
def setGizmoAxisColor(self, axis, color):
"""设置坐标轴颜色 - 使用RenderState强制覆盖"""
try:
# 创建强制颜色状态
color_state = RenderState.make(ColorAttrib.makeFlat(color))
if axis == "x" and self.gizmoXAxis:
self.gizmoXAxis.setState(color_state)
self.gizmoXAxis.setColor(*color)
self.gizmoXAxis.setColorScale(*color)
elif axis == "y" and self.gizmoYAxis:
self.gizmoYAxis.setState(color_state)
self.gizmoYAxis.setColor(*color)
self.gizmoYAxis.setColorScale(*color)
elif axis == "z" and self.gizmoZAxis:
self.gizmoZAxis.setState(color_state)
self.gizmoZAxis.setColor(*color)
self.gizmoZAxis.setColorScale(*color)
except Exception as e:
print(f"设置坐标轴颜色失败: {str(e)}")
# ==================== 射线检测和碰撞检测 ====================
def checkGizmoClick(self, mouseX, mouseY):
"""使用屏幕空间检测是否点击了坐标轴"""
if not self.gizmo or not self.gizmoTarget:
return None
try:
print(f"\n=== 坐标轴点击检测 ===")
print(f"鼠标位置: ({mouseX}, {mouseY})")
# 获取坐标轴中心的世界坐标
gizmo_world_pos = self.gizmo.getPos(self.world.render)
print(f"坐标轴世界位置: {gizmo_world_pos}")
# 计算各轴端点的世界坐标
x_end = gizmo_world_pos + Vec3(self.axis_length, 0, 0)
y_end = gizmo_world_pos + Vec3(0, self.axis_length, 0)
z_end = gizmo_world_pos + Vec3(0, 0, self.axis_length)
# 使用Panda3D的内置投影方法
def worldToScreen(world_pos):
"""将世界坐标转换为屏幕坐标"""
# 将世界坐标转换为相机空间
cam_space_pos = self.world.cam.getRelativePoint(self.world.render, world_pos)
# 检查是否在相机前方
if cam_space_pos.getY() <= 0:
return None
# 使用相机的镜头进行投影
screen_pos = Point2()
if self.world.cam.node().getLens().project(cam_space_pos, screen_pos):
# 获取准确的窗口尺寸
win_width, win_height = self.world.getWindowSize()
# 转换为窗口像素坐标
win_x = (screen_pos.getX() + 1.0) * 0.5 * win_width
win_y = (1.0 - screen_pos.getY()) * 0.5 * win_height
return (win_x, win_y)
return None
# 投影各个关键点
center_screen = worldToScreen(gizmo_world_pos)
x_screen = worldToScreen(x_end)
y_screen = worldToScreen(y_end)
z_screen = worldToScreen(z_end)
# 如果无法获得屏幕坐标,使用备用方法
if not center_screen:
print("使用备用检测方法...")
return self.checkGizmoClickFallback(mouseX, mouseY)
# 计算点击阈值
click_threshold = 30 # 增大检测范围
# 检测各个轴
axes_data = [
("x", x_screen, "X轴"),
("y", y_screen, "Y轴"),
("z", z_screen, "Z轴")
]
for axis_name, axis_screen, axis_label in axes_data:
if axis_screen:
# 计算鼠标到轴线的距离
distance = self.distanceToLine(
(mouseX, mouseY), center_screen, axis_screen
)
print(f"{axis_label}距离: {distance:.2f}")
if distance < click_threshold:
print(f"✓ 点击了{axis_label}")
return axis_name
print("× 没有点击任何轴")
return None
except Exception as e:
print(f"坐标轴点击检测失败: {str(e)}")
import traceback
traceback.print_exc()
return None
def checkGizmoClickFallback(self, mouseX, mouseY):
"""备用检测方法:使用固定的屏幕区域"""
print("使用备用点击检测...")
# 获取准确的窗口尺寸
win_width, win_height = self.world.getWindowSize()
# 获取窗口中心作为参考点
center_x = win_width // 2
center_y = win_height // 2
# 定义相对于中心的轴区域(简化假设坐标轴在屏幕中心附近)
axis_length_pixels = 100 # 假设轴长度在屏幕上约100像素
# X轴从中心向右
x_start = (center_x, center_y)
x_end = (center_x + axis_length_pixels, center_y)
# Y轴从中心向上注意Y轴方向
y_start = (center_x, center_y)
y_end = (center_x, center_y - axis_length_pixels)
# Z轴从中心向右上45度
z_start = (center_x, center_y)
z_end = (center_x + axis_length_pixels * 0.7, center_y - axis_length_pixels * 0.7)
threshold = 25
# 检测各轴
if self.distanceToLine((mouseX, mouseY), x_start, x_end) < threshold:
print("✓ 备用方法检测到X轴")
return "x"
elif self.distanceToLine((mouseX, mouseY), y_start, y_end) < threshold:
print("✓ 备用方法检测到Y轴")
return "y"
elif self.distanceToLine((mouseX, mouseY), z_start, z_end) < threshold:
print("✓ 备用方法检测到Z轴")
return "z"
print("× 备用方法也没有检测到")
return None
def distanceToLine(self, point, line_start, line_end):
"""计算点到线段的距离"""
try:
px, py = point
x1, y1 = line_start
x2, y2 = line_end
# 计算线段长度
line_length = ((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5
if line_length == 0:
return ((px - x1) ** 2 + (py - y1) ** 2) ** 0.5
# 计算点到线的距离
t = max(0, min(1, ((px - x1) * (x2 - x1) + (py - y1) * (y2 - y1)) / (line_length ** 2)))
projection_x = x1 + t * (x2 - x1)
projection_y = y1 + t * (y2 - y1)
distance = ((px - projection_x) ** 2 + (py - projection_y) ** 2) ** 0.5
return distance
except Exception as e:
print(f"距离计算错误: {e}")
return float('inf')
# ==================== 高亮和交互 ====================
def updateGizmoHighlight(self, mouseX, mouseY):
"""更新坐标轴高亮状态"""
if not self.gizmo or self.isDraggingGizmo:
return
# 检测鼠标悬停的轴(使用相同的检测逻辑但不输出调试信息)
hoveredAxis = None
try:
# 获取坐标轴中心的世界坐标
gizmo_world_pos = self.gizmo.getPos(self.world.render)
# 计算各轴端点的世界坐标
x_end = gizmo_world_pos + Vec3(self.axis_length, 0, 0)
y_end = gizmo_world_pos + Vec3(0, self.axis_length, 0)
z_end = gizmo_world_pos + Vec3(0, 0, self.axis_length)
# 将3D坐标投影到屏幕坐标
def worldToScreen(worldPos):
try:
# 转换为相机坐标系
camPos = self.world.cam.getRelativePoint(self.world.render, worldPos)
# 检查点是否在相机前方
if camPos.getY() <= 0:
return None
# 使用相机lens进行投影
screenPos = Point2()
lens = self.world.cam.node().getLens()
if lens.project(camPos, screenPos):
# 获取准确的窗口尺寸
winWidth, winHeight = self.world.getWindowSize()
# 转换为像素坐标
winX = (screenPos.x + 1) * 0.5 * winWidth
winY = (1 - screenPos.y) * 0.5 * winHeight
return (winX, winY)
return None
except:
return None
# 获取各坐标轴的屏幕投影
gizmo_screen = worldToScreen(gizmo_world_pos)
x_screen = worldToScreen(x_end)
y_screen = worldToScreen(y_end)
z_screen = worldToScreen(z_end)
if all([gizmo_screen, x_screen, y_screen, z_screen]):
click_threshold = 25
def isNearLine(mousePos, start, end, threshold):
import math
A = mousePos[1] - start[1]
B = start[0] - mousePos[0]
C = (end[1] - start[1]) * mousePos[0] + (start[0] - end[0]) * mousePos[1] + end[0] * start[1] - start[0] * end[1]
length = math.sqrt((end[0] - start[0])**2 + (end[1] - start[1])**2)
if length == 0:
return False
distance = abs(C) / length
t = ((mousePos[0] - start[0]) * (end[0] - start[0]) +
(mousePos[1] - start[1]) * (end[1] - start[1])) / (length * length)
return distance < threshold and 0 <= t <= 1
mouse_pos = (mouseX, mouseY)
# 按优先级检测轴
if isNearLine(mouse_pos, gizmo_screen, z_screen, click_threshold):
hoveredAxis = "z"
elif isNearLine(mouse_pos, gizmo_screen, x_screen, click_threshold):
hoveredAxis = "x"
elif isNearLine(mouse_pos, gizmo_screen, y_screen, click_threshold):
hoveredAxis = "y"
except Exception as e:
pass # 静默处理错误,避免频繁输出
# 如果高亮状态发生变化
if hoveredAxis != self.gizmoHighlightAxis:
# 恢复之前高亮的轴
if self.gizmoHighlightAxis:
self.setGizmoAxisColor(self.gizmoHighlightAxis, self.gizmo_colors[self.gizmoHighlightAxis])
# 高亮新的轴
if hoveredAxis:
self.setGizmoAxisColor(hoveredAxis, self.gizmo_highlight_colors[hoveredAxis])
self.gizmoHighlightAxis = hoveredAxis
# ==================== 拖拽变换 ====================
def startGizmoDrag(self, axis, mouseX, mouseY):
"""开始坐标轴拖拽"""
try:
self.isDraggingGizmo = True
self.dragGizmoAxis = axis
self.dragStartMousePos = (mouseX, mouseY)
# 保存开始拖拽时目标节点的位置
if self.gizmoTarget:
self.gizmoTargetStartPos = self.gizmoTarget.getPos()
print(f"开始拖拽 {axis}")
except Exception as e:
print(f"开始坐标轴拖拽失败: {str(e)}")
def updateGizmoDrag(self, mouseX, mouseY):
"""更新坐标轴拖拽 - 使用屏幕空间投影"""
try:
if not self.isDraggingGizmo or not self.gizmoTarget or not hasattr(self, 'dragStartMousePos'):
return
# 计算鼠标移动距离(屏幕像素)
mouseDeltaX = mouseX - self.dragStartMousePos[0]
mouseDeltaY = mouseY - self.dragStartMousePos[1]
# 获取坐标轴在屏幕空间的方向向量
gizmo_world_pos = self.gizmoTargetStartPos
if self.dragGizmoAxis == "x":
axis_end = gizmo_world_pos + Vec3(1, 0, 0)
elif self.dragGizmoAxis == "y":
axis_end = gizmo_world_pos + Vec3(0, 1, 0)
elif self.dragGizmoAxis == "z":
axis_end = gizmo_world_pos + Vec3(0, 0, 1)
else:
return
# 投影到屏幕空间
def worldToScreen(worldPos):
# 先转换为相机坐标系
camPos = self.world.cam.getRelativePoint(self.world.render, worldPos)
# 检查是否在相机前方
if camPos.getY() <= 0:
return None
screenPos = Point2()
if self.world.cam.node().getLens().project(camPos, screenPos):
# 获取准确的窗口尺寸
winWidth, winHeight = self.world.getWindowSize()
winX = (screenPos.x + 1) * 0.5 * winWidth
winY = (1 - screenPos.y) * 0.5 * winHeight
return (winX, winY)
return None
gizmo_screen = worldToScreen(gizmo_world_pos)
axis_screen = worldToScreen(axis_end)
if not gizmo_screen or not axis_screen:
return
# 计算轴在屏幕空间的方向向量
screen_axis_dir = (
axis_screen[0] - gizmo_screen[0],
axis_screen[1] - gizmo_screen[1]
)
# 归一化屏幕轴方向
import math
length = math.sqrt(screen_axis_dir[0]**2 + screen_axis_dir[1]**2)
if length > 0:
screen_axis_dir = (screen_axis_dir[0] / length, screen_axis_dir[1] / length)
else:
return
# 将鼠标移动投影到轴方向上
projected_distance = (mouseDeltaX * screen_axis_dir[0] +
mouseDeltaY * screen_axis_dir[1])
# 转换投影距离为世界坐标移动距离
# 这个比例因子需要根据相机距离和视野角度调整
scale_factor = 0.01 # 可以调整这个值来改变拖拽灵敏度
if self.dragGizmoAxis == "x":
movement = Vec3(projected_distance * scale_factor, 0, 0)
elif self.dragGizmoAxis == "y":
movement = Vec3(0, projected_distance * scale_factor, 0)
elif self.dragGizmoAxis == "z":
movement = Vec3(0, 0, projected_distance * scale_factor)
# 应用移动到目标节点
newPos = self.gizmoTargetStartPos + movement
self.gizmoTarget.setPos(newPos)
except Exception as e:
print(f"更新坐标轴拖拽失败: {str(e)}")
def stopGizmoDrag(self):
"""停止坐标轴拖拽"""
self.isDraggingGizmo = False
self.dragGizmoAxis = None
self.dragStartMousePos = None
self.gizmoTargetStartPos = None
print("停止坐标轴拖拽")
# ==================== 选择管理 ====================
def updateSelection(self, nodePath):
"""更新选择状态"""
self.selectedNode = nodePath
if nodePath:
self.createSelectionBox(nodePath)
# 自动显示坐标轴(无需移动工具)
self.createGizmo(nodePath)
print(f"选中了节点: {nodePath.getName()}")
else:
self.clearSelectionBox()
self.clearGizmo()
print("取消选择")
def getSelectedNode(self):
"""获取当前选中的节点"""
return self.selectedNode
def hasSelection(self):
"""检查是否有选中的节点"""
return self.selectedNode is not None

37
core/tool_manager.py Normal file
View File

@ -0,0 +1,37 @@
class ToolManager:
"""工具管理器 - 管理编辑器工具状态"""
def __init__(self, world):
"""初始化工具管理器"""
self.world = world
self.currentTool = None # 当前选中的工具
def setCurrentTool(self, tool):
"""设置当前工具"""
self.currentTool = tool
print(f"\n=== 工具切换 ===")
print(f"当前工具: {tool}")
print(f"选中节点: {self.world.selection.selectedNode.getName() if self.world.selection.selectedNode else ''}")
# 坐标轴现在始终跟随选中状态,不再依赖工具类型
def getCurrentTool(self):
"""获取当前工具"""
return self.currentTool
def isSelectionTool(self):
"""检查当前是否是选择工具"""
return self.currentTool == "选择"
def isMoveTool(self):
"""检查当前是否是移动工具"""
return self.currentTool == "移动"
def isRotateTool(self):
"""检查当前是否是旋转工具"""
return self.currentTool == "旋转"
def isScaleTool(self):
"""检查当前是否是缩放工具"""
return self.currentTool == "缩放"

204
core/world.py Normal file
View File

@ -0,0 +1,204 @@
import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)
from QPanda3D.Panda3DWorld import Panda3DWorld
from panda3d.core import (CardMaker, Vec4, Vec3, AmbientLight, DirectionalLight,
Point3, WindowProperties)
from direct.showbase.ShowBaseGlobal import globalClock
class CoreWorld(Panda3DWorld):
"""核心世界功能类 - 负责基础的3D世界设置和核心功能"""
def __init__(self):
super().__init__()
# 初始化基础属性
self.qtWidget = None # Qt部件引用用于获取准确的渲染区域尺寸
# 设置相机控制参数
self.cameraSpeed = 200.0 # 移动速度
self.cameraRotateSpeed = 40.0 # 旋转速度
# 鼠标控制相关变量
self.lastMouseX = 0
self.lastMouseY = 0
self.mouseRightPressed = False
# 初始化世界
self._setupCamera()
self._setupLighting()
self._setupGround()
self._loadFont()
print("✓ 核心世界初始化完成")
def _setupCamera(self):
"""设置相机位置和朝向"""
self.cam.setPos(0, -50, 20)
self.cam.lookAt(0, 0, 0)
print("✓ 相机设置完成")
def _setupLighting(self):
"""设置基础光照系统"""
# 环境光
alight = AmbientLight('alight')
alight.setColor((0.2, 0.2, 0.2, 1))
alnp = self.render.attachNewNode(alight)
self.render.setLight(alnp)
# 定向光(模拟太阳光)
dlight = DirectionalLight('dlight')
dlight.setColor((0.8, 0.8, 0.8, 1))
dlnp = self.render.attachNewNode(dlight)
dlnp.setHpr(45, -45, 0) # 设置光照方向
self.render.setLight(dlnp)
# 保存光源引用
self.ambient_light = alnp
self.directional_light = dlnp
print("✓ 光照系统设置完成")
def _setupGround(self):
"""创建地板"""
cm = CardMaker('ground')
cm.setFrame(-50, 50, -50, 50)
# 创建地板节点
self.ground = self.render.attachNewNode(cm.generate())
self.ground.setP(-90)
self.ground.setZ(-0.1)
self.ground.setColor(0.8, 0.8, 0.8, 1)
print("✓ 地板创建完成")
def _loadFont(self):
"""加载中文字体"""
try:
self.chinese_font = self.loader.loadFont('/usr/share/fonts/truetype/wqy/wqy-microhei.ttc')
if not self.chinese_font:
print("警告: 无法加载中文字体,将使用默认字体")
else:
print("✓ 中文字体加载成功")
except:
print("警告: 无法加载中文字体,将使用默认字体")
self.chinese_font = None
def setQtWidget(self, widget):
"""设置Qt部件引用"""
self.qtWidget = widget
print(f"✓ 设置Qt部件引用: {widget}")
def getWindowSize(self):
"""获取准确的窗口尺寸"""
if self.qtWidget:
# 优先使用Qt部件的实际尺寸
width, height = self.qtWidget.getActualSize()
if width > 0 and height > 0:
return width, height
# 备用方案使用Panda3D窗口尺寸
if hasattr(self, 'win') and self.win:
width = self.win.getXSize()
height = self.win.getYSize()
print(f"从Panda3D窗口获取尺寸: {width} x {height}")
return width, height
# 最后的默认值
print("使用默认窗口尺寸: 800 x 600")
return 800, 600
# ==================== 相机控制功能 ====================
def wheelForward(self, data=None):
"""处理滚轮向前滚动(前进)"""
# 获取相机的前向向量
forward = self.cam.getMat().getRow3(1)
# 计算移动距离
distance = self.cameraSpeed * globalClock.getDt()
# 更新相机位置
currentPos = self.cam.getPos()
newPos = currentPos + forward * distance
self.cam.setPos(newPos)
def wheelBackward(self, data=None):
"""处理滚轮向后滚动(后退)"""
# 获取相机的前向向量
forward = self.cam.getMat().getRow3(1)
# 计算移动距离
distance = self.cameraSpeed * globalClock.getDt()
# 更新相机位置
currentPos = self.cam.getPos()
newPos = currentPos - forward * distance
self.cam.setPos(newPos)
def moveCamera(self, x, y, z):
"""移动相机位置(垂直移动)"""
# 获取相机的上向量
upVector = self.cam.getMat().getRow3(2)
# 计算移动距离
distance = self.cameraSpeed * globalClock.getDt()
# 更新相机位置
currentPos = self.cam.getPos()
newPos = currentPos + upVector * z * distance
self.cam.setPos(newPos)
# ==================== 鼠标事件处理 ====================
def mousePressEventRight(self, evt):
"""处理鼠标右键按下事件"""
print("右键按下")
self.mouseRightPressed = True
self.lastMouseX = evt['x']
self.lastMouseY = evt['y']
def mouseReleaseEventRight(self, evt):
"""处理鼠标右键释放事件"""
print("右键释放")
self.mouseRightPressed = False
def mouseMoveEvent(self, evt):
"""处理鼠标移动事件 - 只处理相机旋转"""
if not evt:
return
if self.mouseRightPressed:
# 计算鼠标移动距离
dx = evt.get('x', 0) - self.lastMouseX
dy = evt.get('y', 0) - self.lastMouseY
# 计算旋转角度
rotateSpeed = self.cameraRotateSpeed * globalClock.getDt()
# 更新相机朝向
currentH = self.cam.getH()
currentP = self.cam.getP()
# 限制俯仰角度在-90到90度之间
newP = max(-90, min(90, currentP - dy * rotateSpeed))
self.cam.setH(currentH - dx * rotateSpeed)
self.cam.setP(newP)
# 更新鼠标位置
self.lastMouseX = evt.get('x', 0)
self.lastMouseY = evt.get('y', 0)
# ==================== 其他基础功能 ====================
def getChineseFont(self):
"""获取中文字体"""
return self.chinese_font
def getGroundNode(self):
"""获取地板节点"""
return self.ground
def getAmbientLight(self):
"""获取环境光"""
return self.ambient_light
def getDirectionalLight(self):
"""获取定向光"""
return self.directional_light

View File

@ -0,0 +1,370 @@
# PANDA3D GUI 中文显示问题完整修复总结
## 🎯 修复概览
本次修复解决了PANDA3D GUI系统中中文字符显示问题涉及5个测试文件的全面更新。
## 📋 修复清单
### ✅ 已修复的文件
| 文件名 | 修复状态 | 主要修复内容 |
|--------|----------|--------------|
| `chinese_gui_test.py` | ✅ 完成 | 创建专门的中文字体测试程序 |
| `gui_test.py` | ✅ 完成 | 修复DirectRadioButton回调函数+所有组件中文支持 |
| `simple_gui_test.py` | ✅ 完成 | 添加中文字体加载和按钮支持 |
| `basic_gui_demo.py` | ✅ 完成 | 基础组件中文字体支持 |
| `gui_3d_demo.py` | ✅ 完成 | 2D/3D文本组件中文字体支持 |
## 🔧 具体修复内容
### 1. 中文字体加载系统
在每个文件中添加了统一的字体加载函数:
```python
def loadChineseFont(self):
"""尝试加载中文字体"""
import os
font_paths = [
'/usr/share/fonts/truetype/wqy/wqy-microhei.ttc',
'/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc',
'/usr/share/fonts/truetype/arphic/ukai.ttc',
'/usr/share/fonts/truetype/arphic/uming.ttc',
]
for font_path in font_paths:
if os.path.exists(font_path):
try:
font = self.loader.loadFont(font_path)
if font:
return font
except:
continue
return None
```
### 2. GUI组件字体设置
#### OnscreenText 组件
```python
# ✅ 正确的方法
text = OnscreenText(
text="中文文本",
font=self.chinese_font if self.chinese_font else None
)
```
#### DirectButton 组件
```python
# ✅ 正确的方法注意使用text_font参数
button = DirectButton(
text="按钮文本",
text_font=self.chinese_font if self.chinese_font else None
)
```
#### DirectLabel 组件
```python
# ✅ 正确的方法
label = DirectLabel(
text="标签文本",
text_font=self.chinese_font if self.chinese_font else None
)
```
#### DirectCheckButton 组件
```python
# ✅ 正确的方法
checkbox = DirectCheckButton(
text="复选框文本",
text_font=self.chinese_font if self.chinese_font else None
)
```
#### DirectRadioButton 组件
```python
# ✅ 正确的方法
radio = DirectRadioButton(
text="单选按钮文本",
text_font=self.chinese_font if self.chinese_font else None
)
```
#### DirectOptionMenu 组件
```python
# ✅ 正确的方法
menu = DirectOptionMenu(
text="选项菜单",
text_font=self.chinese_font if self.chinese_font else None
)
```
### 3. 3D空间文本组件
#### TextNode3D文本
```python
# ✅ 正确的方法
text3d = TextNode('text-name')
text3d.setText("3D中文文本")
if self.chinese_font:
text3d.setFont(self.chinese_font)
```
### 4. 特殊修复
#### DirectRadioButton 回调函数修复
**问题:** `gui_test.py`中DirectRadioButton的回调函数参数不匹配
**修复前:**
```python
def onRadioButtonSelect(self, status): # ❌ 缺少参数
```
**修复后:**
```python
# 添加extraArgs参数
radio = DirectRadioButton(
command=self.onRadioButtonSelect,
extraArgs=[i], # 传递索引参数
)
# 修改回调函数
def onRadioButtonSelect(self, index, status): # ✅ 正确的参数
```
## 🎨 字体组件对应表
| GUI组件类型 | 字体参数名 | 示例 |
|-------------|------------|------|
| OnscreenText | `font` | `OnscreenText(font=chinese_font)` |
| DirectButton | `text_font` | `DirectButton(text_font=chinese_font)` |
| DirectLabel | `text_font` | `DirectLabel(text_font=chinese_font)` |
| DirectCheckButton | `text_font` | `DirectCheckButton(text_font=chinese_font)` |
| DirectRadioButton | `text_font` | `DirectRadioButton(text_font=chinese_font)` |
| DirectOptionMenu | `text_font` | `DirectOptionMenu(text_font=chinese_font)` |
| TextNode | `setFont()` | `textnode.setFont(chinese_font)` |
## 🚀 测试验证
所有修复的文件都已通过测试:
```bash
# 测试基础GUI
python3 basic_gui_demo.py
# 测试简单GUI
python3 simple_gui_test.py
# 测试完整GUI功能
python3 gui_test.py
# 测试中文字体专门功能
python3 chinese_gui_test.py
# 测试3D空间GUI
python3 gui_3d_demo.py
```
## 📚 使用建议
1. **字体安装:** 确保系统已安装中文字体
```bash
sudo apt-get install fonts-wqy-microhei fonts-wqy-zenhei
```
2. **字体检测:** 运行程序时查看控制台输出,确认字体加载状态
3. **回退机制:** 如果中文字体加载失败,程序会自动使用默认字体
4. **组件选择:** 根据需要选择合适的示例程序开始学习
## ✨ 修复成果
- ✅ 5个测试文件全部支持中文显示
- ✅ 涵盖所有主要DirectGUI组件
- ✅ 支持2D和3D空间文本
- ✅ 提供字体加载状态反馈
- ✅ 包含完整的使用示例和说明
现在PANDA3D GUI系统可以完美显示中文字符了🎉
# 完整的GUI修复总结报告
## 问题诊断
### 用户报告的问题
`test.py` 程序中3D文本和虚拟屏幕能正常显示但其他的GUI组件按钮、标签、输入框看不见。
### 根本原因分析
通过深入分析发现,问题的根本原因是**坐标系统不匹配**
1. **3D GUI组件**3D文本、虚拟屏幕使用**世界坐标系统**
- 坐标范围:任意实数范围
- 挂载到:`render` 节点下
- 位置世界3D空间中的绝对位置
2. **2D GUI组件**(按钮、标签、输入框)使用**屏幕坐标系统**
- 坐标范围:-1 到 1 的标准化坐标
- 挂载到:`render2d/aspect2d` 节点下
- 位置:屏幕空间中的相对位置
### 具体问题
当传入 `(5, 0, 0)` 这样的坐标时:
- 3D组件正常显示世界坐标系可以接受任意值
- 2D组件超出屏幕范围而不可见屏幕坐标系只接受-1到1的值
## 解决方案实施
### 1. 修改GUI创建方法
`test.py` 中修改了三个2D GUI创建方法添加坐标转换
```python
def createGUIButton(self, pos=(0, 0, 0), text="按钮", size=0.1):
# 将3D逻辑坐标转换为2D屏幕坐标
gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1)
# ... 其余代码保持不变
```
**转换公式**:屏幕坐标 = 逻辑坐标 × 0.1
### 2. 优化属性面板
修改了 `updateGUIPropertyPanel()` 方法为2D GUI组件提供专门的编辑界面
```python
def updateGUIPropertyPanel(self, gui_element):
gui_type = gui_element.getTag("gui_type")
if gui_type in ["button", "label", "entry"]:
# 2D GUI使用逻辑坐标编辑用户友好
logical_x = pos.getX() / 0.1 # 反向转换
logical_z = pos.getZ() / 0.1
# 显示实际屏幕坐标(只读)
else:
# 3D GUI继续使用世界坐标编辑
```
### 3. 添加专用编辑方法
新增了 `editGUI2DPosition()` 方法处理2D GUI位置编辑
```python
def editGUI2DPosition(self, gui_element, axis, value):
if axis == "x":
new_screen_x = value * 0.1 # 逻辑坐标转屏幕坐标
gui_element.setPos(new_screen_x, current_pos.getY(), current_pos.getZ())
```
## 测试验证
### 创建的测试脚本
1. **test_2d_gui_fix.py** - ShowBase环境基础测试
- ✅ 运行成功所有GUI组件可见可交互
- 验证了修复逻辑的正确性
2. **test_gui_complete.py** - Qt集成环境完整测试
- ✅ GUI元素创建成功
- ⚠️ 在Qt环境中可能存在渲染显示问题
3. **test_simple_gui.py** - 简化验证测试
- ✅ 验证了修复逻辑正确
4. **test_qt_fix.py** - Qt环境专门测试
- ✅ GUI元素正确创建并添加到scene graph
- ⚠️ Qt环境显示可能有问题
5. **test_qt_debug.py** - Qt深度诊断
- 🔍 发现Qt环境创建的是`GraphicsBuffer`而不是`GraphicsWindow`
- 🔍 这是导致渲染问题的根本原因
6. **test_qt_showbase.py** - ShowBase独立验证
- ✅ 在独立窗口中完全正常显示
## 测试结果总结
### ShowBase环境 ✅
- **状态**:完全成功
- **表现**所有2D GUI组件正常显示和交互
- **验证**:修复方案完全有效
### Qt集成环境 ⚠️
- **状态**GUI元素创建成功但渲染可能有问题
- **原因**Qt环境中Panda3D创建的是Buffer而不是Window
- **影响**可能需要额外的Qt-Panda3D集成调试
## 技术细节
### 坐标转换系统
- **逻辑坐标范围**-50 到 50用户友好
- **屏幕坐标范围**-1 到 1Panda3D标准
- **转换比例**0.1(可调整)
### 用户界面改进
- 2D GUI组件编辑使用逻辑坐标直观
- 显示实际屏幕坐标(调试用)
- 3D GUI组件继续使用世界坐标
- 添加了坐标类型提示
### 场景图结构
```
render2d/
├── aspect2d/
│ ├── DirectButton (2D GUI按钮)
│ ├── DirectLabel (2D GUI标签)
│ └── DirectEntry (2D GUI输入框)
└── camera2d
render/
├── 3d-text-* (3D文本)
├── virtual-screen-* (虚拟屏幕)
└── camera
```
## 使用建议
### 1. 在主程序中使用
修复已应用到主程序 `test.py`所有2D GUI组件现在应该正常显示。
### 2. 创建GUI时的坐标建议
```python
# 2D GUI组件 - 使用逻辑坐标(-50到50范围
world.createGUIButton((0, 0, 0), "中心按钮") # 屏幕中心
world.createGUIButton((-20, 0, 0), "左侧按钮") # 屏幕左侧
world.createGUIButton((20, 0, 0), "右侧按钮") # 屏幕右侧
# 3D GUI组件 - 使用世界坐标
world.createGUI3DText((0, 5, 2), "3D文本") # 世界空间位置
world.createGUIVirtualScreen((3, 8, 0), "屏幕") # 世界空间位置
```
### 3. 属性面板编辑
- 2D GUI编辑逻辑坐标-50到50系统自动转换
- 3D GUI直接编辑世界坐标
- 实际屏幕坐标以只读方式显示
## 后续建议
### 1. Qt集成优化
如果需要在Qt环境中获得完美的渲染效果可能需要
- 深入研究Qt-Panda3D集成机制
- 考虑使用不同的Qt集成方案
- 或使用独立的Panda3D窗口
### 2. 功能扩展
- 可以考虑添加更多坐标系统支持
- 增加GUI组件的更多属性编辑
- 提供坐标转换的可视化工具
### 3. 测试完善
- 添加更多边界条件测试
- 测试更复杂的GUI布局
- 验证不同分辨率下的表现
## 结论
**主要问题已解决**2D GUI组件的坐标转换修复完成在ShowBase环境中测试完全成功。
**Qt环境状态**GUI元素创建正确但可能需要额外的渲染调试。
**修复效果**用户现在应该能够在主程序中看到所有GUI组件包括按钮、标签和输入框。

150
demo/GUI_FIXES_SUMMARY.md Normal file
View File

@ -0,0 +1,150 @@
# GUI 显示问题修复总结
## 问题描述
`test.py` 主程序中3D文本和虚拟屏幕能够正常显示但是2D GUI组件按钮、标签、输入框看不见。
## 问题原因分析
### 1. 坐标系统不匹配
- **3D GUI组件**3D文本、虚拟屏幕使用**世界坐标系统**,坐标范围可以是任意值
- **2D GUI组件**(按钮、标签、输入框)使用**屏幕坐标系统**,坐标范围通常是 -1 到 1
### 2. 具体问题
原代码中所有GUI组件都使用相同的位置参数 `pos=(0, 0, 0)`
```python
# 原来的错误代码
button = DirectButton(
text=text,
pos=pos, # 直接使用3D坐标错误
scale=size,
# ...
)
```
当传入像 `(5, 0, 0)` 这样的3D坐标时
- 3D组件正常显示在世界坐标 (5, 0, 0)
- 2D组件会被显示在屏幕坐标 (5, 0, 0),这超出了屏幕范围 [-1, 1],因此不可见
## 解决方案
### 1. 坐标转换
为2D GUI组件添加坐标转换逻辑
```python
def createGUIButton(self, pos=(0, 0, 0), text="按钮", size=0.1):
from direct.gui.DirectGui import DirectButton
# 将3D坐标转换为2D屏幕坐标
# DirectGUI使用屏幕坐标系范围是-1到1
gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1) # 缩放到合适的屏幕范围
button = DirectButton(
text=text,
pos=gui_pos, # 使用转换后的屏幕坐标
scale=size,
# ...
)
```
### 2. 转换规则
- **输入坐标**: 3D逻辑坐标范围 -50 到 50用户友好
- **输出坐标**: 2D屏幕坐标范围 -1 到 1DirectGUI要求
- **转换公式**: `屏幕坐标 = 逻辑坐标 × 0.1`
### 3. 属性面板优化
为了更好地编辑2D GUI组件属性面板中添加了特殊处理
```python
def updateGUIPropertyPanel(self, gui_element):
gui_type = gui_element.getTag("gui_type")
if gui_type in ["button", "label", "entry"]:
# 2D GUI组件使用逻辑坐标编辑
logical_x = pos.getX() / 0.1 # 反向转换为逻辑坐标
logical_z = pos.getZ() / 0.1
# 提供逻辑坐标编辑界面
xPos.setValue(logical_x)
xPos.valueChanged.connect(lambda v: self.editGUI2DPosition(gui_element, "x", v))
# 同时显示实际屏幕坐标(只读)
actualXLabel = QLabel(f"{pos.getX():.3f}")
else:
# 3D GUI组件使用世界坐标编辑
# 正常的3D坐标编辑逻辑
```
## 修复效果验证
### 1. 测试脚本
创建了两个测试脚本验证修复效果:
- `test_2d_gui_fix.py`: 简单的2D GUI显示测试
- `test_gui_complete.py`: 完整的GUI功能测试
### 2. 测试结果
修复后的测试显示:
- ✅ 所有位置的2D按钮都可见且可点击
- ✅ 所有位置的2D标签都正常显示
- ✅ 所有位置的2D输入框都可见且可输入
- ✅ 3D文本和虚拟屏幕继续正常工作
- ✅ 坐标转换计算正确
### 3. 控制台输出示例
```
创建了 中心 位置的GUI元素:
- 按钮位置: (0.0, 0, 0.0)
- 标签位置: (0.0, 0, -0.2)
- 输入框位置: (0.0, 0, -0.4)
创建了 左侧 位置的GUI元素:
- 按钮位置: (-0.5, 0, 0.0)
- 标签位置: (-0.5, 0, -0.2)
- 输入框位置: (-0.5, 0, -0.4)
```
## 关键改进点
### 1. 用户体验
- 用户仍然可以使用直观的"逻辑坐标"(如 -5, 0, 5来指定GUI位置
- 系统自动处理坐标转换,对用户透明
### 2. 一致性
- 3D GUI组件继续使用世界坐标系统
- 2D GUI组件使用合适的屏幕坐标系统
- 属性面板根据组件类型提供相应的编辑界面
### 3. 可维护性
- 坐标转换逻辑集中在创建方法中
- 添加了专门的2D GUI位置编辑方法 `editGUI2DPosition()`
- 保持了代码结构的清晰性
## 使用建议
### 1. 创建2D GUI组件
```python
# 使用逻辑坐标创建(推荐)
button = world.createGUIButton((-5, 0, 0), "左侧按钮") # 显示在屏幕左侧
button = world.createGUIButton((5, 0, 0), "右侧按钮") # 显示在屏幕右侧
button = world.createGUIButton((0, 0, 5), "上方按钮") # 显示在屏幕上方
```
### 2. 创建3D GUI组件
```python
# 使用世界坐标创建(不变)
text3d = world.createGUI3DText((0, 5, 2), "3D文本")
screen = world.createGUIVirtualScreen((3, 8, 0), (2, 1), "虚拟屏幕")
```
### 3. 坐标范围建议
- **2D GUI逻辑坐标**: -10 到 10对应屏幕范围 -1 到 1
- **3D GUI世界坐标**: 任意合理范围
## 总结
这次修复解决了2D GUI组件不可见的核心问题通过正确的坐标系统转换确保了
1. 所有GUI组件都能正常显示
2. 用户界面保持直观和一致
3. 编辑功能完全可用
4. 代码结构保持清晰
修复验证显示所有功能都正常工作用户现在可以在主程序中成功创建和使用所有类型的GUI元素。

View File

@ -0,0 +1,283 @@
# 🎯 GUI功能实现完整指南
## 📋 功能概述
我已经为你的`test.py`程序添加了完整的GUI管理功能包括
### ✨ 核心功能
- **GUI元素创建** - 支持多种2D和3D GUI组件
- **GUI元素编辑** - 实时属性修改和可视化编辑
- **GUI元素删除** - 安全的元素移除机制
- **场景树显示** - GUI元素在层级窗口中的组织
- **属性面板** - 专门的GUI属性编辑界面
---
## 🎮 支持的GUI类型
### 📱 2D GUI组件
1. **DirectButton (按钮)**
- 可点击的交互按钮
- 支持自定义文本和颜色
- 带有点击事件响应
2. **DirectLabel (标签)**
- 静态文本显示
- 透明背景
- 可自定义字体和颜色
3. **DirectEntry (输入框)**
- 文本输入组件
- 支持占位符文本
- 带有提交事件
### 🌐 3D空间GUI组件
1. **3D TextNode (3D文本)**
- 3D空间中的文本节点
- Billboard效果始终面向相机
- 支持中文字体
2. **Virtual Screen (虚拟屏幕)**
- 3D空间中的平面屏幕
- 可显示文本内容
- 半透明效果
---
## 🛠️ 使用方法
### 创建GUI元素
#### 方法1: 使用菜单
```
GUI菜单 → 创建按钮/标签/输入框/3D文本/虚拟屏幕
```
#### 方法2: 使用工具栏
```
工具栏 → 创建按钮/创建标签/3D文本
```
#### 方法3: 程序化创建
```python
# 创建按钮
button = world.createGUIButton(pos=(0, 0, 0.5), text="我的按钮")
# 创建标签
label = world.createGUILabel(pos=(0, 0, 0.3), text="标签文本")
# 创建3D文本
text3d = world.createGUI3DText(pos=(0, 5, 2), text="3D文本")
```
### 编辑GUI元素
#### 属性面板编辑
1. 在层级窗口选中GUI元素
2. 在属性面板中修改:
- 文本内容
- 位置坐标
- 缩放大小
- 颜色属性
#### 右键菜单编辑
1. 右键点击场景树中的GUI元素
2. 选择"编辑"打开编辑对话框
3. 批量修改属性
#### 程序化编辑
```python
# 修改文本
world.editGUIElement(gui_element, "text", "新文本")
# 修改位置
world.editGUIElement(gui_element, "position", [1, 2, 3])
# 修改缩放
world.editGUIElement(gui_element, "scale", 1.5)
```
### 删除GUI元素
#### 方法1: 右键删除
```
场景树中右键GUI元素 → 删除GUI元素
```
#### 方法2: 程序化删除
```python
world.deleteGUIElement(gui_element)
```
---
## 🎛️ 界面布局
### 场景树组织
```
场景
├── 相机
├── 模型
│ └── [所有3D模型]
├── GUI元素
│ ├── button: 按钮文本
│ ├── label: 标签文本
│ ├── 3d_text: 3D文本
│ └── virtual_screen: 屏幕文本
└── 地板
```
### 属性面板
- **通用属性**: 名称、GUI类型
- **文本属性**: 可编辑的文本内容
- **位置属性**: X/Y/Z坐标控制
- **缩放属性**: 统一缩放控制
- **颜色属性**: 2D GUI背景色选择
- **旋转属性**: 3D GUI的H/P/R旋转
---
## 🔧 高级功能
### GUI元素复制
```
右键GUI元素 → 复制
```
自动在原位置偏移处创建副本。
### 批量编辑
通过编辑对话框可以一次性修改多个属性。
### 事件响应
```python
def onGUIButtonClick(self, button_id):
"""自定义按钮点击处理"""
print(f"按钮 {button_id} 被点击")
# 添加你的自定义逻辑
def onGUIEntrySubmit(self, text, entry_id):
"""自定义输入框提交处理"""
print(f"输入内容: {text}")
# 添加你的自定义逻辑
```
---
## 💡 使用技巧
### 1. 中文字体支持
程序会自动检测并加载系统中文字体:
- 微米黑体 (wqy-microhei)
- 文泉驿正黑 (wqy-zenhei)
- 文鼎字体系列
### 2. 3D GUI最佳实践
- 3D文本适合显示场景标注
- 虚拟屏幕适合信息面板
- 合理使用Billboard效果
### 3. 性能优化
- 避免创建过多GUI元素
- 适时删除不需要的元素
- 使用合适的缩放比例
### 4. 交互设计
- 按钮提供即时反馈
- 输入框有明确的提示
- 颜色搭配要易于识别
---
## 🚀 扩展开发
### 添加新GUI类型
在`MyWorld`类中添加新的创建方法:
```python
def createGUISlider(self, pos=(0, 0, 0), range=(0, 100)):
"""创建滑块组件"""
from direct.gui.DirectGui import DirectSlider
slider = DirectSlider(
range=range,
pos=pos,
scale=0.5,
# ... 其他属性
)
slider.setTag("gui_type", "slider")
slider.setTag("gui_id", f"slider_{len(self.gui_elements)}")
self.gui_elements.append(slider)
self.updateSceneTree()
return slider
```
### 自定义事件处理
修改事件处理方法来实现特定功能:
```python
def onGUIButtonClick(self, button_id):
"""自定义按钮行为"""
if button_id == "button_0":
# 第一个按钮的特殊行为
self.performSpecialAction()
elif button_id.startswith("menu_"):
# 菜单按钮的处理
self.handleMenuAction(button_id)
```
---
## 🎨 界面定制
### 主题配置
```python
# 在创建GUI时设置主题色
button = DirectButton(
frameColor=(0.2, 0.6, 0.8, 1), # 蓝色主题
text_fg=(1, 1, 1, 1), # 白色文字
)
```
### 布局管理
建议将相关GUI元素进行分组管理
```python
# 创建UI组
ui_group = {
'main_menu': [],
'game_hud': [],
'settings': []
}
# 按组管理GUI元素
ui_group['main_menu'].append(start_button)
ui_group['main_menu'].append(exit_button)
```
---
## 📚 API参考
### 核心方法
- `createGUIButton(pos, text, size)` - 创建按钮
- `createGUILabel(pos, text, size)` - 创建标签
- `createGUIEntry(pos, placeholder, size)` - 创建输入框
- `createGUI3DText(pos, text, size)` - 创建3D文本
- `createGUIVirtualScreen(pos, size, text)` - 创建虚拟屏幕
- `editGUIElement(element, property, value)` - 编辑元素
- `deleteGUIElement(element)` - 删除元素
- `duplicateGUIElement(element)` - 复制元素
### 属性类型
- `"text"` - 文本内容
- `"position"` - 位置坐标 [x, y, z]
- `"scale"` - 缩放大小
- `"color"` - 颜色值 [r, g, b, a]
---
现在你的`test.py`程序具备了完整的GUI创建、编辑、删除功能🎉
试试通过菜单或工具栏创建一些GUI元素然后在属性面板中编辑它们的属性。GUI元素会自动出现在场景树的"GUI元素"分组中,方便管理。

262
demo/GUI_README.md Normal file
View File

@ -0,0 +1,262 @@
# PANDA3D GUI功能测试文件
本项目包含了多个PANDA3D GUI功能的测试示例帮助您了解和学习PANDA3D的GUI系统。
## 文件说明
### 1. `basic_gui_demo.py` - 基础GUI演示 🌟
**推荐从这个开始学习**
这是最简单的GUI示例包含
- 基本按钮DirectButton
- 文本标签DirectLabel
- 屏幕文本OnscreenText
- 简单的事件处理
- 按钮颜色变化效果
**运行方式:**
```bash
python3 basic_gui_demo.py
```
**功能:**
- 点击"点击我!"按钮可以看到计数增加
- 按钮颜色会循环变化
- 状态标签会显示当前点击次数
- 红色"退出"按钮可以关闭程序
---
### 2. `simple_gui_test.py` - 完整GUI组件测试 ⭐⭐
**中级学习示例**
包含更多GUI组件
- 多种按钮样式
- 文本输入框DirectEntry
- 滑块控件DirectSlider
- 复选框DirectCheckButton
- 选项菜单DirectOptionMenu
- 实时状态更新
**运行方式:**
```bash
python3 simple_gui_test.py
```
**功能:**
- 测试按钮:点击增加计数
- 重置按钮:重置所有控件状态
- 文本输入:在输入框中输入文本并按回车
- 滑块:拖动查看数值实时变化
- 复选框:切换特殊模式
- 选项菜单:选择不同选项
- 状态栏显示当前操作
---
### 3. `gui_test.py` - 高级GUI功能展示 ⭐⭐⭐
**高级功能演示**
最复杂的GUI示例包含
- 所有DirectGUI组件
- 单选按钮组DirectRadioButton
- 滚动框架DirectScrolledFrame
- 密码输入框
- 复杂的事件处理系统
- 自定义字体加载
**运行方式:**
```bash
python3 gui_test.py
```
**注意:** 如果字体加载失败,程序可能无法正常运行。
---
### 4. `gui_3d_demo.py` - 3D空间GUI演示 🚀
**3D GUI概念展示**
展示如何在3D空间中使用GUI
- 2D界面控制3D场景
- 3D空间中的文本元素
- 虚拟3D屏幕
- 3D对象与GUI交互
- 动画和场景控制
**运行方式:**
```bash
python3 gui_3d_demo.py
```
**功能:**
- 旋转场景让3D对象旋转
- 变换颜色:改变场景颜色方案
- 重置:恢复初始状态
- 3D文本在3D空间中显示文字
- 虚拟屏幕3D空间中的信息显示板
---
### 5. `chinese_gui_test.py` - 中文字体支持测试 🇨🇳
**中文字体显示解决方案**
专门解决中文字体在PANDA3D中的显示问题
- 自动检测和加载中文字体
- 支持OnscreenText中文显示
- 支持DirectButton中文显示
- 字体加载状态检测
- 多种中文字体路径支持
**运行方式:**
```bash
python3 chinese_gui_test.py
```
**功能:**
- 测试中文字体在各种GUI组件中的显示
- 显示字体加载状态和建议
- 演示正确的中文字体使用方法
### 📊 中文字体修复状态
| 文件 | 修复状态 | 支持组件 |
|------|----------|----------|
| `chinese_gui_test.py` | ✅ 完成 | OnscreenText, DirectButton |
| `gui_test.py` | ✅ 完成 | 所有DirectGUI组件 |
| `simple_gui_test.py` | ✅ 完成 | 基础GUI组件 |
| `basic_gui_demo.py` | ✅ 完成 | 基础组件 |
| `gui_3d_demo.py` | ✅ 完成 | 2D/3D文本组件 |
---
## PANDA3D GUI系统概述
### DirectGUI 组件
PANDA3D的DirectGUI系统提供了以下主要组件
1. **DirectButton** - 按钮
- 可自定义颜色、大小、文本
- 支持点击事件处理
2. **DirectLabel** - 标签
- 显示静态文本
- 可设置背景和文字颜色
3. **DirectEntry** - 文本输入框
- 单行或多行文本输入
- 支持密码模式obscured
4. **DirectSlider** - 滑块
- 水平或垂直方向
- 数值范围可自定义
5. **DirectCheckButton** - 复选框
- 开/关状态切换
- 可绑定状态变化事件
6. **DirectRadioButton** - 单选按钮
- 单选按钮组
- 共享变量控制选择状态
7. **DirectOptionMenu** - 选项菜单
- 下拉选择列表
- 支持动态选项更新
8. **DirectScrolledFrame** - 滚动框架
- 可滚动的内容区域
- 支持垂直和水平滚动
### 3D空间GUI
PANDA3D还支持在3D空间中创建GUI元素
1. **TextNode** - 3D文本节点
- 可放置在3D空间任意位置
- 支持billboard效果总是面向相机
2. **虚拟屏幕** - 使用CardMaker创建
- 3D空间中的平面显示区域
- 可在上面显示文本或图像
3. **混合GUI** - 2D控制3D场景
- 2D界面控制3D对象
- 实时交互和反馈
### 中文字体支持
PANDA3D的中文字体支持需要特殊处理
1. **OnscreenText** - 屏幕文本
```python
text = OnscreenText(
text="中文文本",
font=chinese_font # 设置中文字体
)
```
2. **DirectButton** - 按钮文本
```python
button = DirectButton(
text="按钮文本",
text_font=chinese_font # 注意使用text_font参数
)
```
3. **字体加载** - 自动检测中文字体
```python
font_paths = [
'/usr/share/fonts/truetype/wqy/wqy-microhei.ttc',
'/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc'
]
```
---
## 学习建议
1. **从基础开始**:先运行 `basic_gui_demo.py`
2. **逐步深入**:然后尝试 `simple_gui_test.py`
3. **探索高级功能**:学习 `gui_test.py` 中的复杂组件
4. **创新应用**:最后体验 `gui_3d_demo.py` 的3D GUI概念
## 常见问题
### Q: 程序无法启动?
A: 确保已正确安装PANDA3D`pip install panda3d`
### Q: 字体加载失败?
A: 使用 `basic_gui_demo.py``simple_gui_test.py`,它们不依赖外部字体
### Q: 中文显示为方块?
A: 需要安装中文字体:
```bash
# 运行字体安装脚本
bash install_chinese_fonts.sh
# 或手动安装
sudo apt-get install fonts-wqy-microhei fonts-wqy-zenhei
```
### Q: 按钮中的中文无法显示?
A: DirectButton需要使用 `text_font` 参数来设置字体,参考 `chinese_gui_test.py` 的解决方案
### Q: 如何自定义GUI样式
A: 查看代码中的 `frameColor`、`text_fg` 等参数设置
### Q: 如何添加更多交互?
A: 参考现有的事件处理函数,如 `onButtonClick()`
---
## 扩展学习
这些示例为您提供了PANDA3D GUI系统的基础。您可以
1. 修改现有代码,尝试不同的参数设置
2. 组合不同的GUI组件创建自己的界面
3. 将GUI集成到您的3D应用程序中
4. 探索PANDA3D的其他高级GUI功能
祝您学习愉快!🎮

162
demo/GUI_TESTING_README.md Normal file
View File

@ -0,0 +1,162 @@
# 🧪 GUI功能测试和故障排除指南
## 📋 问题修复说明
已经修复了GUI文本编辑功能中的`TextNode`导入问题和逻辑错误。现在GUI文本编辑应该可以正常工作了。
## 🔧 主要修复内容
### 1. 导入问题修复
- 在`editGUIElement`方法中添加了`TextNode`导入
- 解决了"name 'TextNode' is not defined"错误
### 2. 编辑逻辑优化
- **3D文本**: 直接修改自身的TextNode因为3D文本本身就是TextNode
- **虚拟屏幕**: 查找TextNode子节点并修改其内容
- **2D GUI**: 使用标准的DirectGUI属性修改方式
### 3. 调试信息增强
- 添加了详细的调试输出
- 显示节点类型和子节点信息
- 便于排查问题
---
## 🎯 如何测试GUI功能
### 方法1: 使用主程序测试
1. 运行`python3 test.py`
2. 通过菜单或工具栏创建GUI元素
- `GUI菜单 → 创建按钮/标签/3D文本/虚拟屏幕`
- 或点击工具栏中的GUI创建按钮
3. 在场景树中选择GUI元素
4. 在属性面板中编辑文本内容
5. 检查GUI元素是否实时更新
### 方法2: 使用测试脚本
```bash
python3 test_gui_edit.py
```
测试脚本提供以下功能:
- 按`Space`键循环测试所有GUI元素的文本编辑
- 按`1`键:单独测试按钮文本编辑
- 按`2`键:单独测试标签文本编辑
- 按`3`键单独测试3D文本编辑
---
## 🔍 故障排除
### 常见问题和解决方案
#### 1. "TextNode未定义"错误
**症状**: 控制台显示"name 'TextNode' is not defined"
**解决**: 已修复,确保使用最新版本的代码
#### 2. 3D文本无法编辑
**症状**: 3D文本创建成功但编辑时文本不变化
**排查步骤**:
1. 检查控制台输出,查看是否显示"成功更新3D文本"
2. 确认3D文本的节点类型是TextNode
3. 查看调试信息中的节点类型报告
#### 3. 虚拟屏幕文本无法编辑
**症状**: 虚拟屏幕的文本内容不更新
**排查步骤**:
1. 查看控制台输出中的子节点数量
2. 检查是否找到TextNode类型的子节点
3. 确认虚拟屏幕创建时正确添加了文本子节点
#### 4. 属性面板编辑无响应
**症状**: 在属性面板中输入文本但GUI不更新
**排查步骤**:
1. 检查场景树中是否正确选中了GUI元素
2. 确认GUI元素的类型标签设置正确
3. 查看控制台是否有错误信息
---
## 🧪 测试用例
### 测试用例1: 2D GUI文本编辑
```python
# 创建按钮
button = world.createGUIButton(pos=(0, 0, 0.5), text="原始按钮")
# 编辑文本
success = world.editGUIElement(button, "text", "修改后的按钮")
# 期望结果: 按钮显示"修改后的按钮"
```
### 测试用例2: 3D文本编辑
```python
# 创建3D文本
text3d = world.createGUI3DText(pos=(0, 5, 2), text="原始3D文本")
# 编辑文本
success = world.editGUIElement(text3d, "text", "修改后的3D文本")
# 期望结果: 3D文本显示"修改后的3D文本"
```
### 测试用例3: 虚拟屏幕文本编辑
```python
# 创建虚拟屏幕
screen = world.createGUIVirtualScreen(pos=(3, 5, 0), text="原始屏幕文本")
# 编辑文本
success = world.editGUIElement(screen, "text", "修改后的屏幕文本")
# 期望结果: 虚拟屏幕显示"修改后的屏幕文本"
```
---
## 📊 调试输出示例
### 正常工作的输出
```
开始编辑GUI元素: 类型=button, 属性=text, 值=新按钮文本
成功更新2D GUI文本: 新按钮文本
编辑GUI元素 button: text = 新按钮文本
```
### 3D文本编辑输出
```
开始编辑GUI元素: 类型=3d_text, 属性=text, 值=新3D文本
成功更新3D文本: 新3D文本
编辑GUI元素 3d_text: text = 新3D文本
```
### 虚拟屏幕编辑输出
```
开始编辑GUI元素: 类型=virtual_screen, 属性=text, 值=新屏幕文本
虚拟屏幕有 1 个子节点
子节点 0: screen-text-0, 类型: <class 'panda3d.core.TextNode'>
成功更新虚拟屏幕文本: 新屏幕文本
编辑GUI元素 virtual_screen: text = 新屏幕文本
```
---
## 🚀 进一步测试建议
1. **批量测试**: 创建多个GUI元素并逐一测试编辑功能
2. **交互测试**: 测试右键菜单的编辑功能
3. **属性面板测试**: 测试实时属性编辑
4. **复制功能测试**: 测试GUI元素的复制功能
5. **删除功能测试**: 确保删除功能正常工作
---
## 💡 开发提示
如果需要添加新的GUI类型请确保
1. 在创建时正确设置`gui_type`标签
2. 在`editGUIElement`中添加对应的编辑逻辑
3. 在`updateGUIPropertyPanel`中添加属性面板支持
4. 编写相应的测试用例
现在GUI文本编辑功能应该可以正常工作了🎉

View File

@ -0,0 +1,231 @@
# GUI编辑模式使用指南
## 概述
主程序现在集成了完整的GUI编辑模式基于 `test_qt_showbase.py` 中验证有效的方法实现。这个功能允许你在3D场景中轻松创建、编辑和管理各种GUI元素。
## 功能特点
### ✨ 核心功能
- **可视化GUI编辑**: 右侧浮动面板提供直观的工具选择
- **多种GUI类型**: 支持2D和3D GUI元素
- **实时预览**: 即时查看创建的GUI效果
- **属性编辑**: 通过属性面板编辑GUI元素属性
- **场景集成**: GUI元素与3D场景完美融合
### 🎯 支持的GUI类型
#### 2D GUI元素
1. **按钮 (Button)**: 可点击的2D按钮
2. **标签 (Label)**: 显示文本信息的标签
3. **输入框 (Entry)**: 用户可输入文本的输入框
#### 3D GUI元素
1. **3D文本**: 在3D空间中显示的文本支持广告牌效果
2. **虚拟屏幕**: 3D空间中的虚拟显示屏
## 使用方法
### 🚀 启动GUI编辑模式
#### 方法1: 通过菜单
1. 启动主程序 `python3 test.py`
2. 点击菜单栏 `GUI` -> `进入GUI编辑模式`
3. 右侧将出现GUI编辑面板
#### 方法2: 运行测试程序
```bash
cd /home/hello/eg
python3 test_gui_edit_mode.py
```
### 🎨 创建GUI元素
#### 步骤1: 选择工具
在右侧GUI编辑面板中
- 点击 `按钮` - 创建2D按钮
- 点击 `标签` - 创建2D标签
- 点击 `输入框` - 创建2D输入框
- 点击 `3D文本` - 创建3D文本
- 点击 `虚拟屏幕` - 创建3D虚拟屏幕
#### 步骤2: 放置元素
选择工具后:
1. 在3D场景中点击鼠标左键
2. GUI元素将在点击位置创建
3. 新创建的元素会自动选中并显示在层级树中
### 📝 编辑GUI元素
#### 选择元素
- **方法1**: 在GUI编辑模式下直接点击GUI元素
- **方法2**: 在左侧层级树中点击GUI元素
#### 编辑属性
选中GUI元素后右侧属性面板会显示可编辑的属性
**通用属性**:
- 名称
- 位置 (X, Y, Z)
- 缩放
**特定属性**:
- **文本类元素**: 可编辑显示文本
- **2D元素**: 屏幕坐标和实际坐标
- **3D元素**: 世界坐标和旋转
### 🔧 管理GUI元素
#### 删除元素
1. 选中要删除的GUI元素
2. 点击GUI编辑面板中的 `删除` 按钮
3. 或在层级树中右键选择 `删除GUI元素`
#### 复制元素
1. 选中要复制的GUI元素
2. 点击GUI编辑面板中的 `复制` 按钮
3. 或在层级树中右键选择 `复制`
#### 在层级树中查看
- 所有GUI元素显示在 `场景` -> `GUI元素` 节点下
- 显示格式: `类型: 文本内容`
- 支持右键菜单操作
### 🎮 交互控制
#### 鼠标操作
- **左键点击**: 选择GUI元素或创建新元素
- **右键拖拽**: 移动选中的GUI元素 (仅3D元素)
- **中键拖拽**: Z轴移动 (仅3D元素)
#### 键盘快捷键
- **Delete**: 删除选中的GUI元素
- **ESC**: 退出GUI编辑模式 (测试程序)
### 💾 保存和加载
#### 保存项目
GUI元素会随项目一起保存
1. `文件` -> `保存`
2. GUI元素信息保存在BAM场景文件中
#### 加载项目
加载项目时GUI元素会自动恢复
1. `文件` -> `打开`
2. 选择包含GUI的项目文件夹
## 坐标系统说明
### 2D GUI坐标
- **屏幕坐标系**: 范围 -1 到 1
- **逻辑坐标**: 用户友好的坐标,自动转换为屏幕坐标
- **转换公式**: `屏幕坐标 = 逻辑坐标 × 0.1`
### 3D GUI坐标
- **世界坐标系**: 与3D场景使用相同的坐标系
- **支持完整的3D变换**: 位置、旋转、缩放
## 技术实现细节
### 基于验证有效的方法
- 使用 `test_qt_showbase.py` 中验证的GUI创建逻辑
- 确保在Qt集成环境中的稳定性
- 统一的错误处理和调试输出
### 关键组件
```python
# GUI编辑模式状态
self.guiEditMode = False
self.guiEditPanel = None
self.currentGUITool = None
# GUI创建方法 (基于test_qt_showbase.py)
def createGUIButton(pos, text, size)
def createGUILabel(pos, text, size)
def createGUIEntry(pos, placeholder, size)
def createGUI3DText(pos, text, size)
def createGUIVirtualScreen(pos, size, text)
```
### 事件处理
- 鼠标事件优先处理GUI编辑模式
- 碰撞检测用于GUI元素选择
- 树形控件与场景实时同步
## 常见问题解答
### Q: GUI元素创建后看不到
**A**: 检查以下几点:
1. 确保已进入GUI编辑模式
2. 检查创建位置是否在视野范围内
3. 查看控制台输出确认创建成功
### Q: 如何调整GUI元素大小
**A**:
1. 选中GUI元素
2. 在属性面板中修改 `缩放` 属性
3. 或通过代码设置: `element.setScale(new_scale)`
### Q: 2D GUI元素位置不准确
**A**:
- 2D GUI使用屏幕坐标系
- 在属性面板中查看 `实际屏幕坐标`
- 使用 `逻辑坐标` 进行编辑更容易
### Q: 中文字体显示问题?
**A**:
- 程序会自动尝试加载系统中文字体
- 如果中文显示异常,检查系统是否安装了中文字体包
- 支持的字体路径: `/usr/share/fonts/truetype/wqy/`
### Q: 如何扩展新的GUI类型
**A**:
1. 在 `MyWorld` 类中添加新的创建方法
2. 在 `createGUIEditPanel()` 中添加对应按钮
3. 在 `createGUIAtPosition()` 中添加处理逻辑
## 示例代码
### 程序化创建GUI元素
```python
# 创建按钮
button = world.createGUIButton((0, 0, 5), "我的按钮", 0.1)
# 创建3D文本
text3d = world.createGUI3DText((0, 5, 2), "3D文本", 0.5)
# 创建虚拟屏幕
screen = world.createGUIVirtualScreen((3, 8, 0), (2, 1), "信息显示")
```
### 编辑GUI属性
```python
# 修改文本
world.editGUIElement(gui_element, "text", "新文本")
# 修改位置
world.editGUIElement(gui_element, "position", [x, y, z])
# 修改缩放
world.editGUIElement(gui_element, "scale", 1.5)
```
## 未来扩展
### 计划功能
- [ ] 更多GUI组件类型 (滑块、复选框等)
- [ ] GUI动画系统
- [ ] 主题和样式编辑器
- [ ] GUI模板系统
- [ ] 导出为独立GUI文件
### 贡献指南
如需添加新功能或修复问题:
1. 参考现有的GUI创建方法
2. 保持与 `test_qt_showbase.py` 验证逻辑的一致性
3. 添加适当的错误处理和调试输出
4. 更新文档和使用示例
---
*最后更新: 2024年*

View File

@ -0,0 +1,169 @@
# GUI预览窗口使用说明
## 概述
GUI预览窗口是一个独立的Panda3D窗口专门用于正确显示2D GUI元素解决Qt集成中DirectGUI显示的问题。
## 主要特性
### ✅ 解决的问题
- **2D GUI显示问题**: 在Qt集成的主窗口中DirectGUI元素可能无法正确显示
- **独立渲染环境**: 提供纯Panda3D环境确保GUI元素正确渲染
- **实时同步**: 自动与主程序中的GUI元素保持同步
### 🚀 核心功能
- **独立窗口**: 完全独立的Panda3D窗口不受Qt影响
- **自动同步**: 每0.5秒自动检测主程序GUI元素变化
- **支持所有GUI类型**: 按钮、标签、输入框、滑块、3D文本、虚拟屏幕
- **交互预览**: 在预览窗口中可以真实地与GUI元素交互
## 使用方式
### 1. 在主程序中使用
```python
from gui_preview_window import GUIPreviewWindow
# 创建预览窗口
preview = GUIPreviewWindow()
# 连接到主程序world对象
preview.set_main_world(your_world_object)
# 预览窗口会自动同步显示GUI元素
```
### 2. 通过GUI编辑模式
1. 在主程序中点击菜单:"GUI" → "进入GUI编辑模式"
2. 预览窗口会自动弹出
3. 使用主程序的工具栏创建GUI元素
4. 在预览窗口中实时查看效果
### 3. 工具栏快速创建
主程序提供了便捷的工具栏按钮:
- **创建按钮**: 在场景中创建可点击按钮
- **创建标签**: 创建文本标签
- **3D文本**: 创建3D空间文本
## 支持的GUI元素类型
### 2D GUI元素 (DirectGUI)
| 类型 | 说明 | 在预览窗口中的表现 |
|------|------|-------------------|
| 按钮 | DirectButton | 完全可交互,支持点击 |
| 标签 | DirectLabel | 正确显示文本内容 |
| 输入框 | DirectEntry | 支持文本输入和提交 |
| 滑块 | DirectSlider | 支持值调整交互 |
### 3D GUI元素
| 类型 | 说明 | 在预览窗口中的表现 |
|------|------|-------------------|
| 3D文本 | TextNode | 3D空间中的文本支持广告牌效果 |
| 虚拟屏幕 | CardMaker + TextNode | 带背景的虚拟显示屏 |
## 技术实现
### 自动同步机制
```python
def sync_with_main(self, task):
"""每0.5秒检测GUI元素变化"""
if self.main_world and hasattr(self.main_world, 'gui_elements'):
main_count = len(self.main_world.gui_elements)
preview_count = len(self.gui_elements)
if main_count != preview_count:
self.refresh_all_gui() # 自动刷新
return task.again
```
### 窗口配置
```python
loadPrcFileData("", """
win-size 800 600
window-title GUI预览窗口 - 2D GUI专用显示
framebuffer-multisample 1
multisamples 2
audio-library-name null
notify-level-audio error
""")
```
## 操作说明
### 鼠标控制
- **滚轮**: 缩放视图
- **右键拖拽**: 旋转视角 (未来版本)
### 状态显示
预览窗口底部会显示当前状态:
- "等待GUI元素..." - 尚未同步到任何GUI
- "已同步 N 个GUI元素" - 正在显示N个GUI元素
## 优势对比
### 与嵌入式方案对比
| 特性 | 独立预览窗口 | Qt嵌入式 |
|------|-------------|-----------|
| 2D GUI显示 | ✅ 完美支持 | ❌ 显示问题 |
| 性能 | ✅ 原生性能 | ⚠️ 集成开销 |
| 交互性 | ✅ 完全交互 | ⚠️ 部分限制 |
| 开发复杂度 | ✅ 简单 | ❌ 复杂 |
### 与独立编辑器对比
| 特性 | 预览窗口 | 独立编辑器 |
|------|----------|------------|
| 操作流程 | ✅ 简化 | ❌ 复杂 |
| 学习成本 | ✅ 低 | ❌ 高 |
| 实时性 | ✅ 自动同步 | ⚠️ 手动刷新 |
| 代码维护 | ✅ 简单 | ❌ 复杂 |
## 故障排除
### 常见问题
**Q: 预览窗口没有显示GUI元素**
A: 检查是否正确连接到主程序world对象确保调用了`set_main_world()`
**Q: GUI元素显示位置不正确**
A: 2D GUI使用屏幕坐标系3D GUI使用世界坐标系检查创建时的坐标参数
**Q: 预览窗口无法交互**
A: 确保预览窗口有焦点,尝试点击窗口使其获得焦点
### 调试信息
预览窗口会在控制台输出详细的调试信息:
```
✓ GUI预览窗口已创建 - 专用于显示2D GUI元素
预览窗口已连接到主程序
检测到GUI元素变化: 主程序1个, 预览0个
预览窗口创建元素: button - 测试按钮
```
## 未来扩展
### 计划功能
- [ ] 预览窗口中直接编辑GUI属性
- [ ] 鼠标右键拖拽旋转视角
- [ ] GUI元素高亮和选择
- [ ] 布局网格显示
- [ ] 多视角预览(正交、透视)
### API扩展
```python
# 未来可能的API
preview.set_view_mode("orthographic") # 设置正交视图
preview.enable_grid(True) # 显示网格
preview.highlight_element(gui_element) # 高亮元素
```
## 总结
GUI预览窗口提供了一个简洁高效的解决方案让开发者能够
1. **在主程序中直接编辑** - 不需要额外的编辑器窗口
2. **在独立窗口中预览** - 确保GUI元素正确显示
3. **实时同步更新** - 无需手动刷新,自动保持同步
这种设计大大简化了GUI开发流程同时解决了Qt集成中的显示问题。

View File

@ -0,0 +1,116 @@
# 屏幕投影拾取系统使用说明
## 🎯 **概述**
为了解决您遇到的射线检测问题(射线偏向屏幕中心),我们实现了一个新的**屏幕投影拾取系统**它直接基于2D屏幕坐标进行对象选择比传统的3D射线检测更准确、更直观。
## 🚀 **新系统优势**
### ✅ **相比射线检测的优点**
1. **准确性高**:完全对应用户的视觉感受,点击哪里就选中哪里
2. **性能优秀**不需要复杂的3D数学计算
3. **稳定可靠**:不会出现射线偏向屏幕中心的问题
4. **支持任意形状**:通过包围盒自动计算选择范围
### ❌ **传统射线检测的问题**
- 射线不是从相机发出,而是从近平面发出
- 在Qt嵌入环境中容易出现坐标系转换问题
- 微小的坐标偏移会导致射线方向计算错误
## 🔧 **技术实现**
### **核心原理**
1. **3D到2D投影**:将对象的世界坐标投影到屏幕坐标
2. **距离计算**计算鼠标点击位置与对象屏幕位置的2D距离
3. **智能选择**:基于距离和优先级选择最合适的对象
### **关键特性**
- **自动半径计算**:根据对象包围盒自动计算屏幕选择半径
- **优先级系统**:支持不同类型对象的选择优先级
- **容差调节**:可调节选择容差,使选择更容易或更精确
- **自动更新**:对象移动后自动更新位置信息
## 🎮 **使用方法**
### **在应用中的集成**
```python
# 1. 系统已自动初始化
# 屏幕投影拾取器在 MyWorld.__init__() 中自动创建
# 2. 模型自动注册
# 导入的模型会自动注册到拾取器
# 3. 手动注册对象(可选)
self.screen_picker.registerObject(node_path, "对象名称", priority=1)
# 4. 更新对象位置(对象移动后)
self.screen_picker.updateObjectPositions()
```
### **键盘快捷键**
- **P键**:切换拾取方法(屏幕投影 ↔ 射线检测)
- **T键**:创建测试立方体(用于测试拾取功能)
- **R键**:切换射线调试显示
## 📋 **测试步骤**
1. **启动应用**:运行 `python3 test.py`
2. **创建测试对象**:按 **T键** 创建测试立方体
3. **测试选择**
- 点击立方体附近区域
- 观察选择效果
4. **比较方法**:按 **P键** 切换拾取方法,比较效果差异
5. **导入模型**拖拽3D模型文件到应用中测试
## 🔍 **调试信息**
系统会在控制台输出详细的调试信息:
```
=== 开始屏幕投影拾取 ===
鼠标位置: (372, 265)
选中对象: TestCube_1
```
## 🛠 **技术细节**
### **坐标系转换**
```python
# Qt坐标系 → 世界坐标系 → 屏幕坐标系
pixel_x = (screen_point.getX() + 1.0) * win_width * 0.5
pixel_y = (1.0 - screen_point.getY()) * win_height * 0.5
```
### **半径计算**
- **自动模式**:基于对象包围盒的最大尺寸
- **自定义模式**:可为特定对象设置自定义选择半径
- **距离补偿**:远距离对象使用基于透视投影的估算
### **选择算法**
```python
# 计算选择分数
distance_score = 1.0 - (distance / effective_radius)
priority_score = priority * 10.0
total_score = distance_score + priority_score
```
## 🎯 **使用建议**
1. **优先使用屏幕投影拾取**:对于大多数交互场景更准确
2. **射线检测作为备选**:某些特殊情况下仍然有用
3. **调整容差**:根据需要调整 `tolerance_multiplier` 参数
4. **设置优先级**:为不同类型的对象设置合适的选择优先级
## 🔄 **后向兼容**
- 原有的射线检测系统仍然保留
- 现在屏幕投影拾取为主要方法,射线检测为备选
- 所有现有的选择、拖拽、GUI编辑功能都正常工作
## 📝 **总结**
新的屏幕投影拾取系统解决了您遇到的射线偏向屏幕中心的问题,提供了更准确、更直观的对象选择体验。系统已完全集成到您的应用中,并保持了向后兼容性。

View File

@ -0,0 +1,249 @@
# Panda3D 视频播放功能集成指南
## 概述
Panda3D 完全支持内嵌视频播放功能,通过 `MovieTexture` 类可以实现:
- ✅ **视频文件播放** - 支持 MP4, AVI, MOV, MKV, WebM 等格式
- ✅ **3D场景中的视频纹理** - 将视频作为材质应用到3D模型
- ✅ **视频控制** - 播放、暂停、停止、调速、循环等
- ✅ **程序化动画** - 创建动态纹理效果
- ✅ **全景视频** - 360度球形视频播放
- ✅ **广告牌视频** - 始终面向相机的视频面板
## 核心组件
### 1. MovieTexture 类
```python
from panda3d.core import MovieTexture
# 创建视频纹理
movie_texture = MovieTexture("video_name")
success = movie_texture.read("path/to/video.mp4")
if success:
# 播放控制
movie_texture.play() # 播放
movie_texture.stop() # 停止
movie_texture.setTime(0) # 设置播放位置
movie_texture.setPlayRate(1.5) # 设置播放速度
movie_texture.setLoop(True) # 设置循环播放
# 获取信息
length = movie_texture.getVideoLength()
current_time = movie_texture.getTime()
is_playing = movie_texture.isPlaying()
```
### 2. 视频管理器 (VideoManager)
```python
from video_integration import VideoManager
# 在 MyWorld 类中初始化
self.video_manager = VideoManager(self)
# 创建视频屏幕
screen = self.video_manager.create_video_screen(
pos=(0, 0, 2),
size=(4, 3),
video_path="video.mp4",
name="main_screen"
)
# 创建广告牌
billboard = self.video_manager.create_video_billboard(
pos=(2, 0, 1),
size=(2, 1.5),
name="info_billboard"
)
# 创建全景视频
spherical = self.video_manager.create_spherical_video(
pos=(0, 0, 0),
radius=10,
video_path="360_video.mp4",
name="vr_environment"
)
```
## 错误修复说明
### 1. 任务管理器参数传递错误
**问题**: `TypeError: missing 1 required positional argument: 'task'`
**原因**: 使用 `extraArgs` 时参数顺序错误
**修复方案**:
```python
# ❌ 错误的方式
self.taskMgr.add(self.update_texture, "task_name", extraArgs=[texture, data])
def update_texture(self, texture, data, task): # task 参数位置错误
pass
# ✅ 正确的方式
self.taskMgr.add(lambda task: self.update_texture(task, texture, data), "task_name")
def update_texture(self, task, texture, data): # task 参数在前
return task.cont
```
### 2. 模型依赖问题
**问题**: `Couldn't load file models/sphere.egg`
**解决方案**: 程序化生成几何体
```python
def create_sphere_geometry(self, radius=1):
"""程序化创建球体几何"""
sphere_root = self.render.attachNewNode("sphere")
faces = [
(Vec3(0, radius, 0), Vec3(0, 0, 0)), # 前面
(Vec3(0, -radius, 0), Vec3(0, 180, 0)), # 后面
(Vec3(radius, 0, 0), Vec3(0, 90, 0)), # 右面
(Vec3(-radius, 0, 0), Vec3(0, -90, 0)), # 左面
(Vec3(0, 0, radius), Vec3(-90, 0, 0)), # 上面
(Vec3(0, 0, -radius), Vec3(90, 0, 0)), # 下面
]
cm = CardMaker("sphere_face")
cm.setFrame(-radius, radius, -radius, radius)
for pos, hpr in faces:
face = sphere_root.attachNewNode(cm.generate())
face.setPos(pos)
face.setHpr(hpr)
return sphere_root
```
## 快速开始
### 1. 基础视频播放
```python
from direct.showbase.ShowBase import ShowBase
from panda3d.core import MovieTexture, CardMaker
class VideoApp(ShowBase):
def __init__(self):
ShowBase.__init__(self)
# 创建视频纹理
movie_texture = MovieTexture("video")
success = movie_texture.read("your_video.mp4")
if success:
# 创建显示面板
cm = CardMaker("video_card")
cm.setFrame(-2, 2, -1.5, 1.5)
video_card = self.render.attachNewNode(cm.generate())
video_card.setTexture(movie_texture)
# 播放视频
movie_texture.play()
# 设置相机
self.cam.setPos(0, -5, 0)
app = VideoApp()
app.run()
```
### 2. 集成到现有编辑器
```python
# 在 MyWorld 类的 __init__ 方法中添加
from video_integration import VideoManager
self.video_manager = VideoManager(self)
# 在文件拖放处理中添加视频支持
def dropEvent(self, event):
if event.mimeData().hasUrls():
for url in event.mimeData().urls():
filepath = url.toLocalFile()
# 检查视频文件
if self.world.video_manager.is_video_file(filepath):
self.world.video_manager.create_video_screen(
pos=(0, 0, 2),
video_path=filepath,
name=f"video_{len(self.world.video_manager.video_objects)}"
)
# 检查模型文件
elif filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx')):
self.world.importModel(filepath)
```
### 3. 视频控制示例
```python
# 播放控制
video_manager.play_video("video_name")
video_manager.pause_video("video_name")
video_manager.stop_video("video_name")
# 速度控制
video_manager.set_video_speed("video_name", 1.5) # 1.5倍速
# 获取信息
info = video_manager.get_video_info("video_name")
print(f"视频长度: {info['length']}秒")
print(f"当前时间: {info['current_time']}秒")
print(f"播放状态: {info['is_playing']}")
# 列出所有视频
videos = video_manager.list_videos()
for video in videos:
print(f"视频: {video['name']}, 类型: {video['type']}, 播放中: {video['is_playing']}")
```
## 支持的视频格式
- **MP4** - 推荐格式,兼容性最好
- **AVI** - 传统格式,支持良好
- **MOV** - QuickTime 格式
- **MKV** - 开源容器格式
- **WebM** - 网络优化格式
## 性能优化建议
1. **视频分辨率**: 建议使用 1080p 或更低分辨率
2. **编码格式**: 优先使用 H.264 编码
3. **文件大小**: 控制在合理范围内避免内存问题
4. **多视频播放**: 同时播放多个视频时注意性能监控
5. **纹理尺寸**: 程序化纹理建议使用 2 的幂次方尺寸
## 常见问题解决
### Q: 视频不播放怎么办?
A: 检查文件路径是否正确,文件格式是否支持,系统是否安装了相应的解码器。
### Q: 动画纹理不更新?
A: 确认任务管理器正常运行,检查 `task.cont` 返回值。
### Q: 全景视频显示异常?
A: 检查球体几何体的法线方向,确保使用 `setTwoSided(True)`
### Q: 内存占用过高?
A: 减小纹理分辨率,限制同时播放的视频数量,及时移除不需要的视频对象。
## 测试文件
项目包含以下测试文件:
- `video_player_example.py` - 完整的视频播放器演示
- `video_integration.py` - 视频管理器模块
- `test_video_fix.py` - 功能测试验证
运行测试:
```bash
python test_video_fix.py
python video_player_example.py
```
## 总结
Panda3D 的视频播放功能非常强大,支持多种使用场景。通过本指南提供的 VideoManager 类,可以轻松集成到现有项目中,实现丰富的多媒体体验。
主要优势:
- 🔥 **易于集成** - 几行代码即可添加视频功能
- 🎯 **功能丰富** - 支持多种视频显示模式
- 🚀 **性能优秀** - 基于 Panda3D 的高效渲染
- 🛠️ **高度可定制** - 支持程序化动画和特效
- 💡 **实时控制** - 完整的播放控制接口

112
demo/basic_gui_demo.py Normal file
View File

@ -0,0 +1,112 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
PANDA3D 基础GUI演示
最简单的DirectGUI使用示例
"""
from direct.showbase.ShowBase import ShowBase
from direct.gui.DirectGui import DirectButton, DirectLabel
from direct.gui.OnscreenText import OnscreenText
from panda3d.core import TextNode
class BasicGUIDemo(ShowBase):
def __init__(self):
ShowBase.__init__(self)
# 设置背景颜色
self.setBackgroundColor(0.2, 0.4, 0.6)
# 尝试加载中文字体
self.chinese_font = self.loadChineseFont()
# 创建标题
self.title = OnscreenText(
text="PANDA3D DirectGUI 基础演示",
pos=(0, 0.8),
scale=0.1,
fg=(1, 1, 1, 1),
align=TextNode.ACenter,
font=self.chinese_font if self.chinese_font else None
)
# 创建一个简单按钮
self.button = DirectButton(
text="点击我!",
pos=(0, 0, 0.2),
scale=0.1,
command=self.buttonClicked,
frameColor=(0.2, 0.8, 0.2, 1),
text_font=self.chinese_font if self.chinese_font else None
)
# 创建标签显示状态
self.statusLabel = DirectLabel(
text="状态: 等待点击",
pos=(0, 0, -0.2),
scale=0.08,
frameColor=(0, 0, 0, 0),
text_fg=(1, 1, 0, 1),
text_font=self.chinese_font if self.chinese_font else None
)
# 创建退出按钮
self.exitButton = DirectButton(
text="退出",
pos=(0, 0, -0.5),
scale=0.08,
command=self.exitDemo,
frameColor=(0.8, 0.2, 0.2, 1),
text_font=self.chinese_font if self.chinese_font else None
)
# 点击计数器
self.clickCount = 0
print("PANDA3D 基础GUI演示启动成功!")
print("请点击窗口中的按钮来测试GUI功能")
def buttonClicked(self):
"""按钮点击处理函数"""
self.clickCount += 1
self.statusLabel['text'] = f"状态: 按钮被点击了 {self.clickCount}"
print(f"按钮被点击! 总共点击了 {self.clickCount}")
# 改变按钮颜色作为反馈
if self.clickCount % 3 == 0:
self.button['frameColor'] = (0.8, 0.2, 0.8, 1) # 紫色
elif self.clickCount % 3 == 1:
self.button['frameColor'] = (0.2, 0.8, 0.2, 1) # 绿色
else:
self.button['frameColor'] = (0.2, 0.2, 0.8, 1) # 蓝色
def exitDemo(self):
"""退出演示"""
print("用户选择退出")
self.userExit()
def loadChineseFont(self):
"""尝试加载中文字体"""
import os
font_paths = [
'/usr/share/fonts/truetype/wqy/wqy-microhei.ttc',
'/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc',
'/usr/share/fonts/truetype/arphic/ukai.ttc',
'/usr/share/fonts/truetype/arphic/uming.ttc',
]
for font_path in font_paths:
if os.path.exists(font_path):
try:
font = self.loader.loadFont(font_path)
if font:
return font
except:
continue
return None
if __name__ == "__main__":
print("正在启动PANDA3D基础GUI演示...")
demo = BasicGUIDemo()
demo.run()

175
demo/basic_ray_test.py Normal file
View File

@ -0,0 +1,175 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
from direct.showbase.ShowBase import ShowBase
from panda3d.core import (
Point3, Vec3, CollisionTraverser, CollisionHandlerQueue,
CollisionNode, CollisionRay, CollisionSphere, BitMask32
)
class BasicRayTest(ShowBase):
def __init__(self):
ShowBase.__init__(self)
print("=== 基础射线测试 ===")
# 简单的相机设置
self.cam.setPos(0, -20, 5)
self.cam.lookAt(0, 0, 0)
print(f"相机位置: {self.cam.getPos()}")
# 创建简单的碰撞球体在原点
self.createSimpleSphere()
# 创建简单的射线检测
self.createSimpleRay()
# 测试简单的射线
self.testSimpleRay()
def createSimpleSphere(self):
"""在原点创建简单的碰撞球体"""
print(f"\n创建测试球体...")
# 创建碰撞节点
collisionNode = CollisionNode('testSphere')
# 创建球体中心在原点半径5
sphere = CollisionSphere(Point3(0, 0, 0), 5.0)
collisionNode.addSolid(sphere)
# 设置掩码
collisionNode.setIntoCollideMask(BitMask32.bit(0))
collisionNode.setFromCollideMask(BitMask32.allOff())
# 附加到render
self.sphereNP = self.render.attachNewNode(collisionNode)
# 显示碰撞体
self.sphereNP.show()
print(f"球体创建在原点: (0, 0, 0)")
print(f"球体半径: 5.0")
print(f"球体 IntoMask: {collisionNode.getIntoCollideMask()}")
def createSimpleRay(self):
"""创建简单的射线检测"""
print(f"\n创建射线检测...")
# 创建遍历器和队列
self.traverser = CollisionTraverser()
self.queue = CollisionHandlerQueue()
# 创建射线节点
rayNode = CollisionNode('testRay')
self.ray = CollisionRay()
rayNode.addSolid(self.ray)
# 设置射线掩码
rayNode.setFromCollideMask(BitMask32.bit(0))
rayNode.setIntoCollideMask(BitMask32.allOff())
# 创建射线节点路径
self.rayNP = self.render.attachNewNode(rayNode)
# 注册到遍历器
self.traverser.addCollider(self.rayNP, self.queue)
print(f"射线 FromMask: {rayNode.getFromCollideMask()}")
print(f"遍历器中的碰撞器数量: {self.traverser.getNumColliders()}")
def testSimpleRay(self):
"""测试简单的射线"""
print(f"\n=== 射线测试 ===")
# 测试1: 从相机直接射向原点
camera_pos = self.cam.getPos()
target_pos = Point3(0, 0, 0)
direction = target_pos - camera_pos
direction.normalize()
print(f"测试1: 从相机射向原点")
print(f" 起点: {camera_pos}")
print(f" 方向: {direction}")
# 设置射线
self.ray.setOrigin(camera_pos)
self.ray.setDirection(direction)
# 执行检测
self.queue.clearEntries()
self.traverser.traverse(self.render)
entries = self.queue.getNumEntries()
print(f" 检测到 {entries} 个碰撞")
if entries > 0:
self.queue.sortEntries()
for i in range(entries):
entry = self.queue.getEntry(i)
hitPos = entry.getSurfacePoint(self.render)
hitNode = entry.getIntoNodePath()
distance = (hitPos - camera_pos).length()
print(f" 碰撞 {i}: {hitNode.getName()}, 距离: {distance:.2f}")
# 测试2: 从固定位置射向球体
print(f"\n测试2: 从固定位置射向球体")
fixed_origin = Point3(-10, 0, 0)
fixed_direction = Vec3(1, 0, 0) # 直接向右
print(f" 起点: {fixed_origin}")
print(f" 方向: {fixed_direction}")
self.ray.setOrigin(fixed_origin)
self.ray.setDirection(fixed_direction)
self.queue.clearEntries()
self.traverser.traverse(self.render)
entries = self.queue.getNumEntries()
print(f" 检测到 {entries} 个碰撞")
if entries > 0:
self.queue.sortEntries()
for i in range(entries):
entry = self.queue.getEntry(i)
hitPos = entry.getSurfacePoint(self.render)
hitNode = entry.getIntoNodePath()
distance = (hitPos - fixed_origin).length()
print(f" 碰撞 {i}: {hitNode.getName()}, 距离: {distance:.2f}")
# 测试3: 检查射线是否正确设置
print(f"\n测试3: 检查射线设置")
print(f" 射线起点: {self.ray.getOrigin()}")
print(f" 射线方向: {self.ray.getDirection()}")
# 手动计算射线是否应该击中球体
origin = self.ray.getOrigin()
direction = self.ray.getDirection()
sphere_center = Point3(0, 0, 0)
sphere_radius = 5.0
# 计算从射线起点到球心的向量
to_sphere = sphere_center - origin
# 计算射线上最接近球心的点
t = to_sphere.dot(direction)
closest_point = origin + direction * t
# 计算最近距离
distance_to_center = (closest_point - sphere_center).length()
print(f" 到球心的最近距离: {distance_to_center:.2f}")
print(f" 球体半径: {sphere_radius:.2f}")
if distance_to_center <= sphere_radius:
print(f" 射线应该击中球体!")
else:
print(f" 射线会错过球体")
if __name__ == "__main__":
app = BasicRayTest()
print("\n基础测试完成!")

169
demo/center_ray_test.py Normal file
View File

@ -0,0 +1,169 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
from direct.showbase.ShowBase import ShowBase
from panda3d.core import (
Point3, Point2, Vec3, CollisionTraverser, CollisionHandlerQueue,
CollisionNode, CollisionRay, CollisionSphere, BitMask32, CardMaker
)
class CenterRayTest(ShowBase):
def __init__(self):
ShowBase.__init__(self)
# 设置相机位置 - 和你的程序一样
self.cam.setPos(-3.87655, -188.38084, 82.602684)
self.cam.lookAt(0, 0, 0)
print(f"相机位置: {self.cam.getPos()}")
print(f"相机朝向: {self.cam.getHpr()}")
# 创建测试立方体
self.createTestCube()
# 设置射线检测
self.setupRayPicking()
# 测试从屏幕中心射出的射线
self.testCenterRay()
# 测试朝向立方体的直接射线
self.testDirectRay()
def createTestCube(self):
"""创建测试立方体"""
cm = CardMaker("TestCube")
cm.setFrame(-1, 1, -1, 1)
self.cube = self.render.attachNewNode(cm.generate())
self.cube.setPos(0, 10, 3)
self.cube.setScale(3, 3, 3)
self.cube.setColor(1, 0, 0, 1)
self.cube.setTwoSided(True)
self.cube.setHpr(45, 15, 0)
print(f"立方体世界位置: {self.cube.getPos(self.render)}")
# 添加碰撞检测
cNode = CollisionNode('TestCubeCollision')
cSphere = CollisionSphere(Point3(0, 0, 0), 4.0)
cNode.addSolid(cSphere)
cNode.setIntoCollideMask(BitMask32.bit(0))
cNode.setFromCollideMask(BitMask32.allOff())
self.cNodePath = self.cube.attachNewNode(cNode)
print(f"碰撞球体半径: 4.0, 实际半径: {4.0 * 3} = 12.0")
def setupRayPicking(self):
"""设置射线检测"""
self.picker = CollisionTraverser()
self.pickerQueue = CollisionHandlerQueue()
self.pickerNode = CollisionNode('mouseRay')
self.pickerRay = CollisionRay()
self.pickerNode.addSolid(self.pickerRay)
mask = BitMask32.bit(0)
self.pickerNode.setFromCollideMask(mask)
self.pickerNode.setIntoCollideMask(BitMask32.allOff())
self.pickerNP = self.cam.attachNewNode(self.pickerNode)
self.picker.addCollider(self.pickerNP, self.pickerQueue)
def testCenterRay(self):
"""测试从屏幕中心射出的射线"""
print(f"\n=== 屏幕中心射线测试 ===")
# 屏幕中心的归一化坐标应该是 (0, 0)
normalized_x = 0.0
normalized_y = 0.0
print(f"屏幕中心归一化坐标: ({normalized_x}, {normalized_y})")
camera_pos = self.cam.getPos(self.render)
lens = self.cam.node().getLens()
nearPoint = Point3()
farPoint = Point3()
if lens.extrude(Point2(normalized_x, normalized_y), nearPoint, farPoint):
nearWorldPoint = self.cam.getRelativePoint(self.render, nearPoint)
farWorldPoint = self.cam.getRelativePoint(self.render, farPoint)
rayDirWorld = farWorldPoint - nearWorldPoint
rayDirWorld.normalize()
print(f"射线起点: {camera_pos}")
print(f"射线方向: {rayDirWorld}")
# 设置射线
self.pickerRay.setOrigin(camera_pos)
self.pickerRay.setDirection(rayDirWorld)
# 执行碰撞检测
self.pickerQueue.clearEntries()
self.picker.traverse(self.render)
numEntries = self.pickerQueue.getNumEntries()
print(f"检测到 {numEntries} 个碰撞")
if numEntries > 0:
self.pickerQueue.sortEntries()
for i in range(numEntries):
entry = self.pickerQueue.getEntry(i)
hitPos = entry.getSurfacePoint(self.render)
hitNode = entry.getIntoNodePath()
distance = (hitPos - camera_pos).length()
print(f" 碰撞 {i}: {hitNode.getName()}, 距离: {distance:.2f}, 位置: {hitPos}")
else:
# 分析射线到立方体的距离
cube_world_pos = self.cube.getPos(self.render)
to_cube = cube_world_pos - camera_pos
cross_product = to_cube.cross(rayDirWorld)
distance_to_ray = cross_product.length()
print(f" 射线到立方体中心的距离: {distance_to_ray:.2f}")
print(f" 需要小于: 12.0")
print(f" 射线错过了立方体")
def testDirectRay(self):
"""测试直接朝向立方体的射线"""
print(f"\n=== 直接射线测试 ===")
camera_pos = self.cam.getPos(self.render)
cube_pos = self.cube.getPos(self.render)
# 计算从相机直接指向立方体的射线
direct_direction = cube_pos - camera_pos
direct_direction.normalize()
print(f"相机位置: {camera_pos}")
print(f"立方体位置: {cube_pos}")
print(f"直接方向: {direct_direction}")
# 设置射线
self.pickerRay.setOrigin(camera_pos)
self.pickerRay.setDirection(direct_direction)
# 执行碰撞检测
self.pickerQueue.clearEntries()
self.picker.traverse(self.render)
numEntries = self.pickerQueue.getNumEntries()
print(f"检测到 {numEntries} 个碰撞")
if numEntries > 0:
self.pickerQueue.sortEntries()
for i in range(numEntries):
entry = self.pickerQueue.getEntry(i)
hitPos = entry.getSurfacePoint(self.render)
hitNode = entry.getIntoNodePath()
distance = (hitPos - camera_pos).length()
print(f" 碰撞 {i}: {hitNode.getName()}, 距离: {distance:.2f}, 位置: {hitPos}")
else:
print(" 直接射线也没击中!可能是碰撞设置问题。")
if __name__ == "__main__":
app = CenterRayTest()
print("\n测试完成!")

261
demo/chinese_gui_test.py Normal file
View File

@ -0,0 +1,261 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
PANDA3D 中文GUI支持测试
解决中文字体显示问题的示例
"""
from direct.showbase.ShowBase import ShowBase
from direct.gui.DirectGui import *
from direct.gui.OnscreenText import OnscreenText
from panda3d.core import *
import os
class ChineseGUITest(ShowBase):
def __init__(self):
ShowBase.__init__(self)
# 设置窗口属性
wp = WindowProperties()
wp.setTitle("PANDA3D Chinese GUI Test")
self.win.requestProperties(wp)
self.setBackgroundColor(0.1, 0.2, 0.4)
# 尝试加载中文字体
self.chinese_font = self.loadChineseFont()
# 创建GUI组件
self.createGUI()
print("="*50)
print("PANDA3D 中文GUI测试程序启动")
print(f"中文字体加载状态: {'成功' if self.chinese_font else '失败,使用默认字体'}")
print("="*50)
def loadChineseFont(self):
"""尝试加载中文字体"""
# 常见的中文字体路径列表
font_paths = [
# Ubuntu/Debian系统的中文字体
'/usr/share/fonts/truetype/wqy/wqy-microhei.ttc',
'/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc',
'/usr/share/fonts/truetype/arphic/ukai.ttc',
'/usr/share/fonts/truetype/arphic/uming.ttc',
'/usr/share/fonts/truetype/droid/DroidSansFallbackFull.ttf',
'/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc',
'/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf',
# 其他可能的字体路径
'/System/Library/Fonts/PingFang.ttc', # macOS
'C:/Windows/Fonts/msyh.ttc', # Windows
'C:/Windows/Fonts/simhei.ttf', # Windows
]
print("正在尝试加载中文字体...")
for font_path in font_paths:
if os.path.exists(font_path):
try:
print(f"尝试加载字体: {font_path}")
font = self.loader.loadFont(font_path)
if font:
print(f"成功加载字体: {font_path}")
return font
else:
print(f"字体文件存在但加载失败: {font_path}")
except Exception as e:
print(f"加载字体时出错: {font_path}, 错误: {e}")
print("未找到可用的中文字体,将使用默认字体")
print("如果中文显示为方块,请安装中文字体:")
print("sudo apt-get install fonts-wqy-microhei fonts-wqy-zenhei")
return None
def createText(self, text, pos, scale=0.06, fg=(1, 1, 1, 1)):
"""创建文本,优先使用中文字体"""
if self.chinese_font:
return OnscreenText(
text=text,
pos=pos,
scale=scale,
fg=fg,
align=TextNode.ALeft,
font=self.chinese_font
)
else:
return OnscreenText(
text=text,
pos=pos,
scale=scale,
fg=fg,
align=TextNode.ALeft
)
def createButton(self, text, pos, scale=0.06, command=None, frameColor=(0.5, 0.5, 0.5, 1)):
"""创建支持中文字体的按钮"""
return DirectButton(
text=text,
pos=pos,
scale=scale,
command=command,
frameColor=frameColor,
text_fg=(1, 1, 1, 1),
text_font=self.chinese_font if self.chinese_font else None
)
def createGUI(self):
"""创建GUI组件"""
# 标题
self.title = self.createText(
"PANDA3D 中文GUI测试",
(0, 0.85),
0.08,
(1, 1, 0, 1)
)
self.title.setAlign(TextNode.ACenter)
# 说明文本
self.info = self.createText(
"这个程序测试中文字体在PANDA3D中的显示效果",
(-0.95, 0.7),
0.05,
(0.8, 0.8, 0.8, 1)
)
# 创建测试按钮 - 支持中文字体
self.testButton = self.createButton(
text="Test Button (测试按钮)",
pos=(-0.6, 0, 0.5),
scale=0.08,
command=self.onTestClick,
frameColor=(0.2, 0.7, 0.2, 1)
)
# 创建标签显示区域
self.statusLabel = self.createText(
"状态: 程序就绪",
(-0.95, 0.3),
0.06,
(0, 1, 0, 1)
)
# 创建功能测试区域
self.createTestArea()
# 创建字体信息显示
self.createFontInfo()
# 退出按钮 - 支持中文字体
self.exitButton = self.createButton(
text="Exit (退出)",
pos=(0.7, 0, -0.7),
scale=0.06,
command=self.exitProgram,
frameColor=(0.8, 0.2, 0.2, 1)
)
def createTestArea(self):
"""创建测试区域"""
# 中文测试文本
chinese_texts = [
"简体中文测试:你好世界!",
"常用汉字:一二三四五六七八九十",
"标点符号:,。!?;:""''",
"数字混合2023年12月25日",
"英中混合Hello 世界 World 中国"
]
self.test_texts = []
for i, text in enumerate(chinese_texts):
text_obj = self.createText(
text,
(-0.95, 0.1 - i*0.08),
0.05,
(1, 1, 1, 1)
)
self.test_texts.append(text_obj)
# 计数器
self.clickCount = 0
self.counterText = self.createText(
f"点击次数: {self.clickCount}",
(-0.95, -0.5),
0.06,
(1, 0.5, 0, 1)
)
def createFontInfo(self):
"""创建字体信息显示"""
font_status = "已加载中文字体" if self.chinese_font else "使用默认字体"
self.fontInfo = self.createText(
f"字体状态: {font_status}",
(0.2, 0.7),
0.05,
(0.8, 0.8, 0.2, 1)
)
# 显示字体路径信息
if self.chinese_font:
# 尝试获取字体信息
try:
font_name = self.chinese_font.getName()
self.fontPath = self.createText(
f"字体名称: {font_name}",
(0.2, 0.6),
0.04,
(0.6, 0.6, 0.6, 1)
)
except:
self.fontPath = self.createText(
"字体信息: 无法获取",
(0.2, 0.6),
0.04,
(0.6, 0.6, 0.6, 1)
)
else:
self.fontPath = self.createText(
"建议安装: fonts-wqy-microhei",
(0.2, 0.6),
0.04,
(1, 0.5, 0.5, 1)
)
def onTestClick(self):
"""测试按钮点击事件"""
self.clickCount += 1
# 更新状态和计数器
self.statusLabel.setText(f"状态: 按钮被点击了 {self.clickCount}")
self.counterText.setText(f"点击次数: {self.clickCount}")
# 循环变换测试文本
test_messages = [
"第一次点击欢迎使用PANDA3D",
"第二次点击:中文显示测试正常",
"第三次点击:功能运行良好",
"继续点击:一切正常运行中..."
]
if self.clickCount <= len(test_messages):
message = test_messages[self.clickCount - 1]
else:
message = f"{self.clickCount}次点击:持续测试中..."
# 更新第一行测试文本
if self.test_texts:
self.test_texts[0].setText(message)
print(f"按钮点击: {self.clickCount} 次, 消息: {message}")
def exitProgram(self):
"""退出程序"""
print("用户选择退出程序")
print("再见Goodbye!")
self.userExit()
if __name__ == "__main__":
print("启动PANDA3D中文GUI测试程序...")
print("如果看到方块字符,说明需要安装中文字体")
app = ChineseGUITest()
app.run()

189
demo/collision_debug.py Normal file
View File

@ -0,0 +1,189 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
from direct.showbase.ShowBase import ShowBase
from panda3d.core import (
Point3, Point2, Vec3, CollisionTraverser, CollisionHandlerQueue,
CollisionNode, CollisionRay, CollisionSphere, BitMask32, CardMaker
)
class CollisionDebugTest(ShowBase):
def __init__(self):
ShowBase.__init__(self)
# 设置相机位置
self.cam.setPos(-3.87655, -188.38084, 82.602684)
self.cam.lookAt(0, 0, 0)
print(f"相机位置: {self.cam.getPos()}")
# 创建测试立方体
self.createTestCube()
# 设置射线检测
self.setupRayPicking()
# 调试碰撞系统
self.debugCollisionSystem()
# 测试不同类型的碰撞体
self.testDifferentCollisionTypes()
def createTestCube(self):
"""创建测试立方体"""
cm = CardMaker("TestCube")
cm.setFrame(-1, 1, -1, 1)
self.cube = self.render.attachNewNode(cm.generate())
self.cube.setPos(0, 10, 3)
self.cube.setScale(3, 3, 3)
self.cube.setColor(1, 0, 0, 1)
self.cube.setTwoSided(True)
print(f"立方体世界位置: {self.cube.getPos(self.render)}")
# 添加碰撞检测
cNode = CollisionNode('TestCubeCollision')
cSphere = CollisionSphere(Point3(0, 0, 0), 4.0)
cNode.addSolid(cSphere)
cNode.setIntoCollideMask(BitMask32.bit(0))
cNode.setFromCollideMask(BitMask32.allOff())
self.cNodePath = self.cube.attachNewNode(cNode)
# 强制显示碰撞体
self.cNodePath.show()
print(f"碰撞体已强制显示")
def setupRayPicking(self):
"""设置射线检测"""
self.picker = CollisionTraverser()
self.pickerQueue = CollisionHandlerQueue()
self.pickerNode = CollisionNode('mouseRay')
self.pickerRay = CollisionRay()
self.pickerNode.addSolid(self.pickerRay)
# 设置掩码
fromMask = BitMask32.bit(0)
self.pickerNode.setFromCollideMask(fromMask)
self.pickerNode.setIntoCollideMask(BitMask32.allOff())
self.pickerNP = self.cam.attachNewNode(self.pickerNode)
self.picker.addCollider(self.pickerNP, self.pickerQueue)
print(f"射线检测器 From 掩码: {fromMask}")
print(f"立方体 Into 掩码: {self.cNodePath.node().getIntoCollideMask()}")
def debugCollisionSystem(self):
"""调试碰撞系统"""
print(f"\n=== 碰撞系统调试 ===")
# 检查碰撞遍历器
print(f"碰撞遍历器中的碰撞器数量: {self.picker.getNumColliders()}")
for i in range(self.picker.getNumColliders()):
collider = self.picker.getCollider(i)
print(f" 碰撞器 {i}: {collider}")
# 检查render下的所有碰撞节点
print(f"\nrender下的所有节点:")
self.printNodeHierarchy(self.render, 0)
def printNodeHierarchy(self, node, depth):
"""打印节点层次结构"""
indent = " " * depth
print(f"{indent}{node.getName()}: {type(node.node()).__name__}")
if isinstance(node.node(), CollisionNode):
cnode = node.node()
print(f"{indent} CollisionNode:")
print(f"{indent} IntoMask: {cnode.getIntoCollideMask()}")
print(f"{indent} FromMask: {cnode.getFromCollideMask()}")
print(f"{indent} Solids: {cnode.getNumSolids()}")
for i in range(cnode.getNumSolids()):
solid = cnode.getSolid(i)
print(f"{indent} Solid {i}: {type(solid).__name__}")
# 递归处理子节点,但限制深度
if depth < 3:
for child in node.getChildren():
self.printNodeHierarchy(child, depth + 1)
def testDifferentCollisionTypes(self):
"""测试不同类型的碰撞体"""
print(f"\n=== 测试不同碰撞体类型 ===")
camera_pos = self.cam.getPos(self.render)
cube_pos = self.cube.getPos(self.render)
# 计算直接射线
direct_direction = cube_pos - camera_pos
direct_direction.normalize()
print(f"使用直接射线: 起点={camera_pos}, 方向={direct_direction}")
# 测试1: 当前设置
print(f"\n1. 测试当前碰撞球体设置:")
self.testRayHit(camera_pos, direct_direction)
# 测试2: 更大的碰撞球体
print(f"\n2. 测试更大的碰撞球体:")
# 移除当前碰撞体
self.cNodePath.removeNode()
# 创建更大的碰撞球体
cNode2 = CollisionNode('LargeSphere')
cSphere2 = CollisionSphere(Point3(0, 0, 0), 20.0) # 更大的半径
cNode2.addSolid(cSphere2)
cNode2.setIntoCollideMask(BitMask32.bit(0))
cNode2.setFromCollideMask(BitMask32.allOff())
self.cNodePath2 = self.cube.attachNewNode(cNode2)
self.cNodePath2.show()
self.testRayHit(camera_pos, direct_direction)
# 测试3: 在render根下创建独立碰撞体
print(f"\n3. 测试独立碰撞体:")
cNode3 = CollisionNode('IndependentSphere')
cSphere3 = CollisionSphere(cube_pos, 15.0) # 直接使用世界坐标
cNode3.addSolid(cSphere3)
cNode3.setIntoCollideMask(BitMask32.bit(0))
cNode3.setFromCollideMask(BitMask32.allOff())
self.cNodePath3 = self.render.attachNewNode(cNode3)
self.cNodePath3.show()
self.testRayHit(camera_pos, direct_direction)
def testRayHit(self, origin, direction):
"""测试射线碰撞"""
# 设置射线
self.pickerRay.setOrigin(origin)
self.pickerRay.setDirection(direction)
# 清除之前的结果
self.pickerQueue.clearEntries()
# 执行碰撞检测
self.picker.traverse(self.render)
numEntries = self.pickerQueue.getNumEntries()
print(f" 检测到 {numEntries} 个碰撞")
if numEntries > 0:
self.pickerQueue.sortEntries()
for i in range(numEntries):
entry = self.pickerQueue.getEntry(i)
hitPos = entry.getSurfacePoint(self.render)
hitNode = entry.getIntoNodePath()
distance = (hitPos - origin).length()
print(f" 碰撞 {i}: {hitNode.getName()}, 距离: {distance:.2f}, 位置: {hitPos}")
else:
print(f" 没有检测到碰撞")
if __name__ == "__main__":
app = CollisionDebugTest()
print("\n调试完成!")

240
demo/color_picking_test.py Normal file
View File

@ -0,0 +1,240 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
from direct.showbase.ShowBase import ShowBase
from panda3d.core import (
Point3, Vec3, Vec4, CardMaker, Texture, GraphicsOutput,
FrameBufferProperties, WindowProperties, RenderState,
ColorWriteAttrib, DepthTestAttrib, BitMask32, PNMImage,
Camera, DisplayRegion, GraphicsStateGuardian, RenderState
)
class ColorPickingTest(ShowBase):
def __init__(self):
ShowBase.__init__(self)
# 相机设置
self.cam.setPos(-3.87655, -188.38084, 82.602684)
self.cam.lookAt(0, 0, 0)
print("=== 颜色编码点击检测测试 ===")
# 创建测试对象
self.createTestObjects()
# 设置颜色拾取缓冲区
self.setupColorPickingBuffer()
# 测试颜色拾取
self.testColorPicking()
def createTestObjects(self):
"""创建测试对象"""
self.objects = []
self.id_to_object = {} # ID到对象的映射
# 创建几个测试立方体
positions = [
(0, 10, 3, "中心立方体"),
(-5, 10, 3, "左侧立方体"),
(5, 10, 3, "右侧立方体"),
(0, 10, 8, "上方立方体"),
(0, 15, 3, "远处立方体")
]
display_colors = [
(1, 0, 0, 1), # 红色
(0, 1, 0, 1), # 绿色
(0, 0, 1, 1), # 蓝色
(1, 1, 0, 1), # 黄色
(1, 0, 1, 1), # 紫色
]
for i, ((x, y, z, name), display_color) in enumerate(zip(positions, display_colors)):
obj_id = i + 1 # 从1开始编号
# 创建立方体
cm = CardMaker(f"cube_{i}")
cm.setFrame(-1, 1, -1, 1)
cube = self.render.attachNewNode(cm.generate())
cube.setPos(x, y, z)
cube.setScale(2, 2, 2)
cube.setColor(*display_color)
cube.setBillboardAxis()
cube.setName(name)
# 为颜色拾取创建唯一颜色
# 将ID编码为RGB颜色
pick_color = self.encodeIdToColor(obj_id)
# 存储对象信息
obj_info = {
'node': cube,
'name': name,
'world_pos': Point3(x, y, z),
'id': obj_id,
'pick_color': pick_color,
'display_color': display_color
}
self.objects.append(obj_info)
self.id_to_object[obj_id] = obj_info
print(f"创建 {name} 在位置 ({x}, {y}, {z}), ID: {obj_id}, 拾取颜色: {pick_color}")
def encodeIdToColor(self, obj_id):
"""将对象ID编码为RGB颜色"""
# 将ID分解为RGB分量24位
r = (obj_id & 0xFF0000) >> 16
g = (obj_id & 0x00FF00) >> 8
b = (obj_id & 0x0000FF)
# 转换为0-1范围的浮点数
return Vec4(r / 255.0, g / 255.0, b / 255.0, 1.0)
def decodeColorToId(self, color):
"""将RGB颜色解码为对象ID"""
if color is None:
return 0
# 转换为0-255范围的整数
r = int(color[0] * 255 + 0.5)
g = int(color[1] * 255 + 0.5)
b = int(color[2] * 255 + 0.5)
# 组合为ID
obj_id = (r << 16) | (g << 8) | b
return obj_id
def setupColorPickingBuffer(self):
"""设置颜色拾取渲染缓冲区"""
# 创建离屏渲染缓冲区
fbp = FrameBufferProperties()
fbp.setRgbColor(True)
fbp.setColorBits(8, 8, 8, 0) # RGB不需要Alpha
fbp.setDepthBits(24)
wp = WindowProperties()
wp.setSize(self.win.getXSize(), self.win.getYSize())
# 创建缓冲区
self.pick_buffer = self.graphicsEngine.makeOutput(
self.pipe, "pick_buffer", -1, fbp, wp,
GraphicsOutput.M_offscreen, self.win.getGsg(), self.win
)
if self.pick_buffer is None:
print("无法创建颜色拾取缓冲区")
return
# 创建拾取相机
self.pick_cam = self.makeCamera(self.pick_buffer)
self.pick_cam.node().setLens(self.cam.node().getLens())
self.pick_cam.setPos(self.cam.getPos())
self.pick_cam.setHpr(self.cam.getHpr())
# 创建拾取场景(带有颜色编码的对象)
self.pick_scene = self.render.attachNewNode("pick_scene")
# 为每个对象创建拾取版本
for obj_info in self.objects:
# 复制几何体
pick_node = obj_info['node'].copyTo(self.pick_scene)
pick_node.setColor(obj_info['pick_color'])
pick_node.setRenderModeWireframe(False)
pick_node.setLightOff() # 关闭光照以确保颜色准确
# 设置拾取相机只渲染拾取场景
self.pick_cam.node().setCameraMask(BitMask32.allOn())
# 创建颜色纹理
self.pick_tex = Texture()
self.pick_tex.setFormat(Texture.FRgb8)
self.pick_buffer.addRenderTexture(self.pick_tex, GraphicsOutput.RTMCopyRam)
print("颜色拾取缓冲区设置完成")
def getColorAtPixel(self, mouseX, mouseY):
"""获取指定像素的颜色值"""
if self.pick_buffer is None:
return None
# 获取窗口大小
winWidth = self.pick_buffer.getXSize()
winHeight = self.pick_buffer.getYSize()
# 确保坐标在窗口范围内
if mouseX < 0 or mouseX >= winWidth or mouseY < 0 or mouseY >= winHeight:
return None
# 强制渲染拾取缓冲区
self.pick_buffer.setActive(True)
self.graphicsEngine.renderFrame()
# 读取颜色纹理
if self.pick_tex.hasRamImage():
# 获取颜色图像
pnm = PNMImage()
self.pick_tex.store(pnm)
# Y坐标需要翻转OpenGL坐标系
flippedY = winHeight - 1 - mouseY
if flippedY < pnm.getYSize() and mouseX < pnm.getXSize():
# 获取颜色值
r = pnm.getRed(mouseX, flippedY)
g = pnm.getGreen(mouseX, flippedY)
b = pnm.getBlue(mouseX, flippedY)
return (r, g, b)
return None
def pickObjectByColor(self, mouseX, mouseY):
"""使用颜色编码选择对象"""
print(f"\n=== 颜色拾取: 鼠标位置 ({mouseX}, {mouseY}) ===")
# 获取点击位置的颜色
color = self.getColorAtPixel(mouseX, mouseY)
if color is None:
print("无法读取颜色值")
return None
print(f"拾取颜色: RGB({color[0]:.3f}, {color[1]:.3f}, {color[2]:.3f})")
# 解码颜色为对象ID
obj_id = self.decodeColorToId(color)
print(f"解码的对象ID: {obj_id}")
# 查找对应的对象
if obj_id in self.id_to_object:
obj_info = self.id_to_object[obj_id]
print(f"选中对象: {obj_info['name']}")
return obj_info
else:
print("没有找到对应的对象")
return None
def testColorPicking(self):
"""测试颜色拾取"""
print(f"\n=== 颜色拾取测试 ===")
# 等待几帧以确保场景渲染完成
for i in range(5):
self.graphicsEngine.renderFrame()
# 模拟不同的点击位置
test_clicks = [
(619, 362, "屏幕中心"),
(400, 362, "屏幕偏左"),
(800, 362, "屏幕偏右"),
(619, 200, "屏幕上方"),
(619, 500, "屏幕下方"),
]
for mouseX, mouseY, description in test_clicks:
print(f"\n--- 测试 {description}: ({mouseX}, {mouseY}) ---")
selected = self.pickObjectByColor(mouseX, mouseY)
if __name__ == "__main__":
app = ColorPickingTest()
print("颜色拾取测试完成")

87
demo/color_test.py Normal file
View File

@ -0,0 +1,87 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
简单颜色测试程序
"""
from direct.showbase.ShowBase import ShowBase
from panda3d.core import LineSegs, Vec3, Vec4
from direct.task import Task
class ColorTest(ShowBase):
def __init__(self):
ShowBase.__init__(self)
# 设置背景色
self.setBackgroundColor(0.2, 0.2, 0.3)
# 设置相机位置
self.cam.setPos(0, -10, 0)
self.cam.lookAt(0, 0, 0)
# 创建测试线段
self.createTestLines()
# 颜色变化任务
self.taskMgr.add(self.colorChangeTask, "colorChange")
self.time = 0
def createTestLines(self):
"""创建测试线段"""
# 创建X轴线段
x_lines = LineSegs()
x_lines.setThickness(5.0)
x_lines.moveTo(-3, 0, 0)
x_lines.drawTo(3, 0, 0)
self.x_axis = self.render.attachNewNode(x_lines.create())
self.x_axis.setColor(1, 0, 0, 1) # 红色
self.x_axis.setLightOff()
# 创建Y轴线段
y_lines = LineSegs()
y_lines.setThickness(5.0)
y_lines.moveTo(0, -3, 0)
y_lines.drawTo(0, 3, 0)
self.y_axis = self.render.attachNewNode(y_lines.create())
self.y_axis.setColor(0, 1, 0, 1) # 绿色
self.y_axis.setLightOff()
# 创建Z轴线段
z_lines = LineSegs()
z_lines.setThickness(5.0)
z_lines.moveTo(0, 0, -3)
z_lines.drawTo(0, 0, 3)
self.z_axis = self.render.attachNewNode(z_lines.create())
self.z_axis.setColor(0, 0, 1, 1) # 蓝色
self.z_axis.setLightOff()
print("测试轴创建完成")
print("X轴颜色:", self.x_axis.getColor())
print("Y轴颜色:", self.y_axis.getColor())
print("Z轴颜色:", self.z_axis.getColor())
def colorChangeTask(self, task):
"""颜色变化任务"""
self.time += globalClock.getDt()
# 每2秒变换一次颜色
if int(self.time) % 2 == 0:
# 原色
self.x_axis.setColor(1, 0, 0, 1)
self.y_axis.setColor(0, 1, 0, 1)
self.z_axis.setColor(0, 0, 1, 1)
else:
# 白色
self.x_axis.setColor(1, 1, 1, 1)
self.y_axis.setColor(1, 1, 1, 1)
self.z_axis.setColor(1, 1, 1, 1)
return task.cont
if __name__ == "__main__":
app = ColorTest()
app.run()

160
demo/coordinate_test.py Normal file
View File

@ -0,0 +1,160 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
def test_coordinate_conversion():
"""测试坐标转换"""
print("=== 坐标转换测试 ===\n")
# 假设窗口大小
window_width = 800
window_height = 600
print(f"窗口大小: {window_width} x {window_height}")
print("\n测试不同位置的坐标转换:")
test_points = [
# (屏幕坐标, 描述)
((0, 0), "左上角"),
((window_width/2, window_height/2), "中心点"),
((window_width, window_height), "右下角"),
((window_width/4, window_height/4), "左上象限"),
((3*window_width/4, 3*window_height/4), "右下象限"),
((419, 96), "用户点击的位置"), # 来自之前的日志
]
for (screen_x, screen_y), description in test_points:
# 方法1标准转换我们当前使用的
normalized_x1 = (2.0 * screen_x / window_width) - 1.0
normalized_y1 = 1.0 - (2.0 * screen_y / window_height)
# 方法2简单转换
normalized_x2 = (screen_x / window_width) * 2 - 1
normalized_y2 = 1 - (screen_y / window_height) * 2
# 方法3不反转Y轴
normalized_x3 = (2.0 * screen_x / window_width) - 1.0
normalized_y3 = (2.0 * screen_y / window_height) - 1.0
print(f"\n{description}: 屏幕坐标({screen_x:.0f}, {screen_y:.0f})")
print(f" 方法1 (当前): ({normalized_x1:.3f}, {normalized_y1:.3f})")
print(f" 方法2 (等价): ({normalized_x2:.3f}, {normalized_y2:.3f})")
print(f" 方法3 (不反转Y): ({normalized_x3:.3f}, {normalized_y3:.3f})")
# 验证是否在有效范围内
in_range1 = -1 <= normalized_x1 <= 1 and -1 <= normalized_y1 <= 1
in_range3 = -1 <= normalized_x3 <= 1 and -1 <= normalized_y3 <= 1
print(f" 方法1 在范围内: {in_range1}")
print(f" 方法3 在范围内: {in_range3}")
def test_qt_coordinate_precision():
"""测试Qt坐标精度问题"""
print("\n=== Qt坐标精度测试 ===")
# 模拟真实的Qt widget大小来自日志
qt_width = 1238.0
qt_height = 725.0
# 模拟真实的点击位置(来自日志)
click_x = 615
click_y = 237
print(f"Qt Widget大小: {qt_width} x {qt_height}")
print(f"点击位置: ({click_x}, {click_y})")
# 当前的转换方法
normalized_x = (2.0 * click_x / qt_width) - 1.0
normalized_y = 1.0 - (2.0 * click_y / qt_height)
print(f"归一化坐标: ({normalized_x:.6f}, {normalized_y:.6f})")
# 验证屏幕中心
center_x = qt_width / 2
center_y = qt_height / 2
center_norm_x = (2.0 * center_x / qt_width) - 1.0
center_norm_y = 1.0 - (2.0 * center_y / qt_height)
print(f"屏幕中心: ({center_x}, {center_y}) -> ({center_norm_x:.6f}, {center_norm_y:.6f})")
# 分析点击位置相对于中心的偏移
offset_from_center_x = click_x - center_x
offset_from_center_y = click_y - center_y
print(f"相对于中心的偏移: ({offset_from_center_x:.1f}, {offset_from_center_y:.1f})")
# 计算归一化偏移
norm_offset_x = offset_from_center_x / (qt_width / 2)
norm_offset_y = -offset_from_center_y / (qt_height / 2) # Y轴反转
print(f"归一化偏移: ({norm_offset_x:.6f}, {norm_offset_y:.6f})")
# 预期的归一化坐标(从中心偏移)
expected_norm_x = norm_offset_x
expected_norm_y = norm_offset_y
print(f"预期归一化坐标: ({expected_norm_x:.6f}, {expected_norm_y:.6f})")
# 比较
diff_x = abs(normalized_x - expected_norm_x)
diff_y = abs(normalized_y - expected_norm_y)
print(f"坐标差异: X={diff_x:.6f}, Y={diff_y:.6f}")
def test_panda3d_coordinate_systems():
"""测试Panda3D的坐标系统"""
print("\n=== Panda3D坐标系统 ===")
print("屏幕坐标系统:")
print(" Qt: 左上角(0,0), 右下角(w,h), Y轴向下")
print(" Panda3D: 左下角(-1,-1), 右上角(1,1), Y轴向上")
print()
print("3D世界坐标系统:")
print(" X轴: 向右为正")
print(" Y轴: 向前为正(远离相机)")
print(" Z轴: 向上为正")
print()
print("相机坐标系统:")
print(" X轴: 向右为正")
print(" Y轴: 向前为正(相机朝向)")
print(" Z轴: 向上为正")
def test_coordinate_alignment_issue():
"""测试坐标对齐问题"""
print("\n=== 坐标对齐问题分析 ===")
qt_width = 1238.0
qt_height = 725.0
print("测试不同位置的归一化坐标:")
test_positions = [
("左边缘", 50, qt_height/2),
("右边缘", qt_width-50, qt_height/2),
("上边缘", qt_width/2, 50),
("下边缘", qt_width/2, qt_height-50),
("左上角", 100, 100),
("右下角", qt_width-100, qt_height-100),
("中心", qt_width/2, qt_height/2),
("用户点击", 615, 237)
]
for name, x, y in test_positions:
# 当前方法
norm_x = (2.0 * x / qt_width) - 1.0
norm_y = 1.0 - (2.0 * y / qt_height)
# 期望点击边缘应该接近±1点击中心应该接近0
# 左边缘应该接近-1右边缘应该接近+1
# 上边缘应该接近+1下边缘应该接近-1
expected_x = (x - qt_width/2) / (qt_width/2)
expected_y = (qt_height/2 - y) / (qt_height/2)
print(f"{name:8s}: 实际({norm_x:6.3f}, {norm_y:6.3f}) 期望({expected_x:6.3f}, {expected_y:6.3f})")
if __name__ == "__main__":
test_coordinate_conversion()
test_qt_coordinate_precision()
test_panda3d_coordinate_systems()
test_coordinate_alignment_issue()

197
demo/depth_picking_test.py Normal file
View File

@ -0,0 +1,197 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
from direct.showbase.ShowBase import ShowBase
from panda3d.core import (
Point3, Vec3, CardMaker, Texture, GraphicsOutput,
FrameBufferProperties, WindowProperties, RenderState,
ColorWriteAttrib, DepthTestAttrib, ColorBlendAttrib,
BitMask32, PNMImage, Filename
)
class DepthPickingTest(ShowBase):
def __init__(self):
ShowBase.__init__(self)
# 相机设置
self.cam.setPos(-3.87655, -188.38084, 82.602684)
self.cam.lookAt(0, 0, 0)
print("=== 深度缓冲区点击检测测试 ===")
# 创建测试对象
self.createTestObjects()
# 设置深度缓冲区读取
self.setupDepthBuffer()
# 测试深度检测
self.testDepthPicking()
def createTestObjects(self):
"""创建测试对象"""
self.objects = []
# 创建几个测试立方体
positions = [
(0, 10, 3, "中心立方体"),
(-5, 10, 3, "左侧立方体"),
(5, 10, 3, "右侧立方体"),
(0, 10, 8, "上方立方体"),
(0, 15, 3, "远处立方体")
]
colors = [
(1, 0, 0, 1), # 红色
(0, 1, 0, 1), # 绿色
(0, 0, 1, 1), # 蓝色
(1, 1, 0, 1), # 黄色
(1, 0, 1, 1), # 紫色
]
for i, ((x, y, z, name), color) in enumerate(zip(positions, colors)):
# 创建立方体
cm = CardMaker(f"cube_{i}")
cm.setFrame(-1, 1, -1, 1)
cube = self.render.attachNewNode(cm.generate())
cube.setPos(x, y, z)
cube.setScale(2, 2, 2)
cube.setColor(*color)
cube.setBillboardAxis()
cube.setName(name)
# 存储对象信息
obj_info = {
'node': cube,
'name': name,
'world_pos': Point3(x, y, z),
'id': i + 1 # 用于颜色编码
}
self.objects.append(obj_info)
print(f"创建 {name} 在位置 ({x}, {y}, {z})")
def setupDepthBuffer(self):
"""设置深度缓冲区读取"""
# 获取主窗口的深度纹理
self.depth_tex = Texture()
self.depth_tex.setFormat(Texture.FDepthComponent)
self.depth_tex.setComponentType(Texture.TFloat)
# 将深度纹理绑定到主窗口
self.win.addRenderTexture(self.depth_tex, GraphicsOutput.RTPDepth)
print("深度缓冲区设置完成")
def getDepthAtPixel(self, mouseX, mouseY):
"""获取指定像素的深度值"""
# 获取窗口大小
winWidth = self.win.getXSize()
winHeight = self.win.getYSize()
# 确保坐标在窗口范围内
if mouseX < 0 or mouseX >= winWidth or mouseY < 0 or mouseY >= winHeight:
return None
# 强制渲染以更新深度缓冲区
self.graphicsEngine.renderFrame()
# 读取深度纹理
if self.depth_tex.hasRamImage():
# 获取深度图像
pnm = PNMImage()
self.depth_tex.store(pnm)
# Y坐标需要翻转OpenGL坐标系
flippedY = winHeight - 1 - mouseY
if flippedY < pnm.getYSize() and mouseX < pnm.getXSize():
# 获取深度值0.0 = 近平面1.0 = 远平面)
depth = pnm.getGray(mouseX, flippedY)
return depth
return None
def depthToWorldDistance(self, depth):
"""将深度值转换为世界距离"""
if depth is None:
return None
# 获取相机镜头参数
lens = self.cam.node().getLens()
near = lens.getNear()
far = lens.getFar()
# 将归一化深度值转换为线性距离
# 这是透视投影的逆变换
z_ndc = 2.0 * depth - 1.0 # 转换到 [-1, 1] 范围
z_eye = 2.0 * near * far / (far + near - z_ndc * (far - near))
return abs(z_eye)
def pickObjectByDepth(self, mouseX, mouseY):
"""使用深度检测选择对象"""
print(f"\n=== 深度检测: 鼠标位置 ({mouseX}, {mouseY}) ===")
# 获取点击位置的深度
depth = self.getDepthAtPixel(mouseX, mouseY)
if depth is None:
print("无法读取深度值")
return None
# 转换为世界距离
worldDistance = self.depthToWorldDistance(depth)
print(f"深度值: {depth:.6f}, 世界距离: {worldDistance:.2f}")
# 查找距离相机最近且在该深度附近的对象
closest_obj = None
min_distance_diff = float('inf')
cam_pos = self.cam.getPos()
for obj_info in self.objects:
# 计算对象到相机的距离
obj_distance = (obj_info['world_pos'] - cam_pos).length()
distance_diff = abs(obj_distance - worldDistance)
print(f"{obj_info['name']}: 距离相机 {obj_distance:.2f}, 差值 {distance_diff:.2f}")
# 使用一定的容差(因为对象有体积)
tolerance = 3.0 # 容差范围
if distance_diff <= tolerance and distance_diff < min_distance_diff:
min_distance_diff = distance_diff
closest_obj = obj_info
print(f" -> 可能被选中 (差值: {distance_diff:.2f})")
if closest_obj:
print(f"\n选中对象: {closest_obj['name']}")
return closest_obj
else:
print(f"\n没有找到匹配的对象")
return None
def testDepthPicking(self):
"""测试深度点击检测"""
print(f"\n=== 深度点击检测测试 ===")
# 等待几帧以确保场景渲染完成
for i in range(5):
self.graphicsEngine.renderFrame()
# 模拟不同的点击位置
test_clicks = [
(619, 362, "屏幕中心"),
(400, 362, "屏幕偏左"),
(800, 362, "屏幕偏右"),
(619, 200, "屏幕上方"),
(619, 500, "屏幕下方"),
]
for mouseX, mouseY, description in test_clicks:
print(f"\n--- 测试 {description}: ({mouseX}, {mouseY}) ---")
selected = self.pickObjectByDepth(mouseX, mouseY)
if __name__ == "__main__":
app = DepthPickingTest()
print("深度检测测试完成")

317
demo/gui_3d_demo.py Normal file
View File

@ -0,0 +1,317 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
PANDA3D 3D空间GUI演示
展示如何在3D场景中使用GUI元素
"""
from direct.showbase.ShowBase import ShowBase
from direct.gui.DirectGui import *
from direct.gui.OnscreenText import OnscreenText
from direct.gui.OnscreenImage import OnscreenImage
from panda3d.core import *
from direct.task import Task
class GUI3DDemo(ShowBase):
def __init__(self):
ShowBase.__init__(self)
# 设置背景色
self.setBackgroundColor(0.1, 0.1, 0.3)
# 设置相机位置
self.cam.setPos(0, -10, 0)
self.cam.lookAt(0, 0, 0)
# 尝试加载中文字体
self.chinese_font = self.loadChineseFont()
# 创建标题2D界面
self.title = OnscreenText(
text="PANDA3D 3D空间GUI演示",
pos=(0, 0.9),
scale=0.08,
fg=(1, 1, 1, 1),
align=TextNode.ACenter,
font=self.chinese_font if self.chinese_font else None
)
# 创建基本的2D GUI
self.create2DGUI()
# 创建3D场景中的GUI元素
self.create3DGUI()
# 加载一个简单的3D模型作为背景
self.createScene()
print("3D GUI演示启动成功!")
print(f"中文字体加载状态: {'成功' if self.chinese_font else '失败,使用默认字体'}")
print("你可以看到2D界面元素和3D空间中的GUI")
def loadChineseFont(self):
"""尝试加载中文字体"""
import os
font_paths = [
'/usr/share/fonts/truetype/wqy/wqy-microhei.ttc',
'/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc',
'/usr/share/fonts/truetype/arphic/ukai.ttc',
'/usr/share/fonts/truetype/arphic/uming.ttc',
]
for font_path in font_paths:
if os.path.exists(font_path):
try:
font = self.loader.loadFont(font_path)
if font:
return font
except:
continue
return None
def create2DGUI(self):
"""创建传统的2D GUI元素"""
# 控制面板标题
self.controlTitle = OnscreenText(
text="控制面板",
pos=(-1.2, 0.8),
scale=0.06,
fg=(1, 1, 0, 1),
align=TextNode.ALeft,
font=self.chinese_font if self.chinese_font else None
)
# 旋转控制按钮
self.rotateButton = DirectButton(
text="旋转场景",
pos=(-1.2, 0, 0.6),
scale=0.05,
command=self.toggleRotation,
frameColor=(0.2, 0.6, 0.8, 1),
text_font=self.chinese_font if self.chinese_font else None
)
# 颜色变化按钮
self.colorButton = DirectButton(
text="变换颜色",
pos=(-1.2, 0, 0.4),
scale=0.05,
command=self.changeColors,
frameColor=(0.8, 0.2, 0.6, 1),
text_font=self.chinese_font if self.chinese_font else None
)
# 重置按钮
self.resetButton = DirectButton(
text="重置",
pos=(-1.2, 0, 0.2),
scale=0.05,
command=self.resetScene,
frameColor=(0.6, 0.8, 0.2, 1),
text_font=self.chinese_font if self.chinese_font else None
)
# 状态显示
self.statusText = OnscreenText(
text="状态: 就绪",
pos=(-1.2, -0.8),
scale=0.04,
fg=(0, 1, 0, 1),
align=TextNode.ALeft,
font=self.chinese_font if self.chinese_font else None
)
# 退出按钮
self.exitButton = DirectButton(
text="退出",
pos=(1.2, 0, -0.8),
scale=0.05,
command=self.exitDemo,
frameColor=(0.8, 0.2, 0.2, 1),
text_font=self.chinese_font if self.chinese_font else None
)
def create3DGUI(self):
"""创建3D空间中的GUI元素"""
# 创建一个在3D空间中的按钮
# 这实际上是一个2D GUI元素但我们可以将其父级设置为3D节点
# 创建3D空间中的文本
self.text3D = TextNode('3d-text')
self.text3D.setText("3D空间中的文本")
self.text3D.setAlign(TextNode.ACenter)
if self.chinese_font:
self.text3D.setFont(self.chinese_font)
self.textNodePath = self.render.attachNewNode(self.text3D)
self.textNodePath.setPos(0, 5, 2)
self.textNodePath.setScale(0.5)
self.textNodePath.setColor(1, 1, 0, 1)
self.textNodePath.setBillboardAxis() # 让文本总是面向相机
# 创建另一个3D文本显示说明
self.info3D = TextNode('info-text')
self.info3D.setText("这是3D空间中的GUI元素\n它们可以与3D场景交互")
self.info3D.setAlign(TextNode.ACenter)
if self.chinese_font:
self.info3D.setFont(self.chinese_font)
self.infoNodePath = self.render.attachNewNode(self.info3D)
self.infoNodePath.setPos(0, 3, -1)
self.infoNodePath.setScale(0.3)
self.infoNodePath.setColor(0, 1, 1, 1)
self.infoNodePath.setBillboardAxis()
# 创建一个3D平面作为"虚拟屏幕"
self.createVirtualScreen()
def createVirtualScreen(self):
"""创建一个3D空间中的虚拟屏幕"""
# 创建一个卡片(平面)
cm = CardMaker("virtual-screen")
cm.setFrame(-2, 2, -1, 1)
self.virtualScreen = self.render.attachNewNode(cm.generate())
self.virtualScreen.setPos(3, 5, 0)
self.virtualScreen.setColor(0.2, 0.2, 0.2, 0.8)
self.virtualScreen.setTransparency(TransparencyAttrib.MAlpha)
# 在虚拟屏幕上添加文本
screenText = TextNode('screen-text')
screenText.setText("虚拟3D屏幕\n\n这里可以显示\n任何信息")
screenText.setAlign(TextNode.ACenter)
if self.chinese_font:
screenText.setFont(self.chinese_font)
screenTextNP = self.virtualScreen.attachNewNode(screenText)
screenTextNP.setPos(0, 0.01, 0) # 稍微偏移避免z-fighting
screenTextNP.setScale(0.3)
screenTextNP.setColor(0, 1, 0, 1)
def createScene(self):
"""创建3D场景"""
# 创建一个简单的立方体
from panda3d.core import CardMaker
# 创建地面
cm = CardMaker("ground")
cm.setFrame(-10, 10, -10, 10)
ground = self.render.attachNewNode(cm.generate())
ground.setP(-90) # 旋转成水平面
ground.setColor(0.5, 0.5, 0.5, 1)
# 创建一些立方体
self.cubes = []
for i in range(3):
for j in range(3):
# 使用内置的立方体几何
cube = self.loader.loadModel("models/environment")
if not cube:
# 如果找不到模型,创建一个简单的几何体
cube = self.render.attachNewNode("cube")
cube.reparentTo(self.render)
cube.setPos(-2 + i*2, 2 + j*2, 0.5)
cube.setScale(0.5)
cube.setColor(
0.2 + i*0.3,
0.2 + j*0.3,
0.5,
1
)
self.cubes.append(cube)
# 添加光照
dlight = DirectionalLight('dlight')
dlight.setColor((1, 1, 1, 1))
dlnp = self.render.attachNewNode(dlight)
dlnp.setHpr(45, -45, 0)
self.render.setLight(dlnp)
alight = AmbientLight('alight')
alight.setColor((0.3, 0.3, 0.3, 1))
alnp = self.render.attachNewNode(alight)
self.render.setLight(alnp)
# 初始化旋转状态
self.rotating = False
self.colorIndex = 0
def toggleRotation(self):
"""切换场景旋转"""
if self.rotating:
self.taskMgr.remove("rotate-scene")
self.rotating = False
self.rotateButton['text'] = "旋转场景"
self.statusText.setText("状态: 停止旋转")
else:
self.taskMgr.add(self.rotateSceneTask, "rotate-scene")
self.rotating = True
self.rotateButton['text'] = "停止旋转"
self.statusText.setText("状态: 正在旋转")
print(f"旋转状态: {'开启' if self.rotating else '关闭'}")
def rotateSceneTask(self, task):
"""旋转场景的任务"""
for cube in self.cubes:
cube.setH(cube.getH() + 50 * globalClock.getDt())
# 旋转3D文本
self.textNodePath.setH(self.textNodePath.getH() + 30 * globalClock.getDt())
self.virtualScreen.setH(self.virtualScreen.getH() + 20 * globalClock.getDt())
return task.cont
def changeColors(self):
"""改变场景颜色"""
self.colorIndex = (self.colorIndex + 1) % 3
colors = [
[(1, 0.5, 0.5), (0.5, 1, 0.5), (0.5, 0.5, 1)], # 红绿蓝
[(1, 1, 0.5), (1, 0.5, 1), (0.5, 1, 1)], # 黄洋红青
[(0.8, 0.2, 0.8), (0.2, 0.8, 0.8), (0.8, 0.8, 0.2)] # 紫青黄
]
colorSet = colors[self.colorIndex]
for i, cube in enumerate(self.cubes):
color = colorSet[i % 3]
cube.setColor(*color, 1)
# 改变3D文本颜色
textColors = [(1, 1, 0), (0, 1, 1), (1, 0, 1)]
self.textNodePath.setColor(*textColors[self.colorIndex], 1)
self.statusText.setText(f"状态: 颜色方案 {self.colorIndex + 1}")
print(f"切换到颜色方案 {self.colorIndex + 1}")
def resetScene(self):
"""重置场景"""
# 停止旋转
if self.rotating:
self.toggleRotation()
# 重置立方体位置和颜色
for i, cube in enumerate(self.cubes):
row = i // 3
col = i % 3
cube.setPos(-2 + col*2, 2 + row*2, 0.5)
cube.setHpr(0, 0, 0)
cube.setColor(0.2 + col*0.3, 0.2 + row*0.3, 0.5, 1)
# 重置3D文本
self.textNodePath.setHpr(0, 0, 0)
self.textNodePath.setColor(1, 1, 0, 1)
self.virtualScreen.setHpr(0, 0, 0)
self.colorIndex = 0
self.statusText.setText("状态: 场景已重置")
print("场景已重置")
def exitDemo(self):
"""退出演示"""
print("退出3D GUI演示")
self.userExit()
if __name__ == "__main__":
print("启动PANDA3D 3D GUI演示...")
demo = GUI3DDemo()
demo.run()

1008
demo/gui_editor_window.py Normal file

File diff suppressed because it is too large Load Diff

358
demo/gui_test.py Normal file
View File

@ -0,0 +1,358 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
PANDA3D GUI功能测试示例
展示DirectGUI系统的各种组件使用方法
"""
import sys
from direct.showbase.ShowBase import ShowBase
from direct.gui.DirectGui import *
from direct.gui.OnscreenText import OnscreenText
from direct.gui.OnscreenImage import OnscreenImage
from panda3d.core import *
class GUITestApp(ShowBase):
def __init__(self):
ShowBase.__init__(self)
# 设置窗口标题
wp = WindowProperties()
wp.setTitle("PANDA3D GUI功能测试")
self.win.requestProperties(wp)
# 设置背景颜色
self.setBackgroundColor(0.2, 0.3, 0.5)
# 尝试加载中文字体
self.chinese_font = self.loadChineseFont()
# 创建标题文本
self.title = OnscreenText(
text="PANDA3D DirectGUI 组件示例",
pos=(0, 0.9),
scale=0.08,
fg=(1, 1, 1, 1),
align=TextNode.ACenter,
font=self.chinese_font if self.chinese_font else None
)
# 创建各种GUI组件
self.createButtons()
self.createLabels()
self.createEntries()
self.createSliders()
self.createCheckboxes()
self.createRadioButtons()
self.createScrolledFrame()
self.createOptionMenu()
# 状态显示文本
self.statusText = OnscreenText(
text="状态: 准备就绪",
pos=(-1.2, -0.9),
scale=0.05,
fg=(0, 1, 0, 1),
align=TextNode.ALeft
)
print("GUI测试应用启动成功!")
print(f"中文字体加载状态: {'成功' if self.chinese_font else '失败,使用默认字体'}")
print("尝试点击各种GUI组件来测试功能")
def loadChineseFont(self):
"""尝试加载中文字体"""
import os
font_paths = [
'/usr/share/fonts/truetype/wqy/wqy-microhei.ttc',
'/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc',
'/usr/share/fonts/truetype/arphic/ukai.ttc',
'/usr/share/fonts/truetype/arphic/uming.ttc',
]
for font_path in font_paths:
if os.path.exists(font_path):
try:
font = self.loader.loadFont(font_path)
if font:
return font
except:
continue
return None
def createButtons(self):
"""创建按钮组件"""
# 普通按钮
self.button1 = DirectButton(
text="普通按钮",
pos=(-0.8, 0, 0.6),
scale=0.06,
command=self.onButton1Click,
rolloverSound=None,
clickSound=None,
text_font=self.chinese_font if self.chinese_font else None
)
# 带图像的按钮
self.button2 = DirectButton(
text="彩色按钮",
pos=(-0.4, 0, 0.6),
scale=0.06,
command=self.onButton2Click,
frameColor=(0.2, 0.8, 0.2, 1),
rolloverSound=None,
clickSound=None,
text_font=self.chinese_font if self.chinese_font else None
)
# 退出按钮
self.exitButton = DirectButton(
text="退出程序",
pos=(0.8, 0, 0.6),
scale=0.06,
command=self.exitApp,
frameColor=(0.8, 0.2, 0.2, 1),
rolloverSound=None,
clickSound=None,
text_font=self.chinese_font if self.chinese_font else None
)
def createLabels(self):
"""创建标签组件"""
self.label1 = DirectLabel(
text="这是一个标签",
pos=(-0.8, 0, 0.4),
scale=0.05,
frameColor=(0, 0, 0, 0), # 透明背景
text_fg=(1, 1, 0, 1),
text_font=self.chinese_font if self.chinese_font else None
)
self.label2 = DirectLabel(
text="带背景的标签",
pos=(-0.4, 0, 0.4),
scale=0.05,
frameColor=(0.3, 0.3, 0.7, 0.8),
text_fg=(1, 1, 1, 1),
text_font=self.chinese_font if self.chinese_font else None
)
def createEntries(self):
"""创建文本输入框"""
# 普通输入框
self.entry1 = DirectEntry(
text="",
pos=(-0.8, 0, 0.2),
scale=0.05,
command=self.onEntry1Submit,
initialText="在这里输入文本...",
numLines=1,
focus=0,
width=12
)
# 密码输入框
self.entry2 = DirectEntry(
text="",
pos=(-0.4, 0, 0.2),
scale=0.05,
command=self.onEntry2Submit,
initialText="密码输入框",
numLines=1,
focus=0,
width=12,
obscured=1 # 密码模式
)
def createSliders(self):
"""创建滑块组件"""
# 水平滑块
self.slider1 = DirectSlider(
range=(0, 100),
value=50,
pos=(0.2, 0, 0.4),
scale=0.4,
command=self.onSlider1Change,
orientation=DGG.HORIZONTAL
)
# 垂直滑块
self.slider2 = DirectSlider(
range=(0, 100),
value=25,
pos=(0.6, 0, 0.2),
scale=0.3,
command=self.onSlider2Change,
orientation=DGG.VERTICAL
)
# 滑块值显示
self.sliderValueText = OnscreenText(
text="滑块值: H=50, V=25",
pos=(0.4, 0.1),
scale=0.04,
fg=(1, 1, 1, 1),
font=self.chinese_font if self.chinese_font else None
)
def createCheckboxes(self):
"""创建复选框"""
self.checkbox1 = DirectCheckButton(
text="选项 1",
pos=(-0.8, 0, 0),
scale=0.05,
command=self.onCheckbox1Toggle,
rolloverSound=None,
clickSound=None,
text_font=self.chinese_font if self.chinese_font else None
)
self.checkbox2 = DirectCheckButton(
text="选项 2",
pos=(-0.4, 0, 0),
scale=0.05,
command=self.onCheckbox2Toggle,
rolloverSound=None,
clickSound=None,
text_font=self.chinese_font if self.chinese_font else None
)
def createRadioButtons(self):
"""创建单选按钮组"""
self.radioButtons = []
self.radioVar = [0] # 用列表来存储选中的索引
for i in range(3):
radio = DirectRadioButton(
text=f"单选 {i+1}",
pos=(-0.8 + i*0.2, 0, -0.2),
scale=0.05,
variable=self.radioVar,
value=[i],
command=self.onRadioButtonSelect,
extraArgs=[i], # 添加额外参数
rolloverSound=None,
clickSound=None,
text_font=self.chinese_font if self.chinese_font else None
)
self.radioButtons.append(radio)
def createScrolledFrame(self):
"""创建滚动框架"""
# 创建滚动框架
self.scrollFrame = DirectScrolledFrame(
frameSize=(-0.5, 0.5, -0.3, 0.3),
canvasSize=(-0.5, 0.5, -1, 1),
pos=(0.2, 0, -0.2),
frameColor=(0.2, 0.2, 0.2, 0.8),
scrollBarWidth=0.04,
verticalScroll_relief=DGG.SUNKEN,
horizontalScroll_relief=DGG.SUNKEN
)
# 在滚动框架中添加内容
for i in range(10):
label = DirectLabel(
text=f"滚动项目 {i+1}",
pos=(0, 0, 0.8 - i*0.15),
scale=0.04,
parent=self.scrollFrame.getCanvas(),
frameColor=(0, 0, 0, 0),
text_fg=(1, 1, 1, 1),
text_font=self.chinese_font if self.chinese_font else None
)
def createOptionMenu(self):
"""创建选项菜单"""
items = ["选项A", "选项B", "选项C", "选项D"]
self.optionMenu = DirectOptionMenu(
text="选择选项",
pos=(0.6, 0, 0),
scale=0.05,
items=items,
initialitem=0,
command=self.onOptionMenuSelect,
rolloverSound=None,
clickSound=None,
text_font=self.chinese_font if self.chinese_font else None
)
# 事件处理函数
def onButton1Click(self):
"""按钮1点击事件"""
self.updateStatus("普通按钮被点击!")
print("普通按钮被点击")
def onButton2Click(self):
"""按钮2点击事件"""
self.updateStatus("彩色按钮被点击!")
print("彩色按钮被点击")
def onEntry1Submit(self, text):
"""文本输入框提交事件"""
self.updateStatus(f"输入的文本: {text}")
print(f"输入的文本: {text}")
def onEntry2Submit(self, text):
"""密码输入框提交事件"""
self.updateStatus(f"输入的密码长度: {len(text)}")
print(f"输入的密码: {text}")
def onSlider1Change(self):
"""水平滑块变化事件"""
value = self.slider1['value']
self.updateSliderValues()
print(f"水平滑块值: {value}")
def onSlider2Change(self):
"""垂直滑块变化事件"""
value = self.slider2['value']
self.updateSliderValues()
print(f"垂直滑块值: {value}")
def updateSliderValues(self):
"""更新滑块值显示"""
h_val = int(self.slider1['value'])
v_val = int(self.slider2['value'])
self.sliderValueText.setText(f"滑块值: H={h_val}, V={v_val}")
def onCheckbox1Toggle(self, status):
"""复选框1切换事件"""
state = "选中" if status else "未选中"
self.updateStatus(f"复选框1: {state}")
print(f"复选框1: {state}")
def onCheckbox2Toggle(self, status):
"""复选框2切换事件"""
state = "选中" if status else "未选中"
self.updateStatus(f"复选框2: {state}")
print(f"复选框2: {state}")
def onRadioButtonSelect(self, index, status):
"""单选按钮选择事件"""
if status:
self.updateStatus(f"选中单选按钮: {index + 1}")
print(f"选中单选按钮: {index + 1}")
def onOptionMenuSelect(self, arg):
"""选项菜单选择事件"""
self.updateStatus(f"选择了: {arg}")
print(f"选择了选项: {arg}")
def updateStatus(self, message):
"""更新状态显示"""
self.statusText.setText(f"状态: {message}")
def exitApp(self):
"""退出应用程序"""
print("正在退出程序...")
self.userExit()
if __name__ == "__main__":
try:
app = GUITestApp()
app.run()
except Exception as e:
print(f"程序运行出错: {e}")
sys.exit(1)

View File

@ -0,0 +1,351 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
from direct.showbase.ShowBase import ShowBase
from panda3d.core import (
Point3, Point2, Vec3, Vec4, CardMaker, CollisionTraverser,
CollisionHandlerQueue, CollisionNode, CollisionRay, CollisionSphere,
BitMask32, PerspectiveLens
)
class ImprovedPickingTest(ShowBase):
def __init__(self):
ShowBase.__init__(self)
# 相机设置
self.cam.setPos(-3.87655, -188.38084, 82.602684)
self.cam.lookAt(0, 0, 0)
print("=== 改进的点击检测测试 ===")
# 创建测试对象
self.createTestObjects()
# 测试各种点击检测方法
self.testAllPickingMethods()
def createTestObjects(self):
"""创建测试对象"""
self.objects = []
# 创建几个测试立方体
positions = [
(0, 10, 3, "中心立方体"),
(-5, 10, 3, "左侧立方体"),
(5, 10, 3, "右侧立方体"),
(0, 10, 8, "上方立方体"),
(0, 15, 3, "远处立方体")
]
colors = [
(1, 0, 0, 1), # 红色
(0, 1, 0, 1), # 绿色
(0, 0, 1, 1), # 蓝色
(1, 1, 0, 1), # 黄色
(1, 0, 1, 1), # 紫色
]
for i, ((x, y, z, name), color) in enumerate(zip(positions, colors)):
# 创建立方体
cm = CardMaker(f"cube_{i}")
cm.setFrame(-1, 1, -1, 1)
cube = self.render.attachNewNode(cm.generate())
cube.setPos(x, y, z)
cube.setScale(2, 2, 2)
cube.setColor(*color)
cube.setBillboardAxis()
cube.setName(name)
# 为每个对象添加球形碰撞体(用于精确的碰撞检测)
collider = cube.attachNewNode(CollisionNode(f"cube_collider_{i}"))
collider.node().addSolid(CollisionSphere(0, 0, 0, 2.0))
collider.node().setFromCollideMask(BitMask32.bit(0))
# 存储对象信息
obj_info = {
'node': cube,
'name': name,
'world_pos': Point3(x, y, z),
'radius': 2.0,
'collider': collider
}
self.objects.append(obj_info)
print(f"创建 {name} 在位置 ({x}, {y}, {z})")
# 方法1改进的射线检测从相机发出
def pickByRayFromCamera(self, mouseX, mouseY):
"""使用从相机发出的射线进行检测"""
print(f"\n=== 方法1: 从相机发出的射线检测 ===")
# 获取窗口尺寸
winWidth = self.win.getXSize()
winHeight = self.win.getYSize()
# 转换为归一化坐标
normalizedX = (2.0 * mouseX / winWidth) - 1.0
normalizedY = 1.0 - (2.0 * mouseY / winHeight)
print(f"鼠标位置: ({mouseX}, {mouseY})")
print(f"归一化坐标: ({normalizedX:.6f}, {normalizedY:.6f})")
# 获取相机镜头
lens = self.cam.node().getLens()
# 计算射线在相机坐标系中的方向
# 使用镜头的逆投影来获取正确的射线方向
near_point = Point3()
far_point = Point3()
if lens.extrude(Point2(normalizedX, normalizedY), near_point, far_point):
# 转换到世界坐标系
near_world = self.cam.getRelativePoint(self.render, near_point)
far_world = self.cam.getRelativePoint(self.render, far_point)
# 计算射线方向
ray_origin = near_world
ray_direction = (far_world - near_world).normalized()
print(f"射线起点 (相机近平面): {near_world}")
print(f"射线方向: {ray_direction}")
# 手动检测与对象的交集
return self.intersectRayWithObjects(ray_origin, ray_direction)
else:
print("无法生成射线")
return None
def intersectRayWithObjects(self, ray_origin, ray_direction):
"""手动计算射线与对象的交集"""
closest_obj = None
closest_distance = float('inf')
for obj_info in self.objects:
# 计算射线与球体的交点
center = obj_info['world_pos']
radius = obj_info['radius']
# 射线到球心的向量
oc = ray_origin - center
# 二次方程系数
a = ray_direction.dot(ray_direction)
b = 2.0 * oc.dot(ray_direction)
c = oc.dot(oc) - radius * radius
# 判别式
discriminant = b * b - 4 * a * c
if discriminant >= 0:
# 有交点,计算距离
sqrt_discriminant = discriminant ** 0.5
t1 = (-b - sqrt_discriminant) / (2 * a)
t2 = (-b + sqrt_discriminant) / (2 * a)
# 取最近的正值交点
t = t1 if t1 > 0 else t2
if t > 0:
distance = t
if distance < closest_distance:
closest_distance = distance
closest_obj = obj_info
print(f"{obj_info['name']}: 交点距离 {distance:.2f}")
else:
print(f"{obj_info['name']}: 在射线后方")
else:
print(f"{obj_info['name']}: 无交点")
if closest_obj:
print(f"选中: {closest_obj['name']}")
else:
print("没有选中任何对象")
return closest_obj
# 方法2基于屏幕投影的检测
def pickByProjection(self, mouseX, mouseY):
"""使用屏幕投影进行检测"""
print(f"\n=== 方法2: 屏幕投影检测 ===")
closest_obj = None
closest_distance = float('inf')
for obj_info in self.objects:
# 将3D位置投影到屏幕
screenPos = self.worldToScreen(obj_info['world_pos'])
if screenPos:
screenX, screenY = screenPos
# 计算屏幕半径
screenRadius = self.getObjectScreenRadius(obj_info)
# 计算鼠标到对象中心的距离
dx = mouseX - screenX
dy = mouseY - screenY
distance = (dx * dx + dy * dy) ** 0.5
print(f"{obj_info['name']}: 屏幕位置({screenX:.1f}, {screenY:.1f}), 半径{screenRadius:.1f}, 距离{distance:.1f}")
# 检查是否在点击范围内
if distance <= screenRadius and distance < closest_distance:
closest_distance = distance
closest_obj = obj_info
else:
print(f"{obj_info['name']}: 不在屏幕内")
if closest_obj:
print(f"选中: {closest_obj['name']}")
else:
print("没有选中任何对象")
return closest_obj
def worldToScreen(self, worldPos):
"""将世界坐标转换为屏幕坐标"""
lens = self.cam.node().getLens()
camPos = self.cam.getRelativePoint(self.render, worldPos)
screenPoint = Point2()
if lens.project(camPos, screenPoint):
winWidth = self.win.getXSize()
winHeight = self.win.getYSize()
pixelX = (screenPoint.getX() + 1.0) * winWidth * 0.5
pixelY = (1.0 - screenPoint.getY()) * winHeight * 0.5
return pixelX, pixelY
return None
def getObjectScreenRadius(self, obj_info):
"""计算对象在屏幕上的半径"""
worldPos = obj_info['world_pos']
worldRadius = obj_info['radius']
centerScreen = self.worldToScreen(worldPos)
if not centerScreen:
return 0
edgePos = worldPos + Vec3(worldRadius, 0, 0)
edgeScreen = self.worldToScreen(edgePos)
if edgeScreen:
return abs(edgeScreen[0] - centerScreen[0])
return 0
# 方法3混合检测投影预选 + 射线精确)
def pickByHybrid(self, mouseX, mouseY):
"""使用混合方法进行检测"""
print(f"\n=== 方法3: 混合检测(投影预选 + 射线精确)===")
# 第一步:使用投影快速预选
candidates = []
for obj_info in self.objects:
screenPos = self.worldToScreen(obj_info['world_pos'])
if screenPos:
screenX, screenY = screenPos
screenRadius = self.getObjectScreenRadius(obj_info)
dx = mouseX - screenX
dy = mouseY - screenY
distance = (dx * dx + dy * dy) ** 0.5
# 使用更大的容差进行预选
tolerance = screenRadius * 1.5
if distance <= tolerance:
candidates.append((obj_info, distance))
print(f"预选候选: {obj_info['name']}, 屏幕距离: {distance:.1f}")
if not candidates:
print("投影预选: 没有候选对象")
return None
# 第二步:对候选对象使用精确射线检测
print("对候选对象进行精确射线检测:")
winWidth = self.win.getXSize()
winHeight = self.win.getYSize()
normalizedX = (2.0 * mouseX / winWidth) - 1.0
normalizedY = 1.0 - (2.0 * mouseY / winHeight)
lens = self.cam.node().getLens()
near_point = Point3()
far_point = Point3()
if lens.extrude(Point2(normalizedX, normalizedY), near_point, far_point):
near_world = self.cam.getRelativePoint(self.render, near_point)
far_world = self.cam.getRelativePoint(self.render, far_point)
ray_origin = near_world
ray_direction = (far_world - near_world).normalized()
# 只检测候选对象
closest_obj = None
closest_distance = float('inf')
for obj_info, _ in candidates:
center = obj_info['world_pos']
radius = obj_info['radius']
oc = ray_origin - center
a = ray_direction.dot(ray_direction)
b = 2.0 * oc.dot(ray_direction)
c = oc.dot(oc) - radius * radius
discriminant = b * b - 4 * a * c
if discriminant >= 0:
sqrt_discriminant = discriminant ** 0.5
t1 = (-b - sqrt_discriminant) / (2 * a)
t2 = (-b + sqrt_discriminant) / (2 * a)
t = t1 if t1 > 0 else t2
if t > 0 and t < closest_distance:
closest_distance = t
closest_obj = obj_info
print(f" {obj_info['name']}: 精确射线距离 {t:.2f}")
if closest_obj:
print(f"最终选中: {closest_obj['name']}")
else:
print("精确检测: 没有有效交点")
return closest_obj
return None
def testAllPickingMethods(self):
"""测试所有点击检测方法"""
print(f"\n=== 测试所有点击检测方法 ===")
# 测试点击位置(模拟点击左侧立方体附近)
test_clicks = [
(372, 265, "左侧立方体附近"),
(399, 265, "中心立方体附近"),
(425, 265, "右侧立方体附近"),
]
for mouseX, mouseY, description in test_clicks:
print(f"\n{'='*60}")
print(f"测试位置: {description} ({mouseX}, {mouseY})")
print(f"{'='*60}")
# 方法1从相机发出的射线
result1 = self.pickByRayFromCamera(mouseX, mouseY)
# 方法2屏幕投影
result2 = self.pickByProjection(mouseX, mouseY)
# 方法3混合方法
result3 = self.pickByHybrid(mouseX, mouseY)
# 比较结果
print(f"\n--- 结果比较 ---")
print(f"射线检测: {result1['name'] if result1 else ''}")
print(f"投影检测: {result2['name'] if result2 else ''}")
print(f"混合检测: {result3['name'] if result3 else ''}")
if __name__ == "__main__":
app = ImprovedPickingTest()
print("改进的点击检测测试完成")

View File

@ -0,0 +1,283 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
生产级屏幕投影点击检测系统
适用于Qt嵌入的Panda3D环境
"""
from panda3d.core import Point3, Point2, Vec3
from typing import List, Optional, Tuple, Dict, Any
import math
class ScreenProjectionPicker:
def __init__(self, camera, lens, window):
"""
初始化屏幕投影拾取器
Args:
camera: Panda3D相机节点
lens: 相机镜头
window: 渲染窗口
"""
self.camera = camera
self.lens = lens
self.window = window
self.objects = [] # 可选中的对象列表
def registerObject(self, node_path, name: str, priority: int = 0,
custom_radius: Optional[float] = None):
"""
注册可选中的对象
Args:
node_path: Panda3D节点路径
name: 对象名称
priority: 选择优先级数值越大优先级越高
custom_radius: 自定义选择半径世界坐标
"""
obj_info = {
'node': node_path,
'name': name,
'priority': priority,
'custom_radius': custom_radius,
'world_pos': node_path.getPos(),
'bounds': node_path.getBounds()
}
self.objects.append(obj_info)
def unregisterObject(self, node_path):
"""取消注册对象"""
self.objects = [obj for obj in self.objects if obj['node'] != node_path]
def updateObjectPositions(self):
"""更新所有对象的世界位置(在对象移动后调用)"""
for obj_info in self.objects:
obj_info['world_pos'] = obj_info['node'].getPos()
obj_info['bounds'] = obj_info['node'].getBounds()
def worldToScreen(self, world_pos: Point3) -> Optional[Tuple[float, float]]:
"""
将世界坐标转换为屏幕像素坐标
Args:
world_pos: 世界坐标位置
Returns:
(pixel_x, pixel_y) None如果不在屏幕内
"""
# 转换到相机坐标系
cam_pos = self.camera.getRelativePoint(self.camera.getParent(), world_pos)
# 投影到屏幕坐标
screen_point = Point2()
if self.lens.project(cam_pos, screen_point):
# 转换为像素坐标
win_width = self.window.getXSize()
win_height = self.window.getYSize()
pixel_x = (screen_point.getX() + 1.0) * win_width * 0.5
pixel_y = (1.0 - screen_point.getY()) * win_height * 0.5
return pixel_x, pixel_y
return None
def getObjectScreenRadius(self, obj_info: Dict) -> float:
"""
计算对象在屏幕上的选择半径
Args:
obj_info: 对象信息字典
Returns:
屏幕像素半径
"""
world_pos = obj_info['world_pos']
# 如果有自定义半径,使用自定义半径
if obj_info['custom_radius'] is not None:
world_radius = obj_info['custom_radius']
else:
# 否则根据包围盒计算半径
bounds = obj_info['bounds']
if bounds.isEmpty():
world_radius = 1.0 # 默认半径
else:
# 使用包围盒的最大尺寸作为半径
min_point = bounds.getMin()
max_point = bounds.getMax()
size = max_point - min_point
world_radius = max(size.getX(), size.getY(), size.getZ()) * 0.5
# 计算屏幕半径
center_screen = self.worldToScreen(world_pos)
if not center_screen:
return 0
# 使用对象右侧边缘点计算屏幕半径
edge_pos = world_pos + Vec3(world_radius, 0, 0)
edge_screen = self.worldToScreen(edge_pos)
if edge_screen:
return abs(edge_screen[0] - center_screen[0])
# 如果边缘点不可见,使用距离相机的距离估算
cam_distance = (world_pos - self.camera.getPos()).length()
if cam_distance > 0:
# 基于透视投影的简单估算
angular_size = world_radius / cam_distance
fov = self.lens.getHfov() # 水平视场角
win_width = self.window.getXSize()
screen_radius = angular_size * (win_width * 0.5) / math.tan(math.radians(fov * 0.5))
return max(10.0, screen_radius) # 最小10像素
return 20.0 # 默认半径
def pickObject(self, mouse_x: int, mouse_y: int,
tolerance_multiplier: float = 1.0) -> Optional[Dict]:
"""
在指定鼠标位置选择对象
Args:
mouse_x: 鼠标X坐标像素
mouse_y: 鼠标Y坐标像素
tolerance_multiplier: 容差倍数增大可使选择更容易
Returns:
选中的对象信息字典或None
"""
candidates = []
for obj_info in self.objects:
# 计算对象在屏幕上的位置
screen_pos = self.worldToScreen(obj_info['world_pos'])
if not screen_pos:
continue # 对象不在屏幕内
screen_x, screen_y = screen_pos
# 计算屏幕半径
screen_radius = self.getObjectScreenRadius(obj_info)
# 应用容差倍数
effective_radius = screen_radius * tolerance_multiplier
# 计算鼠标到对象中心的距离
dx = mouse_x - screen_x
dy = mouse_y - screen_y
distance = math.sqrt(dx * dx + dy * dy)
# 检查是否在选择范围内
if distance <= effective_radius:
# 计算选择分数(距离越近、优先级越高分数越高)
distance_score = 1.0 - (distance / effective_radius)
priority_score = obj_info['priority'] * 10.0
total_score = distance_score + priority_score
candidates.append((obj_info, distance, total_score))
if not candidates:
return None
# 按分数排序,选择最高分的对象
candidates.sort(key=lambda x: x[2], reverse=True)
return candidates[0][0]
def pickMultipleObjects(self, mouse_x: int, mouse_y: int,
tolerance_multiplier: float = 1.0) -> List[Dict]:
"""
在指定位置选择所有可能的对象用于上下文菜单等
Args:
mouse_x: 鼠标X坐标
mouse_y: 鼠标Y坐标
tolerance_multiplier: 容差倍数
Returns:
按优先级排序的对象列表
"""
candidates = []
for obj_info in self.objects:
screen_pos = self.worldToScreen(obj_info['world_pos'])
if not screen_pos:
continue
screen_x, screen_y = screen_pos
screen_radius = self.getObjectScreenRadius(obj_info)
effective_radius = screen_radius * tolerance_multiplier
dx = mouse_x - screen_x
dy = mouse_y - screen_y
distance = math.sqrt(dx * dx + dy * dy)
if distance <= effective_radius:
distance_score = 1.0 - (distance / effective_radius)
priority_score = obj_info['priority'] * 10.0
total_score = distance_score + priority_score
candidates.append((obj_info, distance, total_score))
# 按分数排序
candidates.sort(key=lambda x: x[2], reverse=True)
return [candidate[0] for candidate in candidates]
def getDebugInfo(self, mouse_x: int, mouse_y: int) -> str:
"""获取调试信息"""
debug_lines = [f"鼠标位置: ({mouse_x}, {mouse_y})"]
debug_lines.append(f"注册对象数量: {len(self.objects)}")
debug_lines.append("")
for obj_info in self.objects:
screen_pos = self.worldToScreen(obj_info['world_pos'])
if screen_pos:
screen_x, screen_y = screen_pos
screen_radius = self.getObjectScreenRadius(obj_info)
dx = mouse_x - screen_x
dy = mouse_y - screen_y
distance = math.sqrt(dx * dx + dy * dy)
debug_lines.append(f"{obj_info['name']}:")
debug_lines.append(f" 世界位置: {obj_info['world_pos']}")
debug_lines.append(f" 屏幕位置: ({screen_x:.1f}, {screen_y:.1f})")
debug_lines.append(f" 屏幕半径: {screen_radius:.1f}")
debug_lines.append(f" 鼠标距离: {distance:.1f}")
debug_lines.append(f" 可选中: {'' if distance <= screen_radius else ''}")
debug_lines.append("")
else:
debug_lines.append(f"{obj_info['name']}: 不在屏幕内")
debug_lines.append("")
return "\n".join(debug_lines)
# 使用示例
def example_usage():
"""使用示例"""
print("""
# 在您的应用中使用屏幕投影拾取器:
# 1. 初始化拾取器
picker = ScreenProjectionPicker(self.cam, self.cam.node().getLens(), self.win)
# 2. 注册可选中的对象
picker.registerObject(cube_node, "立方体", priority=1)
picker.registerObject(sphere_node, "球体", priority=2, custom_radius=3.0)
# 3. 在鼠标点击事件中使用
def on_mouse_click(mouse_x, mouse_y):
selected_obj = picker.pickObject(mouse_x, mouse_y, tolerance_multiplier=1.2)
if selected_obj:
print(f"选中了: {selected_obj['name']}")
else:
print("没有选中任何对象")
# 4. 对象移动后更新位置
picker.updateObjectPositions()
# 5. 获取调试信息
debug_info = picker.getDebugInfo(mouse_x, mouse_y)
print(debug_info)
""")
if __name__ == "__main__":
example_usage()

View File

@ -0,0 +1,172 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
from direct.showbase.ShowBase import ShowBase
from panda3d.core import (
Point3, Point2, Vec3, CardMaker, CollisionNode, CollisionSphere,
BitMask32, PerspectiveLens
)
class ProjectionPickingTest(ShowBase):
def __init__(self):
ShowBase.__init__(self)
# 相机设置
self.cam.setPos(-3.87655, -188.38084, 82.602684)
self.cam.lookAt(0, 0, 0)
print("=== 屏幕投影点击检测测试 ===")
# 创建测试对象
self.createTestObjects()
# 测试投影检测
self.testProjectionPicking()
def createTestObjects(self):
"""创建测试对象"""
self.objects = []
# 创建几个测试立方体
positions = [
(0, 10, 3, "中心立方体"),
(-5, 10, 3, "左侧立方体"),
(5, 10, 3, "右侧立方体"),
(0, 10, 8, "上方立方体"),
(0, 15, 3, "远处立方体")
]
colors = [
(1, 0, 0, 1), # 红色
(0, 1, 0, 1), # 绿色
(0, 0, 1, 1), # 蓝色
(1, 1, 0, 1), # 黄色
(1, 0, 1, 1), # 紫色
]
for i, ((x, y, z, name), color) in enumerate(zip(positions, colors)):
# 创建立方体
cm = CardMaker(f"cube_{i}")
cm.setFrame(-1, 1, -1, 1)
cube = self.render.attachNewNode(cm.generate())
cube.setPos(x, y, z)
cube.setScale(2, 2, 2)
cube.setColor(*color)
cube.setBillboardAxis()
cube.setName(name)
# 存储对象信息
obj_info = {
'node': cube,
'name': name,
'world_pos': Point3(x, y, z),
'radius': 2.0 # 立方体的半径
}
self.objects.append(obj_info)
print(f"创建 {name} 在位置 ({x}, {y}, {z})")
def worldToScreen(self, worldPos):
"""将世界坐标转换为屏幕坐标"""
# 获取相机的投影矩阵和模型视图矩阵
lens = self.cam.node().getLens()
# 将世界坐标转换到相机坐标系
camPos = self.cam.getRelativePoint(self.render, worldPos)
# 使用镜头投影到屏幕坐标
screenPoint = Point2()
if lens.project(camPos, screenPoint):
# 转换为窗口像素坐标
winWidth = self.win.getXSize()
winHeight = self.win.getYSize()
# 归一化坐标转换为像素坐标
pixelX = (screenPoint.getX() + 1.0) * winWidth * 0.5
pixelY = (1.0 - screenPoint.getY()) * winHeight * 0.5
return pixelX, pixelY, True
else:
return 0, 0, False
def getObjectScreenRadius(self, obj_info):
"""计算对象在屏幕上的半径"""
worldPos = obj_info['world_pos']
worldRadius = obj_info['radius']
# 计算对象中心在屏幕上的位置
centerX, centerY, visible = self.worldToScreen(worldPos)
if not visible:
return 0
# 计算对象边缘点在屏幕上的位置
# 使用对象右侧边缘点
edgePoint = worldPos + Vec3(worldRadius, 0, 0)
edgeX, edgeY, edge_visible = self.worldToScreen(edgePoint)
if edge_visible:
# 计算屏幕半径
screenRadius = abs(edgeX - centerX)
return screenRadius
return 0
def pickObjectByProjection(self, mouseX, mouseY):
"""使用投影方法选择对象"""
print(f"\n=== 投影检测: 鼠标位置 ({mouseX}, {mouseY}) ===")
closest_obj = None
closest_distance = float('inf')
for obj_info in self.objects:
# 计算对象在屏幕上的位置
screenX, screenY, visible = self.worldToScreen(obj_info['world_pos'])
if not visible:
print(f"{obj_info['name']}: 不可见")
continue
# 计算屏幕半径
screenRadius = self.getObjectScreenRadius(obj_info)
# 计算鼠标到对象中心的距离
dx = mouseX - screenX
dy = mouseY - screenY
distance = (dx * dx + dy * dy) ** 0.5
print(f"{obj_info['name']}: 屏幕位置({screenX:.1f}, {screenY:.1f}), 半径{screenRadius:.1f}, 距离{distance:.1f}")
# 检查是否在点击范围内
if distance <= screenRadius:
if distance < closest_distance:
closest_distance = distance
closest_obj = obj_info
print(f" -> 在点击范围内!")
if closest_obj:
print(f"\n选中对象: {closest_obj['name']}")
return closest_obj
else:
print(f"\n没有选中任何对象")
return None
def testProjectionPicking(self):
"""测试投影点击检测"""
print(f"\n=== 投影点击检测测试 ===")
# 模拟不同的点击位置
test_clicks = [
(619, 362, "屏幕中心"),
(400, 362, "屏幕偏左"),
(800, 362, "屏幕偏右"),
(619, 200, "屏幕上方"),
(619, 500, "屏幕下方"),
]
for mouseX, mouseY, description in test_clicks:
print(f"\n--- 测试 {description}: ({mouseX}, {mouseY}) ---")
selected = self.pickObjectByProjection(mouseX, mouseY)
if __name__ == "__main__":
app = ProjectionPickingTest()
print("投影检测测试完成")

139
demo/quick_gui_test.py Normal file
View File

@ -0,0 +1,139 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
快速GUI测试
验证主程序中的GUI修复是否有效
"""
import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)
import sys
from direct.showbase.ShowBase import ShowBase
from direct.gui.DirectGui import *
from direct.gui.OnscreenText import OnscreenText
from panda3d.core import *
# 配置Panda3D
loadPrcFileData("", """
win-size 800 600
window-title Quick GUI Test
""")
class QuickGUITest(ShowBase):
def __init__(self):
ShowBase.__init__(self)
print("=== 快速GUI测试 ===")
# 设置背景色
self.setBackgroundColor(0.2, 0.2, 0.3)
# 调整相机位置
self.cam.setPos(0, -10, 5)
self.cam.lookAt(0, 0, 0)
self.gui_elements = []
# 尝试加载中文字体
try:
self.chinese_font = self.loader.loadFont('/usr/share/fonts/truetype/wqy/wqy-microhei.ttc')
except:
self.chinese_font = None
# 创建标题
self.title = OnscreenText(
text="快速GUI测试 - 验证修复效果",
pos=(0, 0.9),
scale=0.08,
fg=(1, 1, 1, 1),
align=TextNode.ACenter,
font=self.chinese_font if self.chinese_font else None
)
# 立即创建GUI元素测试
self.createTestGUI()
def createTestGUI(self):
"""创建测试GUI元素"""
print("\n创建测试GUI元素...")
# 使用和主程序相同的方法创建GUI
# 测试不同的坐标值
test_coords = [
(0, 0, 0), # 中心
(-10, 0, 0), # 左侧
(10, 0, 0), # 右侧
(0, 0, 10), # 上方
(0, 0, -10), # 下方
]
print("使用新的0.05缩放比例...")
# 创建按钮测试
for i, pos in enumerate(test_coords):
gui_pos = (pos[0] * 0.05, 0, pos[2] * 0.05)
button = DirectButton(
text=f"按钮{i+1}",
pos=gui_pos,
scale=0.08,
frameColor=(0.2, 0.6, 0.8, 1),
text_font=self.chinese_font if self.chinese_font else None,
rolloverSound=None,
clickSound=None
)
self.gui_elements.append(button)
print(f"按钮{i+1}: 逻辑坐标{pos} -> 屏幕坐标{gui_pos}")
# 创建标签测试
label_coords = [
(0, 0, -15), # 下方
(-15, 0, -5), # 左下
(15, 0, -5), # 右下
]
for i, pos in enumerate(label_coords):
gui_pos = (pos[0] * 0.05, 0, pos[2] * 0.05)
label = DirectLabel(
text=f"标签{i+1}",
pos=gui_pos,
scale=0.06,
frameColor=(0, 0, 0, 0),
text_fg=(1, 1, 1, 1),
text_font=self.chinese_font if self.chinese_font else None
)
self.gui_elements.append(label)
print(f"标签{i+1}: 逻辑坐标{pos} -> 屏幕坐标{gui_pos}")
# 创建3D元素作为对比
textNode = TextNode('3d-text')
textNode.setText("3D文本对比")
textNode.setAlign(TextNode.ACenter)
if self.chinese_font:
textNode.setFont(self.chinese_font)
text3D = self.render.attachNewNode(textNode)
text3D.setPos(0, 5, 2)
text3D.setScale(0.5)
text3D.setColor(1, 1, 0, 1)
text3D.setBillboardAxis()
print(f"\n创建完成GUI元素总数: {len(self.gui_elements)}")
print("如果你能看到按钮和标签,说明修复成功!")
print("如果只能看到黄色的3D文本说明2D GUI仍有问题。")
def main():
"""主函数"""
print("启动快速GUI测试...")
# 创建并运行应用
app = QuickGUITest()
app.run()
if __name__ == "__main__":
main()

116
demo/ray_offset_debug.py Normal file
View File

@ -0,0 +1,116 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
from direct.showbase.ShowBase import ShowBase
from panda3d.core import (
Point3, Point2, Vec3, CollisionTraverser, CollisionHandlerQueue,
CollisionNode, CollisionRay, CollisionSphere, BitMask32
)
class RayOffsetDebug(ShowBase):
def __init__(self):
ShowBase.__init__(self)
# 使用和主程序相同的相机设置
self.cam.setPos(-3.87655, -188.38084, 82.602684)
self.cam.lookAt(0, 0, 0)
print("=== 射线偏移调试 ===")
print(f"相机位置: {self.cam.getPos()}")
print(f"相机朝向: {self.cam.getHpr()}")
# 显示镜头参数
lens = self.cam.node().getLens()
print(f"镜头FOV: {lens.getFov()}")
print(f"宽高比: {lens.getAspectRatio()}")
# 设置射线检测
self.setupRayPicking()
# 测试不同位置的射线
self.testRayDirections()
print("调试完成")
def setupRayPicking(self):
"""设置射线检测"""
self.pickerNode = CollisionNode('mouseRay')
self.pickerRay = CollisionRay()
self.pickerNode.addSolid(self.pickerRay)
mask = BitMask32.bit(0)
self.pickerNode.setFromCollideMask(mask)
self.pickerNode.setIntoCollideMask(BitMask32.allOff())
self.pickerNP = self.render.attachNewNode(self.pickerNode)
def testRayDirections(self):
"""测试不同位置的射线方向"""
print("\n=== 射线方向测试 ===")
# 模拟不同的屏幕点击位置
test_cases = [
(0.0, 0.0, "屏幕中心"),
(-0.5, 0.0, "屏幕左侧"),
(0.5, 0.0, "屏幕右侧"),
(0.0, 0.5, "屏幕上方"),
(0.0, -0.5, "屏幕下方"),
(-0.006, 0.346, "用户点击位置"),
]
for norm_x, norm_y, description in test_cases:
print(f"\n--- {description}: ({norm_x:.3f}, {norm_y:.3f}) ---")
# 使用setFromLens方法
success = self.pickerRay.setFromLens(self.cam.node(), norm_x, norm_y)
if success:
rayOrigin = self.pickerRay.getOrigin()
rayDirection = self.pickerRay.getDirection()
print(f"射线起点: {rayOrigin}")
print(f"射线方向: {rayDirection}")
# 分析相机坐标系中的分量
camForward = self.cam.getMat().getRow3(1)
camRight = self.cam.getMat().getRow3(0)
camUp = self.cam.getMat().getRow3(2)
forwardComp = rayDirection.dot(camForward)
rightComp = rayDirection.dot(camRight)
upComp = rayDirection.dot(camUp)
print(f"相机坐标系分量:")
print(f" 前向: {forwardComp:.6f}")
print(f" 右向: {rightComp:.6f} (正=右, 负=左)")
print(f" 上向: {upComp:.6f} (正=上, 负=下)")
# 计算射线与(0,10,3)点的交点(地面上的一个点)
targetPoint = Point3(0, 10, 3)
rayToTarget = targetPoint - rayOrigin
# 检查射线是否指向这个方向
if rayDirection.dot(rayToTarget) > 0:
# 计算交点
distance = rayToTarget.length()
angle = rayDirection.dot(rayToTarget.normalized())
print(f"与参考点(0,10,3)的角度: {angle:.6f}")
# 预测射线在y=10平面上的交点
if abs(rayDirection.getY()) > 0.001:
t = (10 - rayOrigin.getY()) / rayDirection.getY()
if t > 0:
intersectionPoint = rayOrigin + rayDirection * t
print(f"在Y=10平面上的交点: {intersectionPoint}")
else:
print("射线不指向Y=10平面")
else:
print("射线平行于Y=10平面")
else:
print("射线背离参考点")
else:
print("setFromLens 失败")
if __name__ == "__main__":
app = RayOffsetDebug()
print("测试完成")

1
demo/ray_offset_test.py Normal file
View File

@ -0,0 +1 @@

177
demo/ray_test.py Normal file
View File

@ -0,0 +1,177 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
from direct.showbase.ShowBase import ShowBase
from panda3d.core import (
Point3, Point2, Vec3, CollisionTraverser, CollisionHandlerQueue,
CollisionNode, CollisionRay, CollisionSphere, BitMask32, CardMaker
)
class RayTest(ShowBase):
def __init__(self):
ShowBase.__init__(self)
# 设置相机位置 - 和你的程序一样
self.cam.setPos(-3.87655, -188.38084, 82.602684)
self.cam.lookAt(0, 0, 0)
print(f"相机位置: {self.cam.getPos()}")
print(f"相机朝向: {self.cam.getHpr()}")
# 创建测试立方体 - 位置和你的程序一样
self.createTestCube()
# 设置射线检测
self.setupRayPicking()
# 测试不同的射线方向
self.testRaycast()
def createTestCube(self):
"""创建测试立方体"""
# 创建简单的平面作为立方体
cm = CardMaker("TestCube")
cm.setFrame(-1, 1, -1, 1)
self.cube = self.render.attachNewNode(cm.generate())
# 设置位置和大小 - 和你的程序完全一样
self.cube.setPos(0, 10, 3)
self.cube.setScale(3, 3, 3)
self.cube.setColor(1, 0, 0, 1)
self.cube.setTwoSided(True)
self.cube.setHpr(45, 15, 0)
print(f"立方体世界位置: {self.cube.getPos(self.render)}")
print(f"立方体缩放: {self.cube.getScale()}")
# 添加碰撞检测
cNode = CollisionNode('TestCubeCollision')
cSphere = CollisionSphere(Point3(0, 0, 0), 4.0)
cNode.addSolid(cSphere)
cNode.setIntoCollideMask(BitMask32.bit(0))
cNode.setFromCollideMask(BitMask32.allOff())
self.cNodePath = self.cube.attachNewNode(cNode)
print(f"碰撞球体半径: 4.0")
# 显示碰撞体
self.cNodePath.show()
def setupRayPicking(self):
"""设置射线检测"""
self.picker = CollisionTraverser()
self.pickerQueue = CollisionHandlerQueue()
self.pickerNode = CollisionNode('mouseRay')
self.pickerRay = CollisionRay()
self.pickerNode.addSolid(self.pickerRay)
# 设置掩码
mask = BitMask32.bit(0)
self.pickerNode.setFromCollideMask(mask)
self.pickerNode.setIntoCollideMask(BitMask32.allOff())
self.pickerNP = self.cam.attachNewNode(self.pickerNode)
self.picker.addCollider(self.pickerNP, self.pickerQueue)
print(f"射线检测器掩码: {mask}")
def testRaycast(self):
"""测试射线投射"""
print(f"\n=== 射线投射测试 ===")
# 测试用例:模拟你的点击位置
window_width = 800 # 假设窗口大小
window_height = 600
mouse_x = 419
mouse_y = 96
# 计算归一化坐标
normalized_x = (2.0 * mouse_x / window_width) - 1.0
normalized_y = 1.0 - (2.0 * mouse_y / window_height)
print(f"鼠标位置: ({mouse_x}, {mouse_y})")
print(f"归一化坐标: ({normalized_x:.3f}, {normalized_y:.3f})")
# 获取相机参数
camera_pos = self.cam.getPos(self.render)
lens = self.cam.node().getLens()
# 计算射线
nearPoint = Point3()
farPoint = Point3()
if lens.extrude(Point2(normalized_x, normalized_y), nearPoint, farPoint):
print(f"lens.extrude() 成功")
print(f"近点(相机坐标): {nearPoint}")
print(f"远点(相机坐标): {farPoint}")
# 转换到世界坐标
nearWorldPoint = self.cam.getRelativePoint(self.render, nearPoint)
farWorldPoint = self.cam.getRelativePoint(self.render, farPoint)
print(f"近点(世界坐标): {nearWorldPoint}")
print(f"远点(世界坐标): {farWorldPoint}")
# 计算射线方向
rayDirWorld = farWorldPoint - nearWorldPoint
rayDirWorld.normalize()
print(f"射线起点: {camera_pos}")
print(f"射线方向: {rayDirWorld}")
# 设置射线
self.pickerRay.setOrigin(camera_pos)
self.pickerRay.setDirection(rayDirWorld)
# 执行碰撞检测
self.pickerQueue.clearEntries()
self.picker.traverse(self.render)
numEntries = self.pickerQueue.getNumEntries()
print(f"\n检测到 {numEntries} 个碰撞")
if numEntries > 0:
self.pickerQueue.sortEntries()
for i in range(numEntries):
entry = self.pickerQueue.getEntry(i)
hitPos = entry.getSurfacePoint(self.render)
hitNode = entry.getIntoNodePath()
distance = (hitPos - camera_pos).length()
print(f" 碰撞 {i}: {hitNode.getName()}, 距离: {distance:.2f}, 位置: {hitPos}")
else:
print(" 没有检测到碰撞")
# 分析为什么没有碰撞
print(f"\n=== 碰撞分析 ===")
cube_world_pos = self.cube.getPos(self.render)
cube_scale = self.cube.getScale()
print(f"立方体中心: {cube_world_pos}")
print(f"立方体缩放: {cube_scale}")
print(f"碰撞球体实际半径: {4.0 * cube_scale.getX()}")
# 计算射线到立方体中心的最近距离
# 射线: P = origin + t * direction
# 立方体中心: C
# 最近距离: |cross(C - origin, direction)| / |direction|
to_cube = cube_world_pos - camera_pos
cross_product = to_cube.cross(rayDirWorld)
distance_to_ray = cross_product.length()
print(f"射线到立方体中心的距离: {distance_to_ray:.2f}")
print(f"需要小于碰撞半径: {4.0 * cube_scale.getX():.2f}")
if distance_to_ray < 4.0 * cube_scale.getX():
print("射线应该能击中立方体!可能是其他问题。")
else:
print("射线错过了立方体。")
else:
print("lens.extrude() 失败")
if __name__ == "__main__":
app = RayTest()
# 不运行主循环,只测试
print("\n测试完成!")

299
demo/simple_gui_test.py Normal file
View File

@ -0,0 +1,299 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
PANDA3D 简化版GUI功能测试
演示DirectGUI基本组件的使用
"""
from direct.showbase.ShowBase import ShowBase
from direct.gui.DirectGui import *
from direct.gui.OnscreenText import OnscreenText
from panda3d.core import *
class SimpleGUITest(ShowBase):
def __init__(self):
ShowBase.__init__(self)
# 设置窗口属性和背景
wp = WindowProperties()
wp.setTitle("PANDA3D 简化GUI测试")
self.win.requestProperties(wp)
self.setBackgroundColor(0.1, 0.2, 0.4)
# 标题文本
self.title = OnscreenText(
text="PANDA3D DirectGUI 基础测试",
pos=(0, 0.85),
scale=0.08,
fg=(1, 1, 1, 1),
align=TextNode.ACenter
)
# 创建GUI组件
self.setupGUI()
# 尝试加载中文字体
self.chinese_font = self.loadChineseFont()
# 变量用于存储状态
self.clickCount = 0
print("="*50)
print("PANDA3D GUI测试程序启动")
print(f"中文字体加载状态: {'成功' if self.chinese_font else '失败,使用默认字体'}")
print("尝试点击不同的GUI组件来测试功能")
print("="*50)
def setupGUI(self):
"""设置所有GUI组件"""
# 1. 按钮测试
self.createButtons()
# 2. 文本输入框测试
self.createTextEntry()
# 3. 滑块测试
self.createSlider()
# 4. 复选框测试
self.createCheckbox()
# 5. 选项菜单测试
self.createOptionMenu()
# 6. 状态显示
self.createStatusDisplay()
def createButtons(self):
"""创建按钮组件"""
# 主要测试按钮
self.testButton = DirectButton(
text="点击测试",
pos=(-0.6, 0, 0.5),
scale=0.08,
command=self.onTestButtonClick,
frameColor=(0.2, 0.7, 0.2, 1),
text_fg=(1, 1, 1, 1),
text_font=self.chinese_font if self.chinese_font else None
)
# 重置按钮
self.resetButton = DirectButton(
text="重置",
pos=(0, 0, 0.5),
scale=0.08,
command=self.onResetClick,
frameColor=(0.7, 0.7, 0.2, 1),
text_fg=(1, 1, 1, 1),
text_font=self.chinese_font if self.chinese_font else None
)
# 退出按钮
self.exitButton = DirectButton(
text="退出",
pos=(0.6, 0, 0.5),
scale=0.08,
command=self.exitProgram,
frameColor=(0.7, 0.2, 0.2, 1),
text_fg=(1, 1, 1, 1),
text_font=self.chinese_font if self.chinese_font else None
)
def createTextEntry(self):
"""创建文本输入框"""
# 输入框标签
self.entryLabel = DirectLabel(
text="文本输入:",
pos=(-0.8, 0, 0.2),
scale=0.06,
frameColor=(0, 0, 0, 0),
text_fg=(1, 1, 1, 1)
)
# 文本输入框
self.textEntry = DirectEntry(
text="",
pos=(-0.3, 0, 0.2),
scale=0.06,
command=self.onTextSubmit,
initialText="在此输入...",
numLines=1,
width=15,
focus=0
)
def createSlider(self):
"""创建滑块"""
# 滑块标签
self.sliderLabel = DirectLabel(
text="滑块:",
pos=(-0.8, 0, 0),
scale=0.06,
frameColor=(0, 0, 0, 0),
text_fg=(1, 1, 1, 1)
)
# 滑块控件
self.slider = DirectSlider(
range=(0, 100),
value=50,
pos=(-0.2, 0, 0),
scale=0.5,
command=self.onSliderChange,
orientation=DGG.HORIZONTAL
)
# 滑块值显示
self.sliderValueLabel = DirectLabel(
text="值: 50",
pos=(0.4, 0, 0),
scale=0.06,
frameColor=(0, 0, 0, 0),
text_fg=(0, 1, 1, 1)
)
def createCheckbox(self):
"""创建复选框"""
self.checkbox = DirectCheckButton(
text="启用特殊模式",
pos=(-0.6, 0, -0.2),
scale=0.06,
command=self.onCheckboxToggle
)
def createOptionMenu(self):
"""创建选项菜单"""
# 选项菜单标签
self.menuLabel = DirectLabel(
text="选择:",
pos=(-0.8, 0, -0.4),
scale=0.06,
frameColor=(0, 0, 0, 0),
text_fg=(1, 1, 1, 1)
)
# 选项列表
options = ["选项1", "选项2", "选项3", "高级选项"]
self.optionMenu = DirectOptionMenu(
text="请选择",
pos=(-0.3, 0, -0.4),
scale=0.06,
items=options,
initialitem=0,
command=self.onOptionSelect
)
def createStatusDisplay(self):
"""创建状态显示区域"""
# 状态标题
self.statusTitle = DirectLabel(
text="状态信息:",
pos=(-0.95, 0, -0.7),
scale=0.05,
frameColor=(0, 0, 0, 0),
text_fg=(1, 1, 0, 1),
text_align=TextNode.ALeft
)
# 状态显示文本
self.statusText = OnscreenText(
text="程序就绪",
pos=(-0.95, -0.8),
scale=0.05,
fg=(0, 1, 0, 1),
align=TextNode.ALeft
)
# 计数器显示
self.counterText = OnscreenText(
text="点击次数: 0",
pos=(0.5, -0.8),
scale=0.05,
fg=(1, 0.5, 0, 1),
align=TextNode.ALeft
)
# 事件处理函数
def onTestButtonClick(self):
"""测试按钮点击事件"""
self.clickCount += 1
self.updateStatus(f"按钮被点击! 第{self.clickCount}")
self.counterText.setText(f"点击次数: {self.clickCount}")
print(f"测试按钮被点击,当前点击次数: {self.clickCount}")
def onResetClick(self):
"""重置按钮点击事件"""
self.clickCount = 0
self.slider['value'] = 50
self.sliderValueLabel['text'] = "值: 50"
self.textEntry.enterText("")
self.textEntry.set("在此输入...")
self.checkbox['indicatorValue'] = 0
self.optionMenu.set(0)
self.updateStatus("所有设置已重置")
self.counterText.setText("点击次数: 0")
print("界面已重置")
def loadChineseFont(self):
"""尝试加载中文字体"""
import os
font_paths = [
'/usr/share/fonts/truetype/wqy/wqy-microhei.ttc',
'/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc',
'/usr/share/fonts/truetype/arphic/ukai.ttc',
'/usr/share/fonts/truetype/arphic/uming.ttc',
]
for font_path in font_paths:
if os.path.exists(font_path):
try:
font = self.loader.loadFont(font_path)
if font:
return font
except:
continue
return None
def onTextSubmit(self, text):
"""文本提交事件"""
if text.strip():
self.updateStatus(f"输入文本: {text}")
print(f"用户输入: {text}")
else:
self.updateStatus("输入为空")
def onSliderChange(self):
"""滑块变化事件"""
value = int(self.slider['value'])
self.sliderValueLabel['text'] = f"值: {value}"
self.updateStatus(f"滑块值: {value}")
print(f"滑块值变更为: {value}")
def onCheckboxToggle(self, status):
"""复选框切换事件"""
state = "开启" if status else "关闭"
self.updateStatus(f"特殊模式: {state}")
print(f"复选框状态: {state}")
def onOptionSelect(self, selectedOption):
"""选项菜单选择事件"""
self.updateStatus(f"选择: {selectedOption}")
print(f"选项变更为: {selectedOption}")
def updateStatus(self, message):
"""更新状态显示"""
self.statusText.setText(message)
def exitProgram(self):
"""退出程序"""
print("用户点击退出按钮")
print("程序即将关闭...")
self.userExit()
# 程序入口
if __name__ == "__main__":
print("启动PANDA3D GUI测试程序...")
app = SimpleGUITest()
app.run()

View File

@ -0,0 +1,581 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
独立的坐标轴点击测试demo
专门用于测试和调试坐标轴的点击检测和拖拽约束功能
"""
import sys
import math
from direct.showbase.ShowBase import ShowBase
from panda3d.core import (
Vec3, Vec4, Point3, Point2, LineSegs,
AmbientLight, DirectionalLight, TextNode,
CollisionTraverser, CollisionHandlerQueue,
CollisionNode, CollisionRay, GeomNode,
CardMaker, Material
)
from direct.task import Task
from direct.gui.DirectGui import DirectFrame, DirectLabel
class GizmoTest(ShowBase):
def __init__(self):
ShowBase.__init__(self)
# 关闭默认的鼠标控制
self.disableMouse()
print("坐标轴测试程序启动")
# 设置背景色
self.setBackgroundColor(0.2, 0.2, 0.3)
# 设置相机位置
self.cam.setPos(10, -20, 10)
self.cam.lookAt(0, 0, 0)
# 添加光照
self.setupLighting()
# 创建参考立方体
self.createReferenceCube()
# 创建坐标轴
self.createGizmo()
# 设置点击检测
self.setupClickDetection()
# 创建信息显示
self.createInfoDisplay()
# 拖拽状态变量
self.isDragging = False
self.dragAxis = None
self.dragStartMousePos = None
self.dragStartCubePos = None
# 高亮状态
self.highlightAxis = None
# 设置事件处理
self.accept("mouse1", self.onMouseClick)
self.accept("mouse1-up", self.onMouseRelease)
self.taskMgr.add(self.mouseTask, "mouseTask")
# 添加颜色测试任务
self.taskMgr.add(self.colorTestTask, "colorTest")
self.test_time = 0
print("=== Gizmo Click Test Demo Started ===")
print("Instructions:")
print("- ONLY click on red/green/blue gizmo axes to drag")
print("- Each axis constrains movement to that direction only")
print("- Click elsewhere should do nothing")
print("- Real-time display of mouse and projection info")
print("- Color test will cycle through colors every 3 seconds")
def setupLighting(self):
"""设置光照"""
# 环境光
alight = AmbientLight('alight')
alight.setColor((0.3, 0.3, 0.3, 1))
alnp = self.render.attachNewNode(alight)
self.render.setLight(alnp)
# 定向光
dlight = DirectionalLight('dlight')
dlight.setColor((0.8, 0.8, 0.8, 1))
dlnp = self.render.attachNewNode(dlight)
dlnp.setHpr(45, -45, 0)
self.render.setLight(dlnp)
def createReferenceCube(self):
"""创建参考立方体"""
# 创建立方体
cm = CardMaker('cube')
cm.setFrame(-1, 1, -1, 1)
# 创建6个面
faces = [
('front', (0, 1, 0), (0, 0, 0), (1, 0, 0, 1)), # 红色前面
('back', (0, -1, 0), (0, 0, 180), (0, 1, 0, 1)), # 绿色后面
('left', (-1, 0, 0), (0, 0, 90), (0, 0, 1, 1)), # 蓝色左面
('right', (1, 0, 0), (0, 0, -90), (1, 1, 0, 1)), # 黄色右面
('top', (0, 0, 1), (-90, 0, 0), (1, 0, 1, 1)), # 紫色顶面
('bottom', (0, 0, -1), (90, 0, 0), (0, 1, 1, 1)) # 青色底面
]
self.cube = self.render.attachNewNode("cube")
for name, pos, hpr, color in faces:
face = self.cube.attachNewNode(cm.generate())
face.setPos(*pos)
face.setHpr(*hpr)
face.setColor(*color)
face.setName(name)
print(f"✓ 参考立方体创建完成,位置: {self.cube.getPos()}")
def createGizmo(self):
"""创建坐标轴"""
self.gizmo = self.render.attachNewNode("gizmo")
self.gizmo.setPos(0, 0, 3) # 放在立方体上方
self.axis_length = 3.0
self.click_threshold = 25 # 点击检测阈值(像素)
# 创建X轴不在LineSegs级别设置颜色
x_lines = LineSegs("x_axis")
x_lines.setThickness(6.0)
# 不在LineSegs级别设置颜色
x_lines.moveTo(0, 0, 0)
x_lines.drawTo(self.axis_length, 0, 0)
# 箭头
x_lines.moveTo(self.axis_length - 0.3, -0.1, 0)
x_lines.drawTo(self.axis_length, 0, 0)
x_lines.drawTo(self.axis_length - 0.3, 0.1, 0)
x_geom = x_lines.create()
self.gizmoXAxis = self.gizmo.attachNewNode(x_geom)
self.gizmoXAxis.setLightOff()
# 使用RenderState强制设置颜色
from panda3d.core import RenderState, ColorAttrib
red_state = RenderState.make(ColorAttrib.makeFlat((1, 0, 0, 1)))
self.gizmoXAxis.setState(red_state)
self.gizmoXAxis.setColor(1, 0, 0, 1)
# 创建Y轴不在LineSegs级别设置颜色
y_lines = LineSegs("y_axis")
y_lines.setThickness(6.0)
# 不在LineSegs级别设置颜色
y_lines.moveTo(0, 0, 0)
y_lines.drawTo(0, self.axis_length, 0)
# 箭头
y_lines.moveTo(-0.1, self.axis_length - 0.3, 0)
y_lines.drawTo(0, self.axis_length, 0)
y_lines.drawTo(0.1, self.axis_length - 0.3, 0)
y_geom = y_lines.create()
self.gizmoYAxis = self.gizmo.attachNewNode(y_geom)
self.gizmoYAxis.setLightOff()
# 使用RenderState强制设置颜色
green_state = RenderState.make(ColorAttrib.makeFlat((0, 1, 0, 1)))
self.gizmoYAxis.setState(green_state)
self.gizmoYAxis.setColor(0, 1, 0, 1)
# 创建Z轴不在LineSegs级别设置颜色
z_lines = LineSegs("z_axis")
z_lines.setThickness(6.0)
# 不在LineSegs级别设置颜色
z_lines.moveTo(0, 0, 0)
z_lines.drawTo(0, 0, self.axis_length)
# 箭头
z_lines.moveTo(-0.1, 0, self.axis_length - 0.3)
z_lines.drawTo(0, 0, self.axis_length)
z_lines.drawTo(0.1, 0, self.axis_length - 0.3)
z_geom = z_lines.create()
self.gizmoZAxis = self.gizmo.attachNewNode(z_geom)
self.gizmoZAxis.setLightOff()
# 使用RenderState强制设置颜色
blue_state = RenderState.make(ColorAttrib.makeFlat((0, 0, 1, 1)))
self.gizmoZAxis.setState(blue_state)
self.gizmoZAxis.setColor(0, 0, 1, 1)
# 轴颜色字典
self.axis_colors = {
"x": (1, 0, 0, 1), # 红色
"y": (0, 1, 0, 1), # 绿色
"z": (0, 0, 1, 1) # 蓝色
}
print(f"✓ 坐标轴创建完成,长度={self.axis_length}")
print(f"X轴节点颜色: {self.gizmoXAxis.getColor()}")
print(f"Y轴节点颜色: {self.gizmoYAxis.getColor()}")
print(f"Z轴节点颜色: {self.gizmoZAxis.getColor()}")
def setupClickDetection(self):
"""设置点击检测"""
self.picker = CollisionTraverser()
self.pq = CollisionHandlerQueue()
self.pickerNode = CollisionNode('mouseRay')
self.pickerNP = self.camera.attachNewNode(self.pickerNode)
self.pickerNode.setFromCollideMask(GeomNode.getDefaultCollideMask())
self.picker.addCollider(self.pickerNP, self.pq)
def createInfoDisplay(self):
"""创建信息显示面板"""
self.infoPanel = DirectFrame(
frameColor=(0, 0, 0, 0.7),
frameSize=(-0.4, 0.4, -0.3, 0.3),
pos=(-0.6, 0, 0.8)
)
self.infoText = DirectLabel(
parent=self.infoPanel,
text="Mouse Info\nWaiting...",
text_scale=0.05,
text_fg=(1, 1, 1, 1),
frameColor=(0, 0, 0, 0),
pos=(0, 0, 0)
)
def worldToScreen(self, world_pos):
"""将世界坐标转换为屏幕坐标"""
try:
# 转换为相机坐标系
cam_pos = self.cam.getRelativePoint(self.render, world_pos)
# 检查是否在相机前方
if cam_pos.getY() <= 0:
return None
# 使用相机lens进行投影
screen_pos = Point2()
if self.cam.node().getLens().project(cam_pos, screen_pos):
# 转换为像素坐标
win_x = (screen_pos.getX() + 1.0) * 0.5 * self.win.getXSize()
win_y = (1.0 - screen_pos.getY()) * 0.5 * self.win.getYSize()
return (win_x, win_y)
return None
except:
return None
def distanceToLine(self, point, line_start, line_end):
"""计算点到线段的距离"""
try:
px, py = point
x1, y1 = line_start
x2, y2 = line_end
# 计算线段长度
line_length_sq = (x2 - x1) ** 2 + (y2 - y1) ** 2
if line_length_sq < 0.001:
return math.sqrt((px - x1) ** 2 + (py - y1) ** 2)
# 计算投影参数
t = max(0, min(1, ((px - x1) * (x2 - x1) + (py - y1) * (y2 - y1)) / line_length_sq))
# 计算投影点
projection_x = x1 + t * (x2 - x1)
projection_y = y1 + t * (y2 - y1)
# 返回距离
distance = math.sqrt((px - projection_x) ** 2 + (py - projection_y) ** 2)
return distance
except:
return float('inf')
def checkGizmoClick(self, mouse_x, mouse_y):
"""检查是否点击了坐标轴"""
if not self.gizmo:
return None
# 获取坐标轴中心的世界坐标
gizmo_world_pos = self.gizmo.getPos(self.render)
# 计算各轴端点的世界坐标
x_end = gizmo_world_pos + Vec3(self.axis_length, 0, 0)
y_end = gizmo_world_pos + Vec3(0, self.axis_length, 0)
z_end = gizmo_world_pos + Vec3(0, 0, self.axis_length)
# 投影到屏幕坐标
center_screen = self.worldToScreen(gizmo_world_pos)
x_screen = self.worldToScreen(x_end)
y_screen = self.worldToScreen(y_end)
z_screen = self.worldToScreen(z_end)
if not center_screen:
return None
# 检测各个轴
axes_data = [
("x", x_screen, "X轴"),
("y", y_screen, "Y轴"),
("z", z_screen, "Z轴")
]
closest_axis = None
closest_distance = float('inf')
for axis_name, axis_screen, axis_label in axes_data:
if axis_screen:
distance = self.distanceToLine(
(mouse_x, mouse_y), center_screen, axis_screen
)
if distance < self.click_threshold and distance < closest_distance:
closest_axis = axis_name
closest_distance = distance
return closest_axis
def setAxisColor(self, axis, color):
"""设置坐标轴颜色 - 使用RenderState强制覆盖"""
from panda3d.core import RenderState, ColorAttrib
# 创建强制颜色状态
color_state = RenderState.make(ColorAttrib.makeFlat(color))
if axis == "x" and self.gizmoXAxis:
self.gizmoXAxis.setState(color_state)
self.gizmoXAxis.setColor(*color)
self.gizmoXAxis.setColorScale(*color)
print(f"✓ 强制设置X轴颜色: {color}")
elif axis == "y" and self.gizmoYAxis:
self.gizmoYAxis.setState(color_state)
self.gizmoYAxis.setColor(*color)
self.gizmoYAxis.setColorScale(*color)
print(f"✓ 强制设置Y轴颜色: {color}")
elif axis == "z" and self.gizmoZAxis:
self.gizmoZAxis.setState(color_state)
self.gizmoZAxis.setColor(*color)
self.gizmoZAxis.setColorScale(*color)
print(f"✓ 强制设置Z轴颜色: {color}")
def mouseTask(self, task):
"""鼠标任务 - 处理高亮和拖拽"""
if not self.mouseWatcherNode.hasMouse():
return task.cont
# 获取鼠标屏幕坐标
mouse_x = (self.mouseWatcherNode.getMouseX() + 1.0) * 0.5 * self.win.getXSize()
mouse_y = (1.0 - self.mouseWatcherNode.getMouseY()) * 0.5 * self.win.getYSize()
# 检查是否在颜色测试周期内
cycle_time = int(self.test_time) % 15
is_color_testing = (cycle_time >= 3 and cycle_time < 12) # 在黄、白、橙色测试期间
# 更新信息显示
info_text = f"Mouse Info:\n"
info_text += f"Normalized: ({self.mouseWatcherNode.getMouseX():.3f}, {self.mouseWatcherNode.getMouseY():.3f})\n"
info_text += f"Pixels: ({mouse_x:.1f}, {mouse_y:.1f})\n"
info_text += f"Window Size: {self.win.getXSize()} x {self.win.getYSize()}\n"
if is_color_testing:
info_text += f"Color Test Active (cycle: {cycle_time})\n"
# 只在不拖拽且不在颜色测试时检测高亮
if not self.isDragging and not is_color_testing:
# 简化的高亮检测
hovered_axis = None
if self.gizmo:
# 获取坐标轴中心的世界坐标
gizmo_world_pos = self.gizmo.getPos(self.render)
# 计算各轴端点的世界坐标
x_end = gizmo_world_pos + Vec3(self.axis_length, 0, 0)
y_end = gizmo_world_pos + Vec3(0, self.axis_length, 0)
z_end = gizmo_world_pos + Vec3(0, 0, self.axis_length)
# 投影到屏幕坐标
center_screen = self.worldToScreen(gizmo_world_pos)
x_screen = self.worldToScreen(x_end)
y_screen = self.worldToScreen(y_end)
z_screen = self.worldToScreen(z_end)
if center_screen:
# 检测距离最近的轴
min_distance = float('inf')
for axis_name, axis_screen in [("x", x_screen), ("y", y_screen), ("z", z_screen)]:
if axis_screen:
distance = self.distanceToLine((mouse_x, mouse_y), center_screen, axis_screen)
if distance < self.click_threshold and distance < min_distance:
min_distance = distance
hovered_axis = axis_name
# 处理高亮状态变化
if hovered_axis != self.highlightAxis:
# 恢复之前高亮的轴
if self.highlightAxis:
self.setAxisColor(self.highlightAxis, self.axis_colors[self.highlightAxis])
print(f"恢复 {self.highlightAxis} 轴原色")
# 高亮新的轴
if hovered_axis:
self.setAxisColor(hovered_axis, (1, 1, 0, 1)) # 纯黄色高亮
print(f"高亮 {hovered_axis} 轴为黄色")
self.highlightAxis = hovered_axis
# 更新信息显示
if hovered_axis:
info_text += f"Hover: {hovered_axis.upper()} axis (YELLOW)"
else:
info_text += "Hover: None"
elif is_color_testing:
info_text += "Hover: Disabled (Color Test)"
else:
info_text += f"Dragging: {self.dragAxis.upper()} axis"
# 处理拖拽
if self.isDragging:
self.handleDrag(mouse_x, mouse_y)
self.infoText.setText(info_text)
return task.cont
def onMouseClick(self):
"""鼠标点击事件 - 只有点击坐标轴才开始拖拽"""
if not self.mouseWatcherNode.hasMouse():
return
# 检查是否在颜色测试期间
cycle_time = int(self.test_time) % 15
is_color_testing = (cycle_time >= 3 and cycle_time < 12)
if is_color_testing:
print("点击被禁用 - 颜色测试进行中")
return
# 获取鼠标坐标
mouse_x = (self.mouseWatcherNode.getMouseX() + 1.0) * 0.5 * self.win.getXSize()
mouse_y = (1.0 - self.mouseWatcherNode.getMouseY()) * 0.5 * self.win.getYSize()
print(f"\n=== Mouse Click ===")
print(f"Pixel Coords: ({mouse_x:.1f}, {mouse_y:.1f})")
# 检测坐标轴点击
clicked_axis = self.checkGizmoClick(mouse_x, mouse_y)
if clicked_axis:
print(f"✓ Clicked {clicked_axis.upper()} Axis - Starting drag")
# 只有真正点击到轴才开始拖拽
self.isDragging = True
self.dragAxis = clicked_axis
self.dragStartMousePos = (mouse_x, mouse_y)
self.dragStartCubePos = self.cube.getPos()
# 显示拖拽颜色 - 使用更明显的橙色
self.setAxisColor(clicked_axis, (1, 0.5, 0, 1)) # 更亮的橙色表示拖拽
print(f" 开始位置: {self.dragStartCubePos}")
else:
print("✗ No axis clicked - No action")
# 确保没有点击到轴时不开始拖拽
self.isDragging = False
self.dragAxis = None
def onMouseRelease(self):
"""鼠标释放事件"""
if self.isDragging and self.dragAxis:
print(f"Stop dragging {self.dragAxis.upper()} axis")
# 恢复轴颜色
self.setAxisColor(self.dragAxis, self.axis_colors[self.dragAxis])
# 清理所有拖拽状态
self.isDragging = False
self.dragAxis = None
self.dragStartMousePos = None
self.dragStartCubePos = None
def handleDrag(self, mouse_x, mouse_y):
"""处理拖拽 - 真正的轴约束拖拽"""
if not self.dragAxis or not self.dragStartMousePos or not self.dragStartCubePos:
return
# 计算从拖拽开始到现在的总鼠标移动距离
total_dx = mouse_x - self.dragStartMousePos[0]
total_dy = mouse_y - self.dragStartMousePos[1]
# 根据拖拽的轴,确定世界空间中的轴方向向量
if self.dragAxis == "x":
world_axis_dir = Vec3(1, 0, 0) # X轴方向
elif self.dragAxis == "y":
world_axis_dir = Vec3(0, 1, 0) # Y轴方向
elif self.dragAxis == "z":
world_axis_dir = Vec3(0, 0, 1) # Z轴方向
else:
return
# 将世界轴方向投影到屏幕空间
axis_start_world = self.dragStartCubePos
axis_end_world = self.dragStartCubePos + world_axis_dir
axis_start_screen = self.worldToScreen(axis_start_world)
axis_end_screen = self.worldToScreen(axis_end_world)
if not axis_start_screen or not axis_end_screen:
return
# 计算轴在屏幕空间的方向向量
screen_axis_dir = (
axis_end_screen[0] - axis_start_screen[0],
axis_end_screen[1] - axis_start_screen[1]
)
# 归一化屏幕轴方向
screen_axis_length = math.sqrt(screen_axis_dir[0]**2 + screen_axis_dir[1]**2)
if screen_axis_length > 0.001:
screen_axis_dir = (
screen_axis_dir[0] / screen_axis_length,
screen_axis_dir[1] / screen_axis_length
)
else:
return
# 将总鼠标移动向量投影到屏幕轴方向上
projected_distance = (
total_dx * screen_axis_dir[0] +
total_dy * screen_axis_dir[1]
)
# 将投影距离转换为世界空间移动量
move_scale = 0.02 # 移动灵敏度
world_movement = world_axis_dir * projected_distance * move_scale
# 从拖拽开始位置计算新位置(确保只沿选定轴移动)
new_cube_pos = self.dragStartCubePos + world_movement
self.cube.setPos(new_cube_pos)
# 坐标轴跟随立方体移动
self.gizmo.setPos(new_cube_pos + Vec3(0, 0, 3))
def colorTestTask(self, task):
"""颜色测试任务 - 每3秒循环测试坐标轴颜色"""
from panda3d.core import RenderState, ColorAttrib
self.test_time += globalClock.getDt()
# 每3秒切换一次测试
cycle_time = int(self.test_time) % 15 # 15秒一个完整周期
if cycle_time < 3:
# 正常颜色
if hasattr(self, 'gizmoXAxis'):
self.setAxisColor("x", (1, 0, 0, 1)) # 红色
self.setAxisColor("y", (0, 1, 0, 1)) # 绿色
self.setAxisColor("z", (0, 0, 1, 1)) # 蓝色
elif cycle_time < 6:
# 全部黄色
if hasattr(self, 'gizmoXAxis'):
self.setAxisColor("x", (1, 1, 0, 1))
self.setAxisColor("y", (1, 1, 0, 1))
self.setAxisColor("z", (1, 1, 0, 1))
elif cycle_time < 9:
# 全部白色
if hasattr(self, 'gizmoXAxis'):
self.setAxisColor("x", (1, 1, 1, 1))
self.setAxisColor("y", (1, 1, 1, 1))
self.setAxisColor("z", (1, 1, 1, 1))
elif cycle_time < 12:
# 全部橙色
if hasattr(self, 'gizmoXAxis'):
self.setAxisColor("x", (1, 0.5, 0, 1))
self.setAxisColor("y", (1, 0.5, 0, 1))
self.setAxisColor("z", (1, 0.5, 0, 1))
else:
# 恢复正常颜色
if hasattr(self, 'gizmoXAxis'):
self.setAxisColor("x", (1, 0, 0, 1)) # 红色
self.setAxisColor("y", (0, 1, 0, 1)) # 绿色
self.setAxisColor("z", (0, 0, 1, 1)) # 蓝色
return task.cont
if __name__ == "__main__":
app = GizmoTest()
app.run()

165
demo/test_2d_gui_fix.py Normal file
View File

@ -0,0 +1,165 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
测试2D GUI组件显示修复
验证按钮标签输入框是否能正常显示
"""
import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)
from direct.showbase.ShowBase import ShowBase
from direct.gui.DirectGui import *
from panda3d.core import *
import os
class GUITestApp(ShowBase):
def __init__(self):
ShowBase.__init__(self)
# 设置背景色
self.setBackgroundColor(0.2, 0.2, 0.3)
# 尝试加载中文字体
self.chinese_font = self.loadChineseFont()
# 创建测试GUI元素
self.createTestGUI()
print("GUI测试应用启动成功!")
print(f"中文字体加载状态: {'成功' if self.chinese_font else '失败,使用默认字体'}")
def loadChineseFont(self):
"""尝试加载中文字体"""
font_paths = [
'/usr/share/fonts/truetype/wqy/wqy-microhei.ttc',
'/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc',
'/usr/share/fonts/truetype/arphic/ukai.ttc',
'/usr/share/fonts/truetype/arphic/uming.ttc',
]
for font_path in font_paths:
if os.path.exists(font_path):
try:
font = self.loader.loadFont(font_path)
if font:
return font
except:
continue
return None
def createTestGUI(self):
"""创建测试GUI元素"""
# 标题
self.title = OnscreenText(
text="2D GUI 显示测试",
pos=(0, 0.9),
scale=0.08,
fg=(1, 1, 1, 1),
align=TextNode.ACenter,
font=self.chinese_font if self.chinese_font else None
)
# 测试不同位置的GUI元素
positions = [
("中心", (0, 0, 0)),
("左侧", (-5, 0, 0)),
("右侧", (5, 0, 0)),
("上方", (0, 0, 5)),
("下方", (0, 0, -5))
]
self.gui_elements = []
y_offset = 0.6
for i, (name, pos) in enumerate(positions):
# 计算屏幕位置(模拟修复后的坐标转换)
gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1)
# 创建按钮
button = DirectButton(
text=f"{name}按钮",
pos=gui_pos,
scale=0.08,
command=self.onButtonClick,
extraArgs=[name],
frameColor=(0.2, 0.6, 0.8, 1),
text_font=self.chinese_font if self.chinese_font else None,
rolloverSound=None,
clickSound=None
)
# 偏移位置创建标签
label_pos = (gui_pos[0], 0, gui_pos[2] - 0.2)
label = DirectLabel(
text=f"{name}标签",
pos=label_pos,
scale=0.06,
frameColor=(0, 0, 0, 0),
text_fg=(1, 1, 0, 1),
text_font=self.chinese_font if self.chinese_font else None
)
# 偏移位置创建输入框
entry_pos = (gui_pos[0], 0, gui_pos[2] - 0.4)
entry = DirectEntry(
text="",
pos=entry_pos,
scale=0.05,
command=self.onEntrySubmit,
extraArgs=[name],
initialText=f"{name}输入框",
numLines=1,
width=12,
focus=0
)
self.gui_elements.extend([button, label, entry])
print(f"创建了 {name} 位置的GUI元素:")
print(f" - 按钮位置: {gui_pos}")
print(f" - 标签位置: {label_pos}")
print(f" - 输入框位置: {entry_pos}")
# 创建退出按钮
self.exitButton = DirectButton(
text="退出测试",
pos=(0, 0, -0.8),
scale=0.06,
command=self.exitTest,
frameColor=(0.8, 0.2, 0.2, 1),
text_font=self.chinese_font if self.chinese_font else None
)
# 状态信息
self.statusText = OnscreenText(
text="如果你能看到各个位置的按钮、标签和输入框,说明修复成功!",
pos=(0, -0.9),
scale=0.04,
fg=(0, 1, 0, 1),
align=TextNode.ACenter,
font=self.chinese_font if self.chinese_font else None
)
def onButtonClick(self, name):
"""按钮点击事件"""
print(f"点击了 {name} 按钮")
self.statusText.setText(f"最后点击: {name} 按钮")
def onEntrySubmit(self, text, name):
"""输入框提交事件"""
print(f"{name} 输入框内容: {text}")
self.statusText.setText(f"{name} 输入框: {text}")
def exitTest(self):
"""退出测试"""
print("退出GUI测试")
self.userExit()
if __name__ == "__main__":
print("启动2D GUI显示测试...")
print("如果修复成功,你应该能看到不同位置的按钮、标签和输入框")
app = GUITestApp()
app.run()

622
demo/test_gizmo_drag.py Normal file
View File

@ -0,0 +1,622 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
测试坐标轴拖拽功能
验证物体移动是否正常工作
"""
import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)
import sys
from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QLabel, QPushButton, QHBoxLayout
from PyQt5.QtCore import Qt
from QPanda3D.QPanda3DWidget import QPanda3DWidget
from QPanda3D.Panda3DWorld import Panda3DWorld
from panda3d.core import *
from direct.task import Task
class GizmoDragTestWorld(Panda3DWorld):
def __init__(self):
super().__init__()
print("=== 坐标轴拖拽测试 ===")
self.setBackgroundColor(0.2, 0.2, 0.3)
# 调整相机位置
self.cam.setPos(10, -20, 10)
self.cam.lookAt(0, 0, 0)
# Qt部件引用
self.qtWidget = None
# 添加基础光照
alight = AmbientLight('alight')
alight.setColor((0.3, 0.3, 0.3, 1))
alnp = self.render.attachNewNode(alight)
self.render.setLight(alnp)
dlight = DirectionalLight('dlight')
dlight.setColor((0.8, 0.8, 0.8, 1))
dlnp = self.render.attachNewNode(dlight)
dlnp.setHpr(45, -45, 0)
self.render.setLight(dlnp)
# 创建测试立方体
self.createTestCube()
# 坐标轴相关变量(复制自主程序)
self.gizmo = None
self.gizmoTarget = None
self.gizmoXAxis = None
self.gizmoYAxis = None
self.gizmoZAxis = None
self.axis_length = 3.0
self.isDraggingGizmo = False
self.dragGizmoAxis = None
self.dragStartMousePos = None
self.gizmoTargetStartPos = None
self.gizmoHighlightAxis = None
self.gizmo_colors = {
"x": (1, 0, 0, 1), # 红色
"y": (0, 1, 0, 1), # 绿色
"z": (0, 0, 1, 1) # 蓝色
}
self.gizmo_highlight_colors = {
"x": (1, 1, 0, 1), # 黄色高亮
"y": (1, 1, 0, 1),
"z": (1, 1, 0, 1)
}
# 当前工具
self.currentTool = "移动"
# 自动选中立方体并创建坐标轴
self.selectedNode = self.testCube
self.createGizmo(self.testCube)
print("测试说明:")
print("1. 立方体已选中,坐标轴已显示")
print("2. 鼠标悬停在坐标轴上应该变为黄色")
print("3. 点击并拖拽坐标轴应该移动立方体")
print("4. 观看控制台输出以了解拖拽过程")
def setQtWidget(self, widget):
"""设置Qt部件引用"""
self.qtWidget = widget
print(f"✓ 设置Qt部件引用")
def getWindowSize(self):
"""获取准确的窗口尺寸"""
if self.qtWidget:
width, height = self.qtWidget.getActualSize()
if width > 0 and height > 0:
return width, height
if hasattr(self, 'win') and self.win:
width = self.win.getXSize()
height = self.win.getYSize()
return width, height
return 800, 600
def createTestCube(self):
"""创建测试立方体"""
from panda3d.core import CardMaker
# 创建一个简单的立方体
cm = CardMaker('cube')
cm.setFrame(-1, 1, -1, 1)
# 创建6个面
self.testCube = self.render.attachNewNode("testCube")
# 前面(红色)
front = self.testCube.attachNewNode(cm.generate())
front.setPos(0, 1, 0)
front.setColor(1, 0.5, 0.5, 1)
# 后面(绿色)
back = self.testCube.attachNewNode(cm.generate())
back.setPos(0, -1, 0)
back.setHpr(0, 0, 180)
back.setColor(0.5, 1, 0.5, 1)
# 左面(蓝色)
left = self.testCube.attachNewNode(cm.generate())
left.setPos(-1, 0, 0)
left.setHpr(0, 0, 90)
left.setColor(0.5, 0.5, 1, 1)
# 右面(黄色)
right = self.testCube.attachNewNode(cm.generate())
right.setPos(1, 0, 0)
right.setHpr(0, 0, -90)
right.setColor(1, 1, 0.5, 1)
# 顶面(紫色)
top = self.testCube.attachNewNode(cm.generate())
top.setPos(0, 0, 1)
top.setHpr(-90, 0, 0)
top.setColor(1, 0.5, 1, 1)
# 底面(青色)
bottom = self.testCube.attachNewNode(cm.generate())
bottom.setPos(0, 0, -1)
bottom.setHpr(90, 0, 0)
bottom.setColor(0.5, 1, 1, 1)
print(f"✓ 创建测试立方体,位置: {self.testCube.getPos()}")
return self.testCube
# 从主程序复制的坐标轴相关方法
def createGizmo(self, nodePath):
"""为选中的节点创建坐标轴工具"""
try:
if self.gizmo:
self.gizmo.removeNode()
self.gizmo = None
if not nodePath:
return
self.gizmo = self.render.attachNewNode("gizmo")
self.gizmoTarget = nodePath
# 将坐标轴放在实体上方
bounds = nodePath.getBounds()
if bounds and not bounds.isEmpty():
center = bounds.getCenter()
maxPoint = bounds.getMax()
gizmo_pos = Point3(center.x, center.y, maxPoint.z + 2.0)
self.gizmo.setPos(gizmo_pos)
self.createGizmoGeometry()
print(f"✓ 为节点 {nodePath.getName()} 创建了坐标轴")
except Exception as e:
print(f"创建坐标轴失败: {str(e)}")
def createGizmoGeometry(self):
"""创建坐标轴的几何体"""
try:
if not self.gizmo:
return
from panda3d.core import LineSegs
# 创建X轴红色
x_lines = LineSegs()
x_lines.setThickness(4.0)
x_lines.setColor(1, 0, 0, 1)
x_lines.moveTo(0, 0, 0)
x_lines.drawTo(self.axis_length, 0, 0)
# 箭头
x_lines.moveTo(self.axis_length - 0.5, -0.2, 0)
x_lines.drawTo(self.axis_length, 0, 0)
x_lines.drawTo(self.axis_length - 0.5, 0.2, 0)
self.gizmoXAxis = self.gizmo.attachNewNode(x_lines.create())
self.gizmoXAxis.setName("gizmo_x_axis")
# 创建Y轴绿色
y_lines = LineSegs()
y_lines.setThickness(4.0)
y_lines.setColor(0, 1, 0, 1)
y_lines.moveTo(0, 0, 0)
y_lines.drawTo(0, self.axis_length, 0)
# 箭头
y_lines.moveTo(-0.2, self.axis_length - 0.5, 0)
y_lines.drawTo(0, self.axis_length, 0)
y_lines.drawTo(0.2, self.axis_length - 0.5, 0)
self.gizmoYAxis = self.gizmo.attachNewNode(y_lines.create())
self.gizmoYAxis.setName("gizmo_y_axis")
# 创建Z轴蓝色
z_lines = LineSegs()
z_lines.setThickness(4.0)
z_lines.setColor(0, 0, 1, 1)
z_lines.moveTo(0, 0, 0)
z_lines.drawTo(0, 0, self.axis_length)
# 箭头
z_lines.moveTo(-0.2, 0, self.axis_length - 0.5)
z_lines.drawTo(0, 0, self.axis_length)
z_lines.drawTo(0.2, 0, self.axis_length - 0.5)
self.gizmoZAxis = self.gizmo.attachNewNode(z_lines.create())
self.gizmoZAxis.setName("gizmo_z_axis")
# 确保坐标轴不被光照影响
self.gizmoXAxis.setLightOff()
self.gizmoYAxis.setLightOff()
self.gizmoZAxis.setLightOff()
except Exception as e:
print(f"创建坐标轴几何体失败: {str(e)}")
# 主程序的鼠标事件处理方法(简化版)
def mousePressEventLeft(self, evt):
"""处理鼠标左键按下事件"""
print(f"\n=== 鼠标左键点击 ===")
if not evt:
return
x = evt.get('x', 0)
y = evt.get('y', 0)
print(f"鼠标位置: ({x}, {y})")
# 检查是否点击了坐标轴
if self.currentTool == "移动" and self.gizmo:
gizmoAxis = self.checkGizmoClick(x, y)
if gizmoAxis:
print(f"✓ 检测到坐标轴点击: {gizmoAxis}")
self.startGizmoDrag(gizmoAxis, x, y)
return
else:
print("× 没有点击到坐标轴")
def mouseReleaseEventLeft(self, evt):
"""处理鼠标左键释放事件"""
if self.isDraggingGizmo:
self.stopGizmoDrag()
def mouseMoveEvent(self, evt):
"""处理鼠标移动事件"""
if not evt:
return
# 处理坐标轴拖拽
if self.isDraggingGizmo:
x = evt.get('x', 0)
y = evt.get('y', 0)
self.updateGizmoDrag(x, y)
return
# 更新坐标轴高亮
if self.currentTool == "移动" and self.gizmo and not self.isDraggingGizmo:
x = evt.get('x', 0)
y = evt.get('y', 0)
self.updateGizmoHighlight(x, y)
# 从主程序复制的方法(这里需要包含所有坐标轴相关的方法)
# [为了简洁,这里只展示核心方法,实际应该包含所有方法]
def checkGizmoClick(self, mouseX, mouseY):
"""简化的坐标轴点击检测"""
if not self.gizmo or not self.gizmoTarget:
return None
try:
print(f"检查坐标轴点击: 鼠标({mouseX}, {mouseY})")
# 获取坐标轴中心的世界坐标
gizmo_world_pos = self.gizmo.getPos(self.render)
print(f"坐标轴世界位置: {gizmo_world_pos}")
# 计算各轴端点的世界坐标
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)
def worldToScreen(world_pos):
cam_pos = self.cam.getRelativePoint(self.render, world_pos)
if cam_pos.getY() <= 0:
return None
screen_pos = Point2()
if self.cam.node().getLens().project(cam_pos, screen_pos):
win_width, win_height = self.getWindowSize()
win_x = (screen_pos.getX() + 1.0) * 0.5 * win_width
win_y = (1.0 - screen_pos.getY()) * 0.5 * win_height
return (win_x, win_y)
return None
center_screen = worldToScreen(gizmo_world_pos)
x_screen = worldToScreen(x_end)
y_screen = worldToScreen(y_end)
z_screen = worldToScreen(z_end)
if not center_screen:
return None
print(f"中心屏幕坐标: {center_screen}")
print(f"X轴端点屏幕坐标: {x_screen}")
click_threshold = 30
# 简化的距离检测
def distance_to_line(point, line_start, line_end):
import math
px, py = point
x1, y1 = line_start
x2, y2 = line_end
line_length_sq = (x2 - x1) ** 2 + (y2 - y1) ** 2
if line_length_sq < 0.001:
return math.sqrt((px - x1) ** 2 + (py - y1) ** 2)
t = max(0, min(1, ((px - x1) * (x2 - x1) + (py - y1) * (y2 - y1)) / line_length_sq))
projection_x = x1 + t * (x2 - x1)
projection_y = y1 + t * (y2 - y1)
distance = math.sqrt((px - projection_x) ** 2 + (py - projection_y) ** 2)
return distance
# 检测各个轴
axes_data = [
("x", x_screen, "X轴"),
("y", y_screen, "Y轴"),
("z", z_screen, "Z轴")
]
for axis_name, axis_screen, axis_label in axes_data:
if axis_screen:
distance = distance_to_line(
(mouseX, mouseY), center_screen, axis_screen
)
print(f"{axis_label}距离: {distance:.2f}")
if distance < click_threshold:
print(f"✓ 点击了{axis_label}")
return axis_name
print("× 没有点击任何轴")
return None
except Exception as e:
print(f"坐标轴点击检测失败: {str(e)}")
return None
def startGizmoDrag(self, axis, mouseX, mouseY):
"""开始坐标轴拖拽"""
try:
self.isDraggingGizmo = True
self.dragGizmoAxis = axis
self.dragStartMousePos = (mouseX, mouseY)
# 保存开始拖拽时目标节点的位置
if self.gizmoTarget:
self.gizmoTargetStartPos = self.gizmoTarget.getPos()
print(f"✓ 开始拖拽 {axis} 轴,开始位置: {self.gizmoTargetStartPos}")
except Exception as e:
print(f"开始坐标轴拖拽失败: {str(e)}")
def updateGizmoDrag(self, mouseX, mouseY):
"""更新坐标轴拖拽"""
try:
if not self.isDraggingGizmo or not self.gizmoTarget or not hasattr(self, 'dragStartMousePos'):
return
# 计算鼠标移动距离(屏幕像素)
mouseDeltaX = mouseX - self.dragStartMousePos[0]
mouseDeltaY = mouseY - self.dragStartMousePos[1]
# 获取坐标轴在屏幕空间的方向向量
gizmo_world_pos = self.gizmoTargetStartPos
if self.dragGizmoAxis == "x":
axis_end = gizmo_world_pos + Vec3(1, 0, 0)
elif self.dragGizmoAxis == "y":
axis_end = gizmo_world_pos + Vec3(0, 1, 0)
elif self.dragGizmoAxis == "z":
axis_end = gizmo_world_pos + Vec3(0, 0, 1)
else:
return
# 投影到屏幕空间
def worldToScreen(worldPos):
# 先转换为相机坐标系
camPos = self.cam.getRelativePoint(self.render, worldPos)
# 检查是否在相机前方
if camPos.getY() <= 0:
return None
screenPos = Point2()
if self.cam.node().getLens().project(camPos, screenPos):
# 获取准确的窗口尺寸
winWidth, winHeight = self.getWindowSize()
winX = (screenPos.x + 1) * 0.5 * winWidth
winY = (1 - screenPos.y) * 0.5 * winHeight
return (winX, winY)
return None
gizmo_screen = worldToScreen(gizmo_world_pos)
axis_screen = worldToScreen(axis_end)
if not gizmo_screen or not axis_screen:
return
# 计算轴在屏幕空间的方向向量
screen_axis_dir = (
axis_screen[0] - gizmo_screen[0],
axis_screen[1] - gizmo_screen[1]
)
# 归一化屏幕轴方向
import math
length = math.sqrt(screen_axis_dir[0]**2 + screen_axis_dir[1]**2)
if length > 0:
screen_axis_dir = (screen_axis_dir[0] / length, screen_axis_dir[1] / length)
else:
return
# 将鼠标移动投影到轴方向上
projected_distance = (mouseDeltaX * screen_axis_dir[0] +
mouseDeltaY * screen_axis_dir[1])
# 转换投影距离为世界坐标移动距离
scale_factor = 0.01 # 可以调整这个值来改变拖拽灵敏度
if self.dragGizmoAxis == "x":
movement = Vec3(projected_distance * scale_factor, 0, 0)
elif self.dragGizmoAxis == "y":
movement = Vec3(0, projected_distance * scale_factor, 0)
elif self.dragGizmoAxis == "z":
movement = Vec3(0, 0, projected_distance * scale_factor)
# 应用移动到目标节点
newPos = self.gizmoTargetStartPos + movement
self.gizmoTarget.setPos(newPos)
# 添加调试输出(只在前几次输出,避免刷屏)
if not hasattr(self, '_drag_debug_count'):
self._drag_debug_count = 0
if self._drag_debug_count < 3:
print(f"拖拽调试 - 轴: {self.dragGizmoAxis}")
print(f" 鼠标移动: ({mouseDeltaX:.1f}, {mouseDeltaY:.1f})")
print(f" 投影距离: {projected_distance:.3f}")
print(f" 世界移动: {movement}")
print(f" 开始位置: {self.gizmoTargetStartPos}")
print(f" 新位置: {newPos}")
print(f" 实际设置位置: {self.gizmoTarget.getPos()}")
self._drag_debug_count += 1
except Exception as e:
print(f"更新坐标轴拖拽失败: {str(e)}")
def stopGizmoDrag(self):
"""停止坐标轴拖拽"""
self.isDraggingGizmo = False
self.dragGizmoAxis = None
self.dragStartMousePos = None
self.gizmoTargetStartPos = None
# 重置调试计数器
if hasattr(self, '_drag_debug_count'):
self._drag_debug_count = 0
print("✓ 停止坐标轴拖拽")
def updateGizmoHighlight(self, mouseX, mouseY):
"""更新坐标轴高亮状态(简化版)"""
if not self.gizmo or self.isDraggingGizmo:
return
# 检测鼠标悬停的轴
hoveredAxis = self.checkGizmoClick(mouseX, mouseY)
# 如果高亮状态发生变化
if hoveredAxis != self.gizmoHighlightAxis:
# 恢复之前高亮的轴
if self.gizmoHighlightAxis:
self.setGizmoAxisColor(self.gizmoHighlightAxis, self.gizmo_colors[self.gizmoHighlightAxis])
# 高亮新的轴
if hoveredAxis:
self.setGizmoAxisColor(hoveredAxis, self.gizmo_highlight_colors[hoveredAxis])
self.gizmoHighlightAxis = hoveredAxis
def setGizmoAxisColor(self, axis, color):
"""设置坐标轴颜色"""
try:
if axis == "x" and self.gizmoXAxis:
self.gizmoXAxis.setColor(*color)
elif axis == "y" and self.gizmoYAxis:
self.gizmoYAxis.setColor(*color)
elif axis == "z" and self.gizmoZAxis:
self.gizmoZAxis.setColor(*color)
except Exception as e:
print(f"设置坐标轴颜色失败: {str(e)}")
class CustomGizmoDragTestWidget(QPanda3DWidget):
"""支持坐标轴拖拽测试的部件"""
def __init__(self, world, parent=None):
super().__init__(world, parent)
self.world = world
if hasattr(world, 'setQtWidget'):
world.setQtWidget(self)
def getActualSize(self):
return (self.width(), self.height())
def mousePressEvent(self, event):
"""处理Qt鼠标按下事件"""
if event.button() == Qt.LeftButton:
self.world.mousePressEventLeft({
'x': event.x(),
'y': event.y()
})
event.accept()
def mouseReleaseEvent(self, event):
"""处理Qt鼠标释放事件"""
if event.button() == Qt.LeftButton:
self.world.mouseReleaseEventLeft({
'x': event.x(),
'y': event.y()
})
event.accept()
def mouseMoveEvent(self, event):
"""处理Qt鼠标移动事件"""
self.world.mouseMoveEvent({
'x': event.x(),
'y': event.y()
})
event.accept()
def main():
print("启动坐标轴拖拽测试...")
app = QApplication(sys.argv)
# 创建主窗口
mainWindow = QMainWindow()
mainWindow.setWindowTitle("坐标轴拖拽测试")
mainWindow.setGeometry(100, 100, 900, 700)
# 创建布局
centralWidget = QWidget()
layout = QVBoxLayout(centralWidget)
# 添加说明
infoLayout = QHBoxLayout()
label = QLabel("坐标轴拖拽测试 - 立方体已选中,尝试拖拽红色/绿色/蓝色坐标轴")
label.setAlignment(Qt.AlignCenter)
label.setStyleSheet("background-color: lightcyan; padding: 10px; font-size: 12px;")
infoLayout.addWidget(label)
resetBtn = QPushButton("重置立方体位置")
infoLayout.addWidget(resetBtn)
layout.addLayout(infoLayout)
# 创建世界
world = GizmoDragTestWorld()
# 连接重置按钮
def resetCube():
world.testCube.setPos(0, 0, 0)
print("立方体位置已重置")
resetBtn.clicked.connect(resetCube)
# 创建测试部件
pandaWidget = CustomGizmoDragTestWidget(world)
layout.addWidget(pandaWidget)
mainWindow.setCentralWidget(centralWidget)
mainWindow.show()
print("\n使用说明:")
print("1. 立方体已选中,红/绿/蓝坐标轴应该可见")
print("2. 鼠标悬停在坐标轴上应该变为黄色")
print("3. 点击并拖拽坐标轴,立方体应该沿对应轴移动")
print("4. 观察控制台输出了解拖拽过程")
return app.exec_()
if __name__ == "__main__":
sys.exit(main())

View File

@ -0,0 +1,210 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
GraphicsBuffer DirectGUI专门测试
验证DirectGUI在GraphicsBuffer中的行为
"""
import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)
import sys
from direct.showbase.ShowBase import ShowBase
from direct.gui.DirectGui import *
from direct.gui.OnscreenText import OnscreenText
from panda3d.core import *
class GraphicsBufferGUITest(ShowBase):
def __init__(self):
ShowBase.__init__(self)
print("=== GraphicsBuffer DirectGUI测试 ===")
# 设置背景色
self.setBackgroundColor(0.2, 0.2, 0.3)
# 创建一个GraphicsBuffer来模拟Qt环境
self.createGraphicsBuffer()
# 在主窗口中创建GUI (作为对照)
self.createMainWindowGUI()
# 在GraphicsBuffer中创建GUI (测试目标)
self.createBufferGUI()
print("\n测试说明:")
print("这个测试创建了GraphicsBuffer来模拟Qt环境")
print("比较主窗口和GraphicsBuffer中DirectGUI的行为差异")
def createGraphicsBuffer(self):
"""创建GraphicsBuffer"""
print("\n--- 创建GraphicsBuffer ---")
# 创建缓冲区
fbp = FrameBufferProperties()
wp = WindowProperties.size(800, 600)
wp.setUndecorated(True)
self.buffer = self.graphicsEngine.makeOutput(
self.pipe,
"test-buffer",
-100, # sort
fbp,
wp,
GraphicsPipe.BFRequireWindow # flags
)
if not self.buffer:
print("❌ 创建GraphicsBuffer失败")
return
print(f"✓ GraphicsBuffer创建成功: {self.buffer}")
print(f" 类型: {type(self.buffer)}")
print(f" 大小: {self.buffer.getXSize()} x {self.buffer.getYSize()}")
# 为缓冲区创建显示区域
self.bufferDR = self.buffer.makeDisplayRegion()
print(f"✓ 显示区域创建: {self.bufferDR}")
# 创建专用的2D相机给GraphicsBuffer
self.bufferCamera2d = self.makeCamera2d(self.buffer)
print(f"✓ 2D相机创建: {self.bufferCamera2d}")
# 设置相机
self.bufferDR.setCamera(self.bufferCamera2d)
# 获取render2d节点 - 这是关键
self.bufferRender2d = self.bufferCamera2d.getParent()
print(f"✓ 缓冲区render2d: {self.bufferRender2d}")
# 获取aspect2d节点
self.bufferAspect2d = self.bufferRender2d.find("**/aspect2d")
if self.bufferAspect2d.isEmpty():
print("⚠️ 缓冲区中没有找到aspect2d手动创建")
self.bufferAspect2d = self.bufferRender2d.attachNewNode("aspect2d")
else:
print(f"✓ 缓冲区aspect2d: {self.bufferAspect2d}")
def createMainWindowGUI(self):
"""在主窗口中创建GUI对照组"""
print("\n--- 主窗口GUI对照组---")
self.mainTitle = OnscreenText(
text="主窗口GUI正常",
pos=(-0.5, 0.8),
scale=0.08,
fg=(1, 1, 1, 1),
align=TextNode.ACenter
)
self.mainButton = DirectButton(
text="主窗口按钮",
pos=(-0.5, 0, 0.5),
scale=0.1,
frameColor=(0, 1, 0, 1),
command=self.onMainButtonClick
)
print(f"✓ 主窗口标题: {self.mainTitle}")
print(f"✓ 主窗口按钮: {self.mainButton}")
print(f" 按钮父节点: {self.mainButton.getParent()}")
def createBufferGUI(self):
"""在GraphicsBuffer中创建GUI测试组"""
print("\n--- GraphicsBuffer GUI测试组---")
if not hasattr(self, 'bufferAspect2d'):
print("❌ 缓冲区aspect2d不可用")
return
# 方法1: 直接在缓冲区的aspect2d下创建
try:
self.bufferTitle = OnscreenText(
text="缓冲区GUI测试",
pos=(0.5, 0.8),
scale=0.08,
fg=(1, 1, 0, 1),
align=TextNode.ACenter,
parent=self.bufferAspect2d # 指定父节点
)
print(f"✓ 缓冲区标题: {self.bufferTitle}")
print(f" 标题父节点: {self.bufferTitle.getParent()}")
except Exception as e:
print(f"❌ 缓冲区标题创建失败: {str(e)}")
# 方法2: 创建DirectGUI组件并重新父化
try:
self.bufferButton = DirectButton(
text="缓冲区按钮",
pos=(0.5, 0, 0.5),
scale=0.1,
frameColor=(1, 0, 0, 1),
command=self.onBufferButtonClick
)
# 重新父化到缓冲区的aspect2d
self.bufferButton.reparentTo(self.bufferAspect2d)
print(f"✓ 缓冲区按钮: {self.bufferButton}")
print(f" 按钮父节点: {self.bufferButton.getParent()}")
except Exception as e:
print(f"❌ 缓冲区按钮创建失败: {str(e)}")
# 方法3: 直接在bufferRender2d下创建TextNode
try:
textNode = TextNode('buffer-direct-text')
textNode.setText("直接创建的文本")
textNode.setAlign(TextNode.ACenter)
self.bufferDirectText = self.bufferRender2d.attachNewNode(textNode)
self.bufferDirectText.setPos(0.5, 0, 0)
self.bufferDirectText.setScale(0.08)
self.bufferDirectText.setColor(0, 1, 1, 1)
print(f"✓ 缓冲区直接文本: {self.bufferDirectText}")
except Exception as e:
print(f"❌ 缓冲区直接文本创建失败: {str(e)}")
# 检查缓冲区状态
self.checkBufferState()
def checkBufferState(self):
"""检查缓冲区状态"""
print("\n--- 缓冲区状态检查 ---")
if hasattr(self, 'buffer'):
print(f"缓冲区有效: {self.buffer.isValid()}")
print(f"缓冲区激活: {self.buffer.isActive()}")
print(f"显示区域数量: {self.buffer.getNumDisplayRegions()}")
for i in range(self.buffer.getNumDisplayRegions()):
dr = self.buffer.getDisplayRegion(i)
print(f" 显示区域 {i}: {dr}")
print(f" 相机: {dr.getCamera()}")
print(f" 激活: {dr.isActive()}")
if hasattr(self, 'bufferAspect2d'):
print(f"缓冲区aspect2d子节点数: {self.bufferAspect2d.getNumChildren()}")
for i in range(self.bufferAspect2d.getNumChildren()):
child = self.bufferAspect2d.getChild(i)
print(f" 子节点 {i}: {child}")
def onMainButtonClick(self):
"""主窗口按钮点击"""
print("🟢 主窗口按钮被点击!")
def onBufferButtonClick(self):
"""缓冲区按钮点击"""
print("🔴 缓冲区按钮被点击!")
def main():
"""主函数"""
print("启动GraphicsBuffer DirectGUI专门测试...")
app = GraphicsBufferGUITest()
app.run()
if __name__ == "__main__":
main()

311
demo/test_gui_complete.py Normal file
View File

@ -0,0 +1,311 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
完整的GUI功能测试
测试主程序中的所有GUI创建和编辑功能
"""
import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)
import sys
import builtins
from PyQt5.QtWidgets import (QApplication, QMainWindow, QMenuBar, QMenu, QAction,
QDockWidget, QTreeWidget, QListWidget, QWidget, QVBoxLayout, QTreeWidgetItem,
QLabel, QLineEdit, QFormLayout, QDoubleSpinBox, QScrollArea, QTreeView, QInputDialog, QFileDialog, QMessageBox, QDialog, QGroupBox, QHBoxLayout, QPushButton, QDialogButtonBox)
from PyQt5.QtCore import Qt, QDir, QUrl
from PyQt5.QtGui import QDrag, QPainter, QPixmap
from QPanda3D.QPanda3DWidget import QPanda3DWidget
from QPanda3D.Panda3DWorld import Panda3DWorld
from direct.gui.DirectGui import *
from direct.gui.OnscreenText import OnscreenText
from panda3d.core import *
import os
# 简化的世界类专门用于GUI测试
class GUITestWorld(Panda3DWorld):
def __init__(self):
super().__init__()
# 设置相机位置
self.cam.setPos(0, -10, 5)
self.cam.lookAt(0, 0, 0)
# 设置背景色
self.setBackgroundColor(0.2, 0.2, 0.3)
self.gui_elements = []
# 尝试加载中文字体
try:
self.chinese_font = self.loader.loadFont('/usr/share/fonts/truetype/wqy/wqy-microhei.ttc')
if not self.chinese_font:
print("警告: 无法加载中文字体,将使用默认字体")
except:
print("警告: 无法加载中文字体,将使用默认字体")
self.chinese_font = None
# 延迟创建GUI元素确保所有系统都已初始化
self.taskMgr.doMethodLater(0.1, self.createTestGUIElements, "create-gui")
def createTestGUIElements(self, task=None):
"""创建测试用的GUI元素"""
print("\n=== 开始创建测试GUI元素 ===")
# 测试2D GUI组件
print("创建2D GUI组件...")
# 按钮测试 - 不同位置
self.button1 = self.createGUIButton((0, 0, 0), "中心按钮", 0.08)
self.button2 = self.createGUIButton((-5, 0, 0), "左侧按钮", 0.08)
self.button3 = self.createGUIButton((5, 0, 0), "右侧按钮", 0.08)
self.button4 = self.createGUIButton((0, 0, 5), "上方按钮", 0.08)
self.button5 = self.createGUIButton((0, 0, -5), "下方按钮", 0.08)
# 标签测试
self.label1 = self.createGUILabel((0, 0, -8), "测试标签", 0.06)
self.label2 = self.createGUILabel((-8, 0, 0), "左标签", 0.06)
self.label3 = self.createGUILabel((8, 0, 0), "右标签", 0.06)
# 输入框测试
self.entry1 = self.createGUIEntry((0, 0, -12), "中心输入框", 0.05)
self.entry2 = self.createGUIEntry((-8, 0, -8), "左输入框", 0.05)
# 测试3D GUI组件
print("创建3D GUI组件...")
# 3D文本测试
self.text3d1 = self.createGUI3DText((0, 5, 2), "3D测试文本", 0.5)
self.text3d2 = self.createGUI3DText((-3, 3, 1), "左3D文本", 0.3)
self.text3d3 = self.createGUI3DText((3, 3, 1), "右3D文本", 0.3)
# 虚拟屏幕测试
self.screen1 = self.createGUIVirtualScreen((0, 8, 0), (3, 2), "主虚拟屏幕")
self.screen2 = self.createGUIVirtualScreen((-5, 5, 2), (2, 1), "左屏幕")
self.screen3 = self.createGUIVirtualScreen((5, 5, 2), (2, 1), "右屏幕")
print(f"=== GUI元素创建完成总计: {len(self.gui_elements)} 个 ===\n")
# 验证每个元素的显示状态
self.verifyGUIElements()
# 添加标题
self.title = OnscreenText(
text="Qt集成GUI测试",
pos=(0, 0.9),
scale=0.08,
fg=(1, 1, 1, 1),
align=TextNode.ACenter,
font=self.chinese_font if self.chinese_font else None
)
return task.done if task else None
def verifyGUIElements(self):
"""验证GUI元素的显示状态"""
print("=== 验证GUI元素显示状态 ===")
for i, element in enumerate(self.gui_elements):
gui_type = element.getTag("gui_type") if hasattr(element, 'getTag') else "unknown"
gui_text = element.getTag("gui_text") if hasattr(element, 'getTag') else "unnamed"
if hasattr(element, 'getPos'):
pos = element.getPos()
print(f"{i+1}. {gui_type}: '{gui_text}' 位置: {pos}")
else:
print(f"{i+1}. {gui_type}: '{gui_text}' (无位置信息)")
print("=== 验证完成 ===\n")
# 复制主程序中的GUI创建方法
def createGUIButton(self, pos=(0, 0, 0), text="按钮", size=0.1):
"""创建2D GUI按钮"""
from direct.gui.DirectGui import DirectButton
# 将3D坐标转换为2D屏幕坐标
gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1)
try:
button = DirectButton(
text=text,
pos=gui_pos,
scale=size,
command=self.onGUIButtonClick,
extraArgs=[f"button_{len(self.gui_elements)}"],
frameColor=(0.2, 0.6, 0.8, 1),
text_font=self.chinese_font if self.chinese_font else None,
rolloverSound=None,
clickSound=None
)
button.setTag("gui_type", "button")
button.setTag("gui_id", f"button_{len(self.gui_elements)}")
button.setTag("gui_text", text)
self.gui_elements.append(button)
print(f"✓ 创建GUI按钮成功: {text} 在位置 {gui_pos}")
print(f" 按钮节点: {button}")
print(f" 按钮父节点: {button.getParent()}")
# 验证按钮是否真的可见
if hasattr(button, 'isHidden'):
print(f" 按钮隐藏状态: {button.isHidden()}")
return button
except Exception as e:
print(f"✗ 创建GUI按钮失败: {text} - 错误: {str(e)}")
return None
def createGUILabel(self, pos=(0, 0, 0), text="标签", size=0.08):
"""创建2D GUI标签"""
from direct.gui.DirectGui import DirectLabel
gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1)
label = DirectLabel(
text=text,
pos=gui_pos,
scale=size,
frameColor=(0, 0, 0, 0),
text_fg=(1, 1, 1, 1),
text_font=self.chinese_font if self.chinese_font else None
)
label.setTag("gui_type", "label")
label.setTag("gui_id", f"label_{len(self.gui_elements)}")
label.setTag("gui_text", text)
self.gui_elements.append(label)
print(f"创建GUI标签: {text} 在位置 {gui_pos}")
return label
def createGUIEntry(self, pos=(0, 0, 0), placeholder="输入文本...", size=0.08):
"""创建2D GUI文本输入框"""
from direct.gui.DirectGui import DirectEntry
gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1)
entry = DirectEntry(
text="",
pos=gui_pos,
scale=size,
command=self.onGUIEntrySubmit,
extraArgs=[f"entry_{len(self.gui_elements)}"],
initialText=placeholder,
numLines=1,
width=12,
focus=0
)
entry.setTag("gui_type", "entry")
entry.setTag("gui_id", f"entry_{len(self.gui_elements)}")
entry.setTag("gui_placeholder", placeholder)
self.gui_elements.append(entry)
print(f"创建GUI输入框: {placeholder} 在位置 {gui_pos}")
return entry
def createGUI3DText(self, pos=(0, 0, 0), text="3D文本", size=0.5):
"""创建3D空间文本"""
from panda3d.core import TextNode
textNode = TextNode(f'3d-text-{len(self.gui_elements)}')
textNode.setText(text)
textNode.setAlign(TextNode.ACenter)
if self.chinese_font:
textNode.setFont(self.chinese_font)
textNodePath = self.render.attachNewNode(textNode)
textNodePath.setPos(*pos)
textNodePath.setScale(size)
textNodePath.setColor(1, 1, 0, 1)
textNodePath.setBillboardAxis()
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")
self.gui_elements.append(textNodePath)
print(f"创建3D文本: {text} 在位置 {pos}")
return textNodePath
def createGUIVirtualScreen(self, pos=(0, 0, 0), size=(2, 1), text="虚拟屏幕"):
"""创建3D虚拟屏幕"""
from panda3d.core import CardMaker, TransparencyAttrib, TextNode
cm = CardMaker(f"virtual-screen-{len(self.gui_elements)}")
cm.setFrame(-size[0]/2, size[0]/2, -size[1]/2, size[1]/2)
virtualScreen = self.render.attachNewNode(cm.generate())
virtualScreen.setPos(*pos)
virtualScreen.setColor(0.2, 0.2, 0.2, 0.8)
virtualScreen.setTransparency(TransparencyAttrib.MAlpha)
screenText = TextNode(f'screen-text-{len(self.gui_elements)}')
screenText.setText(text)
screenText.setAlign(TextNode.ACenter)
if self.chinese_font:
screenText.setFont(self.chinese_font)
screenTextNP = virtualScreen.attachNewNode(screenText)
screenTextNP.setPos(0, 0.01, 0)
screenTextNP.setScale(0.3)
screenTextNP.setColor(0, 1, 0, 1)
virtualScreen.setTag("gui_type", "virtual_screen")
virtualScreen.setTag("gui_id", f"virtual_screen_{len(self.gui_elements)}")
virtualScreen.setTag("gui_text", text)
virtualScreen.setTag("is_gui_element", "1")
self.gui_elements.append(virtualScreen)
print(f"创建虚拟屏幕: {text} 在位置 {pos}")
return virtualScreen
def onGUIButtonClick(self, button_id):
"""GUI按钮点击事件处理"""
print(f"✓ GUI按钮点击测试成功: {button_id}")
def onGUIEntrySubmit(self, text, entry_id):
"""GUI输入框提交事件处理"""
print(f"✓ GUI输入框提交测试成功: {entry_id} = {text}")
class GUITestWidget(QPanda3DWidget):
def __init__(self, world, parent=None):
super().__init__(world, parent)
self.world = world
def testMainGUIFeatures():
"""测试主程序的GUI功能"""
print("=== 开始测试主程序GUI功能 ===")
app = QApplication(sys.argv)
# 创建测试世界
world = GUITestWorld()
# 创建主窗口
mainWindow = QMainWindow()
mainWindow.setWindowTitle("GUI功能完整测试")
mainWindow.setGeometry(100, 100, 1200, 800)
# 创建Panda3D部件
pandaWidget = GUITestWidget(world)
mainWindow.setCentralWidget(pandaWidget)
# 显示窗口
mainWindow.show()
print("测试窗口已打开,你应该能看到:")
print("- 5个不同位置的2D按钮可点击")
print("- 3个不同位置的2D标签")
print("- 2个不同位置的2D输入框可输入")
print("- 3个不同位置的3D文本面向相机")
print("- 3个不同位置的3D虚拟屏幕")
print("如果所有元素都可见,说明修复成功!")
# 运行应用
return app.exec_()
if __name__ == "__main__":
exit_code = testMainGUIFeatures()

158
demo/test_gui_edit.py Normal file
View File

@ -0,0 +1,158 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
GUI文本编辑功能测试脚本
"""
import sys
import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)
from direct.showbase.ShowBase import ShowBase
from direct.gui.DirectGui import DirectButton, DirectLabel
from panda3d.core import TextNode, WindowProperties
class GUIEditTest(ShowBase):
def __init__(self):
ShowBase.__init__(self)
# 设置窗口
wp = WindowProperties()
wp.setTitle("GUI文本编辑测试")
self.win.requestProperties(wp)
self.setBackgroundColor(0.2, 0.3, 0.5)
# 创建GUI元素列表
self.gui_elements = []
# 创建测试按钮
self.button = DirectButton(
text="测试按钮",
pos=(0, 0, 0.3),
scale=0.1,
command=self.testButtonEdit,
frameColor=(0.2, 0.6, 0.8, 1)
)
self.button.setTag("gui_type", "button")
self.button.setTag("gui_text", "测试按钮")
self.gui_elements.append(self.button)
# 创建测试标签
self.label = DirectLabel(
text="测试标签",
pos=(0, 0, 0),
scale=0.08,
frameColor=(0, 0, 0, 0),
text_fg=(1, 1, 1, 1)
)
self.label.setTag("gui_type", "label")
self.label.setTag("gui_text", "测试标签")
self.gui_elements.append(self.label)
# 创建3D文本
textNode = TextNode('3d-test-text')
textNode.setText("3D测试文本")
textNode.setAlign(TextNode.ACenter)
self.text3d = self.render.attachNewNode(textNode)
self.text3d.setPos(0, 5, 0)
self.text3d.setScale(0.5)
self.text3d.setColor(1, 1, 0, 1)
self.text3d.setBillboardAxis()
self.text3d.setTag("gui_type", "3d_text")
self.text3d.setTag("gui_text", "3D测试文本")
self.gui_elements.append(self.text3d)
# 测试计数器
self.test_count = 0
print("GUI编辑测试启动")
print("按任意键开始测试文本编辑功能")
# 绑定键盘事件
self.accept("space", self.testGUIEdit)
self.accept("1", self.testButtonTextEdit)
self.accept("2", self.testLabelTextEdit)
self.accept("3", self.test3DTextEdit)
# 显示说明
self.instructions = DirectLabel(
text="按键说明:\nSpace - 循环测试所有GUI\n1 - 测试按钮\n2 - 测试标签\n3 - 测试3D文本",
pos=(-0.9, 0, -0.5),
scale=0.05,
frameColor=(0, 0, 0, 0),
text_fg=(1, 1, 1, 1),
text_align=TextNode.ALeft
)
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_type}: {property_name} = {value}")
if property_name == "text":
if gui_type in ["button", "label"]:
gui_element['text'] = value
print(f"成功更新2D GUI文本: {value}")
elif gui_type == "3d_text":
if isinstance(gui_element.node(), TextNode):
gui_element.node().setText(value)
print(f"成功更新3D文本: {value}")
else:
print(f"警告: {gui_type}不是TextNode类型")
gui_element.setTag("gui_text", value)
return True
except Exception as e:
print(f"编辑GUI元素失败: {str(e)}")
import traceback
traceback.print_exc()
return False
def testGUIEdit(self):
"""循环测试所有GUI元素"""
self.test_count += 1
for i, gui_element in enumerate(self.gui_elements):
gui_type = gui_element.getTag("gui_type")
new_text = f"{gui_type}_测试_{self.test_count}"
print(f"\n测试编辑GUI元素 {i}: {gui_type}")
success = self.editGUIElement(gui_element, "text", new_text)
print(f"结果: {'成功' if success else '失败'}")
print(f"\n{self.test_count} 轮测试完成")
def testButtonTextEdit(self):
"""测试按钮文本编辑"""
new_text = f"按钮_修改_{self.test_count}"
self.test_count += 1
success = self.editGUIElement(self.button, "text", new_text)
print(f"按钮文本编辑: {'成功' if success else '失败'}")
def testLabelTextEdit(self):
"""测试标签文本编辑"""
new_text = f"标签_修改_{self.test_count}"
self.test_count += 1
success = self.editGUIElement(self.label, "text", new_text)
print(f"标签文本编辑: {'成功' if success else '失败'}")
def test3DTextEdit(self):
"""测试3D文本编辑"""
new_text = f"3D文本_修改_{self.test_count}"
self.test_count += 1
success = self.editGUIElement(self.text3d, "text", new_text)
print(f"3D文本编辑: {'成功' if success else '失败'}")
def testButtonEdit(self):
"""按钮点击测试"""
print("测试按钮被点击!")
self.testGUIEdit()
if __name__ == "__main__":
print("启动GUI文本编辑测试...")
test = GUIEditTest()
test.run()

361
demo/test_gui_edit_mode.py Normal file
View File

@ -0,0 +1,361 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
GUI编辑模式功能测试
验证主程序中的GUI编辑模式是否正常工作
"""
import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)
import sys
from direct.showbase.ShowBase import ShowBase
from direct.gui.DirectGui import *
from panda3d.core import *
# 配置Panda3D
loadPrcFileData("", """
win-size 800 600
window-title GUI编辑模式测试
framebuffer-multisample 1
multisamples 2
""")
class GUIEditModeTest(ShowBase):
def __init__(self):
ShowBase.__init__(self)
print("=== GUI编辑模式功能测试 ===")
# 设置背景色
self.setBackgroundColor(0.2, 0.2, 0.3)
# 初始化GUI相关变量
self.gui_elements = []
self.guiEditMode = False
self.guiEditPanel = None
self.currentGUITool = None
self.selectedGUI = None
# 尝试加载中文字体
try:
self.chinese_font = self.loader.loadFont('/usr/share/fonts/truetype/wqy/wqy-microhei.ttc')
if not self.chinese_font:
print("警告: 无法加载中文字体,将使用默认字体")
except:
print("警告: 无法加载中文字体,将使用默认字体")
self.chinese_font = None
# 创建测试说明
self.createInstructions()
# 自动进入GUI编辑模式进行测试
self.enterGUIEditMode()
print("\n=== 测试说明 ===")
print("1. 应该能看到右侧的GUI编辑面板")
print("2. 点击面板中的按钮选择不同工具")
print("3. 点击场景中的位置应该能创建相应的GUI元素")
print("4. 按ESC键退出")
# 绑定退出键
self.accept("escape", self.exitTest)
def createInstructions(self):
"""创建说明文本"""
self.title = OnscreenText(
text="GUI编辑模式功能测试",
pos=(0, 0.9),
scale=0.08,
fg=(1, 1, 1, 1),
align=TextNode.ACenter,
font=self.chinese_font if self.chinese_font else None
)
self.instruction = OnscreenText(
text="右侧应该显示GUI编辑面板\n点击面板中的工具然后点击场景创建GUI元素",
pos=(0, -0.8),
scale=0.05,
fg=(1, 1, 0, 1),
align=TextNode.ACenter,
font=self.chinese_font if self.chinese_font else None
)
def enterGUIEditMode(self):
"""进入GUI编辑模式"""
self.guiEditMode = True
print("\n=== 进入GUI编辑模式 ===")
# 创建GUI编辑面板
self.createGUIEditPanel()
print("GUI编辑模式已激活")
def createGUIEditPanel(self):
"""创建GUI编辑面板"""
# 创建主面板
self.guiEditPanel = DirectFrame(
pos=(0.85, 0, 0),
frameSize=(-0.15, 0.15, -0.9, 0.9),
frameColor=(0.1, 0.1, 0.1, 0.8),
text="GUI编辑器",
text_pos=(0, 0.85),
text_scale=0.05,
text_fg=(1, 1, 1, 1),
text_font=self.chinese_font if self.chinese_font else None
)
# 创建工具按钮
y_pos = 0.7
spacing = 0.12
# 2D GUI工具标签
label_2d = DirectLabel(
parent=self.guiEditPanel,
text="2D GUI",
pos=(0, 0, y_pos),
scale=0.04,
text_fg=(1, 1, 0, 1),
frameColor=(0, 0, 0, 0),
text_font=self.chinese_font if self.chinese_font else None
)
y_pos -= 0.08
# 按钮工具
btn_button = DirectButton(
parent=self.guiEditPanel,
text="按钮",
pos=(0, 0, y_pos),
scale=0.04,
command=self.setGUICreateTool,
extraArgs=["button"],
frameColor=(0.2, 0.6, 0.8, 1),
text_font=self.chinese_font if self.chinese_font else None
)
y_pos -= spacing
# 标签工具
btn_label = DirectButton(
parent=self.guiEditPanel,
text="标签",
pos=(0, 0, y_pos),
scale=0.04,
command=self.setGUICreateTool,
extraArgs=["label"],
frameColor=(0.6, 0.8, 0.2, 1),
text_font=self.chinese_font if self.chinese_font else None
)
y_pos -= spacing
# 输入框工具
btn_entry = DirectButton(
parent=self.guiEditPanel,
text="输入框",
pos=(0, 0, y_pos),
scale=0.04,
command=self.setGUICreateTool,
extraArgs=["entry"],
frameColor=(0.8, 0.6, 0.2, 1),
text_font=self.chinese_font if self.chinese_font else None
)
y_pos -= spacing
# 3D GUI工具标签
label_3d = DirectLabel(
parent=self.guiEditPanel,
text="3D GUI",
pos=(0, 0, y_pos),
scale=0.04,
text_fg=(1, 1, 0, 1),
frameColor=(0, 0, 0, 0),
text_font=self.chinese_font if self.chinese_font else None
)
y_pos -= 0.08
# 3D文本工具
btn_3dtext = DirectButton(
parent=self.guiEditPanel,
text="3D文本",
pos=(0, 0, y_pos),
scale=0.04,
command=self.setGUICreateTool,
extraArgs=["3d_text"],
frameColor=(0.8, 0.2, 0.6, 1),
text_font=self.chinese_font if self.chinese_font else None
)
y_pos -= spacing
# 虚拟屏幕工具
btn_screen = DirectButton(
parent=self.guiEditPanel,
text="虚拟屏幕",
pos=(0, 0, y_pos),
scale=0.04,
command=self.setGUICreateTool,
extraArgs=["virtual_screen"],
frameColor=(0.6, 0.2, 0.8, 1),
text_font=self.chinese_font if self.chinese_font else None
)
y_pos -= spacing
# 退出按钮
btn_exit = DirectButton(
parent=self.guiEditPanel,
text="退出测试",
pos=(0, 0, -0.8),
scale=0.04,
command=self.exitTest,
frameColor=(0.8, 0.2, 0.2, 1),
text_font=self.chinese_font if self.chinese_font else None
)
print("✓ GUI编辑面板创建成功")
def setGUICreateTool(self, tool_type):
"""设置GUI创建工具"""
self.currentGUITool = tool_type
print(f"✓ 选择GUI创建工具: {tool_type}")
# 绑定鼠标点击事件
self.accept("mouse1", self.handleMouseClick)
def handleMouseClick(self):
"""处理鼠标点击"""
if not self.guiEditMode or not self.currentGUITool:
return
# 获取鼠标位置
if not base.mouseWatcherNode.hasMouse():
return
mouse_x = base.mouseWatcherNode.getMouseX()
mouse_y = base.mouseWatcherNode.getMouseY()
print(f"鼠标点击位置: ({mouse_x:.2f}, {mouse_y:.2f})")
# 创建GUI元素
if self.currentGUITool == "button":
self.createGUIButton((mouse_x * 10, 0, mouse_y * 10), f"按钮{len(self.gui_elements)}")
elif self.currentGUITool == "label":
self.createGUILabel((mouse_x * 10, 0, mouse_y * 10), f"标签{len(self.gui_elements)}")
elif self.currentGUITool == "entry":
self.createGUIEntry((mouse_x * 10, 0, mouse_y * 10), f"输入框{len(self.gui_elements)}")
elif self.currentGUITool == "3d_text":
self.createGUI3DText((mouse_x * 5, 0, mouse_y * 5), f"3D文本{len(self.gui_elements)}")
elif self.currentGUITool == "virtual_screen":
self.createGUIVirtualScreen((mouse_x * 5, 5, mouse_y * 5), text=f"屏幕{len(self.gui_elements)}")
def createGUIButton(self, pos=(0, 0, 0), text="按钮"):
"""创建2D GUI按钮"""
gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1)
button = DirectButton(
text=text,
pos=gui_pos,
scale=0.08,
frameColor=(0.2, 0.6, 0.8, 1),
text_font=self.chinese_font if self.chinese_font else None
)
self.gui_elements.append(button)
print(f"✓ 创建GUI按钮: {text} 在位置 {gui_pos}")
return button
def createGUILabel(self, pos=(0, 0, 0), text="标签"):
"""创建2D GUI标签"""
gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1)
label = DirectLabel(
text=text,
pos=gui_pos,
scale=0.06,
frameColor=(0, 0, 0, 0),
text_fg=(1, 1, 1, 1),
text_font=self.chinese_font if self.chinese_font else None
)
self.gui_elements.append(label)
print(f"✓ 创建GUI标签: {text} 在位置 {gui_pos}")
return label
def createGUIEntry(self, pos=(0, 0, 0), placeholder="输入框"):
"""创建2D GUI输入框"""
gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1)
entry = DirectEntry(
text="",
pos=gui_pos,
scale=0.05,
initialText=placeholder,
numLines=1,
width=12,
focus=0
)
self.gui_elements.append(entry)
print(f"✓ 创建GUI输入框: {placeholder} 在位置 {gui_pos}")
return entry
def createGUI3DText(self, pos=(0, 0, 0), text="3D文本"):
"""创建3D文本"""
textNode = TextNode(f'3d-text-{len(self.gui_elements)}')
textNode.setText(text)
textNode.setAlign(TextNode.ACenter)
if self.chinese_font:
textNode.setFont(self.chinese_font)
textNodePath = self.render.attachNewNode(textNode)
textNodePath.setPos(*pos)
textNodePath.setScale(0.5)
textNodePath.setColor(1, 1, 0, 1)
textNodePath.setBillboardAxis()
self.gui_elements.append(textNodePath)
print(f"✓ 创建3D文本: {text} 在位置 {pos}")
return textNodePath
def createGUIVirtualScreen(self, pos=(0, 0, 0), text="虚拟屏幕"):
"""创建3D虚拟屏幕"""
from panda3d.core import CardMaker, TransparencyAttrib
cm = CardMaker(f"virtual-screen-{len(self.gui_elements)}")
cm.setFrame(-1, 1, -0.5, 0.5)
virtualScreen = self.render.attachNewNode(cm.generate())
virtualScreen.setPos(*pos)
virtualScreen.setColor(0.2, 0.2, 0.2, 0.8)
virtualScreen.setTransparency(TransparencyAttrib.MAlpha)
# 添加文本
screenText = TextNode(f'screen-text-{len(self.gui_elements)}')
screenText.setText(text)
screenText.setAlign(TextNode.ACenter)
if self.chinese_font:
screenText.setFont(self.chinese_font)
screenTextNP = virtualScreen.attachNewNode(screenText)
screenTextNP.setPos(0, 0.01, 0)
screenTextNP.setScale(0.3)
screenTextNP.setColor(0, 1, 0, 1)
self.gui_elements.append(virtualScreen)
print(f"✓ 创建虚拟屏幕: {text} 在位置 {pos}")
return virtualScreen
def exitTest(self):
"""退出测试"""
print(f"\n=== 测试完成 ===")
print(f"总共创建了 {len(self.gui_elements)} 个GUI元素")
print("如果能看到GUI编辑面板并成功创建元素说明功能正常!")
self.userExit()
def main():
"""主函数"""
print("启动GUI编辑模式功能测试...")
print("这将测试主程序中新增的GUI编辑模式功能")
# 创建并运行测试
app = GUIEditModeTest()
app.run()
if __name__ == "__main__":
main()

View File

@ -0,0 +1,237 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
独立GUI编辑器测试脚本
测试GUI编辑器窗口的功能
"""
import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)
import sys
from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton, QVBoxLayout, QWidget
from PyQt5.QtCore import Qt
# 模拟world对象
class MockWorld:
def __init__(self):
self.gui_elements = [] # 存储GUI元素
def createGUIButton(self, pos, text, scale):
"""模拟创建GUI按钮"""
print(f"模拟创建按钮: {text} at {pos} scale {scale}")
button = MockGUIElement("button", text, pos, scale)
self.gui_elements.append(button)
return button
def createGUILabel(self, pos, text, scale):
"""模拟创建GUI标签"""
print(f"模拟创建标签: {text} at {pos} scale {scale}")
label = MockGUIElement("label", text, pos, scale)
self.gui_elements.append(label)
return label
def createGUIEntry(self, pos, text, scale):
"""模拟创建GUI输入框"""
print(f"模拟创建输入框: {text} at {pos} scale {scale}")
entry = MockGUIElement("entry", text, pos, scale)
self.gui_elements.append(entry)
return entry
def createGUISlider(self, pos, text, scale):
"""模拟创建GUI滑块"""
print(f"模拟创建滑块: {text} at {pos} scale {scale}")
slider = MockGUIElement("slider", text, pos, scale)
self.gui_elements.append(slider)
return slider
def createGUI3DText(self, pos, text, scale):
"""模拟创建3D文本"""
print(f"模拟创建3D文本: {text} at {pos} scale {scale}")
text3d = MockGUIElement("3d_text", text, pos, scale)
self.gui_elements.append(text3d)
return text3d
def createGUIVirtualScreen(self, pos, size, text):
"""模拟创建虚拟屏幕"""
print(f"模拟创建虚拟屏幕: {text} at {pos} size {size}")
screen = MockGUIElement("virtual_screen", text, pos, 1.0, size=size)
self.gui_elements.append(screen)
return screen
def deleteGUIElement(self, element):
"""模拟删除GUI元素"""
if element in self.gui_elements:
self.gui_elements.remove(element)
print(f"模拟删除GUI元素: {element.text}")
return True
return False
def editGUIElement(self, element, property_name, value):
"""模拟编辑GUI元素"""
if property_name == "text":
element.text = value
print(f"模拟编辑GUI元素文本: {element.text} -> {value}")
elif property_name == "position":
element.pos = value
print(f"模拟编辑GUI元素位置: {element.pos} -> {value}")
elif property_name == "scale":
element.scale = value
print(f"模拟编辑GUI元素缩放: {element.scale} -> {value}")
return True
class MockGUIElement:
"""模拟GUI元素"""
def __init__(self, gui_type, text, pos, scale, size=None):
self.gui_type = gui_type
self.text = text
self.pos = pos
self.scale = scale
self.size = size or (1, 1)
def getTag(self, tag_name):
"""模拟获取标签"""
if tag_name == "gui_type":
return self.gui_type
elif tag_name == "gui_text":
return self.text
return ""
def setTag(self, tag_name, value):
"""模拟设置标签"""
if tag_name == "gui_text":
self.text = value
def getPos(self):
"""模拟获取位置"""
return MockVector3(self.pos)
def setPos(self, *args):
"""模拟设置位置"""
if len(args) == 1:
self.pos = (args[0].getX(), args[0].getY(), args[0].getZ())
else:
self.pos = args[:3]
def getScale(self):
"""模拟获取缩放"""
return MockVector3((self.scale, self.scale, self.scale))
def setScale(self, *args):
"""模拟设置缩放"""
if len(args) == 1:
if hasattr(args[0], 'getX'):
self.scale = args[0].getX()
else:
self.scale = args[0]
else:
self.scale = args[0]
class MockVector3:
"""模拟三维向量"""
def __init__(self, values):
if isinstance(values, (list, tuple)):
self.x, self.y, self.z = values[:3]
else:
self.x = self.y = self.z = values
def getX(self):
return self.x
def getY(self):
return self.y
def getZ(self):
return self.z
class TestMainWindow(QMainWindow):
"""测试主窗口"""
def __init__(self):
super().__init__()
self.world = MockWorld()
self.gui_editor_window = None
self.setupUI()
def setupUI(self):
"""设置UI"""
self.setWindowTitle("GUI编辑器测试")
self.setGeometry(100, 100, 400, 200)
central_widget = QWidget()
self.setCentralWidget(central_widget)
layout = QVBoxLayout(central_widget)
# 打开GUI编辑器按钮
open_button = QPushButton("打开GUI编辑器")
open_button.clicked.connect(self.openGUIEditor)
layout.addWidget(open_button)
# 添加一些测试GUI元素按钮
add_button = QPushButton("添加测试GUI元素")
add_button.clicked.connect(self.addTestGUIElements)
layout.addWidget(add_button)
# 显示状态
self.status_label = QPushButton("状态: 准备就绪")
self.status_label.setEnabled(False)
layout.addWidget(self.status_label)
def openGUIEditor(self):
"""打开GUI编辑器"""
try:
from gui_editor_window import GUIEditorWindow
if self.gui_editor_window is None:
self.gui_editor_window = GUIEditorWindow(world=self.world)
self.gui_editor_window.editor_closed.connect(self.onGUIEditorClosed)
self.gui_editor_window.show()
self.status_label.setText("状态: GUI编辑器已打开")
print("GUI编辑器窗口已打开")
else:
self.gui_editor_window.raise_()
self.gui_editor_window.activateWindow()
except ImportError as e:
print(f"无法导入GUI编辑器: {e}")
self.status_label.setText("状态: 导入GUI编辑器失败")
except Exception as e:
print(f"打开GUI编辑器失败: {e}")
self.status_label.setText(f"状态: 打开失败 - {e}")
def onGUIEditorClosed(self):
"""GUI编辑器关闭回调"""
self.gui_editor_window = None
self.status_label.setText("状态: GUI编辑器已关闭")
print("GUI编辑器窗口已关闭")
def addTestGUIElements(self):
"""添加测试GUI元素"""
# 添加一些测试元素
self.world.createGUIButton((0, 0, 0), "测试按钮", 0.08)
self.world.createGUILabel((3, 0, 0), "测试标签", 0.06)
self.world.createGUI3DText((0, 5, 0), "测试3D文本", 0.5)
# 如果GUI编辑器窗口打开同步元素
if self.gui_editor_window:
self.gui_editor_window.syncGUIElements()
self.status_label.setText(f"状态: 已添加测试元素,总计 {len(self.world.gui_elements)}")
def main():
"""主函数"""
app = QApplication(sys.argv)
# 创建测试窗口
main_window = TestMainWindow()
main_window.show()
print("GUI编辑器测试启动")
print("点击'打开GUI编辑器'按钮来测试独立的GUI编辑器窗口")
sys.exit(app.exec_())
if __name__ == "__main__":
main()

View File

@ -0,0 +1,285 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
新方法GUI测试
测试按照gui_3d_demo.py的成功方法修改后的GUI功能
"""
import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)
import sys
from direct.showbase.ShowBase import ShowBase
from direct.gui.DirectGui import *
from direct.gui.OnscreenText import OnscreenText
from panda3d.core import *
# 配置Panda3D
loadPrcFileData("", """
win-size 1024 768
window-title New Approach GUI Test
framebuffer-multisample 1
multisamples 2
""")
class NewApproachGUITest(ShowBase):
def __init__(self):
ShowBase.__init__(self)
print("=== 新方法GUI测试 ===")
# 设置背景色
self.setBackgroundColor(0.2, 0.2, 0.3)
# 调整相机位置
self.cam.setPos(0, -10, 5)
self.cam.lookAt(0, 0, 0)
# 初始化GUI元素列表
self.gui_elements = []
# 尝试加载中文字体
try:
self.chinese_font = self.loader.loadFont('/usr/share/fonts/truetype/wqy/wqy-microhei.ttc')
if not self.chinese_font:
print("警告: 无法加载中文字体,将使用默认字体")
except:
print("警告: 无法加载中文字体,将使用默认字体")
self.chinese_font = None
# 创建测试内容
self.createTestContent()
# 创建GUI元素
self.createGUIElements()
print("\n测试说明:")
print("这是使用新方法的GUI测试")
print("参考gui_3d_demo.py的成功实现")
print("如果GUI元素正常显示说明修复是成功的!")
def createTestContent(self):
"""创建测试内容"""
print("\n--- 创建3D测试内容 ---")
# 创建地面
cm = CardMaker("ground")
cm.setFrame(-10, 10, -10, 10)
ground = self.render.attachNewNode(cm.generate())
ground.setP(-90)
ground.setColor(0.5, 0.5, 0.5, 1)
# 创建一个立方体
cube = self.loader.loadModel("models/environment")
if not cube:
# 如果找不到内置模型,创建一个简单几何体
cm = CardMaker("test-cube")
cm.setFrame(-1, 1, -1, 1)
cube = self.render.attachNewNode(cm.generate())
cube.reparentTo(self.render)
cube.setPos(0, 5, 1)
cube.setColor(0.8, 0.2, 0.2, 1)
# 添加光照
alight = AmbientLight('alight')
alight.setColor((0.3, 0.3, 0.3, 1))
alnp = self.render.attachNewNode(alight)
self.render.setLight(alnp)
dlight = DirectionalLight('dlight')
dlight.setColor((0.8, 0.8, 0.8, 1))
dlnp = self.render.attachNewNode(dlight)
dlnp.setHpr(45, -45, 0)
self.render.setLight(dlnp)
print("3D内容创建完成")
def createGUIElements(self):
"""创建GUI元素"""
print("\n--- 使用新方法创建GUI元素 ---")
# 创建标题
self.title = OnscreenText(
text="新方法GUI功能测试",
pos=(0, 0.9),
scale=0.08,
fg=(1, 1, 1, 1),
align=TextNode.ACenter,
font=self.chinese_font if self.chinese_font else None
)
# 使用新的GUI创建方法
print("开始创建各种GUI元素...")
# 按钮测试 - 使用新的逻辑坐标
self.button1 = self.createGUIButton((0, 0, 0), "中心按钮", 0.08)
self.button2 = self.createGUIButton((-10, 0, 0), "左按钮", 0.08)
self.button3 = self.createGUIButton((10, 0, 0), "右按钮", 0.08)
self.button4 = self.createGUIButton((0, 0, 8), "上按钮", 0.08)
self.button5 = self.createGUIButton((0, 0, -8), "下按钮", 0.08)
# 标签测试
self.label1 = self.createGUILabel((0, 0, -12), "中心标签", 0.06)
self.label2 = self.createGUILabel((-15, 0, -5), "左标签", 0.06)
self.label3 = self.createGUILabel((15, 0, -5), "右标签", 0.06)
# 输入框测试
self.entry1 = self.createGUIEntry((0, 0, -18), "中心输入框", 0.05)
self.entry2 = self.createGUIEntry((-15, 0, -12), "左输入框", 0.05)
self.entry3 = self.createGUIEntry((15, 0, -12), "右输入框", 0.05)
# 3D文本测试
self.text3d1 = self.createGUI3DText((0, 5, 3), "主3D文本", 0.5)
self.text3d2 = self.createGUI3DText((-3, 3, 1), "左3D文本", 0.3)
self.text3d3 = self.createGUI3DText((3, 3, 1), "右3D文本", 0.3)
# 虚拟屏幕测试
self.screen1 = self.createGUIVirtualScreen((0, 8, 0), (3, 2), "主虚拟屏幕")
self.screen2 = self.createGUIVirtualScreen((-5, 5, 2), (2, 1), "左屏幕")
self.screen3 = self.createGUIVirtualScreen((5, 5, 2), (2, 1), "右屏幕")
print(f"\nGUI创建完成总计: {len(self.gui_elements)} 个元素")
print("检查窗口中是否能看到所有GUI元素...")
# 使用修改后的GUI创建方法
def createGUIButton(self, pos=(0, 0, 0), text="按钮", size=0.1):
"""创建2D GUI按钮 - 新方法"""
# 直接使用合理的屏幕坐标参考gui_3d_demo.py的成功实现
if pos == (0, 0, 0): # 默认中心位置
gui_pos = (0, 0, 0)
else:
# 简单映射:将输入坐标范围映射到合理的屏幕坐标
scale_factor = 0.075 # 调整这个值来控制GUI分布范围
gui_pos = (pos[0] * scale_factor, 0, pos[2] * scale_factor)
button = DirectButton(
text=text,
pos=gui_pos,
scale=size,
command=self.onGUIButtonClick,
extraArgs=[f"button_{len(self.gui_elements)}"],
frameColor=(0.2, 0.6, 0.8, 1),
text_font=self.chinese_font if self.chinese_font else None,
rolloverSound=None,
clickSound=None
)
self.gui_elements.append(button)
print(f"✓ 创建GUI按钮: {text}")
print(f" 逻辑位置: {pos}")
print(f" 屏幕位置: {gui_pos}")
return button
def createGUILabel(self, pos=(0, 0, 0), text="标签", size=0.08):
"""创建2D GUI标签 - 新方法"""
# 使用与按钮相同的坐标映射逻辑
if pos == (0, 0, 0):
gui_pos = (0, 0, -0.3) # 标签默认稍微偏下
else:
scale_factor = 0.075
gui_pos = (pos[0] * scale_factor, 0, pos[2] * scale_factor)
label = DirectLabel(
text=text,
pos=gui_pos,
scale=size,
frameColor=(0, 0, 0, 0), # 透明背景
text_fg=(1, 1, 1, 1),
text_font=self.chinese_font if self.chinese_font else None
)
self.gui_elements.append(label)
print(f"✓ 创建GUI标签: {text}")
print(f" 逻辑位置: {pos}")
print(f" 屏幕位置: {gui_pos}")
return label
def createGUIEntry(self, pos=(0, 0, 0), placeholder="输入文本...", size=0.08):
"""创建2D GUI文本输入框 - 新方法"""
# 使用与按钮相同的坐标映射逻辑
if pos == (0, 0, 0):
gui_pos = (0, 0, -0.6) # 输入框默认更靠下
else:
scale_factor = 0.075
gui_pos = (pos[0] * scale_factor, 0, pos[2] * scale_factor)
entry = DirectEntry(
text="",
pos=gui_pos,
scale=size,
command=self.onGUIEntrySubmit,
extraArgs=[f"entry_{len(self.gui_elements)}"],
initialText=placeholder,
numLines=1,
width=12,
focus=0
)
self.gui_elements.append(entry)
print(f"✓ 创建GUI输入框: {placeholder}")
print(f" 逻辑位置: {pos}")
print(f" 屏幕位置: {gui_pos}")
return entry
def createGUI3DText(self, pos=(0, 0, 0), text="3D文本", size=0.5):
"""创建3D空间文本"""
textNode = TextNode(f'3d-text-{len(self.gui_elements)}')
textNode.setText(text)
textNode.setAlign(TextNode.ACenter)
if self.chinese_font:
textNode.setFont(self.chinese_font)
textNodePath = self.render.attachNewNode(textNode)
textNodePath.setPos(*pos)
textNodePath.setScale(size)
textNodePath.setColor(1, 1, 0, 1)
textNodePath.setBillboardAxis()
self.gui_elements.append(textNodePath)
print(f"✓ 创建3D文本: {text} (世界位置: {pos})")
return textNodePath
def createGUIVirtualScreen(self, pos=(0, 0, 0), size=(2, 1), text="虚拟屏幕"):
"""创建3D虚拟屏幕"""
cm = CardMaker(f"virtual-screen-{len(self.gui_elements)}")
cm.setFrame(-size[0]/2, size[0]/2, -size[1]/2, size[1]/2)
virtualScreen = self.render.attachNewNode(cm.generate())
virtualScreen.setPos(*pos)
virtualScreen.setColor(0.2, 0.2, 0.2, 0.8)
virtualScreen.setTransparency(TransparencyAttrib.MAlpha)
screenText = TextNode(f'screen-text-{len(self.gui_elements)}')
screenText.setText(text)
screenText.setAlign(TextNode.ACenter)
if self.chinese_font:
screenText.setFont(self.chinese_font)
screenTextNP = virtualScreen.attachNewNode(screenText)
screenTextNP.setPos(0, 0.01, 0)
screenTextNP.setScale(0.3)
screenTextNP.setColor(0, 1, 0, 1)
self.gui_elements.append(virtualScreen)
print(f"✓ 创建虚拟屏幕: {text} (世界位置: {pos})")
return virtualScreen
def onGUIButtonClick(self, button_id):
"""GUI按钮点击事件处理"""
print(f"🎯 GUI按钮被点击: {button_id}")
def onGUIEntrySubmit(self, text, entry_id):
"""GUI输入框提交事件处理"""
print(f"📝 GUI输入框提交: {entry_id} = '{text}'")
def main():
"""主函数"""
print("启动新方法GUI功能测试...")
print("这将测试使用gui_3d_demo.py成功方法修改后的GUI创建")
# 创建并运行应用
app = NewApproachGUITest()
app.run()
if __name__ == "__main__":
main()

View File

@ -0,0 +1,71 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
测试GUI预览窗口与主程序的集成
验证新的简化GUI编辑模式
"""
import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)
import sys
import time
from test import MyWorld
from gui_preview_window import GUIPreviewWindow
def test_gui_preview_integration():
"""测试GUI预览窗口集成"""
print("开始测试GUI预览窗口集成...")
# 创建模拟的主程序world
world = MyWorld()
# 创建GUI预览窗口
preview = GUIPreviewWindow()
preview.set_main_world(world)
print("✓ 预览窗口已创建并连接到主程序")
# 等待一下让窗口显示
time.sleep(2)
# 在主程序中创建一些GUI元素
print("\n在主程序中创建GUI元素...")
# 创建按钮
button = world.createGUIButton((0, 0, 3), "测试按钮", 0.08)
print("✓ 已创建按钮")
time.sleep(1)
# 创建标签
label = world.createGUILabel((0, 0, 1), "测试标签", 0.06)
print("✓ 已创建标签")
time.sleep(1)
# 创建输入框
entry = world.createGUIEntry((0, 0, -1), "输入框", 0.05)
print("✓ 已创建输入框")
time.sleep(1)
# 创建3D文本
text3d = world.createGUI3DText((5, 5, 0), "3D文本", 0.5)
print("✓ 已创建3D文本")
print(f"\n主程序GUI元素总数: {len(world.gui_elements)}")
print("预览窗口应该会自动同步显示这些元素")
print("\n测试完成请查看预览窗口中的GUI元素")
print("预览窗口会自动与主程序同步")
# 运行预览窗口
if preview.preview_base:
print("\n启动预览窗口事件循环...")
preview.preview_base.run()
if __name__ == "__main__":
test_gui_preview_integration()

130
demo/test_main_gui.py Normal file
View File

@ -0,0 +1,130 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
主程序GUI功能直接测试
使用工具栏按钮创建GUI元素并验证显示
"""
import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)
import sys
import os
import time
from PyQt5.QtWidgets import QApplication, QMainWindow, QMessageBox
from PyQt5.QtCore import QTimer
# 导入主程序
from test import MyWorld, CustomPanda3DWidget
def testMainProgramGUI():
"""测试主程序的GUI功能"""
print("=== 测试主程序GUI功能 ===")
app = QApplication(sys.argv)
# 创建主程序的世界对象
world = MyWorld()
# 创建窗口
mainWindow = QMainWindow()
mainWindow.setWindowTitle("主程序GUI测试")
mainWindow.setGeometry(100, 100, 800, 600)
# 创建Panda3D部件
pandaWidget = CustomPanda3DWidget(world)
mainWindow.setCentralWidget(pandaWidget)
# 显示窗口
mainWindow.show()
# 使用定时器延迟创建GUI元素
def createTestGUIElements():
print("\n开始创建GUI元素...")
try:
# 使用不同的测试坐标
test_positions = [
(0, 0, 0), # 中心
(-5, 0, 0), # 左侧
(5, 0, 0), # 右侧
(0, 0, 5), # 上方
(0, 0, -5), # 下方
]
# 创建按钮
print("创建按钮...")
for i, pos in enumerate(test_positions):
button = world.createGUIButton(pos, f"按钮{i+1}", 0.08)
print(f" 按钮{i+1}: 位置{pos} -> 屏幕位置{button.getPos() if button else 'None'}")
# 创建标签
print("创建标签...")
label_positions = [
(0, 0, -8), # 中心下方
(-8, 0, 0), # 左侧
(8, 0, 0), # 右侧
]
for i, pos in enumerate(label_positions):
label = world.createGUILabel(pos, f"标签{i+1}", 0.06)
print(f" 标签{i+1}: 位置{pos} -> 屏幕位置{label.getPos() if label else 'None'}")
# 创建输入框
print("创建输入框...")
entry_positions = [
(0, 0, -12), # 中心下方
(-10, 0, -8), # 左下
]
for i, pos in enumerate(entry_positions):
entry = world.createGUIEntry(pos, f"输入框{i+1}", 0.05)
print(f" 输入框{i+1}: 位置{pos} -> 屏幕位置{entry.getPos() if entry else 'None'}")
# 创建3D元素作为对比
print("创建3D元素...")
world.createGUI3DText((0, 5, 2), "3D测试文本", 0.5)
world.createGUIVirtualScreen((3, 8, 0), (2, 1), "测试屏幕")
print(f"\n总共创建了 {len(world.gui_elements)} 个GUI元素")
# 检查aspect2d的子节点
print(f"\naspect2d子节点数量: {world.aspect2d.getNumChildren()}")
for i in range(world.aspect2d.getNumChildren()):
child = world.aspect2d.getChild(i)
print(f" 子节点{i}: {child.getName()}")
# 显示消息框告知结果
msg = f"""GUI元素创建完成
创建的元素
- 5个按钮
- 3个标签
- 2个输入框
- 1个3D文本
- 1个虚拟屏幕
总计: {len(world.gui_elements)}
如果你看不到2D GUI元素按钮标签输入框
但能看到3D元素黄色文本虚拟屏幕
说明坐标转换可能还有问题
请观察窗口中的GUI元素"""
QMessageBox.information(mainWindow, "GUI创建完成", msg)
except Exception as e:
error_msg = f"创建GUI元素时出错: {str(e)}"
print(error_msg)
QMessageBox.critical(mainWindow, "错误", error_msg)
# 2秒后创建GUI元素
QTimer.singleShot(2000, createTestGUIElements)
print("窗口已显示2秒后将创建GUI元素...")
print("请观察是否能看到所有GUI组件")
return app.exec_()
if __name__ == "__main__":
testMainProgramGUI()

237
demo/test_qt_debug.py Normal file
View File

@ -0,0 +1,237 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Qt环境深度调试
诊断Qt集成环境中的渲染问题
"""
import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)
import sys
from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QLabel
from PyQt5.QtCore import Qt
from QPanda3D.QPanda3DWidget import QPanda3DWidget
from QPanda3D.Panda3DWorld import Panda3DWorld
from direct.gui.DirectGui import *
from direct.gui.OnscreenText import OnscreenText
from panda3d.core import *
import os
class DebugGUIWorld(Panda3DWorld):
def __init__(self):
super().__init__()
print("=== 开始Qt环境调试 ===")
# 设置背景色
self.setBackgroundColor(0.2, 0.2, 0.3)
print("背景色设置完成")
# 检查基本渲染状态
print(f"render节点: {self.render}")
print(f"render2d节点: {self.render2d}")
print(f"主相机: {self.cam}")
print(f"2D相机: {self.camera2d}")
# 检查图形状态
print(f"图形引擎: {self.graphicsEngine}")
if hasattr(self, 'win'):
print(f"主窗口: {self.win}")
print(f"窗口大小: {self.win.getXSize()} x {self.win.getYSize()}")
# 创建一个简单的3D几何体测试
self.create3DTest()
# 延迟创建2D GUI
self.taskMgr.doMethodLater(1.0, self.create2DTest, "create-2d-test")
def create3DTest(self):
"""创建3D测试内容"""
print("\n--- 创建3D测试内容 ---")
# 创建一个简单的立方体
from panda3d.core import CardMaker
cm = CardMaker("test-cube")
cm.setFrame(-1, 1, -1, 1)
self.testCube = self.render.attachNewNode(cm.generate())
self.testCube.setPos(0, 5, 0)
self.testCube.setColor(1, 0, 0, 1) # 红色
print(f"3D立方体创建: {self.testCube}")
print(f"立方体位置: {self.testCube.getPos()}")
# 创建3D文本
textNode = TextNode('3d-debug-text')
textNode.setText("3D Test")
textNode.setAlign(TextNode.ACenter)
self.text3D = self.render.attachNewNode(textNode)
self.text3D.setPos(0, 3, 2)
self.text3D.setScale(0.5)
self.text3D.setColor(1, 1, 0, 1) # 黄色
self.text3D.setBillboardAxis()
print(f"3D文本创建: {self.text3D}")
# 添加基础光照
alight = AmbientLight('debug-alight')
alight.setColor((0.3, 0.3, 0.3, 1))
alnp = self.render.attachNewNode(alight)
self.render.setLight(alnp)
dlight = DirectionalLight('debug-dlight')
dlight.setColor((0.8, 0.8, 0.8, 1))
dlnp = self.render.attachNewNode(dlight)
dlnp.setHpr(45, -45, 0)
self.render.setLight(dlnp)
print("光照设置完成")
def create2DTest(self, task=None):
"""创建2D测试内容"""
print("\n--- 创建2D测试内容 ---")
# 检查2D渲染状态
print(f"render2d: {self.render2d}")
print(f"aspect2d: {self.aspect2d}")
print(f"camera2d: {self.camera2d}")
print(f"camera2d位置: {self.camera2d.getPos()}")
print(f"camera2d朝向: {self.camera2d.getHpr()}")
# 创建屏幕文本
try:
self.screenText = OnscreenText(
text="2D Screen Test",
pos=(0, 0.8),
scale=0.1,
fg=(1, 1, 1, 1),
align=TextNode.ACenter
)
print(f"屏幕文本创建成功: {self.screenText}")
print(f"屏幕文本父节点: {self.screenText.getParent()}")
except Exception as e:
print(f"屏幕文本创建失败: {str(e)}")
# 创建DirectGUI按钮
try:
self.testButton = DirectButton(
text="Test Button",
pos=(0, 0, 0),
scale=0.15,
frameColor=(1, 0, 0, 1), # 红色背景
text_fg=(1, 1, 1, 1), # 白色文字
command=self.onButtonClick
)
print(f"DirectGUI按钮创建成功: {self.testButton}")
print(f"按钮父节点: {self.testButton.getParent()}")
print(f"按钮位置: {self.testButton.getPos()}")
print(f"按钮隐藏状态: {self.testButton.isHidden()}")
except Exception as e:
print(f"DirectGUI按钮创建失败: {str(e)}")
# 创建DirectGUI标签
try:
self.testLabel = DirectLabel(
text="Test Label",
pos=(0, 0, -0.3),
scale=0.1,
frameColor=(0, 1, 0, 1), # 绿色背景
text_fg=(0, 0, 0, 1) # 黑色文字
)
print(f"DirectGUI标签创建成功: {self.testLabel}")
except Exception as e:
print(f"DirectGUI标签创建失败: {str(e)}")
# 检查aspect2d的所有子节点
print(f"\naspect2d子节点总数: {self.aspect2d.getNumChildren()}")
for i in range(self.aspect2d.getNumChildren()):
child = self.aspect2d.getChild(i)
print(f" 子节点 {i}: {child.getName()} - {child}")
# 检查渲染状态
self.checkRenderState()
return task.done if task else None
def checkRenderState(self):
"""检查渲染状态"""
print("\n--- 检查渲染状态 ---")
# 检查显示区域
if hasattr(self, 'win') and self.win:
print(f"窗口有效: {self.win.isValid()}")
print(f"窗口活跃: {self.win.isActive()}")
print(f"窗口大小: {self.win.getXSize()} x {self.win.getYSize()}")
# 检查显示区域
for i in range(self.win.getNumDisplayRegions()):
dr = self.win.getDisplayRegion(i)
print(f"显示区域 {i}: {dr}")
print(f" 相机: {dr.getCamera()}")
print(f" 激活状态: {dr.isActive()}")
print(f" 维度: {dr.getDimensions()}")
# 强制渲染一帧
try:
if hasattr(self, 'graphicsEngine'):
self.graphicsEngine.renderFrame()
print("强制渲染帧完成")
except Exception as e:
print(f"强制渲染失败: {str(e)}")
def onButtonClick(self):
"""按钮点击事件"""
print("✓ 按钮被点击了!")
class DebugWidget(QPanda3DWidget):
def __init__(self, world, parent=None):
super().__init__(world, parent)
self.world = world
print(f"Qt部件初始化完成: {self}")
def main():
"""主函数"""
print("启动Qt环境深度调试...")
app = QApplication(sys.argv)
# 创建主窗口
mainWindow = QMainWindow()
mainWindow.setWindowTitle("Qt环境深度调试")
mainWindow.setGeometry(100, 100, 800, 600)
# 创建布局和容器
centralWidget = QWidget()
layout = QVBoxLayout(centralWidget)
# 添加状态标签
statusLabel = QLabel("Qt环境调试 - 查看控制台输出")
statusLabel.setAlignment(Qt.AlignCenter)
statusLabel.setStyleSheet("background-color: lightblue; padding: 10px;")
layout.addWidget(statusLabel)
# 创建调试世界
world = DebugGUIWorld()
# 创建Panda3D部件
pandaWidget = DebugWidget(world)
layout.addWidget(pandaWidget)
mainWindow.setCentralWidget(centralWidget)
# 显示窗口
mainWindow.show()
print("调试窗口已显示")
print("如果看到红色立方体和黄色3D文本说明3D渲染正常")
print("如果看到红色按钮和绿色标签说明2D GUI渲染正常")
print("如果窗口是黑色的,说明存在渲染问题")
# 运行应用
return app.exec_()
if __name__ == "__main__":
exit_code = main()

145
demo/test_qt_fix.py Normal file
View File

@ -0,0 +1,145 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Qt环境下的2D GUI测试
专门测试在Qt集成环境中的GUI显示问题
"""
import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)
import sys
from PyQt5.QtWidgets import QApplication, QMainWindow
from PyQt5.QtCore import Qt
from QPanda3D.QPanda3DWidget import QPanda3DWidget
from QPanda3D.Panda3DWorld import Panda3DWorld
from direct.gui.DirectGui import *
from direct.gui.OnscreenText import OnscreenText
from panda3d.core import *
import os
class QtGUITestWorld(Panda3DWorld):
def __init__(self):
super().__init__()
# 设置背景色
self.setBackgroundColor(0.2, 0.2, 0.3)
# 尝试加载中文字体
try:
self.chinese_font = self.loader.loadFont('/usr/share/fonts/truetype/wqy/wqy-microhei.ttc')
except:
self.chinese_font = None
# 延迟创建GUI确保渲染系统就绪
self.taskMgr.doMethodLater(0.5, self.createTestGUI, "create-gui")
def createTestGUI(self, task=None):
"""创建测试GUI"""
print("\n=== Qt环境GUI测试 ===")
# 检查render2d状态
print(f"render2d: {self.render2d}")
print(f"aspect2d: {self.aspect2d}")
print(f"pixel2d: {self.pixel2d}")
# 创建标题
self.title = OnscreenText(
text="Qt环境GUI测试",
pos=(0, 0.9),
scale=0.08,
fg=(1, 1, 1, 1),
align=TextNode.ACenter,
font=self.chinese_font if self.chinese_font else None
)
print(f"标题创建成功: {self.title}")
# 创建简单的按钮
self.testButton = DirectButton(
text="测试按钮",
pos=(0, 0, 0),
scale=0.1,
command=self.onButtonClick,
frameColor=(1, 0, 0, 1), # 使用红色,更容易看见
text_font=self.chinese_font if self.chinese_font else None,
rolloverSound=None,
clickSound=None
)
print(f"按钮创建成功: {self.testButton}")
print(f"按钮父节点: {self.testButton.getParent()}")
print(f"按钮位置: {self.testButton.getPos()}")
print(f"按钮隐藏状态: {self.testButton.isHidden()}")
# 创建标签
self.testLabel = DirectLabel(
text="测试标签",
pos=(0, 0, -0.3),
scale=0.08,
frameColor=(0, 1, 0, 1), # 使用绿色背景
text_fg=(0, 0, 0, 1), # 黑色文字
text_font=self.chinese_font if self.chinese_font else None
)
print(f"标签创建成功: {self.testLabel}")
# 检查render2d的子节点
print(f"\nrender2d子节点数量: {self.render2d.getNumChildren()}")
for i in range(self.render2d.getNumChildren()):
child = self.render2d.getChild(i)
print(f" 子节点 {i}: {child}")
print(f"\naspect2d子节点数量: {self.aspect2d.getNumChildren()}")
for i in range(self.aspect2d.getNumChildren()):
child = self.aspect2d.getChild(i)
print(f" 子节点 {i}: {child}")
# 检查相机设置
print(f"\nrender2d相机: {self.camera2d}")
print(f"render2d相机位置: {self.camera2d.getPos()}")
print(f"render2d相机HPR: {self.camera2d.getHpr()}")
# 尝试手动刷新2D渲染
if hasattr(self, 'graphicsEngine'):
print("\n尝试刷新图形引擎...")
self.graphicsEngine.renderFrame()
return task.done if task else None
def onButtonClick(self):
"""按钮点击事件"""
print("✓ 按钮被点击了!")
class QtGUITestWidget(QPanda3DWidget):
def __init__(self, world, parent=None):
super().__init__(world, parent)
self.world = world
def main():
"""主函数"""
print("启动Qt环境GUI测试...")
app = QApplication(sys.argv)
# 创建测试世界
world = QtGUITestWorld()
# 创建主窗口
mainWindow = QMainWindow()
mainWindow.setWindowTitle("Qt环境GUI测试")
mainWindow.setGeometry(100, 100, 800, 600)
# 创建Panda3D部件
pandaWidget = QtGUITestWidget(world)
mainWindow.setCentralWidget(pandaWidget)
# 显示窗口
mainWindow.show()
print("窗口已显示,检查控制台输出...")
print("如果看到红色按钮和绿色标签说明Qt环境GUI工作正常")
# 运行应用
return app.exec_()
if __name__ == "__main__":
exit_code = main()

338
demo/test_qt_fix_v2.py Normal file
View File

@ -0,0 +1,338 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Qt环境渲染修复版本
解决Qt集成环境中的渲染问题
"""
import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)
import sys
from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QLabel
from PyQt5.QtCore import Qt
from PyQt5.QtOpenGL import QOpenGLWidget
from direct.showbase.ShowBase import ShowBase
from direct.gui.DirectGui import *
from direct.gui.OnscreenText import OnscreenText
from panda3d.core import *
import os
# 强制配置Panda3D使用OpenGL
loadPrcFileData("", """
win-size 800 600
window-title Qt Fixed Test
framebuffer-multisample 1
multisamples 2
prefer-parasite-buffer false
""")
class QtFixedWorld(ShowBase):
def __init__(self):
ShowBase.__init__(self)
print("=== Qt修复版本世界初始化 ===")
# 设置背景色
self.setBackgroundColor(0.2, 0.2, 0.3)
print("背景色设置完成")
# 调整相机位置
self.cam.setPos(0, -10, 5)
self.cam.lookAt(0, 0, 0)
# 检查基本状态
print(f"窗口类型: {type(self.win)}")
print(f"图形引擎: {self.graphicsEngine}")
print(f"窗口大小: {self.win.getXSize()} x {self.win.getYSize()}")
# 初始化GUI元素列表
self.gui_elements = []
# 尝试加载中文字体
try:
self.chinese_font = self.loader.loadFont('/usr/share/fonts/truetype/wqy/wqy-microhei.ttc')
if not self.chinese_font:
print("警告: 无法加载中文字体,将使用默认字体")
except:
print("警告: 无法加载中文字体,将使用默认字体")
self.chinese_font = None
# 创建测试内容
self.createTestContent()
# 延迟创建GUI元素
self.taskMgr.doMethodLater(0.5, self.createGUIElements, "create-gui")
def createTestContent(self):
"""创建测试内容"""
print("\n--- 创建测试内容 ---")
# 创建3D几何体
from panda3d.core import CardMaker
cm = CardMaker("test-plane")
cm.setFrame(-2, 2, -2, 2)
self.testPlane = self.render.attachNewNode(cm.generate())
self.testPlane.setPos(0, 5, 0)
self.testPlane.setColor(0.8, 0.2, 0.2, 1) # 红色
# 创建3D文本
textNode = TextNode('3d-text')
textNode.setText("3D Test Content")
textNode.setAlign(TextNode.ACenter)
if self.chinese_font:
textNode.setFont(self.chinese_font)
self.text3D = self.render.attachNewNode(textNode)
self.text3D.setPos(0, 3, 2)
self.text3D.setScale(0.5)
self.text3D.setColor(1, 1, 0, 1)
self.text3D.setBillboardAxis()
# 添加光照
alight = AmbientLight('alight')
alight.setColor((0.3, 0.3, 0.3, 1))
alnp = self.render.attachNewNode(alight)
self.render.setLight(alnp)
dlight = DirectionalLight('dlight')
dlight.setColor((0.8, 0.8, 0.8, 1))
dlnp = self.render.attachNewNode(dlight)
dlnp.setHpr(45, -45, 0)
self.render.setLight(dlnp)
print("3D内容创建完成")
def createGUIElements(self, task=None):
"""创建GUI元素"""
print("\n--- 创建GUI元素 ---")
# 创建屏幕标题
self.title = OnscreenText(
text="Qt环境修复测试",
pos=(0, 0.9),
scale=0.08,
fg=(1, 1, 1, 1),
align=TextNode.ACenter,
font=self.chinese_font if self.chinese_font else None
)
print("屏幕标题创建完成")
# 测试主程序中的GUI创建方法
try:
# 按钮测试
self.button1 = self.createGUIButton((0, 0, 0), "中心按钮", 0.08)
self.button2 = self.createGUIButton((-10, 0, 0), "左按钮", 0.08)
self.button3 = self.createGUIButton((10, 0, 0), "右按钮", 0.08)
# 标签测试
self.label1 = self.createGUILabel((0, 0, -10), "测试标签", 0.06)
self.label2 = self.createGUILabel((-10, 0, -5), "左标签", 0.06)
# 输入框测试
self.entry1 = self.createGUIEntry((10, 0, -5), "输入框", 0.05)
# 3D文本测试
self.text3d1 = self.createGUI3DText((0, 5, 3), "3D文本测试", 0.5)
self.text3d2 = self.createGUI3DText((-3, 3, 1), "左3D文本", 0.3)
# 虚拟屏幕测试
self.screen1 = self.createGUIVirtualScreen((3, 8, 0), (2, 1), "虚拟屏幕")
print(f"GUI创建完成总计: {len(self.gui_elements)} 个元素")
except Exception as e:
print(f"GUI创建失败: {str(e)}")
return task.done if task else None
# 复制主程序中的GUI创建方法
def createGUIButton(self, pos=(0, 0, 0), text="按钮", size=0.1):
"""创建2D GUI按钮"""
# 将3D坐标转换为2D屏幕坐标
gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1)
button = DirectButton(
text=text,
pos=gui_pos,
scale=size,
command=self.onGUIButtonClick,
extraArgs=[f"button_{len(self.gui_elements)}"],
frameColor=(0.2, 0.6, 0.8, 1),
text_font=self.chinese_font if self.chinese_font else None,
rolloverSound=None,
clickSound=None
)
button.setTag("gui_type", "button")
button.setTag("gui_id", f"button_{len(self.gui_elements)}")
button.setTag("gui_text", text)
self.gui_elements.append(button)
print(f"创建GUI按钮: {text} 在屏幕位置 {gui_pos}")
return button
def createGUILabel(self, pos=(0, 0, 0), text="标签", size=0.08):
"""创建2D GUI标签"""
gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1)
label = DirectLabel(
text=text,
pos=gui_pos,
scale=size,
frameColor=(0, 0, 0, 0),
text_fg=(1, 1, 1, 1),
text_font=self.chinese_font if self.chinese_font else None
)
label.setTag("gui_type", "label")
label.setTag("gui_id", f"label_{len(self.gui_elements)}")
label.setTag("gui_text", text)
self.gui_elements.append(label)
print(f"创建GUI标签: {text} 在屏幕位置 {gui_pos}")
return label
def createGUIEntry(self, pos=(0, 0, 0), placeholder="输入文本...", size=0.08):
"""创建2D GUI文本输入框"""
gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1)
entry = DirectEntry(
text="",
pos=gui_pos,
scale=size,
command=self.onGUIEntrySubmit,
extraArgs=[f"entry_{len(self.gui_elements)}"],
initialText=placeholder,
numLines=1,
width=12,
focus=0
)
entry.setTag("gui_type", "entry")
entry.setTag("gui_id", f"entry_{len(self.gui_elements)}")
entry.setTag("gui_placeholder", placeholder)
self.gui_elements.append(entry)
print(f"创建GUI输入框: {placeholder} 在屏幕位置 {gui_pos}")
return entry
def createGUI3DText(self, pos=(0, 0, 0), text="3D文本", size=0.5):
"""创建3D空间文本"""
textNode = TextNode(f'3d-text-{len(self.gui_elements)}')
textNode.setText(text)
textNode.setAlign(TextNode.ACenter)
if self.chinese_font:
textNode.setFont(self.chinese_font)
textNodePath = self.render.attachNewNode(textNode)
textNodePath.setPos(*pos)
textNodePath.setScale(size)
textNodePath.setColor(1, 1, 0, 1)
textNodePath.setBillboardAxis()
textNodePath.setTag("gui_type", "3d_text")
textNodePath.setTag("gui_id", f"3d_text_{len(self.gui_elements)}")
textNodePath.setTag("gui_text", text)
self.gui_elements.append(textNodePath)
print(f"创建3D文本: {text} 在世界位置 {pos}")
return textNodePath
def createGUIVirtualScreen(self, pos=(0, 0, 0), size=(2, 1), text="虚拟屏幕"):
"""创建3D虚拟屏幕"""
cm = CardMaker(f"virtual-screen-{len(self.gui_elements)}")
cm.setFrame(-size[0]/2, size[0]/2, -size[1]/2, size[1]/2)
virtualScreen = self.render.attachNewNode(cm.generate())
virtualScreen.setPos(*pos)
virtualScreen.setColor(0.2, 0.2, 0.2, 0.8)
virtualScreen.setTransparency(TransparencyAttrib.MAlpha)
screenText = TextNode(f'screen-text-{len(self.gui_elements)}')
screenText.setText(text)
screenText.setAlign(TextNode.ACenter)
if self.chinese_font:
screenText.setFont(self.chinese_font)
screenTextNP = virtualScreen.attachNewNode(screenText)
screenTextNP.setPos(0, 0.01, 0)
screenTextNP.setScale(0.3)
screenTextNP.setColor(0, 1, 0, 1)
virtualScreen.setTag("gui_type", "virtual_screen")
virtualScreen.setTag("gui_id", f"virtual_screen_{len(self.gui_elements)}")
virtualScreen.setTag("gui_text", text)
self.gui_elements.append(virtualScreen)
print(f"创建虚拟屏幕: {text} 在世界位置 {pos}")
return virtualScreen
def onGUIButtonClick(self, button_id):
"""GUI按钮点击事件处理"""
print(f"✓ GUI按钮被点击: {button_id}")
def onGUIEntrySubmit(self, text, entry_id):
"""GUI输入框提交事件处理"""
print(f"✓ GUI输入框提交: {entry_id} = {text}")
class QtFixedWidget(QOpenGLWidget):
"""使用QOpenGLWidget的Panda3D部件"""
def __init__(self, world, parent=None):
super().__init__(parent)
self.world = world
self.setMinimumSize(800, 600)
print("Qt OpenGL部件创建完成")
def paintGL(self):
"""OpenGL绘制函数"""
if self.world and hasattr(self.world, 'graphicsEngine'):
self.world.graphicsEngine.renderFrame()
def main():
"""主函数"""
print("启动Qt环境修复测试...")
app = QApplication(sys.argv)
# 创建主窗口
mainWindow = QMainWindow()
mainWindow.setWindowTitle("Qt环境修复测试")
mainWindow.setGeometry(100, 100, 800, 600)
# 创建布局和容器
centralWidget = QWidget()
layout = QVBoxLayout(centralWidget)
# 添加状态标签
statusLabel = QLabel("Qt环境修复测试 - 应该能看到GUI元素")
statusLabel.setAlignment(Qt.AlignCenter)
statusLabel.setStyleSheet("background-color: lightgreen; padding: 10px; color: black;")
layout.addWidget(statusLabel)
# 创建修复版本的世界
world = QtFixedWorld()
# 创建OpenGL部件
openglWidget = QtFixedWidget(world)
layout.addWidget(openglWidget)
mainWindow.setCentralWidget(centralWidget)
# 显示窗口
mainWindow.show()
print("修复版本窗口已显示")
print("应该能看到:")
print("- 红色的3D平面")
print("- 黄色的3D文本")
print("- 多个2D GUI按钮蓝色")
print("- 2D GUI标签白色文字")
print("- 2D GUI输入框")
print("- 3D虚拟屏幕")
# 运行应用
return app.exec_()
if __name__ == "__main__":
exit_code = main()

259
demo/test_qt_gui_fix.py Normal file
View File

@ -0,0 +1,259 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Qt环境GUI事件修复测试
解决Qt集成环境中DirectGUI事件处理问题
"""
import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)
import sys
from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QLabel
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QMouseEvent
from QPanda3D.QPanda3DWidget import QPanda3DWidget
from QPanda3D.Panda3DWorld import Panda3DWorld
from direct.gui.DirectGui import *
from direct.gui.OnscreenText import OnscreenText
from panda3d.core import *
class QtGUIFixWorld(Panda3DWorld):
def __init__(self):
super().__init__()
print("=== Qt GUI事件修复测试 ===")
self.setBackgroundColor(0.2, 0.2, 0.3)
# 检查环境
print(f"窗口类型: {type(self.win)}")
print(f"图形引擎: {self.graphicsEngine}")
# 延迟创建GUI
self.taskMgr.doMethodLater(0.5, self.createTestGUI, "create-gui")
# 保存GUI元素引用用于事件处理
self.gui_elements = []
def createTestGUI(self, task=None):
"""创建测试GUI"""
print("\n--- 创建Qt环境GUI ---")
# 方法1: 标准DirectGUI创建
try:
self.title = OnscreenText(
text="Qt环境GUI测试",
pos=(0, 0.8),
scale=0.08,
fg=(1, 1, 1, 1),
align=TextNode.ACenter
)
print(f"✓ 标题创建成功: {self.title}")
except Exception as e:
print(f"❌ 标题创建失败: {str(e)}")
# 测试按钮1 - 标准创建
try:
self.button1 = DirectButton(
text="标准按钮",
pos=(0, 0, 0.4),
scale=0.1,
frameColor=(0, 1, 0, 1),
command=self.onButton1Click
)
self.gui_elements.append(('button1', self.button1))
print(f"✓ 标准按钮创建成功: {self.button1}")
except Exception as e:
print(f"❌ 标准按钮创建失败: {str(e)}")
# 测试按钮2 - 手动事件处理
try:
self.button2 = DirectButton(
text="修复按钮",
pos=(0, 0, 0.1),
scale=0.1,
frameColor=(1, 0, 0, 1),
command=self.onButton2Click
)
# 手动设置事件处理
self.setupManualEvents(self.button2)
self.gui_elements.append(('button2', self.button2))
print(f"✓ 修复按钮创建成功: {self.button2}")
except Exception as e:
print(f"❌ 修复按钮创建失败: {str(e)}")
# 测试按钮3 - 自定义点击检测
try:
self.button3 = DirectButton(
text="自检按钮",
pos=(0, 0, -0.2),
scale=0.1,
frameColor=(0, 0, 1, 1),
# 不设置command使用自定义检测
)
self.gui_elements.append(('button3', self.button3))
print(f"✓ 自检按钮创建成功: {self.button3}")
# 启动自定义点击检测
self.taskMgr.add(self.checkButtonClicks, "check-clicks")
except Exception as e:
print(f"❌ 自检按钮创建失败: {str(e)}")
return task.done if task else None
def setupManualEvents(self, button):
"""为按钮设置手动事件处理"""
try:
# 获取按钮的PGItem
if hasattr(button, 'guiItem'):
pgItem = button.guiItem
# 强制启用事件
pgItem.setActive(True)
print(f" 手动启用按钮事件: {pgItem}")
except Exception as e:
print(f" 手动事件设置失败: {str(e)}")
def checkButtonClicks(self, task):
"""自定义按钮点击检测"""
# 检查鼠标状态
if hasattr(self, 'mouseWatcherNode') and self.mouseWatcherNode.hasMouse():
# 获取鼠标位置
mouseX = self.mouseWatcherNode.getMouseX()
mouseY = self.mouseWatcherNode.getMouseY()
# 检查是否在button3范围内
if hasattr(self, 'button3'):
buttonPos = self.button3.getPos()
buttonScale = self.button3.getScale()
# 简单的边界检测
if (abs(mouseX - buttonPos.getX()) < buttonScale.getX() * 0.5 and
abs(mouseY - buttonPos.getZ()) < buttonScale.getZ() * 0.5):
# 检查是否有鼠标点击
if self.mouseWatcherNode.isButtonDown('button1'):
self.onButton3Click()
# 防止重复触发
return task.pause(0.5)
return task.cont
def onButton1Click(self):
"""标准按钮点击"""
print("🟢 标准按钮被点击!")
def onButton2Click(self):
"""修复按钮点击"""
print("🔴 修复按钮被点击!")
def onButton3Click(self):
"""自检按钮点击"""
print("🔵 自检按钮被点击!")
def handleQtMouseEvent(self, event):
"""处理Qt鼠标事件"""
try:
# 将Qt事件转换为Panda3D坐标
if hasattr(self, 'win'):
winX = event.x()
winY = event.y()
winWidth = self.win.getXSize()
winHeight = self.win.getYSize()
# 转换为归一化坐标 (-1 到 1)
normalizedX = (winX / winWidth) * 2.0 - 1.0
normalizedY = 1.0 - (winY / winHeight) * 2.0
print(f"Qt鼠标事件: ({winX}, {winY}) -> 归一化: ({normalizedX:.3f}, {normalizedY:.3f})")
# 手动检查GUI元素
for name, element in self.gui_elements:
if hasattr(element, 'getPos'):
elemPos = element.getPos()
elemScale = element.getScale()
# 简单边界检测
if (abs(normalizedX - elemPos.getX()) < elemScale.getX() * 0.5 and
abs(normalizedY - elemPos.getZ()) < elemScale.getZ() * 0.5):
print(f" 点击在 {name} 范围内!")
# 手动触发回调
if name == 'button1':
self.onButton1Click()
elif name == 'button2':
self.onButton2Click()
elif name == 'button3':
self.onButton3Click()
return True
except Exception as e:
print(f"Qt事件处理失败: {str(e)}")
return False
class CustomQPanda3DWidget(QPanda3DWidget):
"""自定义QPanda3DWidget支持事件转发"""
def __init__(self, world, parent=None):
super().__init__(world, parent)
self.world = world
print("自定义QPanda3D部件初始化")
def mousePressEvent(self, event):
"""重写鼠标按下事件"""
print(f"\n=== Qt鼠标按下事件 ===")
print(f"位置: ({event.x()}, {event.y()})")
print(f"按钮: {event.button()}")
# 尝试让世界处理事件
if hasattr(self.world, 'handleQtMouseEvent'):
handled = self.world.handleQtMouseEvent(event)
if handled:
print("事件已被世界处理")
return
# 调用父类方法
super().mousePressEvent(event)
def main():
print("启动Qt环境GUI事件修复测试...")
app = QApplication(sys.argv)
# 创建主窗口
mainWindow = QMainWindow()
mainWindow.setWindowTitle("Qt GUI事件修复测试")
mainWindow.setGeometry(100, 100, 800, 600)
# 创建布局
centralWidget = QWidget()
layout = QVBoxLayout(centralWidget)
# 添加说明
label = QLabel("Qt环境DirectGUI事件修复测试")
label.setAlignment(Qt.AlignCenter)
label.setStyleSheet("background-color: lightgreen; padding: 10px;")
layout.addWidget(label)
# 创建世界
world = QtGUIFixWorld()
# 创建自定义Panda3D部件
pandaWidget = CustomQPanda3DWidget(world)
layout.addWidget(pandaWidget)
mainWindow.setCentralWidget(centralWidget)
mainWindow.show()
print("\n测试说明:")
print("绿色按钮: 标准DirectGUI事件处理")
print("红色按钮: 手动事件修复")
print("蓝色按钮: 自定义点击检测")
print("查看控制台输出了解事件处理情况")
return app.exec_()
if __name__ == "__main__":
main()

155
demo/test_qt_only.py Normal file
View File

@ -0,0 +1,155 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Qt环境专用GUI测试
"""
import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)
import sys
from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QLabel
from PyQt5.QtCore import Qt
from QPanda3D.QPanda3DWidget import QPanda3DWidget
from QPanda3D.Panda3DWorld import Panda3DWorld
from direct.gui.DirectGui import *
from direct.gui.OnscreenText import OnscreenText
from panda3d.core import *
class QtOnlyWorld(Panda3DWorld):
def __init__(self):
super().__init__()
print("=== 专用Qt环境测试 ===")
self.setBackgroundColor(0.2, 0.2, 0.3)
# 检查环境差异
print(f"基类: {self.__class__.__bases__}")
print(f"render2d: {self.render2d}")
print(f"aspect2d: {self.aspect2d}")
print(f"窗口类型: {type(self.win) if hasattr(self, 'win') else 'None'}")
# 检查更多属性
if hasattr(self, 'win'):
print(f"窗口: {self.win}")
print(f"窗口大小: {self.win.getXSize()} x {self.win.getYSize()}")
print(f"图形引擎: {self.graphicsEngine}")
print(f"显示区域数量: {self.win.getNumDisplayRegions() if hasattr(self, 'win') else 'None'}")
# 延迟创建GUI
self.taskMgr.doMethodLater(0.5, self.createTestGUI, "create-gui")
def createTestGUI(self, task=None):
"""创建测试GUI"""
print("\n--- Qt环境创建GUI ---")
# 检查2D渲染系统状态
print(f"render2d可用: {self.render2d is not None}")
print(f"aspect2d可用: {self.aspect2d is not None}")
print(f"camera2d可用: {hasattr(self, 'camera2d') and self.camera2d is not None}")
if hasattr(self, 'camera2d'):
print(f"camera2d位置: {self.camera2d.getPos()}")
print(f"camera2d朝向: {self.camera2d.getHpr()}")
# 创建标题文本 - 这通常能工作
try:
self.title = OnscreenText(
text="Qt Only Test",
pos=(0, 0.8),
scale=0.08,
fg=(1, 1, 1, 1),
align=TextNode.ACenter
)
print(f"✓ 标题创建成功: {self.title}")
print(f" 标题父节点: {self.title.getParent()}")
except Exception as e:
print(f"✗ 标题创建失败: {str(e)}")
# 创建DirectButton - 问题可能在这里
try:
self.testButton = DirectButton(
text="Test Button",
pos=(0, 0, 0),
scale=0.1,
command=self.onButtonClick,
frameColor=(1, 0, 0, 1),
text_font=None,
rolloverSound=None,
clickSound=None
)
print(f"✓ 按钮创建成功: {self.testButton}")
print(f" 按钮父节点: {self.testButton.getParent()}")
print(f" 按钮位置: {self.testButton.getPos()}")
print(f" 按钮隐藏状态: {self.testButton.isHidden()}")
# 检查按钮的PGButton节点
pg_button = self.testButton.node()
print(f" PGButton节点: {pg_button}")
print(f" PGButton类型: {type(pg_button)}")
except Exception as e:
print(f"✗ 按钮创建失败: {str(e)}")
import traceback
traceback.print_exc()
# 检查aspect2d的子节点
print(f"\naspect2d子节点数量: {self.aspect2d.getNumChildren()}")
for i in range(self.aspect2d.getNumChildren()):
child = self.aspect2d.getChild(i)
print(f" 子节点 {i}: {child}")
# 尝试手动强制渲染
try:
if hasattr(self, 'graphicsEngine'):
print("\n尝试强制渲染...")
self.graphicsEngine.renderFrame()
print("强制渲染完成")
except Exception as e:
print(f"强制渲染失败: {str(e)}")
return task.done if task else None
def onButtonClick(self):
print("🎯 Qt环境按钮被点击了")
def main():
print("启动Qt环境专用测试...")
app = QApplication(sys.argv)
# 创建主窗口
mainWindow = QMainWindow()
mainWindow.setWindowTitle("Qt专用GUI测试")
mainWindow.setGeometry(100, 100, 800, 600)
# 创建布局
centralWidget = QWidget()
layout = QVBoxLayout(centralWidget)
# 添加说明
label = QLabel("Qt环境DirectGUI专用测试 - 检查控制台输出")
label.setAlignment(Qt.AlignCenter)
label.setStyleSheet("background-color: lightcoral; padding: 10px;")
layout.addWidget(label)
# 创建世界
world = QtOnlyWorld()
# 创建Panda3D部件
pandaWidget = QPanda3DWidget(world)
layout.addWidget(pandaWidget)
mainWindow.setCentralWidget(centralWidget)
mainWindow.show()
print("\nQt环境窗口已显示")
print("如果能看到红色按钮说明Qt环境下的DirectGUI是正常的")
print("如果看不到按钮但有标题说明DirectButton在Qt环境中有问题")
return app.exec_()
if __name__ == "__main__":
main()

284
demo/test_qt_showbase.py Normal file
View File

@ -0,0 +1,284 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Qt环境ShowBase测试
使用ShowBase在独立窗口中验证GUI功能
"""
import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)
import sys
from direct.showbase.ShowBase import ShowBase
from direct.gui.DirectGui import *
from direct.gui.OnscreenText import OnscreenText
from panda3d.core import *
# 配置Panda3D
loadPrcFileData("", """
win-size 800 600
window-title ShowBase GUI Test
framebuffer-multisample 1
multisamples 2
""")
class ShowBaseGUITest(ShowBase):
def __init__(self):
ShowBase.__init__(self)
print("=== ShowBase GUI测试 ===")
# 设置背景色
self.setBackgroundColor(0.2, 0.2, 0.3)
# 调整相机位置
self.cam.setPos(0, -10, 5)
self.cam.lookAt(0, 0, 0)
# 初始化GUI元素列表
self.gui_elements = []
# 尝试加载中文字体
try:
self.chinese_font = self.loader.loadFont('/usr/share/fonts/truetype/wqy/wqy-microhei.ttc')
if not self.chinese_font:
print("警告: 无法加载中文字体,将使用默认字体")
except:
print("警告: 无法加载中文字体,将使用默认字体")
self.chinese_font = None
# 创建测试内容
self.createTestContent()
# 立即创建GUI元素
self.createGUIElements()
print("\n测试说明:")
print("这是使用ShowBase的独立窗口测试")
print("如果GUI元素正常显示说明主程序的GUI创建逻辑是正确的")
print("问题可能在于Qt集成部分")
def createTestContent(self):
"""创建测试内容"""
print("\n--- 创建3D测试内容 ---")
# 创建地面
cm = CardMaker("ground")
cm.setFrame(-10, 10, -10, 10)
ground = self.render.attachNewNode(cm.generate())
ground.setP(-90)
ground.setColor(0.5, 0.5, 0.5, 1)
# 创建一个立方体
cube = self.loader.loadModel("models/environment")
if not cube:
# 如果找不到内置模型,创建一个简单几何体
cm = CardMaker("test-cube")
cm.setFrame(-1, 1, -1, 1)
cube = self.render.attachNewNode(cm.generate())
cube.reparentTo(self.render)
cube.setPos(0, 5, 1)
cube.setColor(0.8, 0.2, 0.2, 1)
# 添加光照
alight = AmbientLight('alight')
alight.setColor((0.3, 0.3, 0.3, 1))
alnp = self.render.attachNewNode(alight)
self.render.setLight(alnp)
dlight = DirectionalLight('dlight')
dlight.setColor((0.8, 0.8, 0.8, 1))
dlnp = self.render.attachNewNode(dlight)
dlnp.setHpr(45, -45, 0)
self.render.setLight(dlnp)
print("3D内容创建完成")
def createGUIElements(self):
"""创建GUI元素"""
print("\n--- 创建GUI元素 ---")
# 创建标题
self.title = OnscreenText(
text="ShowBase GUI功能测试",
pos=(0, 0.9),
scale=0.08,
fg=(1, 1, 1, 1),
align=TextNode.ACenter,
font=self.chinese_font if self.chinese_font else None
)
# 测试主程序中的GUI创建方法
print("开始创建各种GUI元素...")
# 按钮测试 - 使用不同的逻辑坐标
self.button1 = self.createGUIButton((0, 0, 0), "中心按钮", 0.08)
self.button2 = self.createGUIButton((-15, 0, 0), "左按钮", 0.08)
self.button3 = self.createGUIButton((15, 0, 0), "右按钮", 0.08)
self.button4 = self.createGUIButton((0, 0, 15), "上按钮", 0.08)
self.button5 = self.createGUIButton((0, 0, -15), "下按钮", 0.08)
# 标签测试
self.label1 = self.createGUILabel((0, 0, -25), "中心标签", 0.06)
self.label2 = self.createGUILabel((-20, 0, -10), "左标签", 0.06)
self.label3 = self.createGUILabel((20, 0, -10), "右标签", 0.06)
# 输入框测试
self.entry1 = self.createGUIEntry((0, 0, -35), "中心输入框", 0.05)
self.entry2 = self.createGUIEntry((-20, 0, -25), "左输入框", 0.05)
self.entry3 = self.createGUIEntry((20, 0, -25), "右输入框", 0.05)
# 3D文本测试
self.text3d1 = self.createGUI3DText((0, 5, 3), "主3D文本", 0.5)
self.text3d2 = self.createGUI3DText((-3, 3, 1), "左3D文本", 0.3)
self.text3d3 = self.createGUI3DText((3, 3, 1), "右3D文本", 0.3)
# 虚拟屏幕测试
self.screen1 = self.createGUIVirtualScreen((0, 8, 0), (3, 2), "主虚拟屏幕")
self.screen2 = self.createGUIVirtualScreen((-5, 5, 2), (2, 1), "左屏幕")
self.screen3 = self.createGUIVirtualScreen((5, 5, 2), (2, 1), "右屏幕")
print(f"\nGUI创建完成总计: {len(self.gui_elements)} 个元素")
print("如果你能看到所有GUI元素说明修复是成功的")
# 复制主程序中的GUI创建方法
def createGUIButton(self, pos=(0, 0, 0), text="按钮", size=0.1):
"""创建2D GUI按钮"""
# 将3D坐标转换为2D屏幕坐标
gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1)
button = DirectButton(
text=text,
pos=gui_pos,
scale=size,
command=self.onGUIButtonClick,
extraArgs=[f"button_{len(self.gui_elements)}"],
frameColor=(0.2, 0.6, 0.8, 1),
text_font=self.chinese_font if self.chinese_font else None,
rolloverSound=None,
clickSound=None
)
button.setTag("gui_type", "button")
button.setTag("gui_id", f"button_{len(self.gui_elements)}")
button.setTag("gui_text", text)
self.gui_elements.append(button)
print(f"✓ 创建GUI按钮: {text} (逻辑位置: {pos}, 屏幕位置: {gui_pos})")
return button
def createGUILabel(self, pos=(0, 0, 0), text="标签", size=0.08):
"""创建2D GUI标签"""
gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1)
label = DirectLabel(
text=text,
pos=gui_pos,
scale=size,
frameColor=(0, 0, 0, 0),
text_fg=(1, 1, 1, 1),
text_font=self.chinese_font if self.chinese_font else None
)
label.setTag("gui_type", "label")
label.setTag("gui_id", f"label_{len(self.gui_elements)}")
label.setTag("gui_text", text)
self.gui_elements.append(label)
print(f"✓ 创建GUI标签: {text} (逻辑位置: {pos}, 屏幕位置: {gui_pos})")
return label
def createGUIEntry(self, pos=(0, 0, 0), placeholder="输入文本...", size=0.08):
"""创建2D GUI文本输入框"""
gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1)
entry = DirectEntry(
text="",
pos=gui_pos,
scale=size,
command=self.onGUIEntrySubmit,
extraArgs=[f"entry_{len(self.gui_elements)}"],
initialText=placeholder,
numLines=1,
width=12,
focus=0
)
entry.setTag("gui_type", "entry")
entry.setTag("gui_id", f"entry_{len(self.gui_elements)}")
entry.setTag("gui_placeholder", placeholder)
self.gui_elements.append(entry)
print(f"✓ 创建GUI输入框: {placeholder} (逻辑位置: {pos}, 屏幕位置: {gui_pos})")
return entry
def createGUI3DText(self, pos=(0, 0, 0), text="3D文本", size=0.5):
"""创建3D空间文本"""
textNode = TextNode(f'3d-text-{len(self.gui_elements)}')
textNode.setText(text)
textNode.setAlign(TextNode.ACenter)
if self.chinese_font:
textNode.setFont(self.chinese_font)
textNodePath = self.render.attachNewNode(textNode)
textNodePath.setPos(*pos)
textNodePath.setScale(size)
textNodePath.setColor(1, 1, 0, 1)
textNodePath.setBillboardAxis()
textNodePath.setTag("gui_type", "3d_text")
textNodePath.setTag("gui_id", f"3d_text_{len(self.gui_elements)}")
textNodePath.setTag("gui_text", text)
self.gui_elements.append(textNodePath)
print(f"✓ 创建3D文本: {text} (世界位置: {pos})")
return textNodePath
def createGUIVirtualScreen(self, pos=(0, 0, 0), size=(2, 1), text="虚拟屏幕"):
"""创建3D虚拟屏幕"""
cm = CardMaker(f"virtual-screen-{len(self.gui_elements)}")
cm.setFrame(-size[0]/2, size[0]/2, -size[1]/2, size[1]/2)
virtualScreen = self.render.attachNewNode(cm.generate())
virtualScreen.setPos(*pos)
virtualScreen.setColor(0.2, 0.2, 0.2, 0.8)
virtualScreen.setTransparency(TransparencyAttrib.MAlpha)
screenText = TextNode(f'screen-text-{len(self.gui_elements)}')
screenText.setText(text)
screenText.setAlign(TextNode.ACenter)
if self.chinese_font:
screenText.setFont(self.chinese_font)
screenTextNP = virtualScreen.attachNewNode(screenText)
screenTextNP.setPos(0, 0.01, 0)
screenTextNP.setScale(0.3)
screenTextNP.setColor(0, 1, 0, 1)
virtualScreen.setTag("gui_type", "virtual_screen")
virtualScreen.setTag("gui_id", f"virtual_screen_{len(self.gui_elements)}")
virtualScreen.setTag("gui_text", text)
self.gui_elements.append(virtualScreen)
print(f"✓ 创建虚拟屏幕: {text} (世界位置: {pos})")
return virtualScreen
def onGUIButtonClick(self, button_id):
"""GUI按钮点击事件处理"""
print(f"🎯 GUI按钮被点击: {button_id}")
def onGUIEntrySubmit(self, text, entry_id):
"""GUI输入框提交事件处理"""
print(f"📝 GUI输入框提交: {entry_id} = '{text}'")
def main():
"""主函数"""
print("启动ShowBase GUI功能测试...")
print("这将在独立的Panda3D窗口中测试GUI功能")
# 创建并运行应用
app = ShowBaseGUITest()
app.run()
if __name__ == "__main__":
main()

224
demo/test_qt_vs_showbase.py Normal file
View File

@ -0,0 +1,224 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Qt环境 vs ShowBase环境 GUI对比测试
验证两种环境中DirectGUI的差异
"""
import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)
import sys
from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QLabel, QPushButton
from PyQt5.QtCore import Qt
from QPanda3D.QPanda3DWidget import QPanda3DWidget
from QPanda3D.Panda3DWorld import Panda3DWorld
from direct.showbase.ShowBase import ShowBase
from direct.gui.DirectGui import *
from direct.gui.OnscreenText import OnscreenText
from panda3d.core import *
class QtTestWorld(Panda3DWorld):
"""Qt集成环境测试"""
def __init__(self):
super().__init__()
print("=== Qt环境GUI测试 ===")
self.setBackgroundColor(0.2, 0.2, 0.3)
# 检查环境
print(f"基类: {self.__class__.__bases__}")
print(f"render2d: {self.render2d}")
print(f"aspect2d: {self.aspect2d}")
print(f"窗口类型: {type(self.win) if hasattr(self, 'win') else 'None'}")
# 延迟创建GUI
self.taskMgr.doMethodLater(1.0, self.createTestGUI, "create-gui")
def createTestGUI(self, task=None):
"""创建测试GUI"""
print("\n--- Qt环境创建GUI ---")
# 完全模仿gui_3d_demo.py的创建方式
try:
self.testButton = DirectButton(
text="Qt测试按钮",
pos=(0, 0, 0),
scale=0.1,
command=self.onButtonClick,
frameColor=(1, 0, 0, 1), # 红色,更显眼
text_font=None
)
print(f"✓ Qt环境按钮创建成功: {self.testButton}")
print(f" 父节点: {self.testButton.getParent()}")
print(f" 位置: {self.testButton.getPos()}")
print(f" 隐藏状态: {self.testButton.isHidden()}")
print(f" 渲染状态: {self.testButton.node().isOnstage()}")
except Exception as e:
print(f"✗ Qt环境按钮创建失败: {str(e)}")
# 创建标题文本
try:
self.title = OnscreenText(
text="Qt Environment Test",
pos=(0, 0.8),
scale=0.08,
fg=(1, 1, 1, 1),
align=TextNode.ACenter
)
print(f"✓ Qt环境标题创建成功: {self.title}")
except Exception as e:
print(f"✗ Qt环境标题创建失败: {str(e)}")
# 检查render2d和aspect2d的状态
print(f"\nQt环境渲染状态:")
print(f" render2d子节点数: {self.render2d.getNumChildren()}")
print(f" aspect2d子节点数: {self.aspect2d.getNumChildren()}")
return task.done if task else None
def onButtonClick(self):
print("🎯 Qt环境按钮被点击了")
class ShowBaseTestWorld(ShowBase):
"""ShowBase环境测试"""
def __init__(self):
ShowBase.__init__(self)
print("\n=== ShowBase环境GUI测试 ===")
self.setBackgroundColor(0.2, 0.2, 0.3)
# 检查环境
print(f"基类: {self.__class__.__bases__}")
print(f"render2d: {self.render2d}")
print(f"aspect2d: {self.aspect2d}")
print(f"窗口类型: {type(self.win)}")
# 立即创建GUI
self.createTestGUI()
def createTestGUI(self):
"""创建测试GUI"""
print("\n--- ShowBase环境创建GUI ---")
# 完全相同的GUI创建方式
try:
self.testButton = DirectButton(
text="ShowBase测试按钮",
pos=(0, 0, 0),
scale=0.1,
command=self.onButtonClick,
frameColor=(0, 1, 0, 1), # 绿色,区分
text_font=None
)
print(f"✓ ShowBase环境按钮创建成功: {self.testButton}")
print(f" 父节点: {self.testButton.getParent()}")
print(f" 位置: {self.testButton.getPos()}")
print(f" 隐藏状态: {self.testButton.isHidden()}")
print(f" 渲染状态: {self.testButton.node().isOnstage()}")
except Exception as e:
print(f"✗ ShowBase环境按钮创建失败: {str(e)}")
# 创建标题文本
try:
self.title = OnscreenText(
text="ShowBase Environment Test",
pos=(0, 0.8),
scale=0.08,
fg=(1, 1, 1, 1),
align=TextNode.ACenter
)
print(f"✓ ShowBase环境标题创建成功: {self.title}")
except Exception as e:
print(f"✗ ShowBase环境标题创建失败: {str(e)}")
# 检查render2d和aspect2d的状态
print(f"\nShowBase环境渲染状态:")
print(f" render2d子节点数: {self.render2d.getNumChildren()}")
print(f" aspect2d子节点数: {self.aspect2d.getNumChildren()}")
def onButtonClick(self):
print("🎯 ShowBase环境按钮被点击了")
def testQtEnvironment():
"""测试Qt环境"""
print("启动Qt环境测试...")
app = QApplication(sys.argv)
# 创建主窗口
mainWindow = QMainWindow()
mainWindow.setWindowTitle("Qt环境测试")
mainWindow.setGeometry(100, 100, 600, 400)
# 创建布局
centralWidget = QWidget()
layout = QVBoxLayout(centralWidget)
# 添加说明
label = QLabel("Qt环境DirectGUI测试")
label.setAlignment(Qt.AlignCenter)
label.setStyleSheet("background-color: lightblue; padding: 10px;")
layout.addWidget(label)
# 创建世界
world = QtTestWorld()
# 创建Panda3D部件
pandaWidget = QPanda3DWidget(world)
layout.addWidget(pandaWidget)
mainWindow.setCentralWidget(centralWidget)
mainWindow.show()
print("Qt环境窗口已显示")
return app.exec_()
def testShowBaseEnvironment():
"""测试ShowBase环境"""
print("\n启动ShowBase环境测试...")
# 配置Panda3D窗口
loadPrcFileData("", """
win-size 600 400
window-title ShowBase Environment Test
""")
# 创建ShowBase应用
app = ShowBaseTestWorld()
app.run()
def main():
"""主函数"""
print("DirectGUI环境对比测试")
print("="*50)
choice = input("选择测试环境:\n1. Qt环境\n2. ShowBase环境\n3. 两个都测试\n请输入(1/2/3): ").strip()
if choice == "1":
testQtEnvironment()
elif choice == "2":
testShowBaseEnvironment()
elif choice == "3":
print("先测试ShowBase环境...")
import threading
import time
# 先启动ShowBase环境
showbase_thread = threading.Thread(target=testShowBaseEnvironment)
showbase_thread.daemon = True
showbase_thread.start()
# 等待一会
time.sleep(2)
print("\n然后测试Qt环境...")
testQtEnvironment()
else:
print("无效选择")
if __name__ == "__main__":
main()

173
demo/test_simple_gui.py Normal file
View File

@ -0,0 +1,173 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
简单的2D GUI测试
基于ShowBase直接测试2D GUI组件显示
"""
import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)
from direct.showbase.ShowBase import ShowBase
from direct.gui.DirectGui import *
from panda3d.core import *
import os
class SimpleGUITest(ShowBase):
def __init__(self):
ShowBase.__init__(self)
# 设置背景色
self.setBackgroundColor(0.2, 0.2, 0.3)
# 尝试加载中文字体
self.chinese_font = self.loadChineseFont()
# 创建测试GUI元素
self.createTestGUI()
print("简单GUI测试启动成功!")
print(f"中文字体加载状态: {'成功' if self.chinese_font else '失败,使用默认字体'}")
def loadChineseFont(self):
"""尝试加载中文字体"""
font_paths = [
'/usr/share/fonts/truetype/wqy/wqy-microhei.ttc',
'/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc'
]
for font_path in font_paths:
if os.path.exists(font_path):
try:
font = self.loader.loadFont(font_path)
if font:
return font
except:
continue
return None
def createTestGUI(self):
"""创建测试GUI元素"""
print("\n=== 创建2D GUI测试元素 ===")
# 标题
self.title = OnscreenText(
text="2D GUI 修复测试",
pos=(0, 0.9),
scale=0.08,
fg=(1, 1, 1, 1),
align=TextNode.ACenter,
font=self.chinese_font if self.chinese_font else None
)
# 测试修复后的坐标转换
test_positions = [
("中心", (0, 0, 0)),
("左侧", (-5, 0, 0)),
("右侧", (5, 0, 0)),
("上方", (0, 0, 5)),
("下方", (0, 0, -5))
]
self.buttons = []
self.labels = []
for i, (name, logical_pos) in enumerate(test_positions):
# 应用修复后的坐标转换
screen_pos = (logical_pos[0] * 0.1, 0, logical_pos[2] * 0.1)
print(f"创建 {name} GUI元素:")
print(f" 逻辑坐标: {logical_pos}")
print(f" 屏幕坐标: {screen_pos}")
# 创建按钮
button = DirectButton(
text=f"{name}按钮",
pos=screen_pos,
scale=0.08,
command=self.onButtonClick,
extraArgs=[name],
frameColor=(0.2, 0.6, 0.8, 1),
text_font=self.chinese_font if self.chinese_font else None,
rolloverSound=None,
clickSound=None
)
self.buttons.append(button)
# 创建标签(偏移位置)
label_pos = (screen_pos[0], 0, screen_pos[2] - 0.15)
label = DirectLabel(
text=f"{name}标签",
pos=label_pos,
scale=0.06,
frameColor=(0, 0, 0, 0),
text_fg=(1, 1, 0, 1),
text_font=self.chinese_font if self.chinese_font else None
)
self.labels.append(label)
print(f" 按钮已创建在: {screen_pos}")
print(f" 标签已创建在: {label_pos}")
# 创建输入框
self.entry = DirectEntry(
text="",
pos=(0, 0, -0.7),
scale=0.06,
command=self.onEntrySubmit,
initialText="测试输入框",
numLines=1,
width=15,
focus=0
)
print(f"输入框已创建在: (0, 0, -0.7)")
# 创建退出按钮
self.exitButton = DirectButton(
text="退出测试",
pos=(0, 0, -0.85),
scale=0.06,
command=self.exitTest,
frameColor=(0.8, 0.2, 0.2, 1),
text_font=self.chinese_font if self.chinese_font else None
)
# 状态信息
self.statusText = OnscreenText(
text="如果你能看到5个按钮和5个标签说明2D GUI修复成功!",
pos=(0, -0.95),
scale=0.04,
fg=(0, 1, 0, 1),
align=TextNode.ACenter,
font=self.chinese_font if self.chinese_font else None
)
print("=== 所有GUI元素创建完成 ===")
print("如果修复成功,你应该看到:")
print("- 5个不同位置的按钮中心、左侧、右侧、上方、下方")
print("- 5个对应的标签")
print("- 1个输入框")
print("- 1个退出按钮")
def onButtonClick(self, name):
"""按钮点击事件"""
print(f"✓ 按钮点击测试成功: {name}")
self.statusText.setText(f"点击了 {name} 按钮 - 测试成功!")
def onEntrySubmit(self, text):
"""输入框提交事件"""
print(f"✓ 输入框测试成功: {text}")
self.statusText.setText(f"输入框内容: {text}")
def exitTest(self):
"""退出测试"""
print("退出简单GUI测试")
self.userExit()
if __name__ == "__main__":
print("启动简单2D GUI测试...")
print("这个测试使用ShowBase基类应该能正确显示2D GUI组件")
app = SimpleGUITest()
app.run()

View File

@ -0,0 +1,61 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
测试简化后的坐标轴系统
验证选中物体自动显示坐标轴功能
"""
import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)
import sys
from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QLabel, QPushButton, QHBoxLayout
from PyQt5.QtCore import Qt
from QPanda3D.QPanda3DWidget import QPanda3DWidget
sys.path.append('..')
from main import MyWorld, CustomPanda3DWidget
def main():
print("启动简化坐标轴测试...")
app = QApplication(sys.argv)
# 创建主窗口
mainWindow = QMainWindow()
mainWindow.setWindowTitle("简化坐标轴测试 - 选中物体自动显示坐标轴")
mainWindow.setGeometry(100, 100, 1000, 800)
# 创建布局
centralWidget = QWidget()
layout = QVBoxLayout(centralWidget)
# 添加说明
infoLayout = QHBoxLayout()
label = QLabel("简化坐标轴测试 - 点击任何物体都会自动显示坐标轴,无需移动工具")
label.setAlignment(Qt.AlignCenter)
label.setStyleSheet("background-color: lightgreen; padding: 10px; font-size: 12px;")
infoLayout.addWidget(label)
layout.addLayout(infoLayout)
# 创建世界
world = MyWorld()
# 创建部件
pandaWidget = CustomPanda3DWidget(world)
layout.addWidget(pandaWidget)
mainWindow.setCentralWidget(centralWidget)
mainWindow.show()
print("\n测试说明:")
print("1. 程序启动后会显示一个空的3D场景")
print("2. 可以导入模型或创建基本几何体")
print("3. 点击任何物体都会自动显示坐标轴")
print("4. 不再需要选择移动工具")
print("5. 直接拖拽坐标轴即可移动物体")
return app.exec_()
if __name__ == "__main__":
sys.exit(main())

158
demo/test_size_fix.py Normal file
View File

@ -0,0 +1,158 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
测试Qt环境下窗口尺寸获取修复
验证从Qt部件获取准确尺寸的功能
"""
import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)
import sys
from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QLabel
from PyQt5.QtCore import Qt
from QPanda3D.QPanda3DWidget import QPanda3DWidget
from QPanda3D.Panda3DWorld import Panda3DWorld
from panda3d.core import *
from direct.task import Task
class SizeTestWorld(Panda3DWorld):
def __init__(self):
super().__init__()
print("=== Qt窗口尺寸测试 ===")
self.setBackgroundColor(0.2, 0.2, 0.3)
# Qt部件引用
self.qtWidget = None
# 启动定时检查任务
self.taskMgr.doMethodLater(2.0, self.checkSizes, "check-sizes")
def setQtWidget(self, widget):
"""设置Qt部件引用"""
self.qtWidget = widget
print(f"✓ 设置Qt部件引用: {widget}")
print(f" Qt部件类型: {type(widget)}")
def getWindowSize(self):
"""获取准确的窗口尺寸"""
if self.qtWidget:
# 优先使用Qt部件的实际尺寸
width, height = self.qtWidget.getActualSize()
if width > 0 and height > 0:
print(f"✓ 从Qt部件获取窗口尺寸: {width} x {height}")
return width, height
# 备用方案使用Panda3D窗口尺寸
if hasattr(self, 'win') and self.win:
width = self.win.getXSize()
height = self.win.getYSize()
print(f"⚠ 从Panda3D窗口获取尺寸: {width} x {height}")
return width, height
# 最后的默认值
print("⚠ 使用默认窗口尺寸: 800 x 600")
return 800, 600
def checkSizes(self, task):
"""检查各种尺寸获取方法的结果"""
print("\n=== 窗口尺寸对比测试 ===")
# 方法1从Qt部件获取
if self.qtWidget:
qt_width, qt_height = self.qtWidget.getActualSize()
print(f"Qt部件尺寸: {qt_width} x {qt_height}")
else:
print("Qt部件尺寸: 未设置")
# 方法2从Panda3D窗口获取
if hasattr(self, 'win') and self.win:
panda_width = self.win.getXSize()
panda_height = self.win.getYSize()
print(f"Panda3D窗口尺寸: {panda_width} x {panda_height}")
else:
print("Panda3D窗口尺寸: 无效")
# 方法3使用新的getWindowSize方法
final_width, final_height = self.getWindowSize()
print(f"最终使用尺寸: {final_width} x {final_height}")
# 检查是否有差异
if self.qtWidget and hasattr(self, 'win') and self.win:
qt_width, qt_height = self.qtWidget.getActualSize()
panda_width = self.win.getXSize()
panda_height = self.win.getYSize()
if qt_width != panda_width or qt_height != panda_height:
print(f"⚠ 发现尺寸差异Qt: {qt_width}x{qt_height}, Panda3D: {panda_width}x{panda_height}")
print("这就是之前坐标轴点击检测失败的原因!")
else:
print("✓ 尺寸一致,没有问题")
# 每5秒检查一次
return task.again
class CustomSizeTestWidget(QPanda3DWidget):
"""支持尺寸获取的测试部件"""
def __init__(self, world, parent=None):
super().__init__(world, parent)
self.world = world
# 让world引用这个widget
if hasattr(world, 'setQtWidget'):
world.setQtWidget(self)
def getActualSize(self):
"""获取Qt部件的实际渲染尺寸"""
return (self.width(), self.height())
def resizeEvent(self, event):
"""处理窗口大小改变事件"""
super().resizeEvent(event)
print(f"\n窗口大小改变: {self.width()} x {self.height()}")
# 触发新的尺寸检查
if hasattr(self.world, 'checkSizes'):
self.world.taskMgr.doMethodLater(0.1, self.world.checkSizes, "check-sizes-after-resize")
def main():
print("启动Qt窗口尺寸测试...")
app = QApplication(sys.argv)
# 创建主窗口
mainWindow = QMainWindow()
mainWindow.setWindowTitle("Qt窗口尺寸测试 - 坐标轴修复验证")
mainWindow.setGeometry(100, 100, 900, 700) # 设置一个明确的大小
# 创建布局
centralWidget = QWidget()
layout = QVBoxLayout(centralWidget)
# 添加说明
label = QLabel("Qt窗口尺寸测试 - 查看控制台输出")
label.setAlignment(Qt.AlignCenter)
label.setStyleSheet("background-color: lightgreen; padding: 10px; font-size: 14px;")
layout.addWidget(label)
# 创建世界
world = SizeTestWorld()
# 创建测试部件
pandaWidget = CustomSizeTestWidget(world)
layout.addWidget(pandaWidget)
mainWindow.setCentralWidget(centralWidget)
mainWindow.show()
print("\n测试说明:")
print("1. 程序会每5秒检查一次窗口尺寸")
print("2. 尝试调整窗口大小,观察尺寸变化")
print("3. 如果Qt尺寸和Panda3D尺寸不同说明修复有效")
print("4. 如果尺寸一致,说明你的环境没有这个问题")
return app.exec_()
if __name__ == "__main__":
sys.exit(main())

91
demo/test_toolbar_gui.py Normal file
View File

@ -0,0 +1,91 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
工具栏GUI测试
模拟用户点击工具栏按钮创建GUI元素
"""
import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)
import sys
from PyQt5.QtWidgets import QApplication, QMainWindow
from PyQt5.QtCore import QTimer
# 导入主程序
from test import MyWorld, CustomPanda3DWidget
def testToolbarGUI():
"""测试工具栏GUI创建功能"""
print("=== 工具栏GUI创建测试 ===")
app = QApplication(sys.argv)
# 创建主程序的世界对象
world = MyWorld()
# 创建窗口
mainWindow = QMainWindow()
mainWindow.setWindowTitle("工具栏GUI测试")
mainWindow.setGeometry(100, 100, 800, 600)
# 创建Panda3D部件
pandaWidget = CustomPanda3DWidget(world)
mainWindow.setCentralWidget(pandaWidget)
# 显示窗口
mainWindow.show()
# 模拟工具栏按钮点击
def createGUIElements():
print("\n模拟工具栏按钮点击...")
# 创建一些GUI元素使用合理的坐标
print("创建GUI按钮...")
button1 = world.createGUIButton((0, 0, 0), "中心按钮", 0.08)
print(f"按钮1屏幕位置: {button1.getPos()}")
button2 = world.createGUIButton((-10, 0, 0), "左按钮", 0.08)
print(f"按钮2屏幕位置: {button2.getPos()}")
button3 = world.createGUIButton((10, 0, 0), "右按钮", 0.08)
print(f"按钮3屏幕位置: {button3.getPos()}")
print("创建GUI标签...")
label1 = world.createGUILabel((0, 0, -10), "测试标签", 0.06)
print(f"标签1屏幕位置: {label1.getPos()}")
print("创建GUI输入框...")
entry1 = world.createGUIEntry((0, 0, -15), "输入测试", 0.05)
print(f"输入框1屏幕位置: {entry1.getPos()}")
print("创建3D文本...")
text3d = world.createGUI3DText((0, 5, 2), "3D测试", 0.5)
print(f"3D文本位置: {text3d.getPos()}")
print(f"\n总共创建了 {len(world.gui_elements)} 个GUI元素")
# 检查所有坐标是否在有效范围内
print("\n检查2D GUI坐标范围:")
for i, element in enumerate(world.gui_elements):
if hasattr(element, 'getTag'):
gui_type = element.getTag("gui_type")
if gui_type in ["button", "label", "entry"]:
pos = element.getPos()
x, z = pos.getX(), pos.getZ()
in_range = (-1 <= x <= 1) and (-1 <= z <= 1)
print(f" {gui_type}: ({x:.3f}, {z:.3f}) - {'' if in_range else '✗ 超出范围'}")
print("\n如果你能看到蓝色按钮、白色标签和输入框,说明修复成功!")
print("如果只能看到黄色3D文本说明2D GUI坐标可能仍有问题。")
# 1秒后创建GUI元素
QTimer.singleShot(1000, createGUIElements)
print("窗口已显示即将测试GUI创建...")
return app.exec_()
if __name__ == "__main__":
testToolbarGUI()

102
demo/test_video_fix.py Normal file
View File

@ -0,0 +1,102 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
视频播放功能修复测试
"""
from direct.showbase.ShowBase import ShowBase
from panda3d.core import loadPrcFileData
import sys
# 配置窗口
loadPrcFileData("", """
win-size 800 600
window-title 视频功能测试
""")
class VideoTest(ShowBase):
def __init__(self):
ShowBase.__init__(self)
# 初始化属性以兼容 VideoManager
self.models = [] # 模型列表
print("🚀 开始视频功能测试...")
# 测试导入
try:
from video_integration import VideoManager
print("✅ 视频管理器导入成功")
except Exception as e:
print(f"❌ 视频管理器导入失败: {e}")
return
# 创建视频管理器
try:
self.video_manager = VideoManager(self)
print("✅ 视频管理器创建成功")
except Exception as e:
print(f"❌ 视频管理器创建失败: {e}")
return
# 测试创建动画屏幕
try:
screen = self.video_manager.create_video_screen(
pos=(0, 0, 2),
name="test_screen"
)
if screen:
print("✅ 动画屏幕创建成功")
else:
print("❌ 动画屏幕创建失败")
except Exception as e:
print(f"❌ 动画屏幕创建错误: {e}")
# 测试创建广告牌
try:
billboard = self.video_manager.create_video_billboard(
pos=(3, 0, 1),
name="test_billboard"
)
if billboard:
print("✅ 视频广告牌创建成功")
else:
print("❌ 视频广告牌创建失败")
except Exception as e:
print(f"❌ 视频广告牌创建错误: {e}")
# 测试创建全景视频
try:
spherical = self.video_manager.create_spherical_video(
pos=(-3, 0, 0),
radius=2,
name="test_spherical"
)
if spherical:
print("✅ 全景视频创建成功")
else:
print("❌ 全景视频创建失败")
except Exception as e:
print(f"❌ 全景视频创建错误: {e}")
# 等待几秒钟查看动画效果
print("\n🎬 测试完成!您应该看到:")
print(" - 左侧: 波纹动画屏幕")
print(" - 中间: 面向相机的广告牌")
print(" - 右侧: 全景动画球体")
print("\n按 ESC 键退出")
# 设置退出键
self.accept("escape", sys.exit)
# 设置相机
self.cam.setPos(0, -8, 2)
self.cam.lookAt(0, 0, 1)
def updateSceneTree(self):
"""空的场景树更新方法(用于兼容 VideoManager"""
pass
if __name__ == "__main__":
app = VideoTest()
app.run()

444
demo/video_integration.py Normal file
View File

@ -0,0 +1,444 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
视频播放集成模块
用于在现有的Panda3D编辑器中添加视频播放功能
"""
from panda3d.core import (
MovieTexture, CardMaker, Texture, PNMImage,
TextureStage, NodePath, Vec3
)
import os
import math
class VideoManager:
"""视频管理器 - 集成到现有的MyWorld类中"""
def __init__(self, world_instance):
self.world = world_instance
self.video_objects = [] # 存储所有视频对象
self.supported_formats = ['.mp4', '.avi', '.mov', '.mkv', '.webm']
def create_video_screen(self, pos=(0, 0, 0), size=(4, 3), video_path=None, name="video_screen"):
"""创建视频屏幕"""
try:
# 创建屏幕几何体
cm = CardMaker(f"video_screen_{len(self.video_objects)}")
cm.setFrame(-size[0]/2, size[0]/2, -size[1]/2, size[1]/2)
video_screen = self.world.render.attachNewNode(cm.generate())
video_screen.setPos(*pos)
video_screen.setName(name)
# 创建视频纹理
if video_path and os.path.exists(video_path):
movie_texture = MovieTexture(name)
success = movie_texture.read(video_path)
if success:
video_screen.setTexture(movie_texture)
# 添加视频对象到管理器
video_obj = {
'node': video_screen,
'texture': movie_texture,
'path': video_path,
'name': name,
'type': 'movie',
'is_playing': False
}
# 播放视频
movie_texture.play()
video_obj['is_playing'] = True
self.video_objects.append(video_obj)
# 添加到模型列表和场景树
self.world.models.append(video_screen)
self.world.updateSceneTree()
print(f"✓ 视频屏幕创建成功: {name}")
return video_screen
else:
print(f"✗ 无法加载视频: {video_path}")
# 创建占位符纹理
self._create_placeholder_texture(video_screen, name)
else:
# 创建程序化动画纹理作为演示
self._create_animated_texture(video_screen, name)
return video_screen
except Exception as e:
print(f"创建视频屏幕失败: {str(e)}")
return None
def create_video_billboard(self, pos=(0, 0, 0), size=(2, 1.5), video_path=None, name="video_billboard"):
"""创建面向相机的视频广告牌"""
try:
video_screen = self.create_video_screen(pos, size, video_path, name)
if video_screen:
# 设置为广告牌模式(总是面向相机)
video_screen.setBillboardAxis()
print(f"✓ 视频广告牌创建成功: {name}")
return video_screen
except Exception as e:
print(f"创建视频广告牌失败: {str(e)}")
return None
def create_spherical_video(self, pos=(0, 0, 0), radius=10, video_path=None, name="spherical_video"):
"""创建球形全景视频"""
try:
# 创建球体几何(使用多个面片模拟)
sphere = self._create_sphere_geometry(radius)
sphere.reparentTo(self.world.render)
sphere.setPos(*pos)
sphere.setName(name)
# 反转法线以便从内部观看
sphere.setTwoSided(True)
sphere.setScale(-1, 1, 1) # 反转X轴以正确显示内部
# 创建视频纹理
if video_path and os.path.exists(video_path):
movie_texture = MovieTexture(name)
success = movie_texture.read(video_path)
if success:
sphere.setTexture(movie_texture)
movie_texture.play()
video_obj = {
'node': sphere,
'texture': movie_texture,
'path': video_path,
'name': name,
'type': 'spherical',
'is_playing': True
}
self.video_objects.append(video_obj)
else:
# 创建程序化全景纹理
self._create_panoramic_texture(sphere, name)
# 添加到模型列表
self.world.models.append(sphere)
self.world.updateSceneTree()
print(f"✓ 球形全景视频创建成功: {name}")
return sphere
except Exception as e:
print(f"创建球形全景视频失败: {str(e)}")
return None
def _create_sphere_geometry(self, radius):
"""创建球体几何"""
# 简化版本:创建立方体作为球体替代
cm = CardMaker("sphere_face")
cm.setFrame(-radius, radius, -radius, radius)
sphere_root = self.world.render.attachNewNode("sphere")
# 创建6个面立方体的6个面
faces = [
(Vec3(0, radius, 0), Vec3(0, 0, 0)), # 前面
(Vec3(0, -radius, 0), Vec3(0, 180, 0)), # 后面
(Vec3(radius, 0, 0), Vec3(0, 90, 0)), # 右面
(Vec3(-radius, 0, 0), Vec3(0, -90, 0)), # 左面
(Vec3(0, 0, radius), Vec3(-90, 0, 0)), # 上面
(Vec3(0, 0, -radius), Vec3(90, 0, 0)), # 下面
]
for i, (pos, hpr) in enumerate(faces):
face = sphere_root.attachNewNode(cm.generate())
face.setPos(pos)
face.setHpr(hpr)
return sphere_root
def _create_placeholder_texture(self, node, name):
"""创建占位符纹理"""
# 创建简单的渐变纹理
texture = Texture()
texture.setup2dTexture(256, 256, Texture.TUnsignedByte, Texture.FRgba)
img = PNMImage(256, 256, 4)
for y in range(256):
for x in range(256):
# 创建棋盘格图案
if (x // 32 + y // 32) % 2:
img.setXel(x, y, 0.8, 0.8, 0.8)
else:
img.setXel(x, y, 0.3, 0.3, 0.3)
img.setAlpha(x, y, 1.0)
texture.load(img)
node.setTexture(texture)
# 添加到视频对象列表
video_obj = {
'node': node,
'texture': texture,
'path': None,
'name': name,
'type': 'placeholder',
'is_playing': False
}
self.video_objects.append(video_obj)
def _create_animated_texture(self, node, name):
"""创建动画纹理"""
texture = Texture()
texture.setup2dTexture(256, 256, Texture.TUnsignedByte, Texture.FRgba)
node.setTexture(texture)
# 启动动画更新任务 - 修复参数传递顺序
task_name = f"animate_texture_{name}"
self.world.taskMgr.add(lambda task: self._update_animated_texture(task, texture, name), task_name)
# 添加到视频对象列表
video_obj = {
'node': node,
'texture': texture,
'path': None,
'name': name,
'type': 'animated',
'is_playing': True,
'task_name': task_name
}
self.video_objects.append(video_obj)
def _create_panoramic_texture(self, node, name):
"""创建全景动画纹理"""
texture = Texture()
texture.setup2dTexture(512, 256, Texture.TUnsignedByte, Texture.FRgba)
node.setTexture(texture)
# 启动全景动画更新任务 - 修复参数传递顺序
task_name = f"animate_panoramic_{name}"
self.world.taskMgr.add(lambda task: self._update_panoramic_texture(task, texture, name), task_name)
# 添加到视频对象列表
video_obj = {
'node': node,
'texture': texture,
'path': None,
'name': name,
'type': 'panoramic',
'is_playing': True,
'task_name': task_name
}
self.video_objects.append(video_obj)
def _update_animated_texture(self, task, texture, name):
"""更新动画纹理"""
try:
time = task.time
img = PNMImage(256, 256, 4)
for y in range(256):
for x in range(256):
# 创建波纹效果
dist_x = (x - 128) / 128.0
dist_y = (y - 128) / 128.0
distance = math.sqrt(dist_x**2 + dist_y**2)
wave = math.sin(distance * 8 - time * 3)
r = (wave + 1) * 0.5
g = (math.sin(dist_x * 4 + time * 2) + 1) * 0.5
b = (math.sin(dist_y * 4 + time * 2.5) + 1) * 0.5
img.setXel(x, y, r, g, b)
img.setAlpha(x, y, 1.0)
texture.load(img)
return task.cont
except Exception as e:
print(f"更新动画纹理失败: {str(e)}")
return task.done
def _update_panoramic_texture(self, task, texture, name):
"""更新全景动画纹理"""
try:
time = task.time
img = PNMImage(512, 256, 4)
for y in range(256):
for x in range(512):
# 创建全景旋转效果
u = x / 512.0 # 经度 (0-1)
v = y / 256.0 # 纬度 (0-1)
angle = u * 2 * math.pi + time * 0.5
height = (v - 0.5) * math.pi
r = (math.sin(angle * 3) + 1) * 0.5
g = (math.cos(angle * 2 + height * 4) + 1) * 0.5
b = (math.sin(height * 6 + time) + 1) * 0.5
img.setXel(x, y, r, g, b)
img.setAlpha(x, y, 1.0)
texture.load(img)
return task.cont
except Exception as e:
print(f"更新全景纹理失败: {str(e)}")
return task.done
def play_video(self, video_name):
"""播放指定视频"""
for video_obj in self.video_objects:
if video_obj['name'] == video_name:
if video_obj['type'] == 'movie' and hasattr(video_obj['texture'], 'play'):
video_obj['texture'].play()
video_obj['is_playing'] = True
print(f"播放视频: {video_name}")
elif video_obj['type'] in ['animated', 'panoramic']:
# 重新启动动画任务
if 'task_name' in video_obj:
task_name = video_obj['task_name']
if video_obj['type'] == 'animated':
self.world.taskMgr.add(
lambda task: self._update_animated_texture(task, video_obj['texture'], video_name),
task_name
)
else:
self.world.taskMgr.add(
lambda task: self._update_panoramic_texture(task, video_obj['texture'], video_name),
task_name
)
video_obj['is_playing'] = True
print(f"恢复动画: {video_name}")
break
def pause_video(self, video_name):
"""暂停指定视频"""
for video_obj in self.video_objects:
if video_obj['name'] == video_name:
if video_obj['type'] == 'movie' and hasattr(video_obj['texture'], 'stop'):
video_obj['texture'].stop()
video_obj['is_playing'] = False
print(f"暂停视频: {video_name}")
elif video_obj['type'] in ['animated', 'panoramic']:
# 停止动画任务
if 'task_name' in video_obj:
self.world.taskMgr.remove(video_obj['task_name'])
video_obj['is_playing'] = False
print(f"暂停动画: {video_name}")
break
def stop_video(self, video_name):
"""停止指定视频"""
for video_obj in self.video_objects:
if video_obj['name'] == video_name:
if video_obj['type'] == 'movie':
if hasattr(video_obj['texture'], 'stop'):
video_obj['texture'].stop()
if hasattr(video_obj['texture'], 'setTime'):
video_obj['texture'].setTime(0)
video_obj['is_playing'] = False
print(f"停止视频: {video_name}")
elif video_obj['type'] in ['animated', 'panoramic']:
# 停止动画任务
if 'task_name' in video_obj:
self.world.taskMgr.remove(video_obj['task_name'])
video_obj['is_playing'] = False
print(f"停止动画: {video_name}")
break
def set_video_speed(self, video_name, speed):
"""设置视频播放速度"""
for video_obj in self.video_objects:
if video_obj['name'] == video_name:
if video_obj['type'] == 'movie' and hasattr(video_obj['texture'], 'setPlayRate'):
video_obj['texture'].setPlayRate(speed)
print(f"设置视频 {video_name} 播放速度: {speed}x")
break
def remove_video(self, video_name):
"""移除指定视频"""
for i, video_obj in enumerate(self.video_objects):
if video_obj['name'] == video_name:
# 停止播放
self.stop_video(video_name)
# 从场景中移除节点
if video_obj['node'] in self.world.models:
self.world.models.remove(video_obj['node'])
video_obj['node'].removeNode()
# 从列表中移除
self.video_objects.pop(i)
# 更新场景树
self.world.updateSceneTree()
print(f"移除视频: {video_name}")
break
def get_video_info(self, video_name):
"""获取视频信息"""
for video_obj in self.video_objects:
if video_obj['name'] == video_name:
info = {
'name': video_obj['name'],
'type': video_obj['type'],
'is_playing': video_obj['is_playing'],
'path': video_obj.get('path', 'N/A')
}
if video_obj['type'] == 'movie' and hasattr(video_obj['texture'], 'getVideoLength'):
info['length'] = video_obj['texture'].getVideoLength()
info['current_time'] = video_obj['texture'].getTime()
info['play_rate'] = video_obj['texture'].getPlayRate()
return info
return None
def list_videos(self):
"""列出所有视频"""
videos = []
for video_obj in self.video_objects:
videos.append({
'name': video_obj['name'],
'type': video_obj['type'],
'is_playing': video_obj['is_playing']
})
return videos
def is_video_file(self, filepath):
"""检查文件是否是支持的视频格式"""
if not filepath:
return False
ext = os.path.splitext(filepath)[1].lower()
return ext in self.supported_formats
# 使用示例(集成到现有的 MyWorld 类中):
"""
# 在 MyWorld 类的 __init__ 方法中添加:
self.video_manager = VideoManager(self)
# 在文件拖放处理中添加视频支持:
def dropEvent(self, event):
if event.mimeData().hasUrls():
for url in event.mimeData().urls():
filepath = url.toLocalFile()
if self.world.video_manager.is_video_file(filepath):
# 创建视频屏幕
self.world.video_manager.create_video_screen(
pos=(0, 0, 2),
video_path=filepath,
name=f"video_{len(self.world.video_manager.video_objects)}"
)
elif filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx')):
self.world.importModel(filepath)
"""

View File

@ -0,0 +1,395 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Panda3D 视频播放示例
演示如何在Panda3D中播放内嵌视频
"""
from direct.showbase.ShowBase import ShowBase
from panda3d.core import (
MovieTexture, CardMaker, TextureStage, NodePath,
WindowProperties, loadPrcFileData, Vec3, Point3,
AmbientLight, DirectionalLight, GeomNode
)
import os
# 配置Panda3D
loadPrcFileData("", """
win-size 1280 720
window-title Panda3D 视频播放器
show-frame-rate-meter true
""")
class VideoPlayerDemo(ShowBase):
def __init__(self):
ShowBase.__init__(self)
# 设置背景色
self.setBackgroundColor(0.1, 0.1, 0.1)
# 设置相机
self.cam.setPos(0, -10, 0)
self.cam.lookAt(0, 0, 0)
# 添加光照
self.setup_lighting()
# 创建视频播放器
self.setup_video_players()
# 设置控制
self.setup_controls()
print("视频播放器已启动!")
print("控制键:")
print(" SPACE - 播放/暂停")
print(" R - 重新播放")
print(" + - 加速播放")
print(" - - 减速播放")
print(" ESC - 退出")
def setup_lighting(self):
"""设置基础光照"""
# 环境光
alight = AmbientLight('ambient')
alight.setColor((0.3, 0.3, 0.3, 1))
alnp = self.render.attachNewNode(alight)
self.render.setLight(alnp)
# 定向光
dlight = DirectionalLight('directional')
dlight.setColor((0.8, 0.8, 0.8, 1))
dlnp = self.render.attachNewNode(dlight)
dlnp.setHpr(45, -45, 0)
self.render.setLight(dlnp)
def setup_video_players(self):
"""设置视频播放器"""
self.videos = []
# 方法1: 平面视频播放器
self.create_flat_video_player()
# 方法2: 3D模型上的视频纹理
self.create_3d_video_screen()
# 方法3: 球形全景视频
self.create_spherical_video()
def create_flat_video_player(self):
"""创建平面视频播放器"""
try:
# 创建视频纹理 (这里使用示例路径,需要替换为实际视频文件)
video_path = "video/sample.mp4" # 支持 .mp4, .avi, .mov 等格式
# 如果文件不存在,创建一个简单的色彩动画作为演示
if not os.path.exists(video_path):
self.create_color_animation_demo()
return
# 加载视频
movie_texture = MovieTexture()
success = movie_texture.read(video_path)
if success:
# 创建卡片几何体
cm = CardMaker("video_card")
cm.setFrame(-2, 2, -1.5, 1.5)
video_card = self.render.attachNewNode(cm.generate())
video_card.setTexture(movie_texture)
video_card.setPos(-4, 0, 0)
# 播放视频
movie_texture.play()
self.videos.append({
'texture': movie_texture,
'node': video_card,
'name': 'flat_video'
})
print(f"✓ 平面视频播放器创建成功: {video_path}")
else:
print(f"✗ 无法加载视频: {video_path}")
except Exception as e:
print(f"创建平面视频播放器失败: {str(e)}")
def create_3d_video_screen(self):
"""在3D模型上创建视频纹理"""
try:
# 创建一个简单的立方体作为"电视屏幕"
from panda3d.core import CardMaker
# 创建屏幕边框
frame_cm = CardMaker("screen_frame")
frame_cm.setFrame(-2.2, 2.2, -1.7, 1.7)
screen_frame = self.render.attachNewNode(frame_cm.generate())
screen_frame.setColor(0.1, 0.1, 0.1, 1)
screen_frame.setPos(0, 0, 0)
# 创建屏幕
screen_cm = CardMaker("video_screen")
screen_cm.setFrame(-2, 2, -1.5, 1.5)
video_screen = screen_frame.attachNewNode(screen_cm.generate())
# 创建程序化视频内容 (颜色渐变动画)
movie_texture = self.create_procedural_video(panoramic=False)
video_screen.setTexture(movie_texture)
self.videos.append({
'texture': movie_texture,
'node': video_screen,
'name': '3d_screen'
})
print("✓ 3D视频屏幕创建成功")
except Exception as e:
print(f"创建3D视频屏幕失败: {str(e)}")
def create_spherical_video(self):
"""创建球形全景视频"""
try:
# 创建球体几何(程序化生成)
sphere = self.create_sphere_geometry(radius=2)
sphere.reparentTo(self.render)
sphere.setPos(4, 0, 0)
sphere.setScale(2)
# 反转法线以便从内部观看
sphere.setTwoSided(True)
# 创建全景视频纹理
panoramic_texture = self.create_procedural_video(panoramic=True)
sphere.setTexture(panoramic_texture)
self.videos.append({
'texture': panoramic_texture,
'node': sphere,
'name': 'spherical_video'
})
print("✓ 球形全景视频创建成功")
except Exception as e:
print(f"创建球形全景视频失败: {str(e)}")
def create_sphere_geometry(self, radius=1):
"""程序化创建球体几何"""
# 创建简化的球体立方体的6个面
sphere_root = self.render.attachNewNode("sphere")
faces = [
# (位置, 旋转)
(Vec3(0, radius, 0), Vec3(0, 0, 0)), # 前面
(Vec3(0, -radius, 0), Vec3(0, 180, 0)), # 后面
(Vec3(radius, 0, 0), Vec3(0, 90, 0)), # 右面
(Vec3(-radius, 0, 0), Vec3(0, -90, 0)), # 左面
(Vec3(0, 0, radius), Vec3(-90, 0, 0)), # 上面
(Vec3(0, 0, -radius), Vec3(90, 0, 0)), # 下面
]
cm = CardMaker("sphere_face")
cm.setFrame(-radius, radius, -radius, radius)
for i, (pos, hpr) in enumerate(faces):
face = sphere_root.attachNewNode(cm.generate())
face.setPos(pos)
face.setHpr(hpr)
return sphere_root
def create_procedural_video(self, panoramic=False):
"""创建程序化视频内容作为演示"""
from panda3d.core import PNMImage, Texture
# 创建动态纹理
texture = Texture()
if panoramic:
texture.setup2dTexture(512, 256, Texture.TUnsignedByte, Texture.FRgba)
else:
texture.setup2dTexture(256, 256, Texture.TUnsignedByte, Texture.FRgba)
# 启动更新任务 - 修复参数传递使用lambda
task_name = f"update_video_{'panoramic' if panoramic else 'normal'}"
self.taskMgr.add(lambda task: self.update_procedural_video(task, texture, panoramic), task_name)
return texture
def update_procedural_video(self, task, texture, panoramic):
"""更新程序化视频内容"""
try:
from panda3d.core import PNMImage
import math
# 获取当前时间
time = task.time
# 根据纹理类型创建不同尺寸的图像
if panoramic:
img = PNMImage(512, 256, 4) # 全景纹理
width, height = 512, 256
else:
img = PNMImage(256, 256, 4) # 普通纹理
width, height = 256, 256
for y in range(height):
for x in range(width):
# 创建动态色彩效果
if panoramic:
# 全景模式:创建旋转的渐变
u = x / float(width) # 经度 (0-1)
v = y / float(height) # 纬度 (0-1)
angle = u * 2 * math.pi + time * 0.5
height_factor = (v - 0.5) * math.pi
r = (math.sin(angle * 3) + 1) * 0.5
g = (math.cos(angle * 2 + height_factor * 4) + 1) * 0.5
b = (math.sin(height_factor * 6 + time) + 1) * 0.5
else:
# 普通模式:创建波纹效果
center_x = width // 2
center_y = height // 2
dist_x = (x - center_x) / float(center_x)
dist_y = (y - center_y) / float(center_y)
distance = math.sqrt(dist_x**2 + dist_y**2)
wave = math.sin(distance * 10 - time * 5)
r = (wave + 1) * 0.5
g = (math.sin(dist_x * 5 + time * 2) + 1) * 0.5
b = (math.sin(dist_y * 5 + time * 3) + 1) * 0.5
img.setXel(x, y, r, g, b)
img.setAlpha(x, y, 1.0)
# 更新纹理
texture.load(img)
return task.cont
except Exception as e:
print(f"更新程序化视频失败: {str(e)}")
return task.done
def create_color_animation_demo(self):
"""创建颜色动画演示(当没有视频文件时)"""
try:
# 创建卡片
cm = CardMaker("color_demo")
cm.setFrame(-2, 2, -1.5, 1.5)
demo_card = self.render.attachNewNode(cm.generate())
demo_card.setPos(-4, 0, 0)
# 创建动态颜色纹理
demo_texture = self.create_procedural_video(panoramic=False)
demo_card.setTexture(demo_texture)
self.videos.append({
'texture': demo_texture,
'node': demo_card,
'name': 'color_demo'
})
print("✓ 颜色动画演示创建成功")
except Exception as e:
print(f"创建颜色动画演示失败: {str(e)}")
def setup_controls(self):
"""设置控制键"""
self.accept("space", self.toggle_play_pause)
self.accept("r", self.restart_videos)
self.accept("plus", self.speed_up)
self.accept("minus", self.slow_down)
self.accept("escape", self.quit_app)
# 鼠标控制相机
self.accept("mouse1", self.start_camera_rotation)
self.accept("mouse1-up", self.stop_camera_rotation)
self.camera_rotating = False
self.last_mouse_x = 0
self.last_mouse_y = 0
# 启动相机更新任务
self.taskMgr.add(self.update_camera_rotation, "camera_rotation")
def toggle_play_pause(self):
"""切换播放/暂停"""
for video in self.videos:
if hasattr(video['texture'], 'isPlaying'):
if video['texture'].isPlaying():
video['texture'].stop()
print(f"暂停视频: {video['name']}")
else:
video['texture'].play()
print(f"播放视频: {video['name']}")
def restart_videos(self):
"""重新播放所有视频"""
for video in self.videos:
if hasattr(video['texture'], 'play'):
video['texture'].setTime(0)
video['texture'].play()
print(f"重新播放视频: {video['name']}")
def speed_up(self):
"""加速播放"""
for video in self.videos:
if hasattr(video['texture'], 'getPlayRate'):
current_rate = video['texture'].getPlayRate()
new_rate = min(current_rate * 1.5, 4.0)
video['texture'].setPlayRate(new_rate)
print(f"加速播放 {video['name']}: {new_rate:.2f}x")
def slow_down(self):
"""减速播放"""
for video in self.videos:
if hasattr(video['texture'], 'getPlayRate'):
current_rate = video['texture'].getPlayRate()
new_rate = max(current_rate / 1.5, 0.25)
video['texture'].setPlayRate(new_rate)
print(f"减速播放 {video['name']}: {new_rate:.2f}x")
def start_camera_rotation(self):
"""开始相机旋转"""
if self.mouseWatcherNode.hasMouse():
self.camera_rotating = True
self.last_mouse_x = self.mouseWatcherNode.getMouseX()
self.last_mouse_y = self.mouseWatcherNode.getMouseY()
def stop_camera_rotation(self):
"""停止相机旋转"""
self.camera_rotating = False
def update_camera_rotation(self, task):
"""更新相机旋转"""
if self.camera_rotating and self.mouseWatcherNode.hasMouse():
mouse_x = self.mouseWatcherNode.getMouseX()
mouse_y = self.mouseWatcherNode.getMouseY()
delta_x = mouse_x - self.last_mouse_x
delta_y = mouse_y - self.last_mouse_y
# 更新相机方向
self.cam.setH(self.cam.getH() - delta_x * 50)
new_p = self.cam.getP() + delta_y * 50
self.cam.setP(max(min(new_p, 89), -89))
self.last_mouse_x = mouse_x
self.last_mouse_y = mouse_y
return task.cont
def quit_app(self):
"""退出应用"""
print("退出视频播放器...")
self.userExit()
if __name__ == "__main__":
app = VideoPlayerDemo()
app.run()

View File

@ -0,0 +1,149 @@
# 坐标轴系统修复说明
## 问题描述
用户报告主程序中的坐标轴系统无响应:
- 鼠标悬停在坐标轴上无高亮效果
- 点击坐标轴无法开始拖拽
- 独立的demo程序`standalone_gizmo_test.py`)工作正常
## 根本原因
经过分析发现,问题出在**Qt集成环境下的窗口尺寸获取不准确**
1. **独立demo**:直接继承自`ShowBase`,使用`self.win.getXSize()`和`self.win.getYSize()`获取窗口尺寸是准确的
2. **主程序**使用Qt集成的`Panda3DWorld`基类,`self.win.getXSize()`返回的尺寸与实际Qt渲染区域尺寸不匹配
3. **坐标轴点击检测**:依赖屏幕空间投影计算,窗口尺寸错误导致投影坐标计算错误
## 修复方案
### 1. 添加Qt部件引用机制
在`MyWorld`类中添加:
```python
# Qt部件引用用于获取准确的渲染区域尺寸
self.qtWidget = None
def setQtWidget(self, widget):
"""设置Qt部件引用"""
self.qtWidget = widget
def getWindowSize(self):
"""获取准确的窗口尺寸"""
if self.qtWidget:
# 优先使用Qt部件的实际尺寸
width, height = self.qtWidget.getActualSize()
if width > 0 and height > 0:
return width, height
# 备用方案使用Panda3D窗口尺寸
if hasattr(self, 'win') and self.win:
width = self.win.getXSize()
height = self.win.getYSize()
return width, height
# 最后的默认值
return 800, 600
```
### 2. 修改CustomPanda3DWidget
添加方法以获取Qt部件实际尺寸
```python
def getActualSize(self):
"""获取Qt部件的实际渲染尺寸"""
return (self.width(), self.height())
```
在构造函数中建立引用:
```python
# 让world引用这个widget以便获取准确的尺寸
if hasattr(world, 'setQtWidget'):
world.setQtWidget(self)
```
### 3. 修改所有屏幕坐标计算
将所有使用`self.win.getXSize()`和`self.win.getYSize()`的地方改为使用新的`getWindowSize()`方法:
#### 修改前:
```python
win_x = (screen_pos.getX() + 1.0) * 0.5 * self.win.getXSize()
win_y = (1.0 - screen_pos.getY()) * 0.5 * self.win.getYSize()
```
#### 修改后:
```python
# 获取准确的窗口尺寸
win_width, win_height = self.getWindowSize()
win_x = (screen_pos.getX() + 1.0) * 0.5 * win_width
win_y = (1.0 - screen_pos.getY()) * 0.5 * win_height
```
### 4. 修改的关键方法
1. `checkGizmoClick()` - 坐标轴点击检测的主方法
2. `updateGizmoHighlight()` - 坐标轴悬停高亮
3. `updateGizmoDrag()` - 坐标轴拖拽更新
4. `mousePressEventLeft()` - 鼠标点击事件处理
5. `mouseMoveEvent()` - 鼠标移动事件处理
6. `checkGizmoClickFallback()` - 备用点击检测方法
## 技术原理
### 屏幕空间投影算法
坐标轴点击检测使用以下步骤:
1. **世界坐标 → 相机坐标**
```python
cam_space_pos = self.cam.getRelativePoint(self.render, world_pos)
```
2. **相机坐标 → 标准化屏幕坐标**
```python
screen_pos = Point2()
self.cam.node().getLens().project(cam_space_pos, screen_pos)
```
3. **标准化坐标 → 像素坐标**
```python
win_x = (screen_pos.getX() + 1.0) * 0.5 * win_width
win_y = (1.0 - screen_pos.getY()) * 0.5 * win_height
```
4. **点到线段距离计算**
```python
distance = self.distanceToLine((mouse_x, mouse_y), center_screen, axis_screen)
```
### 窗口尺寸差异的影响
如果窗口尺寸不准确:
- 步骤3中的像素坐标计算错误
- 坐标轴在屏幕上的投影位置计算错误
- 鼠标点击位置与计算出的轴位置不匹配
- 导致点击检测失败
## 测试验证
创建了测试脚本`demo/test_size_fix.py`来验证修复效果:
- 对比Qt部件尺寸和Panda3D窗口尺寸
- 实时监控尺寸变化
- 验证`getWindowSize()`方法的正确性
## 预期效果
修复后的坐标轴系统应该:
1. ✅ 鼠标悬停时正确高亮对应的轴红色X轴、绿色Y轴、蓝色Z轴变为黄色
2. ✅ 点击坐标轴能够正确开始拖拽
3. ✅ 拖拽时只沿选定轴方向移动物体
4. ✅ 松开鼠标时正确结束拖拽
## 兼容性
修复方案完全向后兼容:
- 如果没有Qt环境自动回退到使用Panda3D窗口尺寸
- 如果Qt部件引用未设置使用备用方案
- 不影响独立的ShowBase应用程序

10
gui/__init__.py Normal file
View File

@ -0,0 +1,10 @@
"""
GUI包
提供GUI元素管理相关的功能
- GUIManager: GUI元素管理器类
"""
from .gui_manager import GUIManager
__all__ = ['GUIManager']

Binary file not shown.

Binary file not shown.

948
gui/gui_manager.py Normal file
View File

@ -0,0 +1,948 @@
"""
GUI元素管理模块
负责2D/3D GUI元素的管理
- GUI元素的创建按钮标签输入框等
- GUI编辑模式
- GUI属性编辑
- GUI元素的复制和删除
"""
from direct.gui.DirectGui import *
from panda3d.core import *
from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QFormLayout, QLineEdit,
QDoubleSpinBox, QPushButton, QDialogButtonBox,
QColorDialog, QLabel, QWidget, QGroupBox, QHBoxLayout)
from PyQt5.QtGui import QColor
from PyQt5.QtCore import Qt
class GUIManager:
"""GUI元素管理系统类"""
def __init__(self, world):
"""初始化GUI管理系统
Args:
world: 核心世界对象引用
"""
self.world = world
# GUI元素列表
self.gui_elements = []
# GUI编辑模式状态
self.guiEditMode = False
self.guiEditPanel = None
self.guiPreviewWindow = None
self.currentGUITool = None
print("✓ GUI管理系统初始化完成")
# ==================== GUI元素创建方法 ====================
def createGUIButton(self, pos=(0, 0, 0), text="按钮", size=0.1):
"""创建2D GUI按钮"""
from direct.gui.DirectGui import DirectButton
# 将3D坐标转换为2D屏幕坐标
gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1)
button = DirectButton(
text=text,
pos=gui_pos,
scale=size,
command=self.onGUIButtonClick,
extraArgs=[f"button_{len(self.gui_elements)}"],
frameColor=(0.2, 0.6, 0.8, 1),
text_font=self.world.getChineseFont() if self.world.getChineseFont() else None,
rolloverSound=None,
clickSound=None
)
# 为GUI元素添加标识
button.setTag("gui_type", "button")
button.setTag("gui_id", f"button_{len(self.gui_elements)}")
button.setTag("gui_text", text)
button.setTag("is_gui_element", "1")
self.gui_elements.append(button)
# 安全地调用updateSceneTree
if hasattr(self.world, 'updateSceneTree'):
self.world.updateSceneTree()
print(f"✓ 创建GUI按钮: {text} (逻辑位置: {pos}, 屏幕位置: {gui_pos})")
return button
def createGUILabel(self, pos=(0, 0, 0), text="标签", size=0.08):
"""创建2D GUI标签"""
from direct.gui.DirectGui import DirectLabel
# 将3D坐标转换为2D屏幕坐标
gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1)
label = DirectLabel(
text=text,
pos=gui_pos,
scale=size,
frameColor=(0, 0, 0, 0), # 透明背景
text_fg=(1, 1, 1, 1),
text_font=self.world.getChineseFont() if self.world.getChineseFont() else None
)
# 为GUI元素添加标识
label.setTag("gui_type", "label")
label.setTag("gui_id", f"label_{len(self.gui_elements)}")
label.setTag("gui_text", text)
label.setTag("is_gui_element", "1")
self.gui_elements.append(label)
# 安全地调用updateSceneTree
if hasattr(self.world, 'updateSceneTree'):
self.world.updateSceneTree()
print(f"✓ 创建GUI标签: {text} (逻辑位置: {pos}, 屏幕位置: {gui_pos})")
return label
def createGUIEntry(self, pos=(0, 0, 0), placeholder="输入文本...", size=0.08):
"""创建2D GUI文本输入框"""
from direct.gui.DirectGui import DirectEntry
# 将3D坐标转换为2D屏幕坐标
gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1)
entry = DirectEntry(
text="",
pos=gui_pos,
scale=size,
command=self.onGUIEntrySubmit,
extraArgs=[f"entry_{len(self.gui_elements)}"],
initialText=placeholder,
numLines=1,
width=12,
focus=0
)
# 为GUI元素添加标识
entry.setTag("gui_type", "entry")
entry.setTag("gui_id", f"entry_{len(self.gui_elements)}")
entry.setTag("gui_placeholder", placeholder)
entry.setTag("is_gui_element", "1")
self.gui_elements.append(entry)
# 安全地调用updateSceneTree
if hasattr(self.world, 'updateSceneTree'):
self.world.updateSceneTree()
print(f"✓ 创建GUI输入框: {placeholder} (逻辑位置: {pos}, 屏幕位置: {gui_pos})")
return entry
def createGUI3DText(self, pos=(0, 0, 0), text="3D文本", size=0.5):
"""创建3D空间文本"""
from panda3d.core import TextNode
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())
textNodePath = self.world.render.attachNewNode(textNode)
textNodePath.setPos(*pos)
textNodePath.setScale(size)
textNodePath.setColor(1, 1, 0, 1)
textNodePath.setBillboardAxis() # 让文本总是面向相机
# 为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")
self.gui_elements.append(textNodePath)
# 安全地调用updateSceneTree
if hasattr(self.world, 'updateSceneTree'):
self.world.updateSceneTree()
print(f"✓ 创建3D文本: {text} (世界位置: {pos})")
return textNodePath
def createGUIVirtualScreen(self, pos=(0, 0, 0), size=(2, 1), text="虚拟屏幕"):
"""创建3D虚拟屏幕"""
from panda3d.core import CardMaker, TransparencyAttrib, TextNode
# 创建虚拟屏幕
cm = CardMaker(f"virtual-screen-{len(self.gui_elements)}")
cm.setFrame(-size[0]/2, size[0]/2, -size[1]/2, size[1]/2)
virtualScreen = self.world.render.attachNewNode(cm.generate())
virtualScreen.setPos(*pos)
virtualScreen.setColor(0.2, 0.2, 0.2, 0.8)
virtualScreen.setTransparency(TransparencyAttrib.MAlpha)
# 在虚拟屏幕上添加文本
screenText = TextNode(f'screen-text-{len(self.gui_elements)}')
screenText.setText(text)
screenText.setAlign(TextNode.ACenter)
if self.world.getChineseFont():
screenText.setFont(self.world.getChineseFont())
screenTextNP = virtualScreen.attachNewNode(screenText)
screenTextNP.setPos(0, 0.01, 0)
screenTextNP.setScale(0.3)
screenTextNP.setColor(0, 1, 0, 1)
# 为GUI元素添加标识
virtualScreen.setTag("gui_type", "virtual_screen")
virtualScreen.setTag("gui_id", f"virtual_screen_{len(self.gui_elements)}")
virtualScreen.setTag("gui_text", text)
virtualScreen.setTag("is_gui_element", "1")
self.gui_elements.append(virtualScreen)
# 安全地调用updateSceneTree
if hasattr(self.world, 'updateSceneTree'):
self.world.updateSceneTree()
print(f"✓ 创建虚拟屏幕: {text} (世界位置: {pos})")
return virtualScreen
def createGUISlider(self, pos=(0, 0, 0), text="滑块", scale=0.3):
"""创建2D GUI滑块"""
from direct.gui.DirectGui import DirectSlider
gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1)
slider = DirectSlider(
pos=gui_pos,
scale=scale,
range=(0, 100),
value=50,
frameColor=(0.6, 0.6, 0.6, 1),
thumbColor=(0.2, 0.8, 0.2, 1)
)
slider.setTag("gui_type", "slider")
slider.setTag("gui_id", f"slider_{len(self.gui_elements)}")
slider.setTag("gui_text", text)
slider.setTag("is_gui_element", "1")
self.gui_elements.append(slider)
# 安全地调用updateSceneTree
if hasattr(self.world, 'updateSceneTree'):
self.world.updateSceneTree()
print(f"✓ 创建GUI滑块: {text} (逻辑位置: {pos}, 屏幕位置: {gui_pos})")
return slider
# ==================== GUI元素管理方法 ====================
def deleteGUIElement(self, gui_element):
"""删除GUI元素"""
try:
if gui_element in self.gui_elements:
# 移除GUI元素
if hasattr(gui_element, 'removeNode'):
gui_element.removeNode()
elif hasattr(gui_element, 'destroy'):
gui_element.destroy()
# 从列表中移除
self.gui_elements.remove(gui_element)
# 更新场景树
# 安全地调用updateSceneTree
if hasattr(self.world, 'updateSceneTree'):
self.world.updateSceneTree()
print(f"删除GUI元素: {gui_element}")
return True
except Exception as e:
print(f"删除GUI元素失败: {str(e)}")
return False
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
print(f"成功更新2D GUI文本: {value}")
elif gui_type == "entry":
gui_element.set(value)
print(f"成功更新输入框文本: {value}")
elif gui_type == "3d_text":
# 对于3D文本直接修改自身的TextNode
if isinstance(gui_element.node(), TextNode):
gui_element.node().setText(value)
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()} 个子节点")
text_found = False
for i, child in enumerate(gui_element.getChildren()):
print(f"子节点 {i}: {child.getName()}, 类型: {type(child.node())}")
if isinstance(child.node(), TextNode):
child.node().setText(value)
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 == "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)}")
return False
def duplicateGUIElement(self, gui_element):
"""复制GUI元素"""
try:
gui_type = gui_element.getTag("gui_type")
gui_text = gui_element.getTag("gui_text")
# 获取当前位置并偏移
pos = gui_element.getPos()
new_pos = (pos.getX() + 0.2, pos.getY(), pos.getZ() + 0.2)
# 根据类型创建新的GUI元素
if gui_type == "button":
self.createGUIButton(new_pos, gui_text + "_副本")
elif gui_type == "label":
self.createGUILabel(new_pos, gui_text + "_副本")
elif gui_type == "entry":
self.createGUIEntry(new_pos, gui_text + "_副本")
elif gui_type == "3d_text":
self.createGUI3DText(new_pos, gui_text + "_副本")
elif gui_type == "virtual_screen":
self.createGUIVirtualScreen(new_pos, text=gui_text + "_副本")
print(f"复制GUI元素: {gui_type} - {gui_text}")
except Exception as e:
print(f"复制GUI元素失败: {str(e)}")
def editGUIElementDialog(self, gui_element):
"""显示GUI元素编辑对话框"""
dialog = QDialog()
dialog.setWindowTitle("编辑GUI元素")
dialog.setMinimumWidth(300)
layout = QVBoxLayout(dialog)
form = QFormLayout()
gui_type = gui_element.getTag("gui_type")
gui_text = gui_element.getTag("gui_text")
# 文本编辑
if gui_type in ["button", "label", "entry", "3d_text", "virtual_screen"]:
textEdit = QLineEdit(gui_text or "")
form.addRow("文本:", textEdit)
# 位置编辑
if hasattr(gui_element, 'getPos'):
pos = gui_element.getPos()
xEdit = QDoubleSpinBox()
xEdit.setRange(-1000, 1000)
xEdit.setValue(pos.getX())
form.addRow("位置 X:", xEdit)
yEdit = QDoubleSpinBox()
yEdit.setRange(-1000, 1000)
yEdit.setValue(pos.getY())
form.addRow("位置 Y:", yEdit)
zEdit = QDoubleSpinBox()
zEdit.setRange(-1000, 1000)
zEdit.setValue(pos.getZ())
form.addRow("位置 Z:", zEdit)
# 缩放编辑
if hasattr(gui_element, 'getScale'):
scale = gui_element.getScale()
scaleEdit = QDoubleSpinBox()
scaleEdit.setRange(0.01, 10)
scaleEdit.setSingleStep(0.1)
scaleEdit.setValue(scale.getX())
form.addRow("缩放:", scaleEdit)
layout.addLayout(form)
# 按钮
buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
buttonBox.accepted.connect(dialog.accept)
buttonBox.rejected.connect(dialog.reject)
layout.addWidget(buttonBox)
# 执行对话框
if dialog.exec_() == QDialog.Accepted:
try:
# 应用更改
if gui_type in ["button", "label", "entry", "3d_text", "virtual_screen"]:
self.editGUIElement(gui_element, "text", textEdit.text())
if hasattr(gui_element, 'getPos'):
self.editGUIElement(gui_element, "position", [xEdit.value(), yEdit.value(), zEdit.value()])
if hasattr(gui_element, 'getScale'):
self.editGUIElement(gui_element, "scale", scaleEdit.value())
# 更新属性面板
if self.world.treeWidget:
currentItem = self.world.treeWidget.currentItem()
if currentItem:
self.world.updatePropertyPanel(currentItem)
print("GUI元素编辑完成")
except Exception as e:
print(f"应用GUI编辑失败: {str(e)}")
# ==================== GUI事件处理方法 ====================
def onGUIButtonClick(self, button_id):
"""GUI按钮点击事件处理"""
print(f"GUI按钮被点击: {button_id}")
def onGUIEntrySubmit(self, text, entry_id):
"""GUI输入框提交事件处理"""
print(f"GUI输入框提交: {entry_id} = {text}")
# ==================== GUI编辑模式方法 ====================
def toggleGUIEditMode(self):
"""切换GUI编辑模式"""
self.guiEditMode = not self.guiEditMode
if self.guiEditMode:
self.enterGUIEditMode()
else:
self.exitGUIEditMode()
def enterGUIEditMode(self):
"""进入GUI编辑模式"""
print("\n=== 进入GUI编辑模式 ===")
# 打开GUI预览窗口
self.openGUIPreviewWindow()
# 创建GUI编辑面板
self.createGUIEditPanel()
# 改变当前工具为GUI选择工具
self.world.currentTool = "GUI编辑"
print("GUI编辑模式已激活")
print("- 使用右侧工具栏创建GUI元素")
print("- 在独立预览窗口中查看效果")
print("- 左键点击现有GUI元素选择和编辑")
def exitGUIEditMode(self):
"""退出GUI编辑模式"""
print("\n=== 退出GUI编辑模式 ===")
# 关闭GUI预览窗口
self.closeGUIPreviewWindow()
# 移除GUI编辑面板
if self.guiEditPanel:
self.guiEditPanel.destroy()
self.guiEditPanel = None
# 恢复普通工具
self.world.currentTool = "选择"
print("GUI编辑模式已关闭")
def createGUIEditPanel(self):
"""创建GUI编辑面板"""
from direct.gui.DirectGui import DirectFrame, DirectButton, DirectLabel
# 创建主面板
self.guiEditPanel = DirectFrame(
pos=(0.85, 0, 0),
frameSize=(-0.15, 0.15, -0.9, 0.9),
frameColor=(0.1, 0.1, 0.1, 0.8),
text="GUI编辑器",
text_pos=(0, 0.85),
text_scale=0.05,
text_fg=(1, 1, 1, 1),
text_font=self.world.getChineseFont() if self.world.getChineseFont() else None
)
# 创建工具按钮
y_pos = 0.7
spacing = 0.12
# 2D GUI工具
label_2d = DirectLabel(
parent=self.guiEditPanel,
text="2D GUI",
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
# 按钮工具
btn_button = DirectButton(
parent=self.guiEditPanel,
text="按钮",
pos=(0, 0, y_pos),
scale=0.04,
command=self.setGUICreateTool,
extraArgs=["button"],
frameColor=(0.2, 0.6, 0.8, 1),
text_font=self.world.getChineseFont() if self.world.getChineseFont() else None
)
y_pos -= spacing
# 标签工具
btn_label = DirectButton(
parent=self.guiEditPanel,
text="标签",
pos=(0, 0, y_pos),
scale=0.04,
command=self.setGUICreateTool,
extraArgs=["label"],
frameColor=(0.6, 0.8, 0.2, 1),
text_font=self.world.getChineseFont() if self.world.getChineseFont() else None
)
y_pos -= spacing
# 输入框工具
btn_entry = DirectButton(
parent=self.guiEditPanel,
text="输入框",
pos=(0, 0, y_pos),
scale=0.04,
command=self.setGUICreateTool,
extraArgs=["entry"],
frameColor=(0.8, 0.6, 0.2, 1),
text_font=self.world.getChineseFont() if self.world.getChineseFont() else None
)
y_pos -= spacing
# 3D GUI工具
label_3d = DirectLabel(
parent=self.guiEditPanel,
text="3D GUI",
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
# 3D文本工具
btn_3dtext = DirectButton(
parent=self.guiEditPanel,
text="3D文本",
pos=(0, 0, y_pos),
scale=0.04,
command=self.setGUICreateTool,
extraArgs=["3d_text"],
frameColor=(0.8, 0.2, 0.6, 1),
text_font=self.world.getChineseFont() if self.world.getChineseFont() else None
)
y_pos -= spacing
# 虚拟屏幕工具
btn_screen = DirectButton(
parent=self.guiEditPanel,
text="虚拟屏幕",
pos=(0, 0, y_pos),
scale=0.04,
command=self.setGUICreateTool,
extraArgs=["virtual_screen"],
frameColor=(0.6, 0.2, 0.8, 1),
text_font=self.world.getChineseFont() if self.world.getChineseFont() else None
)
y_pos -= spacing
# 分隔线
y_pos -= 0.1
# 操作按钮
label_ops = DirectLabel(
parent=self.guiEditPanel,
text="操作",
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
# 删除工具
btn_delete = DirectButton(
parent=self.guiEditPanel,
text="删除",
pos=(0, 0, y_pos),
scale=0.04,
command=self.deleteSelectedGUI,
frameColor=(0.8, 0.2, 0.2, 1),
text_font=self.world.getChineseFont() if self.world.getChineseFont() else None
)
y_pos -= spacing
# 复制工具
btn_copy = DirectButton(
parent=self.guiEditPanel,
text="复制",
pos=(0, 0, y_pos),
scale=0.04,
command=self.copySelectedGUI,
frameColor=(0.2, 0.8, 0.2, 1),
text_font=self.world.getChineseFont() if self.world.getChineseFont() else None
)
y_pos -= spacing
# 退出GUI编辑模式
btn_exit = DirectButton(
parent=self.guiEditPanel,
text="退出",
pos=(0, 0, -0.8),
scale=0.04,
command=self.toggleGUIEditMode,
frameColor=(0.5, 0.5, 0.5, 1),
text_font=self.world.getChineseFont() if self.world.getChineseFont() else None
)
# 存储当前的GUI创建工具
self.currentGUITool = None
def openGUIPreviewWindow(self):
"""打开独立的GUI预览窗口"""
try:
from gui_preview_window import GUIPreviewWindow
self.guiPreviewWindow = GUIPreviewWindow()
self.guiPreviewWindow.set_main_world(self.world)
print("✓ GUI预览窗口已打开")
print("这个独立窗口会实时显示您创建的GUI元素")
except ImportError:
print("错误: 无法导入GUI预览窗口模块")
except Exception as e:
print(f"打开GUI预览窗口失败: {str(e)}")
def closeGUIPreviewWindow(self):
"""关闭GUI预览窗口"""
if self.guiPreviewWindow:
self.guiPreviewWindow.destroy()
self.guiPreviewWindow = None
print("GUI预览窗口已关闭")
# ==================== GUI工具和选择方法 ====================
def setGUICreateTool(self, tool_type):
"""设置GUI创建工具"""
self.currentGUITool = tool_type
print(f"选择GUI创建工具: {tool_type}")
def deleteSelectedGUI(self):
"""删除选中的GUI元素"""
if self.world.selection.selectedNode and hasattr(self.world.selection.selectedNode, 'getTag'):
gui_type = self.world.selection.selectedNode.getTag("gui_type")
if gui_type:
success = self.deleteGUIElement(self.world.selection.selectedNode)
if success:
self.world.selection.updateSelection(None)
print("GUI元素已删除")
else:
print("删除GUI元素失败")
else:
print("选中的不是GUI元素")
else:
print("没有选中的GUI元素")
def copySelectedGUI(self):
"""复制选中的GUI元素"""
if self.world.selection.selectedNode and hasattr(self.world.selection.selectedNode, 'getTag'):
gui_type = self.world.selection.selectedNode.getTag("gui_type")
if gui_type:
self.duplicateGUIElement(self.world.selection.selectedNode)
print("GUI元素已复制")
else:
print("选中的不是GUI元素")
else:
print("没有选中的GUI元素")
def handleGUIEditClick(self, hitPos):
"""处理GUI编辑模式下的点击"""
if not self.guiEditMode:
return False
if self.currentGUITool:
# 创建新的GUI元素
self.createGUIAtPosition(hitPos, self.currentGUITool)
return True
return False
def createGUIAtPosition(self, world_pos, gui_type):
"""在指定位置创建GUI元素"""
print(f"在位置 {world_pos} 创建 {gui_type}")
# 根据GUI类型选择合适的坐标转换
if gui_type in ["button", "label", "entry"]:
# 2D GUI - 将世界坐标转换为屏幕逻辑坐标
screen_x = world_pos.getX() * 2 # 缩放因子
screen_z = world_pos.getZ() * 2
pos = (screen_x, 0, screen_z)
else:
# 3D GUI - 直接使用世界坐标
pos = (world_pos.getX(), world_pos.getY(), world_pos.getZ())
# 创建不同类型的GUI元素
if gui_type == "button":
element = self.createGUIButton(pos, f"按钮_{len(self.gui_elements)}")
elif gui_type == "label":
element = self.createGUILabel(pos, f"标签_{len(self.gui_elements)}")
elif gui_type == "entry":
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 == "virtual_screen":
element = self.createGUIVirtualScreen(pos, text=f"屏幕_{len(self.gui_elements)}")
else:
print(f"未知的GUI类型: {gui_type}")
return
# 自动选中新创建的元素
self.world.selection.updateSelection(element)
self.selectGUIInTree(element)
print(f"创建并选中了新的{gui_type}元素")
def findClickedGUI(self, hitNode):
"""查找被点击的GUI元素"""
# 检查点击的节点是否是GUI元素
current = hitNode
while current != self.world.render:
if hasattr(current, 'getTag') and current.getTag("gui_type"):
return current
current = current.getParent()
return None
def selectGUIInTree(self, gui_element):
"""在树形控件中选中GUI元素"""
if not self.world.treeWidget or not gui_element:
return
def findGUIItem(item):
"""递归查找GUI元素对应的树形项"""
if item.data(0, Qt.UserRole) == gui_element:
return item
for i in range(item.childCount()):
child = item.child(i)
result = findGUIItem(child)
if result:
return result
return None
# 从根开始查找
root = self.world.treeWidget.invisibleRootItem()
for i in range(root.childCount()):
sceneItem = root.child(i)
if sceneItem.text(0) == "场景":
for j in range(sceneItem.childCount()):
childItem = sceneItem.child(j)
if childItem.text(0) == "GUI元素":
foundItem = findGUIItem(childItem)
if foundItem:
self.world.treeWidget.setCurrentItem(foundItem)
self.world.updatePropertyPanel(foundItem)
return
def updateGUISelection(self, gui_element):
"""更新GUI元素选择状态"""
self.world.selection.updateSelection(gui_element)
if gui_element and hasattr(gui_element, 'getTag'):
gui_type = gui_element.getTag("gui_type")
gui_text = gui_element.getTag("gui_text")
print(f"选中GUI元素: {gui_type} - {gui_text}")
# 在树形控件中选中
self.selectGUIInTree(gui_element)
# ==================== GUI属性面板方法 ====================
def updateGUIPropertyPanel(self, gui_element, layout):
"""更新GUI元素属性面板"""
gui_type = gui_element.getTag("gui_type")
gui_text = gui_element.getTag("gui_text")
# GUI类型显示
typeLabel = QLabel("GUI类型:")
typeValue = QLabel(gui_type)
typeValue.setStyleSheet("color: #00AAFF; font-weight: bold;")
layout.addRow(typeLabel, typeValue)
# 文本属性(如果适用)
if gui_type in ["button", "label", "entry", "3d_text", "virtual_screen"]:
textLabel = QLabel("文本:")
textEdit = QLineEdit(gui_text or "")
# 创建一个更新函数来处理文本变化
def updateText(text):
success = self.editGUIElement(gui_element, "text", text)
if success:
# 更新场景树显示的名称
# 安全地调用updateSceneTree
if hasattr(self.world, 'updateSceneTree'):
self.world.updateSceneTree()
textEdit.textChanged.connect(updateText)
layout.addRow(textLabel, textEdit)
# 位置属性
if hasattr(gui_element, 'getPos'):
pos = gui_element.getPos()
# 根据GUI类型决定位置编辑方式
if gui_type in ["button", "label", "entry"]:
# 2D GUI组件使用屏幕坐标
logical_x = pos.getX() / 0.1 # 反向转换为逻辑坐标
logical_z = pos.getZ() / 0.1
xPos = QDoubleSpinBox()
xPos.setRange(-50, 50)
xPos.setValue(logical_x)
xPos.valueChanged.connect(lambda v: self.editGUI2DPosition(gui_element, "x", v))
layout.addRow("屏幕位置 X:", xPos)
zPos = QDoubleSpinBox()
zPos.setRange(-50, 50)
zPos.setValue(logical_z)
zPos.valueChanged.connect(lambda v: self.editGUI2DPosition(gui_element, "z", v))
layout.addRow("屏幕位置 Z:", zPos)
# 显示实际屏幕坐标(只读)
actualXLabel = QLabel(f"{pos.getX():.3f}")
actualXLabel.setStyleSheet("color: gray; font-size: 10px;")
layout.addRow("实际屏幕 X:", actualXLabel)
actualZLabel = QLabel(f"{pos.getZ():.3f}")
actualZLabel.setStyleSheet("color: gray; font-size: 10px;")
layout.addRow("实际屏幕 Z:", actualZLabel)
else:
# 3D GUI组件使用世界坐标
xPos = QDoubleSpinBox()
xPos.setRange(-1000, 1000)
xPos.setValue(pos.getX())
xPos.valueChanged.connect(lambda v: self.editGUIElement(gui_element, "position", [v, pos.getY(), pos.getZ()]))
layout.addRow("位置 X:", xPos)
yPos = QDoubleSpinBox()
yPos.setRange(-1000, 1000)
yPos.setValue(pos.getY())
yPos.valueChanged.connect(lambda v: self.editGUIElement(gui_element, "position", [pos.getX(), v, pos.getZ()]))
layout.addRow("位置 Y:", yPos)
zPos = QDoubleSpinBox()
zPos.setRange(-1000, 1000)
zPos.setValue(pos.getZ())
zPos.valueChanged.connect(lambda v: self.editGUIElement(gui_element, "position", [pos.getX(), pos.getY(), v]))
layout.addRow("位置 Z:", zPos)
# 缩放属性
if hasattr(gui_element, 'getScale'):
scale = gui_element.getScale()
scaleSpinBox = QDoubleSpinBox()
scaleSpinBox.setRange(0.01, 10)
scaleSpinBox.setSingleStep(0.1)
scaleSpinBox.setValue(scale.getX())
scaleSpinBox.valueChanged.connect(lambda v: self.editGUIElement(gui_element, "scale", v))
layout.addRow("缩放:", scaleSpinBox)
# 颜色属性针对2D GUI
if gui_type in ["button", "label"]:
colorButton = QPushButton("选择颜色")
colorButton.clicked.connect(lambda: self.selectGUIColor(gui_element))
layout.addRow("背景颜色:", colorButton)
# 3D特有属性
if gui_type in ["3d_text", "virtual_screen"]:
# 旋转属性
if hasattr(gui_element, 'getHpr'):
hpr = gui_element.getHpr()
hRot = QDoubleSpinBox()
hRot.setRange(-180, 180)
hRot.setValue(hpr.getX())
hRot.valueChanged.connect(lambda v: gui_element.setH(v))
layout.addRow("旋转 H:", hRot)
pRot = QDoubleSpinBox()
pRot.setRange(-180, 180)
pRot.setValue(hpr.getY())
pRot.valueChanged.connect(lambda v: gui_element.setP(v))
layout.addRow("旋转 P:", pRot)
rRot = QDoubleSpinBox()
rRot.setRange(-180, 180)
rRot.setValue(hpr.getZ())
rRot.valueChanged.connect(lambda v: gui_element.setR(v))
layout.addRow("旋转 R:", rRot)
def selectGUIColor(self, gui_element):
"""选择GUI元素颜色"""
color = QColorDialog.getColor(QColor(128, 128, 128), None, "选择颜色")
if color.isValid():
r, g, b = color.red() / 255.0, color.green() / 255.0, color.blue() / 255.0
self.editGUIElement(gui_element, "color", [r, g, b, 1.0])
def editGUI2DPosition(self, gui_element, axis, value):
"""编辑2D GUI元素位置"""
try:
current_pos = gui_element.getPos()
if axis == "x":
# 将逻辑坐标转换为屏幕坐标
new_screen_x = value * 0.1
gui_element.setPos(new_screen_x, current_pos.getY(), current_pos.getZ())
elif axis == "z":
# 将逻辑坐标转换为屏幕坐标
new_screen_z = value * 0.1
gui_element.setPos(current_pos.getX(), current_pos.getY(), new_screen_z)
print(f"更新2D GUI位置: {axis}轴 = {value} (屏幕坐标: {gui_element.getPos()})")
except Exception as e:
print(f"编辑2D GUI位置失败: {str(e)}")

540
gui_preview_window.py Normal file
View File

@ -0,0 +1,540 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
GUI预览窗口 - 使用现有ShowBase创建额外窗口专门用来显示GUI元素
解决Qt集成中2D GUI显示问题避免多个ShowBase实例错误
"""
import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)
from direct.gui.DirectGui import *
from panda3d.core import *
from direct.showbase.ShowBase import ShowBase
class GUIPreviewWindow:
"""使用现有ShowBase的额外窗口进行GUI预览"""
def __init__(self):
self.preview_window = None
self.chinese_font = None
self.gui_elements = []
self.main_world = None # 主程序world对象的引用
self.sync_timer = None
# 预览窗口的渲染节点
self.preview_render2d = None
self.preview_camera2d = None
self.preview_aspect2d = None
print("✓ GUI预览窗口类已初始化")
def set_main_world(self, world):
"""设置主程序world引用并初始化预览窗口"""
self.main_world = world
if world:
self.init_preview_window()
self.start_sync_timer()
def init_preview_window(self):
"""使用现有ShowBase创建预览窗口"""
try:
if not self.main_world:
print("错误: 没有主程序world引用")
return False
# 使用主程序的base创建额外窗口
base = self.main_world # MyWorld继承自ShowBase
# 尝试从Qt部件获取渲染区域的实际尺寸
main_width = 800 # 默认宽度
main_height = 600 # 默认高度
# 检查是否有Qt应用程序实例
try:
from PyQt5.QtWidgets import QApplication
app = QApplication.instance()
if app:
# 查找主窗口
for widget in app.topLevelWidgets():
if hasattr(widget, 'centralWidget'):
central_widget = widget.centralWidget()
if central_widget and hasattr(central_widget, 'world'):
# 找到了Panda3D部件
qt_width = central_widget.width()
qt_height = central_widget.height()
if qt_width > 0 and qt_height > 0:
main_width = qt_width
main_height = qt_height
print(f"从Qt部件获取尺寸: {main_width} x {main_height}")
break
else:
print("未找到Qt Panda3D部件使用默认尺寸")
else:
print("未找到Qt应用程序实例使用默认尺寸")
except ImportError:
print("未找到PyQt5使用默认尺寸")
except Exception as e:
print(f"从Qt获取尺寸失败: {str(e)},使用默认尺寸")
# 如果Qt方法失败尝试从Panda3D窗口获取
if main_width == 800 and main_height == 600:
main_window = base.win
if main_window and main_window.hasSize():
panda_width = main_window.getXSize()
panda_height = main_window.getYSize()
if panda_width > 0 and panda_height > 0:
main_width = panda_width
main_height = panda_height
print(f"从Panda3D窗口获取尺寸: {main_width} x {main_height}")
else:
print(f"Panda3D窗口尺寸无效使用默认尺寸: {main_width} x {main_height}")
# 设置窗口属性,使用获取到的尺寸
wp = WindowProperties()
wp.setSize(main_width, main_height)
wp.setTitle("GUI预览窗口 - 2D GUI专用显示")
wp.setOrigin(main_width + 50, 50) # 放在主窗口右侧
wp.setUndecorated(False) # 保持窗口装饰
# 注意WindowProperties没有setMaximized和setMinimized方法
print(f"设置预览窗口属性: 尺寸={main_width}x{main_height}, 位置=({main_width + 50}, 50)")
# 获取图形管道
pipe = base.pipe
if not pipe:
print("错误: 无法获取图形管道")
return False
# 创建帧缓冲属性
fbp = FrameBufferProperties.getDefault()
# 使用makeOutput创建窗口强制要求窗口类型
self.preview_window = base.graphicsEngine.makeOutput(
pipe, "GUI预览窗口", 0,
fbp, wp, GraphicsPipe.BFRequireWindow
)
if not self.preview_window:
print("错误: 无法创建预览窗口")
return False
print(f"预览窗口创建成功,类型: {type(self.preview_window).__name__}")
# 强制打开窗口
base.graphicsEngine.openWindows()
# 验证创建的窗口尺寸
if hasattr(self.preview_window, 'getXSize'):
actual_width = self.preview_window.getXSize()
actual_height = self.preview_window.getYSize()
print(f"实际创建的预览窗口尺寸: {actual_width} x {actual_height}")
if actual_width != main_width or actual_height != main_height:
print(f"警告: 窗口尺寸不匹配!期望: {main_width}x{main_height}, 实际: {actual_width}x{actual_height}")
# 尝试重新设置尺寸
new_props = WindowProperties()
new_props.setSize(main_width, main_height)
self.preview_window.requestProperties(new_props)
base.graphicsEngine.renderFrame()
# 再次检查
final_width = self.preview_window.getXSize()
final_height = self.preview_window.getYSize()
print(f"重新设置后的尺寸: {final_width} x {final_height}")
# 设置预览窗口的背景色
base.setBackgroundColor(0.15, 0.15, 0.25, 1.0, self.preview_window)
# 创建预览窗口的2D渲染节点
self.preview_render2d = NodePath('preview_render2d')
# 为预览窗口创建2D相机使用正确的宽高比
aspect_ratio = float(main_width) / float(main_height)
self.preview_camera2d = base.makeCamera2d(self.preview_window)
self.preview_camera2d.reparentTo(self.preview_render2d)
# 设置2D渲染属性
self.preview_render2d.setDepthWrite(0)
self.preview_render2d.setMaterialOff(1)
self.preview_render2d.setTwoSided(1)
# 检查窗口类型并设置输入
print(f"预览窗口类型: {type(self.preview_window)}")
print(f"是否为GraphicsWindow: {hasattr(self.preview_window, 'getNumInputDevices')}")
# 只为真正的GraphicsWindow设置输入
if hasattr(self.preview_window, 'getNumInputDevices') and self.preview_window.getNumInputDevices() > 0:
# 设置鼠标和键盘输入
mk = base.dataRoot.attachNewNode(MouseAndKeyboard(self.preview_window, 0, 'previewMouse'))
mw = mk.attachNewNode(MouseWatcher('previewMouseWatcher'))
self.preview_aspect2d = self.preview_render2d.attachNewNode(PGTop('previewAspect2d'))
self.preview_aspect2d.node().setMouseWatcher(mw.node())
print("✓ 已设置预览窗口的鼠标和键盘输入")
else:
print("⚠ 跳过鼠标/键盘设置(窗口不支持或没有输入设备)")
# 创建没有输入的aspect2d节点
self.preview_aspect2d = self.preview_render2d.attachNewNode(PGTop('previewAspect2d'))
# 加载中文字体
self.load_font()
# 创建界面说明
self.create_ui_info()
# 最终验证
if hasattr(self.preview_window, 'getXSize'):
final_width = self.preview_window.getXSize()
final_height = self.preview_window.getYSize()
print(f"✓ GUI预览窗口已成功创建最终尺寸: {final_width}x{final_height}")
else:
print(f"✓ GUI预览窗口已成功创建期望尺寸: {main_width}x{main_height}")
return True
except Exception as e:
print(f"创建GUI预览窗口失败: {str(e)}")
import traceback
traceback.print_exc()
return False
def load_font(self):
"""加载中文字体"""
try:
if self.main_world and hasattr(self.main_world, 'loader'):
self.chinese_font = self.main_world.loader.loadFont('/usr/share/fonts/truetype/wqy/wqy-microhei.ttc')
if not self.chinese_font:
print("预览窗口: 无法加载中文字体,将使用默认字体")
except:
print("预览窗口: 无法加载中文字体,将使用默认字体")
self.chinese_font = None
def create_ui_info(self):
"""创建界面说明"""
if not self.preview_aspect2d:
return
# # 创建欢迎信息
# self.welcome_text = OnscreenText(
# text="GUI预览窗口\n\n这个窗口使用现有的ShowBase\n创建额外的显示窗口\n\n可以正确显示2D GUI元素",
# parent=self.preview_aspect2d,
# pos=(0, 0.2),
# scale=0.08,
# fg=(1, 1, 1, 1),
# align=TextNode.ACenter,
# font=self.chinese_font if self.chinese_font else None
# )
# # 创建提示信息
# self.tip_text = OnscreenText(
# text="这个窗口与主程序共享ShowBase避免了多实例错误",
# parent=self.preview_aspect2d,
# pos=(0, -0.7),
# scale=0.05,
# fg=(0.8, 0.8, 0.8, 1),
# align=TextNode.ACenter,
# font=self.chinese_font if self.chinese_font else None
# )
# # 创建同步状态显示
# self.sync_text = OnscreenText(
# text="同步状态: 等待检测",
# parent=self.preview_aspect2d,
# pos=(-0.9, 0.8),
# scale=0.04,
# fg=(0, 1, 0, 1),
# align=TextNode.ALeft,
# font=self.chinese_font if self.chinese_font else None
# )
def start_sync_timer(self):
"""启动同步定时器"""
if self.main_world and hasattr(self.main_world, 'taskMgr'):
# 每0.5秒检查一次GUI元素变化
self.main_world.taskMgr.doMethodLater(0.5, self.sync_gui_elements, 'sync_preview_gui')
print("✓ GUI同步定时器已启动")
def sync_gui_elements(self, task=None):
"""同步主程序的GUI元素到预览窗口"""
try:
if not self.main_world or not self.preview_aspect2d:
return task.again if task else None
# 获取主程序中的GUI元素
main_gui_elements = getattr(self.main_world, 'gui_elements', [])
# 检查是否需要重新创建所有元素
current_count = len(main_gui_elements)
preview_count = len(self.gui_elements)
need_full_sync = False
# 如果数量有变化,需要完全重新同步
if current_count != preview_count:
need_full_sync = True
print(f"GUI数量变化: {preview_count} -> {current_count}")
else:
# 检查现有元素的属性是否有变化
for i, main_element in enumerate(main_gui_elements):
if i < len(self.gui_elements):
if self.has_element_changed(main_element, self.gui_elements[i]):
need_full_sync = True
print(f"GUI元素 {i} 属性发生变化")
break
# 执行同步
if need_full_sync:
print("执行完整同步...")
self.clear_preview_elements()
self.create_preview_elements(main_gui_elements)
# 更新同步状态显示
if hasattr(self, 'sync_text'):
self.sync_text.setText(f"同步状态: 已同步 {current_count} 个GUI元素")
print(f"同步完成: {current_count} 个GUI元素")
return task.again if task else None
except Exception as e:
print(f"同步GUI元素失败: {str(e)}")
import traceback
traceback.print_exc()
return task.again if task else None
def has_element_changed(self, main_element, preview_element):
"""检查主程序GUI元素是否发生了变化"""
try:
if not hasattr(main_element, 'getTag') or not hasattr(preview_element, 'getTag'):
return True
# 检查文本是否变化
main_text = main_element.getTag("gui_text")
preview_text = getattr(preview_element, '_last_synced_text', "")
if main_text != preview_text:
return True
# 检查位置是否变化
if hasattr(main_element, 'getPos') and hasattr(preview_element, 'getPos'):
main_pos = main_element.getPos()
preview_pos = getattr(preview_element, '_last_synced_pos', Point3(999, 999, 999))
if (abs(main_pos.getX() - preview_pos.getX()) > 0.001 or
abs(main_pos.getY() - preview_pos.getY()) > 0.001 or
abs(main_pos.getZ() - preview_pos.getZ()) > 0.001):
return True
# 检查缩放是否变化
if hasattr(main_element, 'getScale') and hasattr(preview_element, 'getScale'):
main_scale = main_element.getScale().getX()
preview_scale = getattr(preview_element, '_last_synced_scale', -1)
if abs(main_scale - preview_scale) > 0.001:
return True
return False
except Exception as e:
print(f"检查元素变化失败: {str(e)}")
return True # 出错时强制同步
def clear_preview_elements(self):
"""清空预览窗口中的GUI元素"""
for element in self.gui_elements:
try:
if hasattr(element, 'removeNode'):
element.removeNode()
elif hasattr(element, 'destroy'):
element.destroy()
except:
pass
self.gui_elements.clear()
# 如果没有GUI元素了显示说明文本
if hasattr(self, 'welcome_text') and self.welcome_text:
self.welcome_text.show()
if hasattr(self, 'tip_text') and self.tip_text:
self.tip_text.show()
def create_preview_elements(self, main_gui_elements):
"""在预览窗口中创建GUI元素"""
if not main_gui_elements or not self.preview_aspect2d:
return
# 隐藏说明文本
if hasattr(self, 'welcome_text') and self.welcome_text:
self.welcome_text.hide()
if hasattr(self, 'tip_text') and self.tip_text:
self.tip_text.hide()
for main_element in main_gui_elements:
try:
preview_element = self.create_preview_element(main_element)
if preview_element:
self.gui_elements.append(preview_element)
except Exception as e:
print(f"创建预览元素失败: {str(e)}")
def create_preview_element(self, main_element):
"""根据主程序GUI元素创建对应的预览元素"""
try:
if not hasattr(main_element, 'getTag'):
return None
gui_type = main_element.getTag("gui_type")
gui_text = main_element.getTag("gui_text")
if not gui_type:
return None
# 获取位置和缩放
pos = main_element.getPos() if hasattr(main_element, 'getPos') else (0, 0, 0)
scale = main_element.getScale().getX() if hasattr(main_element, 'getScale') else 0.1
preview_element = None
if gui_type == "button":
preview_element = DirectButton(
parent=self.preview_aspect2d,
text=gui_text or "按钮",
pos=pos,
scale=scale,
frameColor=(0.2, 0.6, 0.8, 1),
text_font=self.chinese_font if self.chinese_font else None,
command=lambda: print(f"预览按钮点击: {gui_text}")
)
elif gui_type == "label":
preview_element = DirectLabel(
parent=self.preview_aspect2d,
text=gui_text or "标签",
pos=pos,
scale=scale,
frameColor=(0, 0, 0, 0),
text_fg=(1, 1, 1, 1),
text_font=self.chinese_font if self.chinese_font else None
)
elif gui_type == "entry":
preview_element = DirectEntry(
parent=self.preview_aspect2d,
text="",
pos=pos,
scale=scale,
initialText=gui_text or "输入框",
numLines=1,
width=12,
focus=0,
command=lambda text: print(f"预览输入框: {text}")
)
elif gui_type == "slider":
preview_element = DirectSlider(
parent=self.preview_aspect2d,
pos=pos,
scale=scale,
range=(0, 100),
value=50,
frameColor=(0.6, 0.6, 0.6, 1),
thumbColor=(0.2, 0.8, 0.2, 1),
command=lambda: print("预览滑块值变化")
)
elif gui_type == "3d_text":
# 3D文本需要特殊处理在3D空间中显示
textNode = TextNode(f'preview-3d-text-{len(self.gui_elements)}')
textNode.setText(gui_text or "3D文本")
textNode.setAlign(TextNode.ACenter)
if self.chinese_font:
textNode.setFont(self.chinese_font)
# 需要一个3D渲染节点
if not hasattr(self, 'preview_render3d'):
self.preview_render3d = NodePath('preview_render3d')
# 为预览窗口创建3D相机
camera3d = self.main_world.makeCamera(self.preview_window)
camera3d.reparentTo(self.preview_render3d)
camera3d.setPos(0, -10, 0)
camera3d.lookAt(0, 0, 0)
preview_element = self.preview_render3d.attachNewNode(textNode)
preview_element.setPos(*pos)
preview_element.setScale(scale)
preview_element.setColor(1, 1, 0, 1)
preview_element.setBillboardAxis()
elif gui_type == "virtual_screen":
# 虚拟屏幕也需要3D渲染
if not hasattr(self, 'preview_render3d'):
self.preview_render3d = NodePath('preview_render3d')
camera3d = self.main_world.makeCamera(self.preview_window)
camera3d.reparentTo(self.preview_render3d)
camera3d.setPos(0, -10, 0)
camera3d.lookAt(0, 0, 0)
cm = CardMaker(f"preview-virtual-screen-{len(self.gui_elements)}")
cm.setFrame(-1, 1, -0.5, 0.5)
preview_element = self.preview_render3d.attachNewNode(cm.generate())
preview_element.setPos(*pos)
preview_element.setColor(0.2, 0.2, 0.2, 0.8)
preview_element.setTransparency(TransparencyAttrib.MAlpha)
# 在虚拟屏幕上添加文本
screenText = TextNode(f'preview-screen-text-{len(self.gui_elements)}')
screenText.setText(gui_text or "虚拟屏幕")
screenText.setAlign(TextNode.ACenter)
if self.chinese_font:
screenText.setFont(self.chinese_font)
screenTextNP = preview_element.attachNewNode(screenText)
screenTextNP.setPos(0, 0.01, 0)
screenTextNP.setScale(0.3)
screenTextNP.setColor(0, 1, 0, 1)
# 为预览元素记录同步状态,用于变化检测
if preview_element:
preview_element._last_synced_text = gui_text or ""
preview_element._last_synced_pos = Point3(pos) if hasattr(main_element, 'getPos') else Point3(0, 0, 0)
preview_element._last_synced_scale = scale
print(f"创建预览元素: {gui_type} - {gui_text} (位置: {pos}, 缩放: {scale})")
return preview_element
except Exception as e:
print(f"创建预览元素失败: {str(e)}")
import traceback
traceback.print_exc()
return None
def destroy(self):
"""销毁预览窗口"""
try:
# 停止同步任务
if self.main_world and hasattr(self.main_world, 'taskMgr'):
self.main_world.taskMgr.remove('sync_preview_gui')
# 清空GUI元素
self.clear_preview_elements()
# 移除界面元素
if hasattr(self, 'welcome_text') and self.welcome_text:
self.welcome_text.removeNode()
if hasattr(self, 'tip_text') and self.tip_text:
self.tip_text.removeNode()
if hasattr(self, 'sync_text') and self.sync_text:
self.sync_text.removeNode()
# 关闭窗口
if self.preview_window and self.main_world:
self.main_world.closeWindow(self.preview_window)
self.preview_window = None
print("预览窗口已销毁")
except Exception as e:
print(f"销毁预览窗口失败: {str(e)}")
# 测试代码
if __name__ == "__main__":
print("这是GUI预览窗口模块")
print("需要与主程序集成使用,不能独立运行")

413
main.py Normal file
View File

@ -0,0 +1,413 @@
import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)
import sys
import builtins # 添加这一行
from PyQt5.QtWidgets import (QApplication, QMainWindow, QMenuBar, QMenu, QAction,
QDockWidget, QTreeWidget, QListWidget, QWidget, QVBoxLayout, QTreeWidgetItem,
QLabel, QLineEdit, QFormLayout, QDoubleSpinBox, QScrollArea, QTreeView, QInputDialog, QFileDialog, QMessageBox, QDialog, QGroupBox, QHBoxLayout, QPushButton, QDialogButtonBox)
from PyQt5.QtCore import Qt, QDir, QUrl
from PyQt5.QtGui import QDrag, QPainter, QPixmap
from PyQt5.QtWidgets import QFileSystemModel
from QPanda3D.QPanda3DWidget import QPanda3DWidget
from core.world import CoreWorld
from core.selection import SelectionSystem
from core.event_handler import EventHandler
from core.tool_manager import ToolManager
from gui.gui_manager import GUIManager
from scene.scene_manager import SceneManager
from project.project_manager import ProjectManager
from ui.widgets import CustomPanda3DWidget, CustomFileView, CustomTreeWidget
from ui.property_panel import PropertyPanelManager
from ui.interface_manager import InterfaceManager
from panda3d.core import (CardMaker, Vec4, Vec3, ColorAttrib, MaterialAttrib,
GeomNode, NodePath, Material, CollisionTraverser,
CollisionHandlerQueue, CollisionNode, CollisionRay,
Plane, Point3, Point2, BitMask32, CollisionSphere,
CollisionPlane, ModelPool, Filename, ModelRoot,
AmbientLight, DirectionalLight, TextureAttrib, DriveInterface, WindowProperties)
from panda3d.egg import EggData, EggVertexPool, EggPolygon
from direct.task import Task
from direct.task.TaskManagerGlobal import taskMgr
from direct.showbase.ShowBase import ShowBase
from direct.showbase.DirectObject import DirectObject
from direct.showbase.ShowBaseGlobal import globalClock
import os
import json
import datetime
from direct.actor.Actor import Actor
from PyQt5.sip import wrapinstance
class MyWorld(CoreWorld):
def __init__(self):
super().__init__()
# 初始化选择和变换系统
self.selection = SelectionSystem(self)
# 初始化事件处理系统
self.event_handler = EventHandler(self)
# 初始化工具管理系统
self.tool_manager = ToolManager(self)
# 初始化GUI管理系统
self.gui_manager = GUIManager(self)
# 初始化场景管理系统
self.scene_manager = SceneManager(self)
# 初始化项目管理系统
self.project_manager = ProjectManager(self)
# 初始化属性面板管理系统
self.property_panel = PropertyPanelManager(self)
# 初始化界面管理系统
self.interface_manager = InterfaceManager(self)
print("✓ MyWorld 初始化完成")
# ==================== 兼容性属性 ====================
# 保留models属性以兼容现有代码
@property
def models(self):
"""模型列表的兼容性属性"""
return self.scene_manager.models
@models.setter
def models(self, value):
"""模型列表的兼容性设置器"""
self.scene_manager.models = value
# 保留gui_elements属性以兼容现有代码
@property
def gui_elements(self):
"""GUI元素列表的兼容性属性"""
return self.gui_manager.gui_elements
@gui_elements.setter
def gui_elements(self, value):
"""GUI元素列表的兼容性设置器"""
self.gui_manager.gui_elements = value
# 保留guiEditMode属性以兼容现有代码
@property
def guiEditMode(self):
"""GUI编辑模式的兼容性属性"""
return self.gui_manager.guiEditMode
@guiEditMode.setter
def guiEditMode(self, value):
"""GUI编辑模式的兼容性设置器"""
self.gui_manager.guiEditMode = value
# 保留currentTool属性以兼容现有代码
@property
def currentTool(self):
"""当前工具的兼容性属性"""
return self.tool_manager.currentTool
@currentTool.setter
def currentTool(self, value):
"""当前工具的兼容性设置器"""
self.tool_manager.currentTool = value
# 保留treeWidget属性以兼容现有代码
@property
def treeWidget(self):
"""树形控件的兼容性属性"""
return self.interface_manager.treeWidget
@treeWidget.setter
def treeWidget(self, value):
"""树形控件的兼容性设置器"""
self.interface_manager.treeWidget = value
# ==================== GUI管理功能代理 ====================
# GUI元素创建方法 - 代理到gui_manager
def createGUIButton(self, pos=(0, 0, 0), text="按钮", size=0.1):
"""创建2D GUI按钮"""
return self.gui_manager.createGUIButton(pos, text, size)
def createGUILabel(self, pos=(0, 0, 0), text="标签", size=0.08):
"""创建2D GUI标签"""
return self.gui_manager.createGUILabel(pos, text, size)
def createGUIEntry(self, pos=(0, 0, 0), placeholder="输入文本...", size=0.08):
"""创建2D GUI文本输入框"""
return self.gui_manager.createGUIEntry(pos, placeholder, size)
def createGUI3DText(self, pos=(0, 0, 0), text="3D文本", size=0.5):
"""创建3D空间文本"""
return self.gui_manager.createGUI3DText(pos, text, size)
def createGUIVirtualScreen(self, pos=(0, 0, 0), size=(2, 1), text="虚拟屏幕"):
"""创建3D虚拟屏幕"""
return self.gui_manager.createGUIVirtualScreen(pos, size, text)
def createGUISlider(self, pos=(0, 0, 0), text="滑块", scale=0.3):
"""创建2D GUI滑块"""
return self.gui_manager.createGUISlider(pos, text, scale)
# GUI元素管理方法 - 代理到gui_manager
def deleteGUIElement(self, gui_element):
"""删除GUI元素"""
return self.gui_manager.deleteGUIElement(gui_element)
def editGUIElement(self, gui_element, property_name, value):
"""编辑GUI元素属性"""
return self.gui_manager.editGUIElement(gui_element, property_name, value)
def duplicateGUIElement(self, gui_element):
"""复制GUI元素"""
return self.gui_manager.duplicateGUIElement(gui_element)
def editGUIElementDialog(self, gui_element):
"""显示GUI元素编辑对话框"""
return self.gui_manager.editGUIElementDialog(gui_element)
# GUI事件处理方法 - 代理到gui_manager
def onGUIButtonClick(self, button_id):
"""GUI按钮点击事件处理"""
return self.gui_manager.onGUIButtonClick(button_id)
def onGUIEntrySubmit(self, text, entry_id):
"""GUI输入框提交事件处理"""
return self.gui_manager.onGUIEntrySubmit(text, entry_id)
# GUI编辑模式方法 - 代理到gui_manager
def toggleGUIEditMode(self):
"""切换GUI编辑模式"""
return self.gui_manager.toggleGUIEditMode()
def enterGUIEditMode(self):
"""进入GUI编辑模式"""
return self.gui_manager.enterGUIEditMode()
def exitGUIEditMode(self):
"""退出GUI编辑模式"""
return self.gui_manager.exitGUIEditMode()
def createGUIEditPanel(self):
"""创建GUI编辑面板"""
return self.gui_manager.createGUIEditPanel()
def openGUIPreviewWindow(self):
"""打开独立的GUI预览窗口"""
return self.gui_manager.openGUIPreviewWindow()
def closeGUIPreviewWindow(self):
"""关闭GUI预览窗口"""
return self.gui_manager.closeGUIPreviewWindow()
# GUI工具和选择方法 - 代理到gui_manager
def setGUICreateTool(self, tool_type):
"""设置GUI创建工具"""
return self.gui_manager.setGUICreateTool(tool_type)
def deleteSelectedGUI(self):
"""删除选中的GUI元素"""
return self.gui_manager.deleteSelectedGUI()
def copySelectedGUI(self):
"""复制选中的GUI元素"""
return self.gui_manager.copySelectedGUI()
def handleGUIEditClick(self, hitPos):
"""处理GUI编辑模式下的点击"""
return self.gui_manager.handleGUIEditClick(hitPos)
def createGUIAtPosition(self, world_pos, gui_type):
"""在指定位置创建GUI元素"""
return self.gui_manager.createGUIAtPosition(world_pos, gui_type)
def findClickedGUI(self, hitNode):
"""查找被点击的GUI元素"""
return self.gui_manager.findClickedGUI(hitNode)
def selectGUIInTree(self, gui_element):
"""在树形控件中选中GUI元素"""
return self.gui_manager.selectGUIInTree(gui_element)
def updateGUISelection(self, gui_element):
"""更新GUI元素选择状态"""
return self.gui_manager.updateGUISelection(gui_element)
# GUI属性面板方法 - 代理到gui_manager
def selectGUIColor(self, gui_element):
"""选择GUI元素颜色"""
return self.gui_manager.selectGUIColor(gui_element)
def editGUI2DPosition(self, gui_element, axis, value):
"""编辑2D GUI元素位置"""
return self.gui_manager.editGUI2DPosition(gui_element, axis, value)
# ==================== 事件处理代理 ====================
def onTreeItemClicked(self, item, column):
"""处理树形控件项目点击事件 - 代理到interface_manager"""
return self.interface_manager.onTreeItemClicked(item, column)
def setTreeWidget(self, treeWidget):
"""设置树形控件引用 - 代理到interface_manager"""
return self.interface_manager.setTreeWidget(treeWidget)
def mousePressEventLeft(self, evt):
"""处理鼠标左键按下事件 - 代理到event_handler"""
return self.event_handler.mousePressEventLeft(evt)
def mouseReleaseEventLeft(self, evt):
"""处理鼠标左键释放事件 - 代理到event_handler"""
return self.event_handler.mouseReleaseEventLeft(evt)
def wheelForward(self, data=None):
"""处理滚轮向前滚动(前进) - 代理到event_handler"""
return self.event_handler.wheelForward(data)
def wheelBackward(self, data=None):
"""处理滚轮向后滚动(后退) - 代理到event_handler"""
return self.event_handler.wheelBackward(data)
def mousePressEventMiddle(self, evt):
"""处理鼠标中键按下事件 - 代理到event_handler"""
return self.event_handler.mousePressEventMiddle(evt)
def mouseReleaseEventMiddle(self, evt):
"""处理鼠标中键释放事件 - 代理到event_handler"""
return self.event_handler.mouseReleaseEventMiddle(evt)
def mouseMoveEvent(self, evt):
"""处理鼠标移动事件 - 代理到event_handler"""
return self.event_handler.mouseMoveEvent(evt)
# ==================== 属性面板代理 ====================
def setPropertyLayout(self, layout):
"""设置属性面板布局引用 - 代理到property_panel"""
return self.property_panel.setPropertyLayout(layout)
def clearPropertyPanel(self):
"""清空属性面板 - 代理到property_panel"""
return self.property_panel.clearPropertyPanel()
def updatePropertyPanel(self, item):
"""更新属性面板显示 - 代理到property_panel"""
return self.property_panel.updatePropertyPanel(item)
def updateGUIPropertyPanel(self, gui_element):
"""更新GUI元素属性面板 - 代理到property_panel"""
return self.property_panel.updateGUIPropertyPanel(gui_element)
# ==================== 工具管理代理 ====================
def setCurrentTool(self, tool):
"""设置当前工具 - 代理到tool_manager"""
return self.tool_manager.setCurrentTool(tool)
# ==================== 场景管理功能代理 ====================
# 模型导入和处理方法 - 代理到scene_manager
def importModel(self, filepath):
"""导入模型到场景"""
return self.scene_manager.importModel(filepath)
def importModelAsync(self, filepath):
"""异步导入模型"""
return self.scene_manager.importModelAsync(filepath)
def loadAnimatedModel(self, model_path, anims=None):
"""加载带动画的模型"""
return self.scene_manager.loadAnimatedModel(model_path, anims)
# 材质和几何体处理方法 - 代理到scene_manager
def processMaterials(self, model):
"""处理模型材质"""
return self.scene_manager.processMaterials(model)
def processModelGeometry(self, model):
"""处理模型几何体"""
return self.scene_manager.processModelGeometry(model)
# 碰撞系统方法 - 代理到scene_manager
def setupCollision(self, model):
"""为模型设置碰撞检测"""
return self.scene_manager.setupCollision(model)
# 场景树管理方法 - 代理到scene_manager
def updateSceneTree(self):
"""更新场景树显示"""
return self.scene_manager.updateSceneTree()
# 场景保存和加载方法 - 代理到scene_manager
def saveScene(self, filename):
"""保存场景到BAM文件"""
return self.scene_manager.saveScene(filename)
def loadScene(self, filename):
"""从BAM文件加载场景"""
return self.scene_manager.loadScene(filename)
# 模型管理方法 - 代理到scene_manager
def deleteModel(self, model):
"""删除模型"""
return self.scene_manager.deleteModel(model)
def clearAllModels(self):
"""清除所有模型"""
return self.scene_manager.clearAllModels()
def getModels(self):
"""获取模型列表"""
return self.scene_manager.getModels()
def getModelCount(self):
"""获取模型数量"""
return self.scene_manager.getModelCount()
def findModelByName(self, name):
"""根据名称查找模型"""
return self.scene_manager.findModelByName(name)
# ==================== 项目管理功能代理 ====================
# 以下函数代理到project_manager模块的对应功能
def updateWindowTitle(window, project_name=None):
"""更新窗口标题 - 代理到project_manager"""
from project.project_manager import updateWindowTitle as pm_updateWindowTitle
return pm_updateWindowTitle(window, project_name)
def createNewProject(parent_window):
"""创建新项目 - 代理到project_manager"""
world = parent_window.centralWidget().world
return world.project_manager.createNewProject(parent_window)
def saveProject(appw):
"""保存项目 - 代理到project_manager"""
world = appw.centralWidget().world
return world.project_manager.saveProject(appw)
def openProject(appw):
"""打开项目 - 代理到project_manager"""
world = appw.centralWidget().world
return world.project_manager.openProject(appw)
def buildPackage(appw):
"""打包项目 - 代理到project_manager"""
world = appw.centralWidget().world
return world.project_manager.buildPackage(appw)
if __name__ == "__main__":
world = MyWorld()
# 使用新的UI模块创建主窗口
from ui.main_window import setup_main_window
app, main_window = setup_main_window(world)
# 启动应用程序
sys.exit(app.exec_())

14
project/__init__.py Normal file
View File

@ -0,0 +1,14 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
项目管理包 - 提供项目生命周期管理功能
"""
from .project_manager import ProjectManager, NewProjectDialog
__all__ = ['ProjectManager', 'NewProjectDialog']
# 版本信息
__version__ = '1.0.0'
__author__ = 'Project Management System'

Binary file not shown.

Binary file not shown.

600
project/project_manager.py Normal file
View File

@ -0,0 +1,600 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
项目管理器 - 负责项目的生命周期管理
处理项目创建打开保存打包等功能
"""
import os
import sys
import json
import re
import datetime
import subprocess
import shutil
from PyQt5.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QGroupBox, QLineEdit, QPushButton,
QLabel, QDialogButtonBox, QFileDialog, QMessageBox, QMainWindow
)
from PyQt5.QtCore import Qt
# 导入自定义对话框
from ui.widgets import NewProjectDialog
class ProjectManager:
"""项目管理器 - 统一管理项目的生命周期"""
def __init__(self, world):
"""初始化项目管理器
Args:
world: 主程序world对象引用
"""
self.world = world
self.current_project_path = None
self.project_config = None
print("✓ 项目管理系统初始化完成")
# ==================== 项目生命周期管理 ====================
def createNewProject(self, parent_window):
"""创建新项目"""
dialog = NewProjectDialog(parent_window)
if dialog.exec_() != QDialog.Accepted:
return False
project_path = dialog.projectPath
project_name = dialog.projectName
full_project_path = os.path.join(project_path, project_name)
try:
# 创建项目文件夹结构
os.makedirs(full_project_path)
os.makedirs(os.path.join(full_project_path, "models")) # 模型文件夹
os.makedirs(os.path.join(full_project_path, "textures")) # 贴图文件夹
scenes_path = os.path.join(full_project_path, "scenes") # 场景文件夹
os.makedirs(scenes_path)
# 创建项目配置文件
project_config = {
"name": project_name,
"path": full_project_path,
"created_at": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"last_modified": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"version": "1.0.0",
"engine_version": "1.0.0"
}
config_file = os.path.join(full_project_path, "project.json")
with open(config_file, "w", encoding="utf-8") as f:
json.dump(project_config, f, ensure_ascii=False, indent=4)
print(f"项目配置文件已创建: {config_file}")
# 清空当前场景
self._clearCurrentScene()
# 自动保存初始场景
scene_file = os.path.join(scenes_path, "scene.bam")
if self.world.scene_manager.saveScene(scene_file):
# 更新配置文件中的场景路径
project_config["scene_file"] = os.path.relpath(scene_file, full_project_path)
with open(config_file, "w", encoding="utf-8") as f:
json.dump(project_config, f, ensure_ascii=False, indent=4)
print(f"初始场景已保存到: {scene_file}")
else:
print("初始场景保存失败")
# 更新项目状态
self.current_project_path = full_project_path
self.project_config = project_config
# 更新文件浏览器到新项目路径
if hasattr(parent_window, 'fileView') and hasattr(parent_window, 'fileModel'):
parent_window.fileView.setRootIndex(parent_window.fileModel.index(full_project_path))
# 更新窗口标题
self.updateWindowTitle(parent_window, project_name)
# 保存当前项目路径到主窗口
parent_window.current_project_path = full_project_path
# 显示成功消息
QMessageBox.information(parent_window, "成功", f"项目 '{project_name}' 创建成功!")
return True
except Exception as e:
QMessageBox.critical(parent_window, "错误", f"创建项目失败: {str(e)}")
return False
def openProject(self, parent_window):
"""打开项目"""
try:
# 获取项目路径
project_path = QFileDialog.getExistingDirectory(
parent_window,
"选择项目文件夹",
"",
QFileDialog.ShowDirsOnly
)
if not project_path:
return False
# 检查是否是有效的项目文件夹
config_file = os.path.join(project_path, "project.json")
if not os.path.exists(config_file):
QMessageBox.warning(parent_window, "警告", "选择的不是有效的项目文件夹!")
return False
# 读取项目配置
with open(config_file, "r", encoding="utf-8") as f:
project_config = json.load(f)
# 检查场景文件
scene_file = os.path.join(project_path, "scenes", "scene.bam")
if not os.path.exists(scene_file):
QMessageBox.warning(parent_window, "警告", "没有找到场景文件!")
return False
# 加载场景
if self.world.scene_manager.loadScene(scene_file):
# 更新项目配置
project_config["last_modified"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
project_config["scene_file"] = os.path.relpath(scene_file, project_path)
with open(config_file, "w", encoding="utf-8") as f:
json.dump(project_config, f, ensure_ascii=False, indent=4)
# 更新项目状态
self.current_project_path = project_path
self.project_config = project_config
# 保存当前项目路径到主窗口
parent_window.current_project_path = project_path
# 更新窗口标题
project_name = os.path.basename(project_path)
self.updateWindowTitle(parent_window, project_name)
# 更新文件浏览器
if hasattr(parent_window, 'fileView') and hasattr(parent_window, 'fileModel'):
parent_window.fileView.setRootIndex(parent_window.fileModel.index(project_path))
QMessageBox.information(parent_window, "成功", "项目加载成功!")
return True
else:
QMessageBox.warning(parent_window, "错误", "加载场景失败!")
return False
except Exception as e:
QMessageBox.critical(parent_window, "错误", f"加载项目时发生错误:{str(e)}")
return False
def saveProject(self, parent_window):
"""保存项目"""
try:
# 检查是否有当前项目路径
if not self.current_project_path:
# 尝试从主窗口获取
if hasattr(parent_window, 'current_project_path'):
self.current_project_path = parent_window.current_project_path
else:
QMessageBox.warning(parent_window, "警告", "请先创建或打开一个项目!")
return False
project_path = self.current_project_path
scenes_path = os.path.join(project_path, "scenes")
# 固定的场景文件名
scene_file = os.path.join(scenes_path, "scene.bam")
# 如果存在旧文件,先删除
if os.path.exists(scene_file):
try:
os.remove(scene_file)
print(f"已删除旧场景文件: {scene_file}")
except Exception as e:
print(f"删除旧场景文件失败: {str(e)}")
return False
# 保存场景
if self.world.scene_manager.saveScene(scene_file):
# 更新项目配置文件
config_file = os.path.join(project_path, "project.json")
if os.path.exists(config_file):
with open(config_file, "r", encoding="utf-8") as f:
project_config = json.load(f)
# 更新最后修改时间
project_config["last_modified"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# 记录场景文件路径
project_config["scene_file"] = os.path.relpath(scene_file, project_path)
with open(config_file, "w", encoding="utf-8") as f:
json.dump(project_config, f, ensure_ascii=False, indent=4)
# 更新项目配置
self.project_config = project_config
# 更新窗口标题
project_name = os.path.basename(project_path)
self.updateWindowTitle(parent_window, project_name)
QMessageBox.information(parent_window, "成功", "项目保存成功!")
return True
else:
QMessageBox.warning(parent_window, "错误", "保存场景失败!")
return False
except Exception as e:
QMessageBox.critical(parent_window, "错误", f"保存项目时发生错误:{str(e)}")
return False
# ==================== 项目打包功能 ====================
def buildPackage(self, parent_window):
"""打包项目为可执行文件"""
try:
# 检查是否有当前项目路径
if not self.current_project_path:
if hasattr(parent_window, 'current_project_path'):
self.current_project_path = parent_window.current_project_path
else:
QMessageBox.warning(parent_window, "警告", "请先创建或打开一个项目!")
return False
project_path = self.current_project_path
scenes_path = os.path.join(project_path, "scenes")
scene_file = os.path.join(scenes_path, "scene.bam")
# 检查场景文件是否存在
if not os.path.exists(scene_file):
QMessageBox.warning(parent_window, "警告", "请先保存场景!")
return False
# 创建构建目录
build_dir = os.path.join(project_path, "build")
if not os.path.exists(build_dir):
os.makedirs(build_dir)
# 创建打包文件
self._createBuildFiles(build_dir, scene_file)
# 执行打包命令
success = self._executeBuild(build_dir, parent_window)
if success:
QMessageBox.information(parent_window, "成功", "打包完成!\n可执行文件在build/dist目录中。")
return True
else:
return False
except Exception as e:
QMessageBox.critical(parent_window, "错误", f"打包过程出错:{str(e)}")
return False
def _createBuildFiles(self, build_dir, scene_file):
"""创建打包所需的文件"""
# 创建requirements.txt
requirements_code = '''panda3d>=1.10.13
setuptools>=65.5.1
'''
requirements_path = os.path.join(build_dir, "requirements.txt")
with open(requirements_path, "w", encoding="utf-8") as f:
f.write(requirements_code)
# 创建viewer.py文件 - 内容将在下一个方法中实现
self._createViewerFile(build_dir)
# 复制场景文件
shutil.copy2(scene_file, os.path.join(build_dir, "scene.bam"))
# 创建setup.py文件
self._createSetupFile(build_dir)
def _createViewerFile(self, build_dir):
"""创建查看器文件"""
viewer_code = '''import sys
from direct.showbase.ShowBase import ShowBase
from panda3d.core import WindowProperties, Vec3, Point3
from panda3d.core import AmbientLight, DirectionalLight
from panda3d.core import loadPrcFileData
# 配置窗口和禁用音频
loadPrcFileData("", """
win-size 1280 720
window-title Scene Viewer
audio-library-name null
notify-level-audio error
""")
class SceneViewer(ShowBase):
def __init__(self):
ShowBase.__init__(self)
# 设置背景色
self.setBackgroundColor(0.5, 0.5, 0.5)
# 设置相机
self.cam.setPos(0, -50, 20)
self.cam.lookAt(0, 0, 0)
# 添加光照
alight = AmbientLight('alight')
alight.setColor((0.2, 0.2, 0.2, 1))
alnp = self.render.attachNewNode(alight)
self.render.setLight(alnp)
dlight = DirectionalLight('dlight')
dlight.setColor((0.8, 0.8, 0.8, 1))
dlnp = self.render.attachNewNode(dlight)
dlnp.setHpr(45, -45, 0)
self.render.setLight(dlnp)
# 加载场景
scene = self.loader.loadModel("scene.bam")
if scene:
scene.reparentTo(self.render)
# 设置相机控制
self.accept("wheel_up", self.wheelForward)
self.accept("wheel_down", self.wheelBackward)
self.accept("mouse2", self.startOrbit)
self.accept("mouse2-up", self.stopOrbit)
self.orbiting = False
self.lastMouseX = 0
self.lastMouseY = 0
# 启用每帧更新
self.taskMgr.add(self.updateCamera, "updateCamera")
def wheelForward(self):
# Move camera forward
forward = self.cam.getQuat().getForward()
self.cam.setPos(self.cam.getPos() + forward * 2)
def wheelBackward(self):
# Move camera backward
forward = self.cam.getQuat().getForward()
self.cam.setPos(self.cam.getPos() - forward * 2)
def startOrbit(self):
# Start orbit camera
if base.mouseWatcherNode.hasMouse():
self.orbiting = True
self.lastMouseX = base.mouseWatcherNode.getMouseX()
self.lastMouseY = base.mouseWatcherNode.getMouseY()
def stopOrbit(self):
# Stop orbit camera
self.orbiting = False
def updateCamera(self, task):
# Update camera position
if self.orbiting and base.mouseWatcherNode.hasMouse():
mouseX = base.mouseWatcherNode.getMouseX()
mouseY = base.mouseWatcherNode.getMouseY()
deltaX = mouseX - self.lastMouseX
deltaY = mouseY - self.lastMouseY
# Update camera direction
self.cam.setH(self.cam.getH() - deltaX * 50)
newP = self.cam.getP() + deltaY * 50
self.cam.setP(min(max(newP, -89), 89))
self.lastMouseX = mouseX
self.lastMouseY = mouseY
return task.cont
app = SceneViewer()
app.run()
'''
viewer_path = os.path.join(build_dir, "viewer.py")
with open(viewer_path, "w", encoding="utf-8") as f:
f.write(viewer_code)
def _createSetupFile(self, build_dir):
"""创建setup.py文件"""
setup_code = '''from setuptools import setup
from direct.dist.commands import bdist_apps
import sys
platform_specific = {
"win32": {
"build_apps": {
"console_apps": {},
"gui_apps": {
"SceneViewer": "viewer.py",
},
"include_patterns": [
"scene.bam",
"requirements.txt",
],
"plugins": [
"pandagl",
"pandaegg",
"p3openal_audio",
],
"platforms": [
"win_amd64"
],
"include_modules": {
"*": [
"direct.showbase.ShowBase",
"direct.task",
"direct.actor",
"direct.interval",
"panda3d.core",
]
},
"exclude_modules": {
"*": [
"PyQt5",
"tkinter",
]
},
}
},
"linux": {
"build_apps": {
"console_apps": {},
"gui_apps": {
"SceneViewer": "viewer.py",
},
"include_patterns": [
"scene.bam",
"requirements.txt",
"/usr/lib/x86_64-linux-gnu/libopenal.so*",
],
"plugins": [
"pandagl",
"pandaegg",
"p3openal_audio",
],
"platforms": [
"linux_x86_64"
],
"include_modules": {
"*": [
"direct.showbase.ShowBase",
"direct.task",
"direct.actor",
"direct.interval",
"panda3d.core",
]
},
"exclude_modules": {
"*": [
"PyQt5",
"tkinter",
]
},
}
}
}
# 根据平台选择配置
platform = "linux" if sys.platform.startswith("linux") else "win32"
options = platform_specific[platform]
setup(
name="SceneViewer",
version="1.0",
options=options,
install_requires=[
"panda3d>=1.10.13",
],
)
'''
setup_path = os.path.join(build_dir, "setup.py")
with open(setup_path, "w", encoding="utf-8") as f:
f.write(setup_code)
def _executeBuild(self, build_dir, parent_window):
"""执行打包命令"""
try:
# 显示详细输出
process = subprocess.Popen(
[sys.executable, "setup.py", "bdist_apps"],
cwd=build_dir,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True
)
# 实时显示输出
while True:
output = process.stdout.readline()
if output == '' and process.poll() is not None:
break
if output:
print(output.strip())
# 获取错误输出
stderr = process.stderr.read()
if stderr:
print("错误输出:", stderr)
if process.returncode == 0:
return True
else:
QMessageBox.critical(parent_window, "错误", f"打包失败,返回码:{process.returncode}")
return False
except Exception as e:
QMessageBox.critical(parent_window, "错误", f"打包失败:{str(e)}")
return False
# ==================== 工具方法 ====================
def updateWindowTitle(self, window, project_name=None):
"""更新窗口标题"""
base_title = "引擎编辑器"
if project_name:
window.setWindowTitle(f"{base_title} - {project_name}")
else:
window.setWindowTitle(base_title)
def _clearCurrentScene(self):
"""清空当前场景"""
# 移除所有模型
for model in self.world.models:
model.removeNode()
self.world.models.clear()
# 清空GUI元素
for gui_element in self.world.gui_elements:
gui_element.removeNode()
self.world.gui_elements.clear()
# 更新场景树
if hasattr(self.world, 'updateSceneTree'):
self.world.updateSceneTree()
def getCurrentProjectPath(self):
"""获取当前项目路径"""
return self.current_project_path
def getCurrentProjectConfig(self):
"""获取当前项目配置"""
return self.project_config
def isProjectOpen(self):
"""检查是否有项目打开"""
return self.current_project_path is not None
def getProjectName(self):
"""获取当前项目名称"""
if self.current_project_path:
return os.path.basename(self.current_project_path)
return None
def getProjectInfo(self):
"""获取项目信息"""
if not self.project_config:
return None
return {
"name": self.project_config.get("name", "未知项目"),
"path": self.project_config.get("path", ""),
"created_at": self.project_config.get("created_at", ""),
"last_modified": self.project_config.get("last_modified", ""),
"version": self.project_config.get("version", "1.0.0"),
"engine_version": self.project_config.get("engine_version", "1.0.0")
}
# ==================== 便利函数 ====================
def updateWindowTitle(window, project_name=None):
"""更新窗口标题 - 独立便利函数"""
base_title = "引擎编辑器"
if project_name:
window.setWindowTitle(f"{base_title} - {project_name}")
else:
window.setWindowTitle(base_title)

View File

@ -0,0 +1,90 @@
# 项目部署指南
## 🚀 在新电脑上运行此项目
### 方法1使用Conda推荐
```bash
# 1. 安装Miniconda
wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh
bash Miniconda3-latest-Linux-x86_64.sh
# 2. 克隆项目
git clone <your-repo-url>
cd eg
# 3. 创建conda环境
conda env create -f environment.yml
# 4. 激活环境
conda activate eg-project
# 5. 运行项目
python main.py
```
### 方法2使用virtualenv + pip
```bash
# 1. 克隆项目
git clone <your-repo-url>
cd eg
# 2. 创建虚拟环境
python -m venv venv
source venv/bin/activate # Linux/Mac
# 或 venv\Scripts\activate # Windows
# 3. 安装依赖
pip install -r clean-requirements.txt
# 4. 运行项目
python main.py
```
## 📁 需要复制的文件
**必须复制:**
- `main.py` - 主程序
- `gui_preview_window.py` - GUI窗口
- `demo/` - 演示文件夹
- `environment.yml` - Conda环境配置
- `clean-requirements.txt` - 核心依赖
**不要复制:**
- `.conda/` - 虚拟环境文件夹
- `__pycache__/` - Python缓存
- `requirements.txt` - 包含系统包的混乱依赖
## 🔧 Cursor IDE 设置
1. **打开项目**在Cursor中打开项目文件夹
2. **选择解释器**
- `Ctrl+Shift+P` → "Python: Select Interpreter"
- 选择虚拟环境中的Python路径
3. **验证环境**检查状态栏显示正确的Python版本
## 📦 项目依赖说明
- **Panda3D**: 3D图形引擎
- **PyQt5/PySide6**: GUI框架
- **Pillow**: 图像处理
- **python-dotenv**: 环境变量管理
- **pyassimp**: 3D模型加载
## 🌍 跨平台注意事项
- Linux/Mac: 使用 `source venv/bin/activate`
- Windows: 使用 `venv\Scripts\activate`
- 某些依赖可能需要系统级安装如OpenGL库
## 🐛 常见问题
**Q: 无法安装Panda3D**
A: 确保系统有OpenGL支持或使用conda安装
**Q: PyQt5无法运行**
A: 可能需要安装系统级GUI依赖
**Q: 导入错误?**
A: 检查Python版本需要3.10+

View File

@ -0,0 +1,6 @@
Panda3D>=1.10.15
PyQt5>=5.15.9
PySide6>=6.8.1
Pillow>=9.0.1
python-dotenv>=1.0.1
pyassimp>=5.2.5

View File

@ -0,0 +1,34 @@
# This file may be used to create an environment using:
# $ conda create --name <env> --file <this file>
# platform: linux-64
# created-by: conda 25.5.1
@EXPLICIT
https://repo.anaconda.com/pkgs/main/linux-64/_libgcc_mutex-0.1-main.conda
https://repo.anaconda.com/pkgs/main/linux-64/ca-certificates-2025.2.25-h06a4308_0.conda
https://repo.anaconda.com/pkgs/main/linux-64/ld_impl_linux-64-2.40-h12ee557_0.conda
https://repo.anaconda.com/pkgs/main/linux-64/libstdcxx-ng-11.2.0-h1234567_1.conda
https://repo.anaconda.com/pkgs/main/noarch/tzdata-2025b-h04d1e81_0.conda
https://repo.anaconda.com/pkgs/main/linux-64/libgomp-11.2.0-h1234567_1.conda
https://repo.anaconda.com/pkgs/main/linux-64/_openmp_mutex-5.1-1_gnu.conda
https://repo.anaconda.com/pkgs/main/linux-64/libgcc-ng-11.2.0-h1234567_1.conda
https://repo.anaconda.com/pkgs/main/linux-64/bzip2-1.0.8-h5eee18b_6.conda
https://repo.anaconda.com/pkgs/main/linux-64/expat-2.7.1-h6a678d5_0.conda
https://repo.anaconda.com/pkgs/main/linux-64/libffi-3.4.4-h6a678d5_1.conda
https://repo.anaconda.com/pkgs/main/linux-64/libuuid-1.41.5-h5eee18b_0.conda
https://repo.anaconda.com/pkgs/main/linux-64/ncurses-6.4-h6a678d5_0.conda
https://repo.anaconda.com/pkgs/main/linux-64/openssl-3.0.16-h5eee18b_0.conda
https://repo.anaconda.com/pkgs/main/linux-64/pthread-stubs-0.3-h0ce48e5_1.conda
https://repo.anaconda.com/pkgs/main/linux-64/xorg-libxau-1.0.12-h9b100fa_0.conda
https://repo.anaconda.com/pkgs/main/linux-64/xorg-libxdmcp-1.1.5-h9b100fa_0.conda
https://repo.anaconda.com/pkgs/main/linux-64/xorg-xorgproto-2024.1-h5eee18b_1.conda
https://repo.anaconda.com/pkgs/main/linux-64/xz-5.6.4-h5eee18b_1.conda
https://repo.anaconda.com/pkgs/main/linux-64/zlib-1.2.13-h5eee18b_1.conda
https://repo.anaconda.com/pkgs/main/linux-64/libxcb-1.17.0-h9b100fa_0.conda
https://repo.anaconda.com/pkgs/main/linux-64/readline-8.2-h5eee18b_0.conda
https://repo.anaconda.com/pkgs/main/linux-64/sqlite-3.45.3-h5eee18b_0.conda
https://repo.anaconda.com/pkgs/main/linux-64/xorg-libx11-1.8.12-h9b100fa_1.conda
https://repo.anaconda.com/pkgs/main/linux-64/tk-8.6.14-h993c535_1.conda
https://repo.anaconda.com/pkgs/main/linux-64/python-3.10.18-h1a3bd86_0.conda
https://repo.anaconda.com/pkgs/main/linux-64/setuptools-78.1.1-py310h06a4308_0.conda
https://repo.anaconda.com/pkgs/main/linux-64/wheel-0.45.1-py310h06a4308_0.conda
https://repo.anaconda.com/pkgs/main/noarch/pip-25.1-pyhc872135_2.conda

View File

@ -0,0 +1,14 @@
name: eg-project
channels:
- conda-forge
- defaults
dependencies:
- python=3.10
- pip
- pip:
- Panda3D>=1.10.15
- PyQt5>=5.15.9
- PySide6>=6.8.1
- Pillow>=9.0.1
- python-dotenv>=1.0.1
- pyassimp>=5.2.5

View File

@ -0,0 +1,100 @@
apturl==0.5.2
bcrypt==3.2.0
beautifulsoup4==4.10.0
blinker==1.4
Brlapi==0.8.3
certifi==2020.6.20
chardet==4.0.0
click==8.0.3
colorama==0.4.4
command-not-found==0.3
cryptography==3.4.8
cupshelpers==1.0
dbus-python==1.2.18
defer==1.0.6
DirectFolderBrowser==22.1
DirectGuiDesigner==22.4.1
DirectGuiExtension==22.9
distro==1.7.0
distro-info==1.1+ubuntu0.2
duplicity==0.8.21
fasteners==0.14.1
future==0.18.2
html5lib==1.1
httplib2==0.20.2
idna==3.3
importlib-metadata==4.6.4
jeepney==0.7.1
keyring==23.5.0
language-selector==0.1
launchpadlib==1.10.16
lazr.restfulclient==0.14.4
lazr.uri==1.0.6
lockfile==0.12.2
louis==3.20.0
lxml==4.8.0
macaroonbakery==1.3.1
Mako==1.1.3
MarkupSafe==2.0.1
monotonic==1.6
more-itertools==8.10.0
netifaces==0.11.0
oauthlib==3.2.0
olefile==0.46
Panda3D==1.10.15
panda3d-frame==22.10.post1
Panda3DNodeEditor==22.5
paramiko==2.9.3
pexpect==4.8.0
Pillow==9.0.1
platformdirs==4.3.6
protobuf==3.12.4
ptyprocess==0.7.0
pyassimp==5.2.5
pycairo==1.20.1
pycups==2.0.1
Pygments==2.11.2
PyGObject==3.42.1
PyJWT==2.3.0
pymacaroons==0.13.0
PyNaCl==1.5.0
pyparsing==2.4.7
PyQt5==5.15.9
pyqt5-plugins==5.15.9.2.3
PyQt5-Qt5==5.15.2
pyqt5-tools==5.15.9.3.3
PyQt5_sip==12.16.1
pyRFC3339==1.1
PySide6==6.8.1
PySide6_Addons==6.8.1
PySide6_Essentials==6.8.1
python-apt==2.4.0+ubuntu4
python-dateutil==2.8.1
python-debian==0.1.43+ubuntu1.1
python-dotenv==1.0.1
pytz==2022.1
pyxdg==0.27
PyYAML==5.4.1
QPanda3D==0.2.10
qt5-applications==5.15.2.2.3
qt5-tools==5.15.2.1.3
reportlab==3.6.8
requests==2.25.1
SceneEditor==22.5
screen-resolution-extra==0.0.0
SecretStorage==3.3.1
shiboken6==6.8.1
six==1.16.0
soupsieve==2.3.1
systemd-python==234
ubuntu-drivers-common==0.0.0
ubuntu-pro-client==8001
ufw==0.36.1
unattended-upgrades==0.1
urllib3==1.26.5
usb-creator==0.3.7
wadllib==1.3.6
webencodings==0.5.1
xdg==5
xkit==0.0.0
zipp==1.0.0

14
scene/__init__.py Normal file
View File

@ -0,0 +1,14 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
场景管理包 - 提供场景和模型管理功能
"""
from .scene_manager import SceneManager
__all__ = ['SceneManager']
# 版本信息
__version__ = '1.0.0'
__author__ = 'Scene Management System'

Binary file not shown.

Binary file not shown.

524
scene/scene_manager.py Normal file
View File

@ -0,0 +1,524 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
场景管理器 - 负责场景和模型管理的核心功能
处理模型导入场景树构建材质系统碰撞设置等
"""
import os
from panda3d.core import (
ModelPool, ModelRoot, Filename, NodePath, GeomNode, Material, Vec4,
MaterialAttrib, ColorAttrib, Point3, CollisionNode, CollisionSphere,
BitMask32, TransparencyAttrib
)
from panda3d.egg import EggData, EggVertexPool
from direct.actor.Actor import Actor
class SceneManager:
"""场景管理器 - 统一管理场景中的所有元素"""
def __init__(self, world):
"""初始化场景管理器
Args:
world: 主程序world对象引用
"""
self.world = world
self.models = [] # 模型列表
print("✓ 场景管理系统初始化完成")
# ==================== 模型导入和处理 ====================
def importModel(self, filepath):
"""导入模型到场景"""
try:
print(f"\n=== 开始导入模型: {filepath} ===")
# 首先检查ModelPool中是否已有该模型
model = ModelPool.getModel(filepath, True)
if not model:
print("模型不在缓存中,开始加载...")
# 使用loader加载模型
model = self.world.loader.loadModel(filepath)
if not model:
print("加载模型失败")
return None
# 如果是ModelRoot节点,添加到ModelPool
if isinstance(model.node(), ModelRoot):
print("添加模型到ModelPool缓存")
model.node().setFullpath(Filename(filepath))
ModelPool.addModel(model.node())
else:
print("从ModelPool获取缓存的模型")
# 创建模型的副本
model = NodePath(model.copySubgraph())
# 设置模型名称
model_name = os.path.basename(filepath)
model.setName(model_name)
# 将模型添加到场景
model.reparentTo(self.world.render)
# 处理FBX文件的单位转换
if filepath.lower().endswith('.fbx'):
print("处理FBX模型单位转换...")
# FBX使用厘米需要缩放到米
scale_factor = 0.01 # 将厘米转换为米
model.setScale(scale_factor)
# 调整位置使模型位于地面上
bounds = model.getBounds()
min_point = bounds.getMin()
# 将模型底部对齐到地面(y=0)
model.setZ(-min_point.getZ() * scale_factor)
else:
# 非FBX文件使用默认变换
model.setPos(0, 0, 0)
model.setHpr(0, 0, 0)
model.setScale(1, 1, 1)
# 创建并设置基础材质
print("\n=== 开始设置材质 ===")
self._applyMaterialsToModel(model)
# 添加文件标签用于保存/加载
model.setTag("file", model_name)
model.setTag("is_model_root", "1")
# 添加到模型列表
self.models.append(model)
# 更新场景树
self.updateSceneTree()
print(f"=== 模型导入成功: {model_name} ===\n")
return model
except Exception as e:
print(f"导入模型失败: {str(e)}")
return None
def _applyMaterialsToModel(self, model):
"""递归应用材质到模型的所有GeomNode"""
def apply_material(node_path, depth=0):
indent = " " * depth
print(f"{indent}处理节点: {node_path.getName()}")
print(f"{indent}节点类型: {node_path.node().__class__.__name__}")
if isinstance(node_path.node(), GeomNode):
print(f"{indent}发现GeomNode处理材质")
geom_node = node_path.node()
# 检查所有几何体的状态
has_color = False
color = None
# 首先检查节点自身的状态
node_state = node_path.getState()
if node_state.hasAttrib(MaterialAttrib.getClassType()):
mat_attrib = node_state.getAttrib(MaterialAttrib.getClassType())
node_material = mat_attrib.getMaterial()
if node_material and node_material.hasDiffuse():
color = node_material.getDiffuse()
has_color = True
print(f"{indent}从节点材质获取颜色: {color}")
# 检查FBX特有的属性
for tag_key in node_path.getTagKeys():
print(f"{indent}发现标签: {tag_key}")
if "color" in tag_key.lower() or "diffuse" in tag_key.lower():
tag_value = node_path.getTag(tag_key)
print(f"{indent}颜色相关标签: {tag_key} = {tag_value}")
# 如果还没找到颜色,检查几何体
if not has_color:
for i in range(geom_node.getNumGeoms()):
geom = geom_node.getGeom(i)
state = geom_node.getGeomState(i)
# 检查顶点颜色
vdata = geom.getVertexData()
format = vdata.getFormat()
for j in range(format.getNumColumns()):
column = format.getColumn(j)
# InternalName对象需要使用getName()转换为字符串
column_name = column.getName().getName()
if "color" in column_name.lower():
print(f"{indent}发现顶点颜色数据: {column_name}")
# 这里可以读取顶点颜色,但先记录发现
# 检查材质属性
if state.hasAttrib(MaterialAttrib.getClassType()):
mat_attrib = state.getAttrib(MaterialAttrib.getClassType())
orig_material = mat_attrib.getMaterial()
if orig_material:
if orig_material.hasBaseColor():
color = orig_material.getBaseColor()
has_color = True
print(f"{indent}从基础颜色获取: {color}")
break
elif orig_material.hasDiffuse():
color = orig_material.getDiffuse()
has_color = True
print(f"{indent}从漫反射颜色获取: {color}")
break
# 检查颜色属性
if not has_color and state.hasAttrib(ColorAttrib.getClassType()):
color_attrib = state.getAttrib(ColorAttrib.getClassType())
if not color_attrib.isOff():
color = color_attrib.getColor()
has_color = True
print(f"{indent}从颜色属性获取: {color}")
break
# 创建新材质
material = Material()
if has_color:
print(f"{indent}应用找到的颜色: {color}")
material.setDiffuse(color)
material.setBaseColor(color) # 同时设置基础颜色
node_path.setColor(color)
else:
print(f"{indent}使用默认颜色")
material.setDiffuse((0.8, 0.8, 0.8, 1.0))
# 设置其他材质属性
material.setAmbient((0.2, 0.2, 0.2, 1.0))
material.setSpecular((0.5, 0.5, 0.5, 1.0))
material.setShininess(32.0)
# 应用材质
node_path.setMaterial(material)
print(f"{indent}几何体数量: {geom_node.getNumGeoms()}")
# 递归处理子节点
child_count = node_path.getNumChildren()
print(f"{indent}子节点数量: {child_count}")
for i in range(child_count):
child = node_path.getChild(i)
apply_material(child, depth + 1)
# 应用材质
print("\n开始递归应用材质...")
apply_material(model)
print("=== 材质设置完成 ===\n")
def importModelAsync(self, filepath):
"""异步导入模型"""
try:
# 创建异步加载请求
request = self.world.loader.makeAsyncRequest(filepath)
# 添加完成回调
def modelLoaded(task):
if task.isReady():
model = task.result()
if model:
# 处理加载完成的模型
self.processLoadedModel(model)
return task.done()
request.done_event = modelLoaded
# 开始异步加载
self.world.loader.loadAsync(request)
except Exception as e:
print(f"异步加载模型失败: {str(e)}")
def loadAnimatedModel(self, model_path, anims=None):
"""加载带动画的模型"""
try:
# 创建Actor对象
actor = Actor(model_path, anims)
if actor:
actor.reparentTo(self.world.render)
self.models.append(actor)
# 更新场景树
self.updateSceneTree()
return actor
except Exception as e:
print(f"加载动画模型失败: {str(e)}")
return None
# ==================== 材质和几何体处理 ====================
def processMaterials(self, model):
"""处理模型材质"""
if isinstance(model.node(), GeomNode):
# 创建基础材质
material = Material()
material.setAmbient((0.2, 0.2, 0.2, 1.0))
material.setDiffuse((0.8, 0.8, 0.8, 1.0))
material.setSpecular((0.5, 0.5, 0.5, 1.0))
material.setShininess(32.0)
# 检查FBX材质
state = model.node().getGeomState(0)
if state.hasAttrib(MaterialAttrib.getClassType()):
fbx_material = state.getAttrib(MaterialAttrib.getClassType()).getMaterial()
if fbx_material:
# 复制FBX材质属性
material.setAmbient(fbx_material.getAmbient())
material.setDiffuse(fbx_material.getDiffuse())
material.setSpecular(fbx_material.getSpecular())
material.setShininess(fbx_material.getShininess())
# 应用材质
model.setMaterial(material)
def processModelGeometry(self, model):
"""处理模型几何体"""
# 创建EggData对象
egg_data = EggData()
# 处理顶点数据
vertex_pool = EggVertexPool("vpool")
egg_data.addChild(vertex_pool)
# 处理几何体
if isinstance(model.node(), GeomNode):
for i in range(model.node().getNumGeoms()):
geom = model.node().getGeom(i)
# 处理几何体数据
# ...
# ==================== 碰撞系统 ====================
def setupCollision(self, model):
"""为模型设置碰撞检测"""
# 创建碰撞节点
cNode = CollisionNode(f'modelCollision_{model.getName()}')
# 设置碰撞掩码
cNode.setIntoCollideMask(BitMask32.bit(2)) # 使用第2位作为模型的碰撞掩码
# 获取模型的边界
bounds = model.getBounds()
center = bounds.getCenter()
radius = bounds.getRadius()
# 添加碰撞球体
cSphere = CollisionSphere(center, radius)
cNode.addSolid(cSphere)
# 将碰撞节点附加到模型上
cNodePath = model.attachNewNode(cNode)
# cNodePath.show() # 取消注释可以显示碰撞体,用于调试
# ==================== 场景树管理 ====================
def updateSceneTree(self):
"""更新场景树显示 - 代理到interface_manager"""
if hasattr(self.world, 'interface_manager'):
return self.world.interface_manager.updateSceneTree()
else:
print("界面管理器未初始化,无法更新场景树")
# ==================== 场景保存和加载 ====================
def saveScene(self, filename):
"""保存场景到BAM文件"""
try:
print(f"\n=== 开始保存场景到: {filename} ===")
# 遍历所有模型,保存材质状态
for model in self.models:
# 获取当前状态
state = model.getState()
# 如果有材质属性,保存为标签
if state.hasAttrib(MaterialAttrib.getClassType()):
mat_attrib = state.getAttrib(MaterialAttrib.getClassType())
material = mat_attrib.getMaterial()
if material:
# 保存材质属性到标签
model.setTag("material_ambient", str(material.getAmbient()))
model.setTag("material_diffuse", str(material.getDiffuse()))
model.setTag("material_specular", str(material.getSpecular()))
model.setTag("material_emission", str(material.getEmission()))
model.setTag("material_shininess", str(material.getShininess()))
if material.hasBaseColor():
model.setTag("material_basecolor", str(material.getBaseColor()))
# 如果有颜色属性,保存为标签
if state.hasAttrib(ColorAttrib.getClassType()):
color_attrib = state.getAttrib(ColorAttrib.getClassType())
if not color_attrib.isOff():
model.setTag("color", str(color_attrib.getColor()))
# 保存场景
success = self.world.render.writeBamFile(filename)
return success
except Exception as e:
print(f"保存场景时发生错误: {str(e)}")
return False
def loadScene(self, filename):
"""从BAM文件加载场景"""
try:
print(f"\n=== 开始加载场景: {filename} ===")
# 清除当前场景
print("\n清除当前场景...")
for model in self.models:
model.removeNode()
self.models.clear()
# 加载场景
scene = self.world.loader.loadModel(filename)
if not scene:
return False
# 遍历场景中的所有模型节点
def processNode(nodePath, depth=0):
indent = " " * depth
print(f"{indent}处理节点: {nodePath.getName()}")
# 跳过render节点的递归
if nodePath.getName() == "render" and depth > 0:
print(f"{indent}跳过重复的render节点")
return
# 跳过光源节点
if nodePath.getName() in ["alight", "dlight"]:
print(f"{indent}跳过光源节点: {nodePath.getName()}")
return
# 跳过相机节点
if nodePath.getName() in ["camera", "cam"]:
print(f"{indent}跳过相机节点: {nodePath.getName()}")
return
if isinstance(nodePath.node(), ModelRoot):
print(f"{indent}找到模型根节点!")
# 清除现有材质状态
nodePath.clearMaterial()
nodePath.clearColor()
# 创建新材质
material = Material()
# 从标签恢复材质属性
def parseColor(color_str):
"""解析颜色字符串为Vec4"""
try:
# 移除LVecBase4f标记只保留数值
color_str = color_str.replace('LVecBase4f', '').strip('()')
r, g, b, a = map(float, color_str.split(','))
return Vec4(r, g, b, a)
except:
return Vec4(1, 1, 1, 1) # 默认白色
if nodePath.hasTag("material_ambient"):
material.setAmbient(parseColor(nodePath.getTag("material_ambient")))
if nodePath.hasTag("material_diffuse"):
material.setDiffuse(parseColor(nodePath.getTag("material_diffuse")))
if nodePath.hasTag("material_specular"):
material.setSpecular(parseColor(nodePath.getTag("material_specular")))
if nodePath.hasTag("material_emission"):
material.setEmission(parseColor(nodePath.getTag("material_emission")))
if nodePath.hasTag("material_shininess"):
material.setShininess(float(nodePath.getTag("material_shininess")))
if nodePath.hasTag("material_basecolor"):
material.setBaseColor(parseColor(nodePath.getTag("material_basecolor")))
# 应用材质
nodePath.setMaterial(material)
# 恢复颜色属性
if nodePath.hasTag("color"):
nodePath.setColor(parseColor(nodePath.getTag("color")))
# 将模型重新挂载到render下
nodePath.wrtReparentTo(self.world.render)
self.models.append(nodePath)
# 递归处理子节点
for child in nodePath.getChildren():
processNode(child, depth + 1)
print("\n开始处理场景节点...")
processNode(scene)
# 移除临时场景节点
scene.removeNode()
# 更新场景树
self.updateSceneTree()
print("=== 场景加载完成 ===\n")
return True
except Exception as e:
print(f"加载场景时发生错误: {str(e)}")
return False
# ==================== 模型管理 ====================
def deleteModel(self, model):
"""删除模型"""
try:
if model in self.models:
model.removeNode()
self.models.remove(model)
self.updateSceneTree()
print(f"删除模型: {model.getName()}")
return True
except Exception as e:
print(f"删除模型失败: {str(e)}")
return False
def clearAllModels(self):
"""清除所有模型"""
try:
for model in self.models:
model.removeNode()
self.models.clear()
self.updateSceneTree()
print("清除所有模型完成")
except Exception as e:
print(f"清除所有模型失败: {str(e)}")
def getModels(self):
"""获取模型列表"""
return self.models.copy()
def getModelCount(self):
"""获取模型数量"""
return len(self.models)
def findModelByName(self, name):
"""根据名称查找模型"""
for model in self.models:
if model.getName() == name:
return model
return None
# ==================== 帮助方法 ====================
def processLoadedModel(self, model):
"""处理加载完成的模型(用于异步加载回调)"""
if model:
# 添加到模型列表
self.models.append(model)
# 设置基础变换
model.setPos(0, 0, 0)
model.setHpr(0, 0, 0)
model.setScale(1, 1, 1)
# 应用材质
self.processMaterials(model)
# 更新场景树
self.updateSceneTree()
print(f"异步加载模型完成: {model.getName()}")

18
ui/__init__.py Normal file
View File

@ -0,0 +1,18 @@
"""
UI模块
包含所有用户界面相关的功能
- widgets.py: 自定义Qt部件
- main_window.py: 主窗口设置
"""
from .widgets import CustomPanda3DWidget, CustomFileView, CustomTreeWidget
from .main_window import MainWindow, setup_main_window
__all__ = [
'CustomPanda3DWidget',
'CustomFileView',
'CustomTreeWidget',
'MainWindow',
'setup_main_window'
]

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

196
ui/interface_manager.py Normal file
View File

@ -0,0 +1,196 @@
from PyQt5.QtWidgets import QTreeWidget, QTreeWidgetItem, QMenu
from PyQt5.QtCore import Qt
from panda3d.core import GeomNode, ModelRoot
class InterfaceManager:
"""界面管理器 - 处理树形控件和UI交互"""
def __init__(self, world):
"""初始化界面管理器"""
self.world = world
self.treeWidget = None
def setTreeWidget(self, treeWidget):
"""设置树形控件引用并更新场景树"""
self.treeWidget = treeWidget
# 添加右键菜单
self.treeWidget.setContextMenuPolicy(Qt.CustomContextMenu)
self.treeWidget.customContextMenuRequested.connect(self.showTreeContextMenu)
# 更新场景树
self.world.scene_manager.updateSceneTree()
def onTreeItemClicked(self, item, column):
"""处理树形控件项目点击事件"""
if not item:
return
# 获取节点对象
nodePath = item.data(0, Qt.UserRole)
if nodePath:
# 更新选择状态
self.world.selection.updateSelection(nodePath)
# 更新属性面板
self.world.property_panel.updatePropertyPanel(item)
print(f"树形控件点击: {item.text(0)}")
else:
# 如果没有节点对象,清除选择
self.world.selection.updateSelection(None)
self.world.property_panel.clearPropertyPanel()
def showTreeContextMenu(self, position):
"""显示树形控件的右键菜单"""
item = self.treeWidget.itemAt(position)
if not item:
return
# 获取节点对象
nodePath = item.data(0, Qt.UserRole)
if not nodePath:
return
# 创建菜单
menu = QMenu()
# 检查是否是GUI元素
if hasattr(nodePath, 'getTag') and nodePath.getTag("gui_type"):
# GUI元素菜单
editAction = menu.addAction("编辑")
editAction.triggered.connect(lambda: self.world.gui_manager.editGUIElementDialog(nodePath))
deleteAction = menu.addAction("删除GUI元素")
deleteAction.triggered.connect(lambda: self.world.gui_manager.deleteGUIElement(nodePath))
duplicateAction = menu.addAction("复制")
duplicateAction.triggered.connect(lambda: self.world.gui_manager.duplicateGUIElement(nodePath))
else:
# 为模型节点或其子节点添加删除选项
parentItem = item.parent()
if parentItem:
if self.isModelOrChild(item):
deleteAction = menu.addAction("删除")
deleteAction.triggered.connect(lambda: self.deleteNode(nodePath, item))
# 显示菜单
menu.exec_(self.treeWidget.viewport().mapToGlobal(position))
def isModelOrChild(self, item):
"""检查是否是模型节点或其子节点"""
while item and item.parent():
if item.parent().text(0) == "模型":
return True
item = item.parent()
return False
def deleteNode(self, nodePath, item):
"""删除节点"""
try:
# 从场景中移除
nodePath.removeNode()
# 如果是模型根节点,从模型列表中移除
if item.parent().text(0) == "模型":
if nodePath in self.world.models:
self.world.models.remove(nodePath)
# 从树形控件中移除
parentItem = item.parent()
if parentItem:
parentItem.removeChild(item)
print(f"成功删除节点: {nodePath.getName()}")
# 清空属性面板和选择框
self.world.property_panel.clearPropertyPanel()
self.world.selection.updateSelection(None)
except Exception as e:
print(f"删除节点失败: {str(e)}")
def updateSceneTree(self):
"""更新场景树显示 - 实际实现"""
if not self.treeWidget:
return
print("\n=== 更新场景树 ===")
self.treeWidget.clear()
# 创建场景根节点
sceneRoot = QTreeWidgetItem(self.treeWidget, ['场景'])
# 添加相机节点
cameraItem = QTreeWidgetItem(sceneRoot, ['相机'])
cameraItem.setData(0, Qt.UserRole, self.world.cam)
print("添加相机节点")
# 添加模型节点组
modelsItem = QTreeWidgetItem(sceneRoot, ['模型'])
print(f"模型列表中的模型数量: {len(self.world.models)}")
# 添加GUI元素节点组
guiItem = QTreeWidgetItem(sceneRoot, ['GUI元素'])
print(f"GUI元素数量: {len(self.world.gui_elements)}")
# 递归添加节点及其子节点
def addNodeToTree(node, parentItem):
print(f"\n处理节点: {node.getName()}")
# 创建节点项
nodeItem = QTreeWidgetItem(parentItem, [node.getName()])
nodeItem.setData(0, Qt.UserRole, node)
print(f"添加节点: {node.getName()}")
# 递归处理所有子节点
for child in node.getChildren():
# 检查是否是有效的模型节点
if (isinstance(child.node(), GeomNode) or
child.hasTag("file") or
child.getName() == "RootNode" or
isinstance(child.node(), ModelRoot)):
print(f"处理子节点: {child.getName()}")
addNodeToTree(child, nodeItem)
else:
print(f"跳过节点: {child.getName()}")
# 添加所有模型及其子节点
for model in self.world.models:
print(f"\n处理根模型: {model.getName()}")
addNodeToTree(model, modelsItem)
# 添加所有GUI元素
for gui_element in self.world.gui_elements:
gui_type = gui_element.getTag("gui_type") if hasattr(gui_element, 'getTag') else "unknown"
gui_text = gui_element.getTag("gui_text") if hasattr(gui_element, 'getTag') else "GUI元素"
display_name = f"{gui_type}: {gui_text}"
guiElementItem = QTreeWidgetItem(guiItem, [display_name])
guiElementItem.setData(0, Qt.UserRole, gui_element)
print(f"添加GUI元素: {display_name}")
# 添加地板节点
if hasattr(self.world, 'ground'):
groundItem = QTreeWidgetItem(sceneRoot, ['地板'])
groundItem.setData(0, Qt.UserRole, self.world.ground)
print("添加地板节点")
# 展开所有节点
self.treeWidget.expandAll()
print("=== 场景树更新完成 ===\n")
def findTreeItem(self, node, parentItem):
"""在树形控件中查找指定的节点项"""
for i in range(parentItem.childCount()):
item = parentItem.child(i)
itemNode = item.data(0, Qt.UserRole)
if itemNode == node:
return item
# 递归查找子项
if item.childCount() > 0:
found = self.findTreeItem(node, item)
if found:
return found
return None

258
ui/main_window.py Normal file
View File

@ -0,0 +1,258 @@
"""
主窗口设置模块
负责主窗口的界面构建和事件绑定
- 菜单栏工具栏创建
- 停靠窗口设置
- 事件连接和信号处理
"""
import sys
from PyQt5.QtWidgets import (QApplication, QMainWindow, QMenuBar, QMenu, QAction,
QDockWidget, QTreeWidget, QListWidget, QWidget, QVBoxLayout, QTreeWidgetItem,
QLabel, QLineEdit, QFormLayout, QDoubleSpinBox, QScrollArea,
QFileSystemModel, QButtonGroup, QToolButton)
from PyQt5.QtCore import Qt, QDir
from ui.widgets import CustomPanda3DWidget, CustomFileView, CustomTreeWidget
class MainWindow(QMainWindow):
"""主窗口类"""
def __init__(self, world):
super().__init__()
self.world = world
self.setupWindow()
self.setupMenus()
self.setupDockWindows()
self.setupToolbar()
self.connectEvents()
def setupWindow(self):
"""设置窗口基本属性"""
self.setGeometry(50, 50, 1920, 1080)
self.setWindowTitle("引擎编辑器")
# 使用自定义的 Panda3D 部件作为中央部件
self.pandaWidget = CustomPanda3DWidget(self.world)
self.setCentralWidget(self.pandaWidget)
def setupMenus(self):
"""创建菜单栏"""
menubar = self.menuBar()
# 文件菜单
self.fileMenu = menubar.addMenu('文件')
self.newAction = self.fileMenu.addAction('新建')
self.openAction = self.fileMenu.addAction('打开')
self.saveAction = self.fileMenu.addAction('保存')
self.buildAction = self.fileMenu.addAction('打包')
self.fileMenu.addSeparator()
self.exitAction = self.fileMenu.addAction('退出')
# 编辑菜单
self.editMenu = menubar.addMenu('编辑')
self.undoAction = self.editMenu.addAction('撤销')
self.redoAction = self.editMenu.addAction('重做')
self.editMenu.addSeparator()
self.cutAction = self.editMenu.addAction('剪切')
self.copyAction = self.editMenu.addAction('复制')
self.pasteAction = self.editMenu.addAction('粘贴')
# 视图菜单
self.viewMenu = menubar.addMenu('视图')
self.viewPerspectiveAction = self.viewMenu.addAction('透视图')
self.viewTopAction = self.viewMenu.addAction('俯视图')
self.viewFrontAction = self.viewMenu.addAction('前视图')
self.viewMenu.addSeparator()
self.viewGridAction = self.viewMenu.addAction('显示网格')
# 工具菜单
self.toolsMenu = menubar.addMenu('工具')
self.selectAction = self.toolsMenu.addAction('选择工具')
self.moveAction = self.toolsMenu.addAction('移动工具')
self.rotateAction = self.toolsMenu.addAction('旋转工具')
self.scaleAction = self.toolsMenu.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.guiMenu.addSeparator()
self.create3DTextAction = self.guiMenu.addAction('创建3D文本')
self.createVirtualScreenAction = self.guiMenu.addAction('创建虚拟屏幕')
# 帮助菜单
self.helpMenu = menubar.addMenu('帮助')
self.aboutAction = self.helpMenu.addAction('关于')
def setupDockWindows(self):
"""创建停靠窗口"""
# 创建左侧停靠窗口(层级窗口)
self.leftDock = QDockWidget("层级", self)
self.leftDock.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
self.treeWidget = CustomTreeWidget(self.world)
self.treeWidget.setHeaderLabel("场景")
self.world.setTreeWidget(self.treeWidget) # 设置树形控件引用
self.leftDock.setWidget(self.treeWidget)
self.leftDock.setMinimumWidth(300)
self.addDockWidget(Qt.LeftDockWidgetArea, self.leftDock)
# 创建右侧停靠窗口(属性窗口)
self.rightDock = QDockWidget("属性", self)
self.rightDock.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
# 创建属性面板的主容器和布局
self.propertyContainer = QWidget()
self.propertyContainer.setObjectName("PropertyContainer")
self.propertyLayout = QFormLayout(self.propertyContainer)
# 添加初始提示信息
tipLabel = QLabel("")
tipLabel.setStyleSheet("color: gray;") # 使用灰色字体
self.propertyLayout.addRow(tipLabel)
# 创建滚动区域并设置属性
self.scrollArea = QScrollArea()
self.scrollArea.setWidgetResizable(True)
self.scrollArea.setWidget(self.propertyContainer)
# 设置滚动区域为停靠窗口的主部件
self.rightDock.setWidget(self.scrollArea)
self.rightDock.setMinimumWidth(300)
self.addDockWidget(Qt.RightDockWidgetArea, self.rightDock)
# 设置属性面板到世界对象
self.world.setPropertyLayout(self.propertyLayout)
# 创建底部停靠窗口(资源窗口)
self.bottomDock = QDockWidget("资源", self)
self.bottomDock.setAllowedAreas(Qt.BottomDockWidgetArea)
# 创建文件系统模型
self.fileModel = QFileSystemModel()
self.fileModel.setRootPath(QDir.homePath()) # 设置为用户主目录
# 创建树形视图显示文件系统
self.fileView = CustomFileView(self.world)
self.fileView.setModel(self.fileModel)
self.fileView.setRootIndex(self.fileModel.index(QDir.homePath())) # 设置为用户主目录索引
# 设置列宽
self.fileView.setColumnWidth(0, 250) # 名称列
self.fileView.setColumnWidth(1, 100) # 大小列
self.fileView.setColumnWidth(2, 100) # 类型列
self.fileView.setColumnWidth(3, 150) # 修改日期列
# 设置视图属性
self.fileView.setMinimumHeight(200) # 设置最小高度
self.bottomDock.setWidget(self.fileView)
self.addDockWidget(Qt.BottomDockWidgetArea, self.bottomDock)
def setupToolbar(self):
"""创建工具栏"""
self.toolbar = self.addToolBar('工具栏')
# 创建工具按钮组
self.toolGroup = QButtonGroup()
# 选择工具
self.selectTool = QToolButton()
self.selectTool.setText("选择")
self.selectTool.setCheckable(True)
self.toolGroup.addButton(self.selectTool)
self.toolbar.addWidget(self.selectTool)
# 旋转工具
self.rotateTool = QToolButton()
self.rotateTool.setText("旋转")
self.rotateTool.setCheckable(True)
self.toolGroup.addButton(self.rotateTool)
self.toolbar.addWidget(self.rotateTool)
# 缩放工具
self.scaleTool = QToolButton()
self.scaleTool.setText("缩放")
self.scaleTool.setCheckable(True)
self.toolGroup.addButton(self.scaleTool)
self.toolbar.addWidget(self.scaleTool)
# 添加分隔符
self.toolbar.addSeparator()
# GUI创建工具
self.createButtonTool = QToolButton()
self.createButtonTool.setText("创建按钮")
self.toolbar.addWidget(self.createButtonTool)
self.createLabelTool = QToolButton()
self.createLabelTool.setText("创建标签")
self.toolbar.addWidget(self.createLabelTool)
self.create3DTextTool = QToolButton()
self.create3DTextTool.setText("3D文本")
self.toolbar.addWidget(self.create3DTextTool)
# 默认选择"选择"工具
self.selectTool.setChecked(True)
self.world.setCurrentTool("选择")
def connectEvents(self):
"""连接事件信号"""
# 导入项目管理功能函数
from main import createNewProject, saveProject, openProject, buildPackage
# 连接文件菜单事件
self.newAction.triggered.connect(lambda: createNewProject(self))
self.openAction.triggered.connect(lambda: openProject(self))
self.saveAction.triggered.connect(lambda: saveProject(self))
self.buildAction.triggered.connect(lambda: buildPackage(self))
self.exitAction.triggered.connect(QApplication.instance().quit)
# 连接GUI编辑模式事件
self.guiEditModeAction.triggered.connect(lambda: self.world.toggleGUIEditMode())
# 连接GUI创建按钮事件
self.createButtonAction.triggered.connect(lambda: self.world.createGUIButton())
self.createLabelAction.triggered.connect(lambda: self.world.createGUILabel())
self.createEntryAction.triggered.connect(lambda: self.world.createGUIEntry())
self.create3DTextAction.triggered.connect(lambda: self.world.createGUI3DText())
self.createVirtualScreenAction.triggered.connect(lambda: self.world.createGUIVirtualScreen())
# 连接工具栏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.treeWidget.itemClicked.connect(self.world.onTreeItemClicked)
print("已连接点击信号")
# 连接工具切换信号
self.toolGroup.buttonClicked.connect(self.onToolChanged)
def onToolChanged(self, button):
"""工具切换事件处理"""
if button.isChecked():
tool_name = button.text()
self.world.setCurrentTool(tool_name)
print(f"工具栏: 选择了 {tool_name} 工具")
else:
self.world.setCurrentTool(None)
print("工具栏: 取消选择工具")
def setup_main_window(world):
"""设置主窗口的便利函数"""
app = QApplication.instance()
if app is None:
app = QApplication(sys.argv)
main_window = MainWindow(world)
main_window.show()
return app, main_window

285
ui/property_panel.py Normal file
View File

@ -0,0 +1,285 @@
from PyQt5.QtWidgets import (QLabel, QLineEdit, QDoubleSpinBox, QPushButton,
QTreeWidget, QTreeWidgetItem, QMenu)
from PyQt5.QtCore import Qt
class PropertyPanelManager:
"""属性面板管理器"""
def __init__(self, world):
"""初始化属性面板管理器"""
self.world = world
self._propertyLayout = None
def setPropertyLayout(self, layout):
"""设置属性面板布局引用"""
print("开始设置属性布局")
print(f"布局类型: {type(layout)}")
# 保存布局引用
self._propertyLayout = layout
# 确保布局有父部件
if not layout.parent():
print("布局没有父部件,创建新的容器")
from PyQt5.QtWidgets import QWidget
container = QWidget()
container.setObjectName("PropertyContainer")
container.setLayout(layout)
print(f"布局父部件: {self._propertyLayout.parent().objectName() if self._propertyLayout.parent() else 'None'}")
print(f"布局项目数: {self._propertyLayout.count()}")
return True
def clearPropertyPanel(self):
"""清空属性面板"""
if self._propertyLayout:
while self._propertyLayout.count():
item = self._propertyLayout.takeAt(0)
if item.widget():
item.widget().deleteLater()
def updatePropertyPanel(self, item):
"""更新属性面板显示"""
if not self._propertyLayout or not self._propertyLayout.parent():
print("属性布局未设置或没有父部件!")
return
self.clearPropertyPanel()
itemText = item.text(0)
# 如果点击的是场景根节点,显示提示信息
if itemText == "场景":
tipLabel = QLabel("")
tipLabel.setStyleSheet("color: gray;")
self._propertyLayout.addRow(tipLabel)
return
# 创建通用属性
nameLabel = QLabel("名称:")
nameEdit = QLineEdit(itemText)
self._propertyLayout.addRow(nameLabel, nameEdit)
# 获取节点对象
model = item.data(0, Qt.UserRole)
# 检查是否是GUI元素
if model and hasattr(model, 'getTag') and model.getTag("gui_type"):
self.updateGUIPropertyPanel(model)
# 如果找到模型,显示其属性
elif model:
self._updateModelPropertyPanel(model)
# 强制更新布局
if self._propertyLayout:
self._propertyLayout.update()
propertyWidget = self._propertyLayout.parentWidget()
if propertyWidget:
propertyWidget.update()
def _updateModelPropertyPanel(self, model):
"""更新模型属性面板"""
# 获取父节点
parent = model.getParent()
# 位置属性(相对于父节点)
relativePos = model.getPos(parent) if parent else model.getPos()
xPos = QDoubleSpinBox()
xPos.setRange(-1000, 1000)
xPos.setValue(relativePos.getX())
xPos.valueChanged.connect(lambda v: model.setX(parent, v) if parent else model.setX(v))
self._propertyLayout.addRow("相对位置 X:", xPos)
yPos = QDoubleSpinBox()
yPos.setRange(-1000, 1000)
yPos.setValue(relativePos.getY())
yPos.valueChanged.connect(lambda v: model.setY(parent, v) if parent else model.setY(v))
self._propertyLayout.addRow("相对位置 Y:", yPos)
zPos = QDoubleSpinBox()
zPos.setRange(-1000, 1000)
zPos.setValue(relativePos.getZ())
zPos.valueChanged.connect(lambda v: model.setZ(parent, v) if parent else model.setZ(v))
self._propertyLayout.addRow("相对位置 Z:", zPos)
# 世界位置(只读)
worldPos = model.getPos(self.world.render)
worldXPos = QDoubleSpinBox()
worldXPos.setRange(-1000, 1000)
worldXPos.setValue(worldPos.getX())
worldXPos.setReadOnly(True)
self._propertyLayout.addRow("世界位置 X:", worldXPos)
worldYPos = QDoubleSpinBox()
worldYPos.setRange(-1000, 1000)
worldYPos.setValue(worldPos.getY())
worldYPos.setReadOnly(True)
self._propertyLayout.addRow("世界位置 Y:", worldYPos)
worldZPos = QDoubleSpinBox()
worldZPos.setRange(-1000, 1000)
worldZPos.setValue(worldPos.getZ())
worldZPos.setReadOnly(True)
self._propertyLayout.addRow("世界位置 Z:", worldZPos)
# 旋转属性
hRot = QDoubleSpinBox()
hRot.setRange(-180, 180)
hRot.setValue(model.getH())
hRot.valueChanged.connect(lambda v: model.setH(v))
self._propertyLayout.addRow("旋转 H:", hRot)
pRot = QDoubleSpinBox()
pRot.setRange(-180, 180)
pRot.setValue(model.getP())
pRot.valueChanged.connect(lambda v: model.setP(v))
self._propertyLayout.addRow("旋转 P:", pRot)
rRot = QDoubleSpinBox()
rRot.setRange(-180, 180)
rRot.setValue(model.getR())
rRot.valueChanged.connect(lambda v: model.setR(v))
self._propertyLayout.addRow("旋转 R:", rRot)
# 缩放属性
xScale = QDoubleSpinBox()
xScale.setRange(0.01, 100)
xScale.setSingleStep(0.1)
xScale.setValue(model.getScale().getX())
xScale.valueChanged.connect(lambda v: model.setScale(v, model.getScale().getY(), model.getScale().getZ()))
self._propertyLayout.addRow("缩放 X:", xScale)
yScale = QDoubleSpinBox()
yScale.setRange(0.01, 100)
yScale.setSingleStep(0.1)
yScale.setValue(model.getScale().getY())
yScale.valueChanged.connect(lambda v: model.setScale(model.getScale().getX(), v, model.getScale().getZ()))
self._propertyLayout.addRow("缩放 Y:", yScale)
zScale = QDoubleSpinBox()
zScale.setRange(0.01, 100)
zScale.setSingleStep(0.1)
zScale.setValue(model.getScale().getZ())
zScale.valueChanged.connect(lambda v: model.setScale(model.getScale().getX(), model.getScale().getY(), v))
self._propertyLayout.addRow("缩放 Z:", zScale)
def updateGUIPropertyPanel(self, gui_element):
"""更新GUI元素属性面板"""
gui_type = gui_element.getTag("gui_type")
gui_text = gui_element.getTag("gui_text")
# GUI类型显示
typeLabel = QLabel("GUI类型:")
typeValue = QLabel(gui_type)
typeValue.setStyleSheet("color: #00AAFF; font-weight: bold;")
self._propertyLayout.addRow(typeLabel, typeValue)
# 文本属性(如果适用)
if gui_type in ["button", "label", "entry", "3d_text", "virtual_screen"]:
textLabel = QLabel("文本:")
textEdit = QLineEdit(gui_text or "")
# 创建一个更新函数来处理文本变化
def updateText(text):
success = self.world.gui_manager.editGUIElement(gui_element, "text", text)
if success:
# 更新场景树显示的名称
self.world.scene_manager.updateSceneTree()
textEdit.textChanged.connect(updateText)
self._propertyLayout.addRow(textLabel, textEdit)
# 位置属性
if hasattr(gui_element, 'getPos'):
pos = gui_element.getPos()
# 根据GUI类型决定位置编辑方式
if gui_type in ["button", "label", "entry"]:
# 2D GUI组件使用屏幕坐标
logical_x = pos.getX() / 0.1 # 反向转换为逻辑坐标
logical_z = pos.getZ() / 0.1
xPos = QDoubleSpinBox()
xPos.setRange(-50, 50)
xPos.setValue(logical_x)
xPos.valueChanged.connect(lambda v: self.world.gui_manager.editGUI2DPosition(gui_element, "x", v))
self._propertyLayout.addRow("屏幕位置 X:", xPos)
zPos = QDoubleSpinBox()
zPos.setRange(-50, 50)
zPos.setValue(logical_z)
zPos.valueChanged.connect(lambda v: self.world.gui_manager.editGUI2DPosition(gui_element, "z", v))
self._propertyLayout.addRow("屏幕位置 Z:", zPos)
# 显示实际屏幕坐标(只读)
actualXLabel = QLabel(f"{pos.getX():.3f}")
actualXLabel.setStyleSheet("color: gray; font-size: 10px;")
self._propertyLayout.addRow("实际屏幕 X:", actualXLabel)
actualZLabel = QLabel(f"{pos.getZ():.3f}")
actualZLabel.setStyleSheet("color: gray; font-size: 10px;")
self._propertyLayout.addRow("实际屏幕 Z:", actualZLabel)
else:
# 3D GUI组件使用世界坐标
xPos = QDoubleSpinBox()
xPos.setRange(-1000, 1000)
xPos.setValue(pos.getX())
xPos.valueChanged.connect(lambda v: self.world.gui_manager.editGUIElement(gui_element, "position", [v, pos.getY(), pos.getZ()]))
self._propertyLayout.addRow("位置 X:", xPos)
yPos = QDoubleSpinBox()
yPos.setRange(-1000, 1000)
yPos.setValue(pos.getY())
yPos.valueChanged.connect(lambda v: self.world.gui_manager.editGUIElement(gui_element, "position", [pos.getX(), v, pos.getZ()]))
self._propertyLayout.addRow("位置 Y:", yPos)
zPos = QDoubleSpinBox()
zPos.setRange(-1000, 1000)
zPos.setValue(pos.getZ())
zPos.valueChanged.connect(lambda v: self.world.gui_manager.editGUIElement(gui_element, "position", [pos.getX(), pos.getY(), v]))
self._propertyLayout.addRow("位置 Z:", zPos)
# 缩放属性
if hasattr(gui_element, 'getScale'):
scale = gui_element.getScale()
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))
self._propertyLayout.addRow("缩放:", scaleSpinBox)
# 颜色属性针对2D GUI
if gui_type in ["button", "label"]:
colorButton = QPushButton("选择颜色")
colorButton.clicked.connect(lambda: self.world.gui_manager.selectGUIColor(gui_element))
self._propertyLayout.addRow("背景颜色:", colorButton)
# 3D特有属性
if gui_type in ["3d_text", "virtual_screen"]:
# 旋转属性
if hasattr(gui_element, 'getHpr'):
hpr = gui_element.getHpr()
hRot = QDoubleSpinBox()
hRot.setRange(-180, 180)
hRot.setValue(hpr.getX())
hRot.valueChanged.connect(lambda v: gui_element.setH(v))
self._propertyLayout.addRow("旋转 H:", hRot)
pRot = QDoubleSpinBox()
pRot.setRange(-180, 180)
pRot.setValue(hpr.getY())
pRot.valueChanged.connect(lambda v: gui_element.setP(v))
self._propertyLayout.addRow("旋转 P:", pRot)
rRot = QDoubleSpinBox()
rRot.setRange(-180, 180)
rRot.setValue(hpr.getZ())
rRot.valueChanged.connect(lambda v: gui_element.setR(v))
self._propertyLayout.addRow("旋转 R:", rRot)

394
ui/widgets.py Normal file
View File

@ -0,0 +1,394 @@
"""
自定义Qt部件模块
包含所有自定义的Qt界面组件
- NewProjectDialog: 新建项目对话框
- CustomPanda3DWidget: 自定义Panda3D显示部件
- CustomFileView: 自定义文件浏览器
- CustomTreeWidget: 自定义场景树部件
"""
import os
import re
from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QGroupBox, QHBoxLayout,
QLineEdit, QPushButton, QLabel, QDialogButtonBox,
QTreeView, QTreeWidget, QTreeWidgetItem, QWidget,
QFileDialog, QMessageBox)
from PyQt5.QtCore import Qt, QUrl
from PyQt5.QtGui import QDrag, QPainter, QPixmap
from PyQt5.sip import wrapinstance
from QPanda3D.QPanda3DWidget import QPanda3DWidget
class NewProjectDialog(QDialog):
"""新建项目对话框"""
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("新建项目")
self.setMinimumWidth(500)
# 创建布局
layout = QVBoxLayout(self)
# 创建路径选择部分
pathGroup = QGroupBox("项目路径")
pathLayout = QHBoxLayout()
self.pathEdit = QLineEdit()
self.pathEdit.setReadOnly(True)
browseButton = QPushButton("浏览...")
browseButton.clicked.connect(self.browsePath)
pathLayout.addWidget(self.pathEdit)
pathLayout.addWidget(browseButton)
pathGroup.setLayout(pathLayout)
layout.addWidget(pathGroup)
# 创建项目名称部分
nameGroup = QGroupBox("项目名称")
nameLayout = QVBoxLayout()
self.nameEdit = QLineEdit()
self.nameEdit.setText("新项目")
self.nameEdit.selectAll()
nameLayout.addWidget(self.nameEdit)
# 添加提示标签
self.tipLabel = QLabel("项目名称只能包含字母、数字、下划线、中划线和中文")
self.tipLabel.setStyleSheet("color: gray;")
nameLayout.addWidget(self.tipLabel)
nameGroup.setLayout(nameLayout)
layout.addWidget(nameGroup)
# 添加按钮
buttonBox = QDialogButtonBox(
QDialogButtonBox.Ok | QDialogButtonBox.Cancel
)
buttonBox.accepted.connect(self.validate)
buttonBox.rejected.connect(self.reject)
layout.addWidget(buttonBox)
# 存储结果
self.projectPath = ""
self.projectName = ""
def browsePath(self):
"""浏览选择项目路径"""
path = QFileDialog.getExistingDirectory(self, "选择项目路径")
if path:
self.pathEdit.setText(path)
def validate(self):
"""验证输入并关闭对话框"""
# 获取并验证路径
self.projectPath = self.pathEdit.text()
if not self.projectPath:
QMessageBox.warning(self, "错误", "请选择项目路径!")
return
# 获取并验证项目名称
self.projectName = self.nameEdit.text().strip()
if not self.projectName:
QMessageBox.warning(self, "错误", "请输入项目名称!")
return
# 验证项目名称格式
if not re.match(r'^[a-zA-Z0-9_\-\u4e00-\u9fa5]+$', self.projectName):
QMessageBox.warning(self, "错误",
"项目名称只能包含字母、数字、下划线、中划线和中文!")
return
# 检查项目是否已存在
full_path = os.path.join(self.projectPath, self.projectName)
if os.path.exists(full_path):
QMessageBox.warning(self, "错误", "项目已存在!")
return
self.accept()
class CustomPanda3DWidget(QPanda3DWidget):
"""自定义Panda3D显示部件"""
def __init__(self, world, parent=None):
if parent is None:
parent = wrapinstance(0, QWidget)
super().__init__(world, parent)
self.world = world
self.setFocusPolicy(Qt.StrongFocus) # 允许接收键盘焦点
self.setAcceptDrops(True) # 允许接收拖放
self.setMouseTracking(True) # 启用鼠标追踪
# 让world引用这个widget以便获取准确的尺寸
if hasattr(world, 'setQtWidget'):
world.setQtWidget(self)
def getActualSize(self):
"""获取Qt部件的实际渲染尺寸"""
return (self.width(), self.height())
def dragEnterEvent(self, event):
"""处理拖拽进入事件"""
# 检查是否是文件拖拽
if event.mimeData().hasUrls():
# 检查是否包含支持的模型文件
for url in event.mimeData().urls():
filepath = url.toLocalFile()
if filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')):
event.acceptProposedAction()
return
event.ignore()
def dragMoveEvent(self, event):
"""处理拖拽移动事件"""
if event.mimeData().hasUrls():
event.acceptProposedAction()
else:
event.ignore()
def dropEvent(self, event):
"""处理拖放事件"""
if event.mimeData().hasUrls():
for url in event.mimeData().urls():
filepath = url.toLocalFile()
if filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')):
self.world.importModel(filepath)
event.acceptProposedAction()
else:
event.ignore()
def wheelEvent(self, event):
"""处理滚轮事件"""
if event.angleDelta().y() > 0:
# 滚轮向前滚动
self.world.wheelForward()
else:
# 滚轮向后滚动
self.world.wheelBackward()
event.accept()
def mousePressEvent(self, event):
"""处理 Qt 鼠标按下事件"""
if event.button() == Qt.LeftButton:
self.world.mousePressEventLeft({
'x': event.x(),
'y': event.y()
})
elif event.button() == Qt.RightButton:
self.world.mousePressEventRight({
'x': event.x(),
'y': event.y()
})
elif event.button() == Qt.MiddleButton: # 添加滑轮按下事件处理
self.world.mousePressEventMiddle({
'x': event.x(),
'y': event.y()
})
event.accept()
def mouseReleaseEvent(self, event):
"""处理 Qt 鼠标释放事件"""
if event.button() == Qt.LeftButton:
self.world.mouseReleaseEventLeft({
'x': event.x(),
'y': event.y()
})
elif event.button() == Qt.RightButton:
self.world.mouseReleaseEventRight({
'x': event.x(),
'y': event.y()
})
elif event.button() == Qt.MiddleButton: # 添加滑轮释放事件处理
self.world.mouseReleaseEventMiddle({
'x': event.x(),
'y': event.y()
})
event.accept()
def mouseMoveEvent(self, event):
"""处理 Qt 鼠标移动事件"""
self.world.mouseMoveEvent({
'x': event.x(),
'y': event.y()
})
event.accept()
class CustomFileView(QTreeView):
"""自定义文件浏览器"""
def __init__(self, world, parent=None):
if parent is None:
parent = wrapinstance(0, QWidget)
super().__init__(parent)
self.world = world
self.setDragEnabled(True) # 启用拖拽
self.setSelectionMode(QTreeView.ExtendedSelection) # 允许多选
self.setDragDropMode(QTreeView.DragOnly) # 只允许拖出,不允许拖入
def startDrag(self, supportedActions):
"""开始拖拽操作"""
# 获取选中的文件
indexes = self.selectedIndexes()
if not indexes:
return
# 只处理文件名列
indexes = [idx for idx in indexes if idx.column() == 0]
# 创建 MIME 数据
mimeData = self.model().mimeData(indexes)
# 检查是否包含支持的模型文件
urls = []
for index in indexes:
filepath = self.model().filePath(index)
if filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')):
urls.append(QUrl.fromLocalFile(filepath))
if not urls:
return
# 设置 URL 列表
mimeData.setUrls(urls)
# 创建拖拽对象
drag = QDrag(self)
drag.setMimeData(mimeData)
# 设置拖拽图标(可选)
pixmap = QPixmap(32, 32)
pixmap.fill(Qt.transparent)
painter = QPainter(pixmap)
painter.drawText(pixmap.rect(), Qt.AlignCenter, str(len(urls)))
painter.end()
drag.setPixmap(pixmap)
# 执行拖拽
drag.exec_(supportedActions)
def mouseDoubleClickEvent(self, event):
"""处理双击事件"""
index = self.indexAt(event.pos())
if index.isValid():
model = self.model()
filepath = model.filePath(index)
# 检查是否是模型文件
if filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')):
self.world.importModel(filepath)
else:
print("不支持的文件类型")
super().mouseDoubleClickEvent(event)
class CustomTreeWidget(QTreeWidget):
"""自定义场景树部件"""
def __init__(self, world, parent=None):
if parent is None:
parent = wrapinstance(0, QWidget)
super().__init__(parent)
self.world = world
self.setDragEnabled(True)
self.setAcceptDrops(True)
self.setDragDropMode(QTreeWidget.InternalMove)
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
# 获取节点引用
dragged_node = dragged_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)
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.updatePropertyPanel(dragged_item)
else:
event.ignore()
def isValidParentChild(self, dragged_item, target_item):
"""检查是否是有效的父子关系"""
# 不能拖放到自己上
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:
return False
# 允许在模型节点下的任何位置调整父子关系
while target_parent:
if target_parent.text(0) == "模型":
return True
target_parent = target_parent.parent()
return False
def dragEnterEvent(self, event):
"""处理拖入事件"""
if event.source() == self:
event.accept()
else:
event.ignore()
def dragMoveEvent(self, event):
"""处理拖动事件"""
if event.source() == self:
event.accept()
else:
event.ignore()
def keyPressEvent(self, event):
"""处理键盘按键事件"""
if event.key() == Qt.Key_Delete:
currentItem = self.currentItem()
if currentItem and currentItem.parent():
# 检查是否是模型节点或其子节点
if self.world.isModelOrChild(currentItem):
nodePath = currentItem.data(0, Qt.UserRole)
if nodePath:
print("正在删除节点...")
self.world.deleteNode(nodePath, currentItem)
print("删除完成")
else:
super().keyPressEvent(event)