EG/ui/main_window.py
2025-09-15 14:38:18 +08:00

2205 lines
90 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

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

"""
主窗口设置模块
负责主窗口的界面构建和事件绑定:
- 菜单栏、工具栏创建
- 停靠窗口设置
- 事件连接和信号处理
"""
import os
import sys
from PyQt5.QtGui import QKeySequence, QIcon
from PyQt5.QtWidgets import (QApplication, QMainWindow, QMenuBar, QMenu, QAction,
QDockWidget, QTreeWidget, QListWidget, QWidget, QVBoxLayout, QTreeWidgetItem,
QLabel, QLineEdit, QFormLayout, QDoubleSpinBox, QScrollArea,
QFileSystemModel, QButtonGroup, QToolButton, QPushButton, QHBoxLayout,
QComboBox, QGroupBox, QInputDialog, QFileDialog, QMessageBox, QDesktopWidget, QDialog,
QSpinBox, QFrame)
from PyQt5.QtCore import Qt, QDir, QTimer, QSize, QPoint
from direct.showbase.ShowBaseGlobal import aspect2d
from ui.widgets import CustomPanda3DWidget, CustomFileView, CustomTreeWidget,CustomAssetsTreeWidget, CustomConsoleDockWidget
class MainWindow(QMainWindow):
"""主窗口类"""
def __init__(self, world):
super().__init__()
self.world = world
self.world.main_window = self # 关键让world对象能访问主窗口
self.setupCenterWidget() # 创建中间部分Panda3D
self.setupMenus() # 创建菜单栏
self.setupDockWindows()
# self.setupToolbar()
self.connectEvents()
# 移动窗口到屏幕中央
self.move_center()
# 创建定时器来更新脚本管理面板状态
self.updateTimer = QTimer()
self.updateTimer.timeout.connect(self.updateScriptPanel)
self.updateTimer.start(500) # 每500毫秒更新一次
self.toolbarDragging = False
self.dragStartPos = QPoint(0, 0)
self.toolbarStartPos = QPoint(0, 0)
def setupCenterWidget(self):
"""设置窗口基本属性"""
self.setWindowTitle("引擎编辑器")
# 使用自定义的 Panda3D 部件作为中央部件
self.pandaWidget = CustomPanda3DWidget(self.world)
self.setCentralWidget(self.pandaWidget)
# 创建内嵌工具栏
self.setupEmbeddedToolbar()
def move_center(self):
"""设置窗口居中显示"""
self.setGeometry(50, 50, 1920, 1080)
screen = QDesktopWidget().screenGeometry()
self.move(
int(screen.width() / 2 - self.width() / 2),
int(screen.height() / 2 - self.height() / 2),
)
@staticmethod
def get_icon_path(icon_name):
"""获取图标文件的完整路径"""
# 假设 icons 文件夹在项目根目录下
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
icon_path = os.path.join(project_root, "icons", icon_name)
# 检查文件是否存在如果不存在则返回默认值或None
if not os.path.exists(icon_path):
print(f"警告: 图标文件不存在: {icon_path}")
return "" # 返回空字符串QIcon会处理空路径
return icon_path
def setupEmbeddedToolbar(self):
"""创建Unity风格的内嵌工具栏"""
# 创建工具栏容器
self.embeddedToolbar = QFrame(self.pandaWidget)
self.embeddedToolbar.setObjectName("UnityToolbar")
self.embeddedToolbar.setStyleSheet("""
QFrame#UnityToolbar {
background-color: rgba(240, 240, 240, 180);
border: 1px solid rgba(200, 200, 200, 200);
border-radius: 4px;
}
QToolButton {
background-color: rgba(250, 250, 250, 150);
border: none;
color: #333333;
padding: 5px;
border-radius: 3px;
}
QToolButton:hover {
background-color: rgba(220, 220, 220, 200);
}
QToolButton:checked {
background-color: rgba(100, 150, 220, 180);
color: white;
}
QToolButton:pressed {
background-color: rgba(80, 130, 200, 200);
}
""")
# 水平布局
self.toolbarLayout = QHBoxLayout(self.embeddedToolbar)
self.toolbarLayout.setContentsMargins(5, 5, 5, 5)
self.toolbarLayout.setSpacing(2)
# 创建工具按钮组
self.toolGroup = QButtonGroup()
# 选择工具
self.selectTool = QToolButton()
icon_path = self.get_icon_path("select_tool.png")
if icon_path and os.path.exists(icon_path):
self.selectTool.setIcon(QIcon(icon_path))
else:
self.selectTool.setText('选择') # 如果没有图标则显示文字
self.selectTool.setIconSize(QSize(16, 16))
self.selectTool.setCheckable(True)
self.selectTool.setToolTip("选择工具 (Q)")
# self.selectTool.setShortcut(QKeySequence("Q"))
self.toolGroup.addButton(self.selectTool)
self.toolbarLayout.addWidget(self.selectTool)
# 移动工具
self.moveTool = QToolButton()
icon_path = self.get_icon_path("move_tool.png")
if icon_path and os.path.exists(icon_path):
self.moveTool.setIcon(QIcon(icon_path))
else:
self.moveTool.setText('移动')
self.moveTool.setIconSize(QSize(16, 16))
self.moveTool.setCheckable(True)
self.moveTool.setToolTip("移动工具 (W)")
# self.moveTool.setShortcut(QKeySequence("W"))
self.toolGroup.addButton(self.moveTool)
self.toolbarLayout.addWidget(self.moveTool)
# 旋转工具
self.rotateTool = QToolButton()
icon_path = self.get_icon_path("rotate_tool.png")
if icon_path and os.path.exists(icon_path):
self.rotateTool.setIcon(QIcon(icon_path))
else:
self.rotateTool.setText('旋转')
self.rotateTool.setIconSize(QSize(16, 16))
self.rotateTool.setCheckable(True)
self.rotateTool.setToolTip("旋转工具 (E)")
# self.rotateTool.setShortcut(QKeySequence("E"))
self.toolGroup.addButton(self.rotateTool)
self.toolbarLayout.addWidget(self.rotateTool)
# 缩放工具
self.scaleTool = QToolButton()
icon_path = self.get_icon_path("scale_tool.png")
if icon_path and os.path.exists(icon_path):
self.scaleTool.setIcon(QIcon(icon_path))
else:
self.scaleTool.setText('缩放')
self.scaleTool.setIconSize(QSize(16, 16))
self.scaleTool.setCheckable(True)
self.scaleTool.setToolTip("缩放工具 (R)")
# self.scaleTool.setShortcut(QKeySequence("R"))
self.toolGroup.addButton(self.scaleTool)
self.toolbarLayout.addWidget(self.scaleTool)
# 添加分隔符
separator = QFrame()
separator.setFrameShape(QFrame.VLine)
separator.setFrameShadow(QFrame.Sunken)
separator.setStyleSheet("background-color: rgba(240, 240, 240, 255);")
separator.setFixedWidth(1)
self.toolbarLayout.addWidget(separator)
# 设置位置到左上角
self.embeddedToolbar.move(10, 10)
self.embeddedToolbar.adjustSize()
self.embeddedToolbar.show()
# 连接事件
self.toolGroup.buttonClicked.connect(self.onToolChanged)
# 设置工具栏拖拽事件
self.embeddedToolbar.mousePressEvent = self.toolbarMousePressEvent
self.embeddedToolbar.mouseMoveEvent = self.toolbarMouseMoveEvent
self.embeddedToolbar.mouseReleaseEvent = self.toolbarMouseReleaseEvent
# 默认选择"选择"工具
self.selectTool.setChecked(True)
self.world.setCurrentTool("选择")
def toolbarMousePressEvent(self, event):
"""工具栏鼠标按下事件"""
if event.button() == Qt.LeftButton:
self.toolbarDragging = True
self.dragStartPos = event.globalPos()
self.toolbarStartPos = self.embeddedToolbar.pos()
event.accept()
def toolbarMouseMoveEvent(self, event):
"""工具栏鼠标移动事件"""
try:
if self.toolbarDragging and event.buttons() == Qt.LeftButton:
# 计算新位置
delta = event.globalPos() - self.dragStartPos
new_pos = self.toolbarStartPos + delta
# 边界检测
panda_rect = self.pandaWidget.geometry()
toolbar_size = self.embeddedToolbar.size()
# 限制在Panda3D区域内
new_pos.setX(max(0, min(new_pos.x(), panda_rect.width() - toolbar_size.width())))
new_pos.setY(max(0, min(new_pos.y(), panda_rect.height() - toolbar_size.height())))
self.embeddedToolbar.move(new_pos)
event.accept()
except Exception as e:
print(f"工具栏鼠标移动事件出错")
import traceback
traceback.print_exc()
def toolbarMouseReleaseEvent(self, event):
"""工具栏鼠标释放事件"""
if event.button() == Qt.LeftButton and self.toolbarDragging:
self.toolbarDragging = False
# 自动吸附到最近的预设位置
self.snapToNearestPosition()
event.accept()
def snapToNearestPosition(self):
"""自动吸附到最近的预设位置"""
current_pos = self.embeddedToolbar.pos()
panda_rect = self.pandaWidget.geometry()
toolbar_size = self.embeddedToolbar.size()
margin = 10
# 定义所有预设位置
positions = {
"top_center": QPoint(
(panda_rect.width() - toolbar_size.width()) // 2,
margin
),
"top_left": QPoint(margin, margin),
"top_right": QPoint(
panda_rect.width() - toolbar_size.width() - margin,
margin
),
"bottom_center": QPoint(
(panda_rect.width() - toolbar_size.width()) // 2,
panda_rect.height() - toolbar_size.height() - margin
),
"bottom_left": QPoint(
margin,
panda_rect.height() - toolbar_size.height() - margin
),
"bottom_right": QPoint(
panda_rect.width() - toolbar_size.width() - margin,
panda_rect.height() - toolbar_size.height() - margin
)
}
# 找到最近的位置
min_distance = float('inf')
nearest_position = "top_center"
for pos_name, pos_point in positions.items():
distance = ((current_pos.x() - pos_point.x()) ** 2 +
(current_pos.y() - pos_point.y()) ** 2) ** 0.5
if distance < min_distance:
min_distance = distance
nearest_position = pos_name
# 更新位置并平滑移动
self.toolbarPosition = nearest_position
target_pos = positions[nearest_position]
# 简单的平滑移动动画
from PyQt5.QtCore import QPropertyAnimation, QEasingCurve
self.toolbarAnimation = QPropertyAnimation(self.embeddedToolbar, b"pos")
self.toolbarAnimation.setDuration(200)
self.toolbarAnimation.setStartValue(current_pos)
self.toolbarAnimation.setEndValue(target_pos)
self.toolbarAnimation.setEasingCurve(QEasingCurve.OutCubic)
self.toolbarAnimation.start()
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('缩放工具')
self.sunsetAction = self.toolsMenu.addAction('光照编辑')
self.pluginAction = self.toolsMenu.addAction('图形编辑')
# 统一创建菜单 - 关键修改
self.createMenu = menubar.addMenu('创建')
self.setupCreateMenuActions() # 统一创建菜单动作
# self.createGUIaddMenu = self.createMenu.addMenu('GUI')
# self.createButtonAction = self.createGUIaddMenu.addAction('创建按钮')
# self.createLabelAction = self.createGUIaddMenu.addAction('创建标签')
# self.createEntryAction = self.createGUIaddMenu.addAction('创建输入框')
# self.createGUIaddMenu.addSeparator()
# self.createVirtualScreenAction = self.createGUIaddMenu.addAction('创建虚拟屏幕')
# self.createCesiumViewAction = self.createGUIaddMenu.addAction('创建Cesium地图')
# self.toggleCesiumViewAction = self.createGUIaddMenu.addAction('开关地图')
# self.refreshCesiumViewAction = self.createGUIaddMenu.addAction('刷新地图')
#
# self.createLightaddMenu = self.createMenu.addMenu('光源')
# self.createSpotLightAction = self.createLightaddMenu.addAction('聚光灯')
# self.createPointLightAction = self.createLightaddMenu.addAction('点光源')
#添加地形菜单
self.createTerrainMenu = self.createMenu.addMenu('地形')
self.createFlatTerrainAction = self.createTerrainMenu.addAction('创建平面地形')
self.createHeightmapTerrainAction = self.createTerrainMenu.addAction('从高度图创建地形')
self.createTerrainMenu.addSeparator()
# self.terrainEditModeAction = self.createTerrainMenu.addAction('地形编辑模式')
# GUI菜单
# GUI菜单 - 复用创建菜单的动作
self.guiMenu = menubar.addMenu('GUI')
self.guiEditModeAction = self.guiMenu.addAction('进入GUI编辑模式')
self.guiMenu.addSeparator()
self.guiMenu.addAction(self.createButtonAction)
self.guiMenu.addAction(self.createLabelAction)
self.guiMenu.addAction(self.createEntryAction)
self.guiMenu.addSeparator()
self.guiMenu.addAction(self.create3DTextAction)
self.guiMenu.addAction(self.createVirtualScreenAction)
# 脚本菜单
self.scriptMenu = menubar.addMenu('脚本')
self.createScriptAction = self.scriptMenu.addAction('创建脚本...')
self.loadScriptAction = self.scriptMenu.addAction('加载脚本文件...')
self.loadAllScriptsAction = self.scriptMenu.addAction('重载所有脚本')
self.scriptMenu.addSeparator()
self.toggleHotReloadAction = self.scriptMenu.addAction('启用热重载')
self.toggleHotReloadAction.setCheckable(True)
self.toggleHotReloadAction.setChecked(True) # 默认启用
self.scriptMenu.addSeparator()
self.openScriptsManagerAction = self.scriptMenu.addAction('脚本管理器')
self.cesiumMenu = menubar.addMenu('Cesium')
self.loadCesiumTilesetAction = self.cesiumMenu.addAction('加载3Dtiles')
self.loadCesiumTilesetAction.triggered.connect(self.onLoadCesiumTileset)
# 添加地球视图相关菜单项
self.createCesiumViewAction = self.cesiumMenu.addAction('创建地球视图')
self.createCesiumViewAction.triggered.connect(self.onCreateCesiumView)
self.toggleCesiumViewAction = self.cesiumMenu.addAction('开关地球视图')
self.toggleCesiumViewAction.triggered.connect(self.onToggleCesiumView)
self.refreshCesiumViewAction = self.cesiumMenu.addAction('刷新地球视图')
self.refreshCesiumViewAction.triggered.connect(self.onRefreshCesiumView)
# 添加模型相关菜单项
self.addModelToCesiumAction = self.cesiumMenu.addAction('添加模型到地球')
self.addModelToCesiumAction.triggered.connect(self.onAddModelClicked)
self.infoPanelMenu = menubar.addMenu('信息面板')
# 创建示例面板动作
self.createSamplePanelAction = self.infoPanelMenu.addAction('创建示例面板')
self.createSamplePanelAction.triggered.connect(self.onCreateSampleInfoPanel)
# 添加更多面板创建选项
# self.createSystemStatusPanelAction = self.infoPanelMenu.addAction('创建系统状态面板')
# self.createSystemStatusPanelAction.triggered.connect(self.onCreateSystemStatusPanel)
#
# self.createSensorDataPanelAction = self.infoPanelMenu.addAction('创建传感器数据面板')
# self.createSensorDataPanelAction.triggered.connect(self.onCreateSensorDataPanel)
#
# self.createSceneInfoPanelAction = self.infoPanelMenu.addAction('创建场景信息面板')
# self.createSceneInfoPanelAction.triggered.connect(self.onCreateSceneInfoPanel)
self.infoPanelMenu.addSeparator()
self.create3DSamplePanelAction = self.infoPanelMenu.addAction('创建3D实例面板')
self.create3DSamplePanelAction.triggered.connect(self.onCreate3DSampleInfoPanel)
#
# self.create3DSystemStatusPanelAction = self.infoPanelMenu.addAction('创建3D系统状态面板')
# self.create3DSystemStatusPanelAction.triggered.connect(self.onCreate3DSystemStatusPanel)
# 添加分隔符和批量创建选项
# self.infoPanelMenu.addSeparator()
# self.createAllPanelsAction = self.infoPanelMenu.addAction('创建所有面板')
# self.createAllPanelsAction.triggered.connect(self.onCreateAllInfoPanels)
#资源菜单
self.assetsMenu = menubar.addMenu('资源')
self.refreshAssetsAction = self.assetsMenu.addAction('刷新资源')
self.refreshAssetsAction.triggered.connect(self.refreshAssetsView)
# VR菜单
self.vrMenu = menubar.addMenu('VR')
self.enterVRAction = self.vrMenu.addAction('进入VR模式')
self.exitVRAction = self.vrMenu.addAction('退出VR模式')
self.vrMenu.addSeparator()
self.vrStatusAction = self.vrMenu.addAction('VR状态')
self.vrSettingsAction = self.vrMenu.addAction('VR设置')
# 初始状态下禁用退出VR选项
self.exitVRAction.setEnabled(False)
# 帮助菜单
self.helpMenu = menubar.addMenu('帮助')
self.aboutAction = self.helpMenu.addAction('关于')
def refreshAssetsView(self):
""""刷新资源视图"""
if hasattr(self,'fileView') and self.fileView:
self.fileView.refreshView()
print("资源视图已刷新")
def setupCreateMenuActions(self):
"""统一设置创建菜单的所有动作 - 避免重复代码"""
# 基础对象
self.createEnptyaddAction = self.createMenu.addAction('空对象')
# 3D对象子菜单
self.create3dObjectaddMenu = self.createMenu.addMenu('3D对象')
# 可以在这里添加更多3D对象类型
# 3D GUI子菜单
self.create3dGUIaddMenu = self.createMenu.addMenu('3D GUI')
self.create3DTextAction = self.create3dGUIaddMenu.addAction('3D文本')
self.create3DImageAction = self.create3dGUIaddMenu.addAction('3D图片')
# GUI子菜单
self.createGUIaddMenu = self.createMenu.addMenu('GUI')
self.createButtonAction = self.createGUIaddMenu.addAction('创建按钮')
self.createLabelAction = self.createGUIaddMenu.addAction('创建标签')
self.createEntryAction = self.createGUIaddMenu.addAction('创建输入框')
self.createImageAction = self.createGUIaddMenu.addAction('创建图片')
self.createGUIaddMenu.addSeparator()
self.createVideoScreen = self.createGUIaddMenu.addAction('创建视频屏幕')
self.create2DVideoScreen = self.createGUIaddMenu.addAction('创建2D视频屏幕')
self.createSphericalVideo = self.createGUIaddMenu.addAction('创建球形视频')
self.createGUIaddMenu.addSeparator()
self.createVirtualScreenAction = self.createGUIaddMenu.addAction('创建虚拟屏幕')
# 光源子菜单
self.createLightaddMenu = self.createMenu.addMenu('光源')
self.createSpotLightAction = self.createLightaddMenu.addAction('聚光灯')
self.createPointLightAction = self.createLightaddMenu.addAction('点光源')
# 统一连接信号到处理方法
self.connectCreateMenuActions()
def connectCreateMenuActions(self):
"""统一连接创建菜单的信号到处理方法"""
# 连接到world对象的创建方法
# self.createEnptyaddAction.triggered.connect(self.world.createEmptyObject)
self.create3DTextAction.triggered.connect(lambda: self.world.createGUI3DText())
self.create3DImageAction.triggered.connect(lambda: self.world.createGUI3DImage())
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.createImageAction.triggered.connect(lambda: self.world.createGUI2DImage())
self.createVideoScreen.triggered.connect(self.world.createVideoScreen)
self.create2DVideoScreen.triggered.connect(self.world.create2DVideoScreen)
self.createSphericalVideo.triggered.connect(self.world.createSphericalVideo)
self.createVirtualScreenAction.triggered.connect(lambda: self.world.createGUIVirtualScreen())
self.createSpotLightAction.triggered.connect(lambda :self.world.createSpotLight())
self.createPointLightAction.triggered.connect(lambda :self.world.createPointLight())
# # self.createVideoScreen.triggered.connect(lambda: self.world.video_manager.create_video_screen(
# # pos=(0, 0, 2),
# # size=(4, 3),
# # name=f"video_screen_{len(self.world.video_manager.video_objects) if hasattr(self.world, 'video_manager') else 0}"
# # ))
# # self.createSphericalVideo.triggered.connect(lambda: self.world.createGUISphericalVideo())
# self.createVirtualScreenAction.triggered.connect(lambda: self.world.create_spherical_video())
def getCreateMenuActions(self):
"""获取所有创建菜单动作的字典 - 供右键菜单使用"""
return {
'createEmpty': self.createEnptyaddAction,
'create3DText': self.create3DTextAction,
'create3DImage': self.create3DImageAction,
'createButton': self.createButtonAction,
'createLabel': self.createLabelAction,
'createEntry': self.createEntryAction,
'createImage': self.createImageAction,
'createVideoScreen': self.createVideoScreen,
'create2DVideoScreen':self.create2DVideoScreen,
'createSphericalVideo': self.createSphericalVideo,
'createVirtualScreen': self.createVirtualScreenAction,
'createSpotLight': self.createSpotLightAction,
'createPointLight': self.createPointLightAction,
}
def setupDockWindows(self):
"""创建停靠窗口"""
# 创建左侧停靠窗口(层级窗口)
self.leftDock = QDockWidget("层级", self)
self.treeWidget = CustomTreeWidget(self.world)
self.world.setTreeWidget(self.treeWidget) # 设置树形控件引用
self.leftDock.setWidget(self.treeWidget)
self.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, self.leftDock)
# 创建右侧停靠窗口(属性窗口)
self.rightDock = QDockWidget("属性", self)
# 创建属性面板的主容器和布局
self.propertyContainer = QWidget()
self.propertyContainer.setObjectName("PropertyContainer")
try:
self.propertyLayout = QVBoxLayout(self.propertyContainer)
print(f"✓ 属性布局创建完成: {self.propertyLayout}")
# 添加初始提示信息
tipLabel = QLabel("选择对象以查看属性")
tipLabel.setStyleSheet("color: gray; padding: 10px;") # 使用灰色字体
self.propertyLayout.addWidget(tipLabel)
print("✓ 提示标签添加完成")
# 创建滚动区域并设置属性
self.scrollArea = QScrollArea()
self.scrollArea.setWidgetResizable(True)
self.scrollArea.setWidget(self.propertyContainer)
print("✓ 滚动区域设置完成")
# 设置滚动区域为停靠窗口的主部件
self.rightDock.setWidget(self.scrollArea)
self.rightDock.setMinimumWidth(300)
self.addDockWidget(Qt.RightDockWidgetArea, self.rightDock)
print("✓ 右侧停靠窗口添加完成")
# 设置属性面板到世界对象
if hasattr(self.world, 'setPropertyLayout'):
print("开始设置属性布局到世界对象...")
self.world.setPropertyLayout(self.propertyLayout)
print("✓ 属性布局设置完成")
else:
print("⚠ 世界对象没有 setPropertyLayout 方法")
except Exception as e:
print(f"✗ 设置属性面板时出错: {e}")
import traceback
traceback.print_exc()
# 创建基本的属性面板作为后备方案
fallback_widget = QLabel("属性面板初始化失败\n请查看控制台日志")
fallback_widget.setStyleSheet("color: red; background-color: white; padding: 10px;")
self.rightDock.setWidget(fallback_widget)
self.addDockWidget(Qt.RightDockWidgetArea, self.rightDock)
# 创建脚本管理停靠窗口
self.scriptDock = QDockWidget("脚本管理", self)
# 创建脚本面板的主容器和布局(与属性面板相同结构)
self.scriptContainer = QWidget()
self.scriptContainer.setObjectName("ScriptContainer")
self.scriptLayout = QVBoxLayout(self.scriptContainer)
# 创建滚动区域并设置属性
self.scriptScrollArea = QScrollArea()
self.scriptScrollArea.setWidgetResizable(True)
self.scriptScrollArea.setWidget(self.scriptContainer)
# 设置滚动区域为停靠窗口的主部件
self.scriptDock.setWidget(self.scriptScrollArea)
self.scriptDock.setMinimumWidth(300)
# 设置脚本面板内容 - 这里添加调用
self.setupScriptPanel(self.scriptLayout)
self.addDockWidget(Qt.RightDockWidgetArea, self.scriptDock)
# 将右侧停靠窗口设为标签形式
self.tabifyDockWidget(self.rightDock, self.scriptDock)
# # 创建底部停靠窗口(资源窗口)
# 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)
# 创建底部停靠窗口(资源窗口)
self.bottomDock = QDockWidget("资源", self)
self.fileView = CustomAssetsTreeWidget(self.world)
self.bottomDock.setWidget(self.fileView)
self.addDockWidget(Qt.BottomDockWidgetArea, self.bottomDock)
# 创建底部停靠控制台
# self.consoleDock = QDockWidget("控制台", self)
# self.consoleView = CustomConsoleDockWidget(self.world)
# self.consoleDock.setWidget(self.consoleView)
# self.addDockWidget(Qt.BottomDockWidgetArea, self.consoleDock)
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.create3DImageTool = QToolButton()
self.create3DImageTool.setText("3D图片")
self.toolbar.addWidget(self.create3DImageTool)
self.create2DImageTool = QToolButton()
self.create2DImageTool.setText("2D图片")
self.toolbar.addWidget(self.create2DImageTool)
self.createSpotLight = QToolButton()
self.createSpotLight.setText("聚光灯")
self.toolbar.addWidget(self.createSpotLight)
self.createPointLight = QToolButton()
self.createPointLight.setText("点光灯")
self.toolbar.addWidget(self.createPointLight)
# Cesium 工具按钮
self.cesiumViewTool = QToolButton()
self.cesiumViewTool.setText("地图视图")
self.cesiumViewTool.clicked.connect(self.onCreateCesiumView)
self.toolbar.addWidget(self.cesiumViewTool)
self.refreshCesiumTool = QToolButton()
self.refreshCesiumTool.setText("刷新地图")
self.refreshCesiumTool.clicked.connect(self.onRefreshCesiumView)
self.toolbar.addWidget(self.refreshCesiumTool)
self.addModelTool = QToolButton()
self.addModelTool.setText("添加模型")
self.addModelTool.clicked.connect(self.onAddModelClicked)
self.toolbar.addWidget(self.addModelTool)
#地形
# 地形工具
self.createFlatTerrainTool = QToolButton()
self.createFlatTerrainTool.setText("平面地形")
self.toolbar.addWidget(self.createFlatTerrainTool)
self.createHeightmapTerrainTool = QToolButton()
self.createHeightmapTerrainTool.setText("高度图地形")
self.toolbar.addWidget(self.createHeightmapTerrainTool)
# 默认选择"选择"工具
self.selectTool.setChecked(True)
self.world.setCurrentTool("选择")
def setupScriptPanel(self, layout):
"""创建脚本管理面板"""
# 脚本状态组
statusGroup = QGroupBox("脚本系统状态")
statusLayout = QVBoxLayout()
self.scriptStatusLabel = QLabel("脚本系统: 已启动")
self.scriptStatusLabel.setStyleSheet("color: green; font-weight: bold;")
statusLayout.addWidget(self.scriptStatusLabel)
self.hotReloadLabel = QLabel("热重载: 已启用")
self.hotReloadLabel.setStyleSheet("color: blue;")
statusLayout.addWidget(self.hotReloadLabel)
statusGroup.setLayout(statusLayout)
layout.addWidget(statusGroup)
# 脚本创建组
createGroup = QGroupBox("创建脚本")
createLayout = QVBoxLayout()
# 脚本名称输入
nameLayout = QHBoxLayout()
nameLayout.addWidget(QLabel("脚本名称:"))
self.scriptNameEdit = QLineEdit()
self.scriptNameEdit.setPlaceholderText("输入脚本名称...")
nameLayout.addWidget(self.scriptNameEdit)
createLayout.addLayout(nameLayout)
# 模板选择
templateLayout = QHBoxLayout()
templateLayout.addWidget(QLabel("模板:"))
self.templateCombo = QComboBox()
self.templateCombo.addItems(["basic", "movement", "animation"])
templateLayout.addWidget(self.templateCombo)
createLayout.addLayout(templateLayout)
# 创建按钮
self.createScriptBtn = QPushButton("创建脚本")
self.createScriptBtn.clicked.connect(self.onCreateScript)
createLayout.addWidget(self.createScriptBtn)
createGroup.setLayout(createLayout)
layout.addWidget(createGroup)
# 可用脚本组
scriptsGroup = QGroupBox("可用脚本")
scriptsLayout = QVBoxLayout()
# 脚本列表
self.scriptsList = QListWidget()
self.scriptsList.itemDoubleClicked.connect(self.onScriptDoubleClick)
scriptsLayout.addWidget(self.scriptsList)
# 脚本操作按钮
scriptButtonsLayout = QHBoxLayout()
self.loadScriptBtn = QPushButton("加载脚本")
self.loadScriptBtn.clicked.connect(self.onLoadScript)
scriptButtonsLayout.addWidget(self.loadScriptBtn)
self.reloadAllBtn = QPushButton("重载全部")
self.reloadAllBtn.clicked.connect(self.onReloadAllScripts)
scriptButtonsLayout.addWidget(self.reloadAllBtn)
scriptsLayout.addLayout(scriptButtonsLayout)
scriptsGroup.setLayout(scriptsLayout)
layout.addWidget(scriptsGroup)
# 脚本挂载组
mountGroup = QGroupBox("脚本挂载")
mountLayout = QVBoxLayout()
# 当前选中对象显示
self.selectedObjectLabel = QLabel("未选择对象")
self.selectedObjectLabel.setStyleSheet("color: gray; font-style: italic;")
mountLayout.addWidget(self.selectedObjectLabel)
# 脚本选择和挂载
mountControlLayout = QHBoxLayout()
self.mountScriptCombo = QComboBox()
self.mountScriptCombo.setEnabled(False)
mountControlLayout.addWidget(self.mountScriptCombo)
self.mountBtn = QPushButton("挂载")
self.mountBtn.setEnabled(False)
self.mountBtn.clicked.connect(self.onMountScript)
mountControlLayout.addWidget(self.mountBtn)
mountLayout.addLayout(mountControlLayout)
# 已挂载脚本列表
self.mountedScriptsList = QListWidget()
self.mountedScriptsList.setMaximumHeight(100)
mountLayout.addWidget(QLabel("已挂载脚本:"))
mountLayout.addWidget(self.mountedScriptsList)
# 卸载按钮
self.unmountBtn = QPushButton("卸载选中脚本")
self.unmountBtn.clicked.connect(self.onUnmountScript)
mountLayout.addWidget(self.unmountBtn)
mountGroup.setLayout(mountLayout)
layout.addWidget(mountGroup)
# 添加拉伸以填充剩余空间
layout.addStretch()
# 初始化脚本列表
self.refreshScriptsList()
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)
# 连接工具事件
self.sunsetAction.triggered.connect(lambda: self.world.setCurrentTool("光照编辑"))
self.pluginAction.triggered.connect(lambda: self.world.setCurrentTool("图形编辑"))
# 连接GUI编辑模式事件
self.guiEditModeAction.triggered.connect(lambda: self.world.toggleGUIEditMode())
# 连接创建事件 - 使用菜单动作而不是不存在的工具栏按钮
# self.createSpotLightAction.triggered.connect(lambda: self.world.createSpotLight())
# self.createPointLightAction.triggered.connect(lambda: self.world.createPointLight())
# 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())
self.createCesiumViewAction.triggered.connect(self.onCreateCesiumView)
self.toggleCesiumViewAction.triggered.connect(self.onToggleCesiumView)
self.refreshCesiumViewAction.triggered.connect(self.onRefreshCesiumView)
# 连接地形创建事件
self.createFlatTerrainAction.triggered.connect(self.onCreateFlatTerrain)
self.createHeightmapTerrainAction.triggered.connect(self.onCreateHeightmapTerrain)
# self.terrainEditModeAction.triggered.connect(self.onTerrainEditMode)
# 连接树节点点击信号
self.treeWidget.itemSelectionChanged.connect(
lambda: self.world.onTreeItemClicked(self.treeWidget.currentItem(), 0))
print("已连接点击信号")
# 连接工具切换信号
self.toolGroup.buttonClicked.connect(self.onToolChanged)
# 连接脚本菜单事件
# self.createScriptAction.triggered.connect(self.onCreateScriptDialog)
# self.loadScriptAction.triggered.connect(self.onLoadScriptFile)
# self.loadAllScriptsAction.triggered.connect(self.onReloadAllScripts)
# self.toggleHotReloadAction.triggered.connect(self.onToggleHotReload)
# self.openScriptsManagerAction.triggered.connect(self.onOpenScriptsManager)
# 连接VR菜单事件
self.enterVRAction.triggered.connect(self.onEnterVR)
self.exitVRAction.triggered.connect(self.onExitVR)
self.vrStatusAction.triggered.connect(self.onShowVRStatus)
self.vrSettingsAction.triggered.connect(self.onShowVRSettings)
def onCreateCesiumView(self):
if hasattr(self.world,'gui_manager') and self.world.gui_manager:
self.world.gui_manager.createCesiumView()
else:
QMessageBox.warning(self,"错误","GUI管理其不可用")
def onToggleCesiumView(self):
"""切换 Cesium 视图显示状态"""
if hasattr(self.world, 'gui_manager') and self.world.gui_manager:
self.world.gui_manager.toggleCesiumView()
else:
QMessageBox.warning(self, "错误", "GUI 管理器不可用")
def onRefreshCesiumView(self):
"""刷新 Cesium 视图"""
if hasattr(self.world, 'gui_manager') and self.world.gui_manager:
self.world.gui_manager.refreshCesiumView()
else:
QMessageBox.warning(self, "错误", "GUI 管理器不可用")
def onUpdateCesiumURL(self):
"""更新 Cesium URL"""
url, ok = QInputDialog.getText(self, "更新 Cesium URL", "输入新的 URL:",
QLineEdit.Normal, "http://localhost:8080/Apps/HelloWorld.html")
if ok and url:
if hasattr(self.world, 'gui_manager') and self.world.gui_manager:
self.world.gui_manager.updateCesiumURL(url)
else:
QMessageBox.warning(self, "错误", "GUI 管理器不可用")
def onAddModelClicked(self):
"""处理加入模型按钮点击事件"""
# 检查 Cesium 视图是否存在
cesium_view_exists = False
if hasattr(self.world, 'gui_manager') and self.world.gui_manager:
for element in self.world.gui_manager.gui_elements:
if hasattr(element, 'objectName') and element.objectName() == "CesiumView":
cesium_view_exists = True
break
if not cesium_view_exists:
reply = QMessageBox.question(
self,
'提示',
'Cesium 地图视图尚未打开,是否先打开地图视图?',
QMessageBox.Yes | QMessageBox.No,
QMessageBox.Yes
)
if reply == QMessageBox.Yes:
self.onCreateCesiumView()
# 给一点时间让 Cesium 视图加载
QTimer.singleShot(1000, self.showAddModelDialog)
return
else:
return
self.showAddModelDialog()
def showAddModelDialog(self):
"""显示添加模型对话框"""
# 打开文件选择对话框
file_path, _ = QFileDialog.getOpenFileName(
self,
"选择 3D 模型文件",
"",
"3D 模型文件 (*.glb *.gltf *.obj);;所有文件 (*)"
)
if file_path:
# 获取模型位置信息
coords, ok = self.getModelCoordinates()
if ok:
longitude, latitude, height, scale = coords
# 生成唯一的模型 ID
import uuid
model_id = f"model_{uuid.uuid4().hex[:8]}"
try:
# 添加模型到 Cesium
if hasattr(self.world, 'gui_manager') and self.world.gui_manager:
success = self.world.gui_manager.addModelToCesium(
model_id,
file_path,
longitude,
latitude,
height,
scale
)
if success:
QMessageBox.information(
self,
"成功",
f"模型已成功添加到地图!\n模型ID: {model_id}"
)
else:
QMessageBox.warning(
self,
"失败",
"添加模型失败,请检查控制台输出"
)
except Exception as e:
QMessageBox.critical(
self,
"错误",
f"添加模型时发生错误:\n{str(e)}"
)
def getModelCoordinates(self):
"""获取模型坐标信息的对话框"""
# 创建对话框
dialog = QDialog(self)
dialog.setWindowTitle("设置模型位置")
dialog.setModal(True)
dialog.resize(300, 200)
layout = QVBoxLayout(dialog)
# 经度
lon_layout = QHBoxLayout()
lon_layout.addWidget(QLabel("经度:"))
lon_spin = QDoubleSpinBox()
lon_spin.setRange(-180, 180)
lon_spin.setValue(116.3975) # 默认北京位置
lon_layout.addWidget(lon_spin)
layout.addLayout(lon_layout)
# 纬度
lat_layout = QHBoxLayout()
lat_layout.addWidget(QLabel("纬度:"))
lat_spin = QDoubleSpinBox()
lat_spin.setRange(-90, 90)
lat_spin.setValue(39.9085) # 默认北京位置
lat_layout.addWidget(lat_spin)
layout.addLayout(lat_layout)
# 高度
height_layout = QHBoxLayout()
height_layout.addWidget(QLabel("高度(米):"))
height_spin = QDoubleSpinBox()
height_spin.setRange(-10000, 100000)
height_spin.setValue(0)
height_layout.addWidget(height_spin)
layout.addLayout(height_layout)
# 缩放
scale_layout = QHBoxLayout()
scale_layout.addWidget(QLabel("缩放:"))
scale_spin = QDoubleSpinBox()
scale_spin.setRange(0.001, 100000)
scale_spin.setValue(1.0)
scale_spin.setSingleStep(0.1)
scale_layout.addWidget(scale_spin)
layout.addLayout(scale_layout)
# 按钮
button_layout = QHBoxLayout()
ok_button = QPushButton("确定")
cancel_button = QPushButton("取消")
button_layout.addWidget(ok_button)
button_layout.addWidget(cancel_button)
layout.addLayout(button_layout)
# 连接信号
ok_button.clicked.connect(dialog.accept)
cancel_button.clicked.connect(dialog.reject)
# 显示对话框
if dialog.exec_() == QDialog.Accepted:
return (
lon_spin.value(),
lat_spin.value(),
height_spin.value(),
scale_spin.value()
), True
else:
return None, False
def onLoadCesiumTileset(self):
url,ok = QInputDialog.getText(
self,
"加载 Cesium 3D Tiles",
"输入 tileset.json URL:",
QLineEdit.Normal,
"https://assets.ion.cesium.com/96128/tileset.json"
)
if ok and url:
try:
# 生成唯一的 tileset 名称
import uuid
tileset_name = f"tileset_{uuid.uuid4().hex[:8]}"
# 加载 tileset
if hasattr(self.world, 'addCesiumTileset'):
success = self.world.addCesiumTileset(tileset_name, url, (0, 0, 0))
if success:
QMessageBox.information(
self,
"成功",
f"Cesium 3D Tiles 已加载到场景中!\n名称: {tileset_name}"
)
else:
QMessageBox.warning(
self,
"失败",
"加载 Cesium 3D Tiles 失败"
)
except Exception as e:
QMessageBox.critical(
self,
"错误",
f"加载 Cesium 3D Tiles 时发生错误:\n{str(e)}"
)
def onToolChanged(self, button):
"""工具切换事件处理"""
if button.isChecked():
tool_name = button.text()
self.world.setCurrentTool(tool_name)
print(f"工具栏: 选择了 {tool_name} 工具")
else:
self.world.setCurrentTool(None)
print("工具栏: 取消选择工具")
# 在 MainWindow 类中添加以下方法
def onCreateSampleInfoPanel(self):
"""创建示例天气信息面板(模拟数据)"""
try:
# 获取中文字体
from panda3d.core import TextNode
font = self.world.getChineseFont() if self.world.getChineseFont() else None
# 创建面板
info_manager = self.world.info_panel_manager
info_manager.setParent(aspect2d)
# 使用唯一的面板ID
import time
unique_id = f"weather_info_{int(time.time())}"
# 创建示例面板
weather_panel = info_manager.createInfoPanel(
panel_id=unique_id, # 使用唯一ID
position=(0, 0),
size=(1, 1),
bg_color=(0.15, 0.25, 0.35, 0), # 蓝色背景
border_color=(0.3, 0.5, 0.7, 0), # 蓝色边框
title_color=(0.7, 0.9, 1.0, 1.0), # 浅蓝色标题
content_color=(0.95, 0.95, 0.95, 1.0),
font=font
)
# 更新面板标题
info_manager.updatePanelContent(unique_id, title="北京天气")
# 添加到场景树
self.addInfoPanelToTree(weather_panel, "天气信息面板")
# 立即显示加载中信息
info_manager.updatePanelContent(unique_id, content="正在获取天气数据...")
info_manager.registerDataSource(unique_id, self.getRealWeatherData, update_interval=5.0)
# # 立即显示示例数据
# sample_data = self.getSampleWeatherData()
# info_manager.updatePanelContent(unique_id, content=sample_data)
#
# # 注册数据源,定期更新示例数据
# info_manager.registerDataSource(unique_id, self.getSampleWeatherData, update_interval=2.0)
print("✓ 示例天气信息面板已创建")
except Exception as e:
print(f"✗ 创建示例天气信息面板失败: {e}")
import traceback
traceback.print_exc()
QMessageBox.critical(self, "错误", f"创建示例天气信息面板时出错: {str(e)}")
def getRealWeatherData(self):
"""获取真实天气数据"""
try:
import requests
import json
from datetime import datetime
# 请求天气数据
url = "https://wttr.in/Beijing?format=j1"
response = requests.get(url, timeout=10)
response.raise_for_status()
# 解析JSON数据
weather_data = response.json()
# 提取当前天气信息
current_condition = weather_data['current_condition'][0]
weather_desc = current_condition['weatherDesc'][0]['value']
temp_c = current_condition['temp_C']
feels_like = current_condition['FeelsLikeC']
humidity = current_condition['humidity']
pressure = current_condition['pressure']
visibility = current_condition['visibility']
wind_speed = current_condition['windspeedKmph']
wind_dir = current_condition['winddir16Point']
# 提取空气质量(如果可用)
air_quality = "N/A"
if 'air_quality' in weather_data and weather_data['air_quality']:
if 'us-epa-index' in current_condition:
air_quality_index = current_condition['air_quality_index']
air_quality = f"指数: {air_quality_index}"
# 获取更新时间
update_time = datetime.now().strftime("%Y-%m-%d %H:%M")
# 格式化显示内容
content = f"天气状况: {weather_desc}\n温度: {temp_c}°C (体感 {feels_like}°C)\n湿度: {humidity}%\n气压: {pressure} hPa\n能见度: {visibility} km\n风速: {wind_speed} km/h ({wind_dir})\n空气质量: {air_quality}\n更新时间: {update_time}"
return content
except requests.exceptions.Timeout:
return "错误: 获取天气数据超时"
except requests.exceptions.ConnectionError:
return "错误: 网络连接失败"
except requests.exceptions.HTTPError as e:
return f"HTTP错误: {e}"
except json.JSONDecodeError:
return "错误: 无法解析天气数据"
except KeyError as e:
return f"错误: 天气数据格式不正确 (缺少字段: {e})"
except Exception as e:
return f"获取天气数据失败: {str(e)}"
def getSampleWeatherData(self):
"""获取示例天气数据"""
try:
from datetime import datetime
import random
# 模拟天气数据
cities = ["北京", "上海", "广州", "深圳", "杭州", "成都", "武汉", "西安"]
conditions = ["晴天", "多云", "阴天", "小雨", "雷阵雨", "", ""]
city = random.choice(cities)
condition = random.choice(conditions)
temp = random.randint(-5, 35)
humidity = random.randint(30, 90)
wind_speed = round(random.uniform(0, 20), 1)
pressure = random.randint(980, 1030)
current_time = datetime.now().strftime("%Y-%m-%d %H:%M")
return f"城市: {city}\n天气状况: {condition}\n温度: {temp}°C\n湿度: {humidity}%\n风速: {wind_speed} m/s\n气压: {pressure} hPa\n更新时间: {current_time}"
except Exception as e:
return f"获取示例数据失败: {str(e)}"
# 在 main_window.py 中修改 onCreate3DSampleInfoPanel 方法
def onCreate3DSampleInfoPanel(self):
"""创建3D示例天气信息面板修复透明度问题"""
try:
# 获取中文字体
from panda3d.core import TextNode
font = self.world.getChineseFont() if self.world.getChineseFont() else None
# 创建面板
info_manager = self.world.info_panel_manager
info_manager.setParent(self.world.render)
# 使用唯一的面板ID
import time
unique_id = f"weather_info_3d_{int(time.time())}"
# 创建3D示例面板 - 修复透明度问题
weather_panel = info_manager.create3DInfoPanel(
panel_id=unique_id,
position=(2, 0, 2), # 调整Z坐标避免与其他对象重叠
size=(1, 1),
bg_color=(0.15, 0.25, 0.35, 0.85), # 设置合适的透明度值
border_color=(0.3, 0.5, 0.7, 1.0),
title_color=(0.7, 0.9, 1.0, 1.0),
content_color=(0.95, 0.95, 0.95, 1.0),
font=font
)
# 重要:手动设置面板的透明度渲染模式
# if weather_panel:
# # 确保面板支持透明度
# weather_panel.setTransparency(True)
# # 设置合适的渲染顺序,确保透明对象正确渲染
# weather_panel.setBin("transparent", 0)
# # 启用深度写入,但保持透明度
# weather_panel.setDepthWrite(False)
# 更新面板标题
info_manager.update3DPanelContent(unique_id, title="3D北京天气")
# 添加到场景树
self.addInfoPanelToTree(weather_panel, "3D天气信息面板")
# 显示加载中信息
info_manager.update3DPanelContent(unique_id, content="正在获取天气数据...")
info_manager.registerDataSource(unique_id, self.getRealWeatherData, update_interval=5.0)
print("✓ 3D示例天气信息面板已创建")
except Exception as e:
print(f"✗ 创建3D示例天气信息面板失败: {e}")
import traceback
traceback.print_exc()
QMessageBox.critical(self, "错误", f"创建3D示例天气信息面板时出错: {str(e)}")
def onCreate3DSystemStatusPanel(self):
"""创建3D系统状态信息面板"""
try:
# 获取中文字体
from panda3d.core import TextNode
font = self.world.getChineseFont() if self.world.getChineseFont() else None
# 创建面板
info_manager = self.world.info_panel_manager
info_manager.setParent(self.world.render)
# 使用唯一的面板ID
import time
unique_id = f"system_status_3d_{int(time.time())}"
panel = info_manager.create3DInfoPanel(
panel_id=unique_id,
position=(2, 0, 0),
size=(0.8, 1.2),
bg_color=(0.25, 0.15, 0.15, 0.95), # 红色背景
border_color=(0.7, 0.3, 0.3, 1.0), # 红色边框
title_color=(1.0, 0.5, 0.5, 1.0), # 浅红色标题
content_color=(0.95, 0.95, 0.95, 1.0),
font=font
)
# 添加到场景树
self.addInfoPanelToTree(panel, "3D系统状态信息面板")
# 立即显示初始数据
initial_data = self.getSystemStatusData()
info_manager.update3DPanelContent(unique_id, content=initial_data)
# 注册数据源每5秒更新一次
info_manager.registerDataSource(unique_id, self.getSystemStatusData, update_interval=5.0)
except Exception as e:
print(f"✗ 创建3D系统状态信息面板失败: {e}")
import traceback
traceback.print_exc()
QMessageBox.critical(self, "错误", f"创建3D系统状态信息面板时出错: {str(e)}")
# 更新 addInfoPanelToTree 方法以支持3D面板
def addInfoPanelToTree(self, panel, panel_name):
"""
将信息面板添加到场景树控件中
"""
if panel and self.treeWidget:
# 找到场景根节点
scene_root = None
for i in range(self.treeWidget.topLevelItemCount()):
item = self.treeWidget.topLevelItem(i)
if item.text(0) == "render":
scene_root = item
break
# 如果找不到场景根节点,使用第一个顶级节点
if not scene_root and self.treeWidget.topLevelItemCount() > 0:
scene_root = self.treeWidget.topLevelItem(0)
if scene_root:
# 根据面板类型确定节点类型
node_type = "INFO_PANEL_3D" if "3d" in panel_name.lower() else "INFO_PANEL"
tree_item = self.treeWidget.add_node_to_tree_widget(
node=panel,
parent_item=scene_root,
node_type=node_type
)
if tree_item:
self.treeWidget.setCurrentItem(tree_item)
self.treeWidget.update_selection_and_properties(panel, tree_item)
print(f"{panel_name}节点已添加到场景树")
return True
else:
print(f"⚠️ {panel_name}节点添加到场景树失败")
else:
print("❌ 未找到场景根节点")
return False
def onCreateSystemStatusPanel(self):
"""创建系统状态信息面板"""
try:
# 获取中文字体
from panda3d.core import TextNode
font = self.world.getChineseFont() if self.world.getChineseFont() else None
# 创建面板
info_manager = self.world.info_panel_manager
info_manager.setParent(aspect2d)
panel = info_manager.createInfoPanel(
panel_id="system_status",
position=(1.4, 0.2),
size=(0.8, 1.2),
bg_color=(0.25, 0.15, 0.15, 0.95), # 红色背景
border_color=(0.7, 0.3, 0.3, 1.0), # 红色边框
title_color=(1.0, 0.5, 0.5, 1.0), # 浅红色标题
content_color=(0.95, 0.95, 0.95, 1.0),
font=font
)
# 添加到场景树
self.addInfoPanelToTree(panel, "系统状态信息面板")
# 立即显示初始数据
initial_data = self.getSystemStatusData()
info_manager.updatePanelContent("system_status", content=initial_data)
# 注册数据源每5秒更新一次
info_manager.registerDataSource("system_status", self.getSystemStatusData, update_interval=5.0)
except Exception as e:
print(f"✗ 创建系统状态信息面板失败: {e}")
import traceback
traceback.print_exc()
QMessageBox.critical(self, "错误", f"创建系统状态信息面板时出错: {str(e)}")
def getSystemStatusData(self):
"""
获取系统状态数据的回调函数
"""
try:
import psutil
import time
from datetime import datetime
# 获取系统信息
cpu_percent = psutil.cpu_percent(interval=0.1)
memory = psutil.virtual_memory()
memory_mb = round(memory.used / (1024 * 1024), 1)
memory_total_mb = round(memory.total / (1024 * 1024), 1)
memory_percent = memory.percent
# 网络状态
net_io = psutil.net_io_counters()
bytes_sent = round(net_io.bytes_sent / (1024 * 1024), 2) # MB
bytes_recv = round(net_io.bytes_recv / (1024 * 1024), 2) # MB
# 磁盘使用情况
disk = psutil.disk_usage('/')
disk_used_gb = round(disk.used / (1024 ** 3), 2)
disk_total_gb = round(disk.total / (1024 ** 3), 2)
disk_percent = round((disk.used / disk.total) * 100, 1)
# 时间戳
timestamp = datetime.now().strftime("%H:%M:%S")
return f"CPU使用率: {cpu_percent}%\n内存使用: {memory_mb}MB / {memory_total_mb}MB ({memory_percent}%)\n磁盘使用: {disk_used_gb}GB / {disk_total_gb}GB ({disk_percent}%)\n网络发送: {bytes_sent}MB\n网络接收: {bytes_recv}MB\n更新时间: {timestamp}"
except Exception as e:
return f"获取系统状态失败: {str(e)}"
def onCreateSensorDataPanel(self):
"""创建传感器数据信息面板"""
try:
# 获取中文字体
from panda3d.core import TextNode
font = self.world.getChineseFont() if self.world.getChineseFont() else None
# 创建面板
info_manager = self.world.info_panel_manager
info_manager.setParent(aspect2d)
panel = info_manager.createInfoPanel(
panel_id="sensor_data",
position=(0.8, -0.2),
size=(0.8, 0.6),
bg_color=(0.15, 0.25, 0.15, 0.95), # 绿色背景
border_color=(0.3, 0.7, 0.3, 1.0), # 绿色边框
title_color=(0.5, 1.0, 0.5, 1.0), # 浅绿色标题
content_color=(0.95, 0.95, 0.95, 1.0),
font=font
)
# 添加到场景树
self.addInfoPanelToTree(panel, "传感器数据信息面板")
# 立即显示初始数据
initial_data = self.getSensorData()
info_manager.updatePanelContent("sensor_data", content=initial_data)
# 注册数据源每2秒更新一次
info_manager.registerDataSource("sensor_data", self.getSensorData, update_interval=2.0)
# 绑定键盘事件
info_manager.accept("F3", info_manager.togglePanel, ["sensor_data"])
print("✓ 传感器数据信息面板已创建(按 F3 切换显示)")
except Exception as e:
print(f"✗ 创建传感器数据信息面板失败: {e}")
import traceback
traceback.print_exc()
QMessageBox.critical(self, "错误", f"创建传感器数据信息面板时出错: {str(e)}")
def getSensorData(self):
"""
获取传感器数据的回调函数(模拟数据)
"""
try:
import random
from datetime import datetime
# 模拟传感器数据
temperature = round(random.uniform(20, 35), 1)
humidity = round(random.uniform(30, 70), 1)
pressure = round(random.uniform(990, 1030), 1)
light_level = round(random.uniform(0, 1000), 1)
# 时间戳
timestamp = datetime.now().strftime("%H:%M:%S")
return f"温度: {temperature}°C\n湿度: {humidity}%\n气压: {pressure} hPa\n光照: {light_level} lux\n更新时间: {timestamp}"
except Exception as e:
return f"获取传感器数据失败: {str(e)}"
def onCreateSceneInfoPanel(self):
"""创建场景信息面板"""
try:
# 获取中文字体
from panda3d.core import TextNode
font = self.world.getChineseFont() if self.world.getChineseFont() else None
# 创建面板
info_manager = self.world.info_panel_manager
info_manager.setParent(aspect2d)
panel = info_manager.createInfoPanel(
panel_id="scene_info",
position=(-0.8, 0.5),
size=(0.8, 0.6),
bg_color=(0.12, 0.12, 0.12, 0.95), # 深灰色背景
border_color=(0.4, 0.4, 0.4, 1.0), # 灰色边框
title_color=(0.2, 0.8, 1.0, 1.0), # 蓝色标题
content_color=(0.9, 0.9, 0.9, 1.0),
font=font
)
# 添加到场景树
self.addInfoPanelToTree(panel, "场景信息面板")
# 立即显示初始数据
initial_data = self.getSceneInfoData()
info_manager.updatePanelContent("scene_info", content=initial_data)
# 注册数据源每3秒更新一次
info_manager.registerDataSource("scene_info", self.getSceneInfoData, update_interval=3.0)
# 绑定键盘事件
info_manager.accept("F4", info_manager.togglePanel, ["scene_info"])
print("✓ 场景信息面板已创建(按 F4 切换显示)")
except Exception as e:
print(f"✗ 创建场景信息面板失败: {e}")
import traceback
traceback.print_exc()
QMessageBox.critical(self, "错误", f"创建场景信息面板时出错: {str(e)}")
def getSceneInfoData(self):
"""
获取场景信息数据的回调函数
"""
try:
# 获取场景信息
node_count = 0
texture_count = 0
light_count = 0
# 如果有场景管理器,获取实际数据
if hasattr(self.world, 'scene_graph'):
# 这里可以根据实际的场景结构来统计节点数
node_count = len([node for node in self.world.scene_graph.nodes]) if hasattr(self.world.scene_graph,
'nodes') else 0
# 统计光源数量
if hasattr(self.world, 'lights'):
light_count = len(self.world.lights)
# 统计纹理数量
if hasattr(self.world, 'textures'):
texture_count = len(self.world.textures)
# 当前时间
from datetime import datetime
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
return f"场景节点数: {node_count}\n纹理数量: {texture_count}\n光源数量: {light_count}\nFPS: {self.world.clock.getAverageFrameRate():.1f}\n更新时间: {current_time}"
except Exception as e:
return f"获取场景信息失败: {str(e)}"
def onCreateAllInfoPanels(self):
"""创建所有信息面板"""
try:
self.onCreateSampleInfoPanel()
self.onCreateSystemStatusPanel()
self.onCreateSensorDataPanel()
self.onCreateSceneInfoPanel()
QMessageBox.information(self, "成功",
"所有信息面板已创建完成!\n快捷键:\nF1 - 示例面板\nF2 - 系统状态面板\nF3 - 传感器数据面板\nF4 - 场景信息面板")
except Exception as e:
QMessageBox.critical(self, "错误", f"创建信息面板时出错: {str(e)}")
# ==================== 脚本管理事件处理 ====================
def refreshScriptsList(self):
"""刷新脚本列表"""
self.scriptsList.clear()
self.mountScriptCombo.clear()
available_scripts = self.world.getAvailableScripts()
for script_name in available_scripts:
self.scriptsList.addItem(script_name)
self.mountScriptCombo.addItem(script_name)
def updateScriptPanel(self):
"""更新脚本面板状态"""
# 更新热重载状态
hot_reload_enabled = self.world.script_manager.hot_reload_enabled
self.hotReloadLabel.setText(f"热重载: {'已启用' if hot_reload_enabled else '已禁用'}")
self.hotReloadLabel.setStyleSheet(f"color: {'blue' if hot_reload_enabled else 'gray'};")
# 更新热重载菜单状态
self.toggleHotReloadAction.setChecked(hot_reload_enabled)
# 更新选中对象信息
selected_object = getattr(self.world.selection, 'selectedObject', None)
if selected_object:
self.selectedObjectLabel.setText(f"选中对象: {selected_object.getName()}")
self.selectedObjectLabel.setStyleSheet("color: green; font-weight: bold;")
self.mountScriptCombo.setEnabled(True)
self.mountBtn.setEnabled(True)
# 更新已挂载脚本列表
self.updateMountedScriptsList(selected_object)
else:
self.selectedObjectLabel.setText("未选择对象")
self.selectedObjectLabel.setStyleSheet("color: gray; font-style: italic;")
self.mountScriptCombo.setEnabled(False)
self.mountBtn.setEnabled(False)
self.mountedScriptsList.clear()
def updateMountedScriptsList(self, game_object):
"""更新已挂载脚本列表"""
# 保存当前选中项的脚本名(去除状态前缀)
current_item = self.mountedScriptsList.currentItem()
selected_script_name = None
if current_item:
# 提取脚本名(移除 "✓ " 或 "✗ " 前缀)
selected_script_name = current_item.text()[2:]
# 清空并重新填充列表
self.mountedScriptsList.clear()
scripts = self.world.getScripts(game_object)
for script_component in scripts:
script_name = script_component.script_name
enabled = "" if script_component.enabled else ""
item_text = f"{enabled} {script_name}"
self.mountedScriptsList.addItem(item_text)
# 恢复选中状态(根据脚本名匹配)
if selected_script_name:
for i in range(self.mountedScriptsList.count()):
item = self.mountedScriptsList.item(i)
# 提取当前项的脚本名进行比较
current_script_name = item.text()[2:]
if current_script_name == selected_script_name:
self.mountedScriptsList.setCurrentItem(item)
break
def onCreateScript(self):
"""创建脚本按钮事件"""
script_name = self.scriptNameEdit.text().strip()
if not script_name:
QMessageBox.warning(self, "错误", "请输入脚本名称!")
return
template = self.templateCombo.currentText()
try:
success = self.world.createScript(script_name, template)
if success:
QMessageBox.information(self, "成功", f"脚本 '{script_name}' 创建成功!")
self.scriptNameEdit.clear()
self.refreshScriptsList()
else:
QMessageBox.warning(self, "错误", f"脚本 '{script_name}' 创建失败!")
except Exception as e:
QMessageBox.critical(self, "错误", f"创建脚本时出错: {str(e)}")
def onCreateScriptDialog(self):
"""菜单创建脚本事件"""
script_name, ok = QInputDialog.getText(self, "创建脚本", "输入脚本名称:")
if ok and script_name.strip():
try:
success = self.world.createScript(script_name.strip(), "basic")
if success:
QMessageBox.information(self, "成功", f"脚本 '{script_name}' 创建成功!")
self.refreshScriptsList()
else:
QMessageBox.warning(self, "错误", f"脚本 '{script_name}' 创建失败!")
except Exception as e:
QMessageBox.critical(self, "错误", f"创建脚本时出错: {str(e)}")
def onLoadScript(self):
"""加载脚本按钮事件"""
current_item = self.scriptsList.currentItem()
if not current_item:
QMessageBox.warning(self, "错误", "请选择要加载的脚本!")
return
script_name = current_item.text()
try:
success = self.world.reloadScript(script_name)
if success:
QMessageBox.information(self, "成功", f"脚本 '{script_name}' 重载成功!")
else:
QMessageBox.warning(self, "错误", f"脚本 '{script_name}' 重载失败!")
except Exception as e:
QMessageBox.critical(self, "错误", f"重载脚本时出错: {str(e)}")
def onLoadScriptFile(self):
"""加载脚本文件菜单事件"""
file_path, _ = QFileDialog.getOpenFileName(
self, "选择脚本文件", "", "Python文件 (*.py)"
)
if file_path:
try:
success = self.world.loadScript(file_path)
if success:
QMessageBox.information(self, "成功", "脚本文件加载成功!")
self.refreshScriptsList()
else:
QMessageBox.warning(self, "错误", "脚本文件加载失败!")
except Exception as e:
QMessageBox.critical(self, "错误", f"加载脚本文件时出错: {str(e)}")
def onReloadAllScripts(self):
"""重载所有脚本事件"""
try:
scripts_loaded = self.world.loadAllScripts()
QMessageBox.information(self, "成功", f"重载完成,共加载 {len(scripts_loaded)} 个脚本!")
self.refreshScriptsList()
except Exception as e:
QMessageBox.critical(self, "错误", f"重载脚本时出错: {str(e)}")
def onToggleHotReload(self):
"""切换热重载状态"""
enabled = self.toggleHotReloadAction.isChecked()
self.world.enableHotReload(enabled)
status = "启用" if enabled else "禁用"
QMessageBox.information(self, "热重载", f"热重载已{status}")
def onOpenScriptsManager(self):
"""打开脚本管理器"""
# 显示脚本管理停靠窗口
self.scriptDock.show()
self.scriptDock.raise_()
def onScriptDoubleClick(self, item):
"""脚本列表双击事件"""
# 可以在这里添加打开外部编辑器的功能
script_name = item.text()
QMessageBox.information(self, "提示", f"双击了脚本: {script_name}\n\n可以使用外部编辑器编辑脚本文件。")
def onMountScript(self):
"""挂载脚本事件"""
selected_object = getattr(self.world.selection, 'selectedObject', None)
if not selected_object:
QMessageBox.warning(self, "错误", "请先选择一个对象!")
return
script_name = self.mountScriptCombo.currentText()
if not script_name:
QMessageBox.warning(self, "错误", "请选择要挂载的脚本!")
return
try:
success = self.world.addScript(selected_object, script_name)
if success:
QMessageBox.information(self, "成功", f"脚本 '{script_name}' 已挂载到对象!")
self.updateMountedScriptsList(selected_object)
# 同时更新属性面板
if self.treeWidget and self.treeWidget.currentItem():
self.world.updatePropertyPanel(self.treeWidget.currentItem())
else:
QMessageBox.warning(self, "错误", f"挂载脚本失败!")
except Exception as e:
QMessageBox.critical(self, "错误", f"挂载脚本时出错: {str(e)}")
def onUnmountScript(self):
"""卸载脚本事件"""
selected_object = getattr(self.world.selection, 'selectedObject', None)
if not selected_object:
QMessageBox.warning(self, "错误", "请先选择一个对象!")
return
current_item = self.mountedScriptsList.currentItem()
if not current_item:
QMessageBox.warning(self, "错误", "请选择要卸载的脚本!")
return
# 解析脚本名称(移除状态标记)
item_text = current_item.text()
script_name = item_text[2:] # 移除 "✓ " 或 "✗ " 前缀
try:
success = self.world.removeScript(selected_object, script_name)
if success:
QMessageBox.information(self, "成功", f"脚本 '{script_name}' 已从对象卸载!")
self.updateMountedScriptsList(selected_object)
# 同时更新属性面板
if self.treeWidget and self.treeWidget.currentItem():
self.world.updatePropertyPanel(self.treeWidget.currentItem())
else:
QMessageBox.warning(self, "错误", f"卸载脚本失败!")
except Exception as e:
QMessageBox.critical(self, "错误", f"卸载脚本时出错: {str(e)}")
def closeEvent(self, event):
"""处理窗口关闭事件"""
try:
print("🔄 正在关闭应用程序...")
# 清理工具管理器中的进程
if hasattr(self.world, 'tool_manager') and self.world.tool_manager:
print("🧹 清理工具管理器进程...")
if hasattr(self.world.tool_manager, 'cleanup_processes'):
self.world.tool_manager.cleanup_processes()
else:
print("✓ 工具管理器无需清理进程")
# 停止更新定时器
if hasattr(self, 'updateTimer') and self.updateTimer:
self.updateTimer.stop()
print("⏹️ 更新定时器已停止")
# 清理VR资源
if hasattr(self.world, 'vr_manager') and self.world.vr_manager:
print("🧹 清理VR资源...")
self.world.vr_manager.cleanup()
# 清理Panda3D资源
if hasattr(self, 'pandaWidget') and self.pandaWidget:
print("🧹 清理Panda3D资源...")
self.pandaWidget.cleanup()
print("✅ 应用程序清理完成")
event.accept()
except Exception as e:
print(f"⚠️ 关闭应用程序时出错: {e}")
event.accept() # 即使出错也要关闭
def onCreateFlatTerrain(self):
"""创建平面地形"""
dialog = QDialog(self)
dialog.setWindowTitle("创建平面地形")
dialog.setModal(True)
dialog.resize(300,200)
layout = QVBoxLayout(dialog)
width_layout = QHBoxLayout()
width_layout.addWidget(QLabel("宽度:"))
width_spin = QDoubleSpinBox()
width_spin.setRange(0,10000)
width_spin.setValue(0.3)
width_layout.addWidget(width_spin)
layout.addLayout(width_layout)
# 高度
height_layout = QHBoxLayout()
height_layout.addWidget(QLabel("高度:"))
height_spin = QDoubleSpinBox()
height_spin.setRange(0, 10000)
height_spin.setValue(0.3)
height_layout.addWidget(height_spin)
layout.addLayout(height_layout)
# 分辨率
resolution_layout = QHBoxLayout()
resolution_layout.addWidget(QLabel("分辨率:"))
resolution_spin = QSpinBox()
resolution_spin.setRange(16, 2048)
resolution_spin.setValue(256)
resolution_spin.setSingleStep(16)
resolution_layout.addWidget(resolution_spin)
layout.addLayout(resolution_layout)
# 按钮
button_layout = QHBoxLayout()
ok_button = QPushButton("创建")
cancel_button = QPushButton("取消")
button_layout.addWidget(ok_button)
button_layout.addWidget(cancel_button)
layout.addLayout(button_layout)
# 连接信号
ok_button.clicked.connect(dialog.accept)
cancel_button.clicked.connect(dialog.reject)
# 显示对话框
if dialog.exec_() == QDialog.Accepted:
width = width_spin.value()
height = height_spin.value()
resolution = resolution_spin.value()
# 调用世界对象创建地形
terrain_info = self.world.createFlatTerrain((width, height), resolution)
if terrain_info:
QMessageBox.information(self, "成功", "平面地形创建成功!")
else:
QMessageBox.warning(self, "错误", "平面地形创建失败!")
def onCreateHeightmapTerrain(self):
"""从高度图创建地形"""
file_path,_=QFileDialog.getOpenFileName(
self,
"选择高度图文件",
"",
"图像文件 (*.png *.jpg *.jpeg *.bmp *.tga);;所有文件 (*)"
)
if file_path:
#创建对话框获取地形参数
dialog = QDialog(self)
dialog.setWindowTitle("设置地形参数")
dialog.setModal(True)
dialog.resize(300,250)
layout = QVBoxLayout(dialog)
x_scale_layout = QHBoxLayout()
x_scale_layout.addWidget(QLabel("X缩放"))
x_scale_spin = QDoubleSpinBox()
x_scale_spin.setRange(0.1,1000)
x_scale_spin.setValue(0.3)
x_scale_spin.setSingleStep(10)
x_scale_layout.addWidget(x_scale_spin)
layout.addLayout(x_scale_layout)
# Y缩放
y_scale_layout = QHBoxLayout()
y_scale_layout.addWidget(QLabel("Y缩放:"))
y_scale_spin = QDoubleSpinBox()
y_scale_spin.setRange(0.1, 1000)
y_scale_spin.setValue(0.3)
y_scale_spin.setSingleStep(10)
y_scale_layout.addWidget(y_scale_spin)
layout.addLayout(y_scale_layout)
# Z缩放
z_scale_layout = QHBoxLayout()
z_scale_layout.addWidget(QLabel("Z缩放:"))
z_scale_spin = QDoubleSpinBox()
z_scale_spin.setRange(0.1, 1000)
z_scale_spin.setValue(50)
z_scale_spin.setSingleStep(5)
z_scale_layout.addWidget(z_scale_spin)
layout.addLayout(z_scale_layout)
# 按钮
button_layout = QHBoxLayout()
ok_button = QPushButton("创建")
cancel_button = QPushButton("取消")
button_layout.addWidget(ok_button)
button_layout.addWidget(cancel_button)
layout.addLayout(button_layout)
# 连接信号
ok_button.clicked.connect(dialog.accept)
cancel_button.clicked.connect(dialog.reject)
# 显示对话框
if dialog.exec_() == QDialog.Accepted:
x_scale = x_scale_spin.value()
y_scale = y_scale_spin.value()
z_scale = z_scale_spin.value()
# 调用世界对象创建地形
terrain_info = self.world.createTerrainFromHeightMap(
file_path,
(x_scale, y_scale, z_scale)
)
if terrain_info:
QMessageBox.information(self, "成功", "高度图地形创建成功!")
else:
QMessageBox.warning(self, "错误", "高度图地形创建失败!")
# ==================== VR事件处理 ====================
def onEnterVR(self):
"""进入VR模式"""
try:
if hasattr(self.world, 'vr_manager') and self.world.vr_manager:
success = self.world.vr_manager.enable_vr()
if success:
# 更新菜单状态
self.enterVRAction.setEnabled(False)
self.exitVRAction.setEnabled(True)
QMessageBox.information(self, "成功", "VR模式已启用\n请确保您的VR头显已正确连接。")
else:
QMessageBox.warning(self, "错误", "无法启用VR模式\n请检查:\n1. SteamVR是否正在运行\n2. VR头显是否已连接\n3. OpenVR库是否已正确安装")
else:
QMessageBox.warning(self, "错误", "VR管理器不可用")
except Exception as e:
QMessageBox.critical(self, "错误", f"启用VR模式时发生错误\n{str(e)}")
def onExitVR(self):
"""退出VR模式"""
try:
if hasattr(self.world, 'vr_manager') and self.world.vr_manager:
self.world.vr_manager.disable_vr()
# 更新菜单状态
self.enterVRAction.setEnabled(True)
self.exitVRAction.setEnabled(False)
QMessageBox.information(self, "成功", "已退出VR模式")
else:
QMessageBox.warning(self, "错误", "VR管理器不可用")
except Exception as e:
QMessageBox.critical(self, "错误", f"退出VR模式时发生错误\n{str(e)}")
def onShowVRStatus(self):
"""显示VR状态"""
try:
if hasattr(self.world, 'vr_manager') and self.world.vr_manager:
status = self.world.vr_manager.get_vr_status()
status_text = f"""VR系统状态
可用性: {'✅ 可用' if status['available'] else '❌ 不可用'}
初始化: {'✅ 已初始化' if status['initialized'] else '❌ 未初始化'}
启用状态: {'✅ 已启用' if status['enabled'] else '❌ 未启用'}
渲染分辨率: {status['eye_resolution'][0]}x{status['eye_resolution'][1]}
追踪设备数: {status['device_count']}
提示:
- 如果VR不可用请确保已安装SteamVR并连接VR头显
- 如果OpenVR库未安装请运行pip install openvr
"""
QMessageBox.information(self, "VR状态", status_text)
else:
QMessageBox.warning(self, "错误", "VR管理器不可用")
except Exception as e:
QMessageBox.critical(self, "错误", f"获取VR状态时发生错误\n{str(e)}")
def onShowVRSettings(self):
"""显示VR设置对话框"""
try:
if hasattr(self.world, 'vr_manager') and self.world.vr_manager:
dialog = self.createVRSettingsDialog()
dialog.exec_()
else:
QMessageBox.warning(self, "错误", "VR管理器不可用")
except Exception as e:
QMessageBox.critical(self, "错误", f"打开VR设置时发生错误\n{str(e)}")
def createVRSettingsDialog(self):
"""创建VR设置对话框"""
dialog = QDialog(self)
dialog.setWindowTitle("VR设置")
dialog.setModal(True)
dialog.resize(400, 300)
layout = QVBoxLayout(dialog)
# VR状态显示
status_group = QGroupBox("VR状态")
status_layout = QVBoxLayout()
if hasattr(self.world, 'vr_manager') and self.world.vr_manager:
status = self.world.vr_manager.get_vr_status()
available_label = QLabel(f"VR可用性: {'' if status['available'] else ''}")
available_label.setStyleSheet(f"color: {'green' if status['available'] else 'red'};")
status_layout.addWidget(available_label)
enabled_label = QLabel(f"VR状态: {'已启用' if status['enabled'] else '未启用'}")
enabled_label.setStyleSheet(f"color: {'green' if status['enabled'] else 'gray'};")
status_layout.addWidget(enabled_label)
resolution_label = QLabel(f"渲染分辨率: {status['eye_resolution'][0]}x{status['eye_resolution'][1]}")
status_layout.addWidget(resolution_label)
status_group.setLayout(status_layout)
layout.addWidget(status_group)
# 渲染设置
render_group = QGroupBox("渲染设置")
render_layout = QFormLayout()
# 渲染质量
quality_combo = QComboBox()
quality_combo.addItems(["", "", "", "超高"])
quality_combo.setCurrentText("")
render_layout.addRow("渲染质量:", quality_combo)
# 抗锯齿
aa_combo = QComboBox()
aa_combo.addItems(["", "2x", "4x", "8x"])
aa_combo.setCurrentText("4x")
render_layout.addRow("抗锯齿:", aa_combo)
render_group.setLayout(render_layout)
layout.addWidget(render_group)
# 性能设置
perf_group = QGroupBox("性能设置")
perf_layout = QFormLayout()
# 刷新率
refresh_combo = QComboBox()
refresh_combo.addItems(["72Hz", "90Hz", "120Hz", "144Hz"])
refresh_combo.setCurrentText("90Hz")
perf_layout.addRow("刷新率:", refresh_combo)
# 异步重投影
async_check = QCheckBox("启用异步重投影")
async_check.setChecked(True)
perf_layout.addRow("", async_check)
perf_group.setLayout(perf_layout)
layout.addWidget(perf_group)
# 按钮
button_layout = QHBoxLayout()
apply_button = QPushButton("应用")
ok_button = QPushButton("确定")
cancel_button = QPushButton("取消")
button_layout.addWidget(apply_button)
button_layout.addStretch()
button_layout.addWidget(ok_button)
button_layout.addWidget(cancel_button)
layout.addLayout(button_layout)
# 连接信号
apply_button.clicked.connect(lambda: self.applyVRSettings(dialog))
ok_button.clicked.connect(dialog.accept)
cancel_button.clicked.connect(dialog.reject)
return dialog
def applyVRSettings(self, dialog):
"""应用VR设置"""
try:
# 这里可以实现设置的保存和应用逻辑
QMessageBox.information(dialog, "成功", "VR设置已应用")
except Exception as e:
QMessageBox.critical(dialog, "错误", f"应用VR设置时发生错误\n{str(e)}")
def setup_main_window(world,path = None):
"""设置主窗口的便利函数"""
app = QApplication.instance()
if app is None:
app = QApplication(sys.argv)
main_window = MainWindow(world)
main_window.show()
from main import openProjectForPath
if path:
openProjectForPath(path,main_window)
return app, main_window