1
0
forked from Rowland/EG

信息面板GUI完成,http数据连接成功,9.10合并

This commit is contained in:
Hector 2025-09-10 09:11:46 +08:00
parent f27b4a0bbf
commit 605f2bbdcd
11 changed files with 2797 additions and 536 deletions

4
.idea/EG.iml generated
View File

@ -1,9 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.venv" />
</content>
<content url="file://$MODULE_DIR$" />
<orderEntry type="jdk" jdkName="Python 3.10" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>

File diff suppressed because one or more lines are too long

View File

@ -116,6 +116,7 @@ class InternalLightManager(object):
source.set_slot(slot)
def remove_light(self, light):
print("111111111111111111111111111111111111111111111111")
assert light is not None
if not light.has_slot():
print("ERROR: Could not detach light, light was not attached!")

1054
core/InfoPanelManager.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -38,7 +38,6 @@ class CoreWorld(Panda3DWorld):
self._setupLighting()
self._setupGround()
self._loadFont()
#self.load_and_play_glb_model()
def _setupResourcePaths(self):
"""设置Panda3D资源搜索路径确保能正确找到Resources文件夹中的模型和贴图"""
@ -100,33 +99,6 @@ class CoreWorld(Panda3DWorld):
except Exception as e:
print(f"⚠️ 设置资源路径失败: {e}")
def load_and_play_glb_model(self):
"""加载 glTF 模型并播放动画"""
try:
from direct.actor.Actor import Actor
# 使用 Actor 类加载 glTF 模型
self.model = Actor("/home/tiger/cube.glb")
print("模型加载成功!")
self.model.reparentTo(self.render)
self.model.setPos(0, 10, 0)
self.model.setScale(10)
# 找出所有 AnimBundleNode
print(f"开始寻找动画AnimationBundleNode...")
for np in self.model.findAllMatches("**/+AnimBundleNode"):
print(f"找到AnimBundleNode: {np.getName()}")
bundle = np.node().getBundle()
for i in range(bundle.getNumAnimations()):
anim_name = bundle.getAnimation(i).getName()
print("动画名:", anim_name)
# 这里不能直接 play需要手动把 AnimControl 绑定到节点
except Exception as e:
print(f"模型加载失败: {e}")
return None
def diagnose_fbx_loading(self, fbx_path):
"""诊断FBX加载状态"""
print("=== FBX加载诊断 ===")

View File

@ -683,7 +683,7 @@ class GUIManager:
textNodePath.setPos(*pos)
textNodePath.setScale(size)
textNodePath.setColor(1, 1, 0, 1)
textNodePath.setBillboardAxis() # 让文本总是面向相机
#textNodePath.setBillboardAxis() # 让文本总是面向相机
textNodePath.setName(text_name)
# 设置节点标签
@ -2723,24 +2723,24 @@ class GUIManager:
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()):
@ -2754,7 +2754,7 @@ class GUIManager:
self.world.treeWidget.setCurrentItem(foundItem)
self.world.updatePropertyPanel(foundItem)
return
def updateGUISelection(self, gui_element):
"""更新GUI元素选择状态"""
self.world.selection.updateSelection(gui_element)
@ -3207,7 +3207,7 @@ class GUIManager:
try:
gui_type = gui_element.getTag("gui_type")
if gui_type in ["3d_text", "3d_image", "video_screen"]:
if gui_type in ["3d_text", "3d_image", "video_screen","info_panel"]:
current_pos = gui_element.getPos()
if axis == "x":
@ -3241,7 +3241,7 @@ class GUIManager:
if value == 0:
value = 0.01
if gui_type in ["3d_text", "3d_image","video_screen","virtual_screen"]:
if gui_type in ["3d_text", "3d_image","video_screen","virtual_screen","info_panel"]:
# 3D元素处理
if axis == "x":
new_scale = (value, current_scale.getY(), current_scale.getZ())

11
main.py
View File

@ -1,5 +1,6 @@
import warnings
from core.InfoPanelManager import InfoPanelManager
from demo.video_integration import VideoManager
warnings.filterwarnings("ignore", category=DeprecationWarning)
@ -39,7 +40,7 @@ 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
from direct.showbase.ShowBaseGlobal import globalClock, aspect2d
import os
import json
import datetime
@ -97,6 +98,8 @@ class MyWorld(CoreWorld):
self.terrain_edit_strength=0.3
self.terrain_edit_operation = "add"
self.info_panel_manager = InfoPanelManager(self)
# 初始化碰撞管理器
from core.collision_manager import CollisionManager
self.collision_manager = CollisionManager(self)
@ -489,9 +492,9 @@ class MyWorld(CoreWorld):
"""异步导入模型"""
return self.scene_manager.importModelAsync(filepath)
# def loadAnimatedModel(self, model_path, anims=None):
# """加载带动画的模型"""
# return self.scene_manager.loadAnimatedModel(model_path, anims)
def loadAnimatedModel(self, model_path, anims=None):
"""加载带动画的模型"""
return self.scene_manager.loadAnimatedModel(model_path, anims)
# 材质和几何体处理方法 - 代理到scene_manager
def processMaterials(self, model):

View File

@ -89,34 +89,27 @@ class SceneManager:
# ==================== 模型导入和处理 ====================
def importModel(self, filepath, apply_unit_conversion=False, normalize_scales=True, auto_convert_to_glb=True):
"""导入模型到场景 - 增强错误处理"""
"""导入模型到场景
Args:
filepath: 模型文件路径
apply_unit_conversion: 是否应用单位转换主要针对FBX文件
normalize_scales: 是否标准化子节点缩放推荐开启
auto_convert_to_glb: 是否自动将非GLB格式转换为GLB以获得更好的动画支持
"""
try:
# 预处理文件路径和转换
filepath = util.normalize_model_path(filepath)
original_filepath = filepath
print(f"\n💾 开始导入模型: {os.path.basename(filepath)}")
print(f"\n=== 开始导入模型: {filepath} ===")
print(f"单位转换: {'开启' if apply_unit_conversion else '关闭'}")
print(f"缩放标准化: {'开启' if normalize_scales else '关闭'}")
print(f"自动转换GLB: {'开启' if auto_convert_to_glb else '关闭'}")
# 检查文件是否存在
if not os.path.exists(filepath):
print(f"❌ 模型文件不存在: {filepath}")
return None
filepath = util.normalize_model_path(filepath)
original_filepath = filepath
# 检查文件大小
file_size = os.path.getsize(filepath)
if file_size == 0:
print(f"❌ 模型文件为空: {filepath}")
return None
print(f"文件大小: {file_size} 字节")
# 检查是否需要转换为GLB
# 检查是否需要转换为GLB以获得更好的动画支持
if auto_convert_to_glb and self._shouldConvertToGLB(filepath):
print(f"🔄 检测到需要转换的格式尝试转换为GLB...")
converted_path = self._convertToGLBWithProgress(filepath)
if converted_path and os.path.exists(converted_path) and os.path.getsize(converted_path) > 0:
if converted_path:
print(f"✅ 转换成功: {converted_path}")
filepath = converted_path
# 显示成功消息
@ -128,250 +121,73 @@ class SceneManager:
except:
pass
else:
print(f"⚠️ 转换失败或文件无效,使用原始文件")
# 验证原始文件是否有效
if not os.path.exists(original_filepath) or os.path.getsize(original_filepath) == 0:
print(f"❌ 原始文件也无效: {original_filepath}")
return None
filepath = original_filepath
# 直接在render根节点下创建模型
print("--- 在根节点下创建模型实例 ---")
# 添加模型加载前的安全检查
print("准备加载模型...")
# 使用try-except包装模型加载过程
model = None
max_retry_attempts = 3 # 最大重试次数
for attempt in range(max_retry_attempts):
try:
print(f"直接从文件加载模型... (尝试 {attempt + 1}/{max_retry_attempts})")
model = self.world.loader.loadModel(filepath)
if model:
print(f"✅ 模型加载成功: {model}")
break
else:
print(f"⚠️ 加载模型返回空对象 (尝试 {attempt + 1})")
if attempt < max_retry_attempts - 1:
import time
time.sleep(0.1) # 短暂延迟后重试
except AssertionError as ae:
error_msg = str(ae)
if "mismatched number of frames" in error_msg:
print(f"⚠️ 动画帧数不匹配错误 (尝试 {attempt + 1}): {error_msg}")
# 清理模型池并重试
if attempt < max_retry_attempts - 1:
self._clearModelCache(filepath)
import time
time.sleep(0.2) # 增加延迟后重试
else:
# 最后一次尝试失败,尝试无动画加载
print("🔄 尝试无动画加载...")
model = self._loadModelWithoutAnimations(filepath)
else:
raise ae # 其他断言错误直接抛出
except Exception as load_error:
print(f"❌ 模型加载过程出错 (尝试 {attempt + 1}): {str(load_error)}")
if attempt < max_retry_attempts - 1:
import time
time.sleep(0.1) # 短暂延迟后重试
else:
import traceback
traceback.print_exc()
print(f"⚠️ 转换失败,使用原始文件")
# 总是重新加载模型以确保材质信息完整
# 不使用ModelPool缓存避免材质信息丢失问题
print("直接从文件加载模型...")
model = self.world.loader.loadModel(filepath)
if not model:
print("❌ 所有加载尝试都失败了")
print("加载模型失败")
return None
# 设置模型名称
model_name = os.path.basename(filepath)
model.setName(model_name)
# 将模型挂载到render根节点
# 将模型添加到场景
model.reparentTo(self.world.render)
# 设置标签和路径信息
# 保存原始路径和转换后的路径
model.setTag("model_path", filepath)
model.setTag("original_path", original_filepath)
model.setTag("file", model.getName())
model.setTag("is_model_root", "1")
model.setTag("is_scene_element", "1")
model.setTag("created_by_user", "1")
if filepath != original_filepath:
model.setTag("converted_from", os.path.splitext(original_filepath)[1])
model.setTag("converted_to_glb", "true")
# 应用处理选项
# 对于GLB文件通常不需要单位转换因为它们已经是标准单位
if apply_unit_conversion and filepath.lower().endswith(
('.fbx', '.obj')) and not filepath.lower().endswith('.glb'):
print("应用单位转换(厘米到米)...")
# 可选的单位转换主要针对FBX
if apply_unit_conversion and filepath.lower().endswith('.fbx'):
print("应用FBX单位转换厘米到米...")
self._applyUnitConversion(model, 0.01)
model.setTag("unit_conversion_applied", "true")
if normalize_scales and filepath.lower().endswith(('.fbx', '.obj')):
print("标准化模型缩放层级...")
# 智能缩放标准化处理FBX子节点的大缩放值
if normalize_scales and filepath.lower().endswith('.fbx'):
print("标准化FBX模型缩放层级...")
self._normalizeModelScales(model)
model.setTag("scale_normalization_applied", "true")
# 调整模型位置到地面
self._adjustModelToGround(model)
# 创建并设置基础材质
print("设置材质...")
print("\n=== 开始设置材质 ===")
self._applyMaterialsToModel(model)
# 设置碰撞检测(重要!用于选择功能)
print("设置碰撞检测...")
print("\n=== 设置碰撞检测 ===")
self.setupCollision(model)
# 添加文件标签用于保存/加载
model.setTag("file", model_name)
model.setTag("is_model_root", "1")
# 记录应用的处理选项
if apply_unit_conversion:
model.setTag("unit_conversion_applied", "true")
if normalize_scales:
model.setTag("scale_normalization_applied", "true")
# 添加到模型列表
self.models.append(model)
print(f"✅ 创建模型成功: {model.getName()}")
# 更新场景树
self.updateSceneTree()
# 获取树形控件并添加到Qt树中
tree_widget = self._get_tree_widget()
if tree_widget:
# 找到根节点项
root_item = None
for i in range(tree_widget.topLevelItemCount()):
item = tree_widget.topLevelItem(i)
if item.text(0) == "render" or item.data(0, Qt.UserRole) == self.world.render:
root_item = item
break
if root_item:
qt_item = tree_widget.add_node_to_tree_widget(model, root_item, "IMPORTED_MODEL_NODE")
if qt_item:
tree_widget.setCurrentItem(qt_item)
# 更新选择和属性面板
tree_widget.update_selection_and_properties(model, qt_item)
print("✅ Qt树节点添加成功")
else:
print("⚠️ Qt树节点添加失败但Panda3D对象已创建")
else:
print("⚠️ 未找到根节点项无法添加到Qt树")
else:
print("⚠️ 无法访问树形控件")
print(f"🎉 模型导入完成")
print(f"=== 模型导入成功: {model_name} ===\n")
return model
except Exception as e:
print(f"❌ 导入模型过程失败: {str(e)}")
import traceback
traceback.print_exc()
print(f"导入模型失败: {str(e)}")
return None
def _clearModelCache(self, filepath):
"""清理模型缓存以解决加载问题"""
try:
# 清理特定文件的缓存
filename = Filename.fromOsSpecific(filepath)
if ModelPool.hasModel(filename):
ModelPool.releaseModel(filename)
print(f"🧹 已清理模型缓存: {filepath}")
# 清理所有动画缓存
from panda3d.core import AnimBundleNode
AnimBundleNode.clearAllBundleCache()
print("🧹 已清理动画缓存")
except Exception as e:
print(f"⚠️ 清理缓存时出错: {e}")
def _loadModelWithoutAnimations(self, filepath):
"""尝试无动画加载模型"""
try:
from panda3d.core import LoaderOptions
print("🔄 尝试无动画加载模型...")
# 创建不加载动画的加载选项
loader_options = LoaderOptions()
loader_options.setFlags(LoaderOptions.LF_no_cache) # 不使用缓存
loader_options.setAllowInstance(False) # 不允许实例化
# 尝试加载不带动画的模型
model = self.world.loader.loadModel(filepath, loaderOptions=loader_options)
if model:
print("✅ 无动画加载成功")
return model
else:
print("❌ 无动画加载也失败了")
return None
except Exception as e:
print(f"❌ 无动画加载失败: {e}")
return None
# def importAnimatedFBX(self, filepath, scale=0.01, auto_play=True):
# """导入带动画的FBX模型使用assimp
#
# Args:
# filepath: FBX文件路径
# scale: 缩放比例默认0.01,从厘米转换到米)
# auto_play: 是否自动播放第一个动画
#
# Returns:
# Actor对象如果加载失败返回None
# """
# try:
# print(f"\n=== 导入动画FBX模型: {filepath} ===")
# filepath = util.normalize_model_path(filepath)
#
# # 使用动画管理器加载FBX
# actor = self.animation_manager.load_fbx_with_animations(filepath, scale)
#
# if actor:
# # 设置模型名称
# model_name = os.path.basename(filepath)
# actor.setName(model_name)
#
# # 调整模型位置到地面
# self._adjustModelToGround(actor)
#
# # 设置碰撞检测
# self.setupCollision(actor)
#
# # 添加文件标签
# actor.setTag("file", model_name)
# actor.setTag("is_animated_model", "1")
#
# # 添加到模型列表
# self.models.append(actor)
#
# # 自动播放第一个动画
# if auto_play:
# available_anims = self.animation_manager.get_available_animations(actor)
# if available_anims:
# first_anim = available_anims[0]
# self.animation_manager.play_animation(actor, first_anim, loop=True)
# print(f"🎬 自动播放动画: {first_anim}")
#
# # 更新场景树
# self.updateSceneTree()
#
# print(f"=== 动画FBX模型导入成功: {model_name} ===\n")
# return actor
# else:
# print("❌ 动画FBX模型导入失败")
# return None
#
# except Exception as e:
# print(f"导入动画FBX模型失败: {str(e)}")
# import traceback
# traceback.print_exc()
# return None
def _applyMaterialsToModel(self, model):
"""递归应用材质到模型的所有GeomNode"""
@ -946,6 +762,9 @@ class SceneManager:
model.setTag("color", str(color_attrib.getColor()))
try:
print("--- 打印当前场景图 (render) ---")
self.world.render.ls()
print("---------------------------------")
# 保存场景
success = self.world.render.writeBamFile(filename)

View File

@ -17,6 +17,8 @@ from PyQt5.QtWidgets import (QApplication, QMainWindow, QMenuBar, QMenu, QAction
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):
@ -377,6 +379,26 @@ class MainWindow(QMainWindow):
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.createAllPanelsAction = self.infoPanelMenu.addAction('创建所有面板')
self.createAllPanelsAction.triggered.connect(self.onCreateAllInfoPanels)
#资源菜单
self.assetsMenu = menubar.addMenu('资源')
@ -584,10 +606,10 @@ class MainWindow(QMainWindow):
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)
# self.consoleDock = QDockWidget("控制台", self)
# self.consoleView = CustomConsoleDockWidget(self.world)
# self.consoleDock.setWidget(self.consoleView)
# self.addDockWidget(Qt.BottomDockWidgetArea, self.consoleDock)
def setupToolbar(self):
"""创建工具栏"""
@ -1071,6 +1093,396 @@ class MainWindow(QMainWindow):
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) # 每10分钟更新一次
# # 立即显示示例数据
# 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)}"
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 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:
tree_item = self.treeWidget.add_node_to_tree_widget(
node=panel,
parent_item=scene_root,
node_type="INFO_PANEL"
)
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 refreshScriptsList(self):

File diff suppressed because it is too large Load Diff

View File

@ -1375,6 +1375,8 @@ class CustomTreeWidget(QTreeWidget):
self.setupDragDrop() # 设置拖拽功能
self.original_scales={}
def initData(self):
"""初始化变量"""
# 定义2D GUI元素类型
@ -1493,96 +1495,93 @@ class CustomTreeWidget(QTreeWidget):
else:
print("用户取消了菜单选择")
# 在 CustomTreeWidget 类的 dropEvent 方法中替换缩放处理部分
def dropEvent(self, event):
dragged_item = self.currentItem()
target_item = self.itemAt(event.pos())
if not dragged_item or not target_item:
# 1. 获取所有被拖拽的项
dragged_items = self.selectedItems()
if not dragged_items:
event.ignore()
return
if not self.isValidParentChild(dragged_item, target_item):
event.ignore()
return
# 2. 在执行Qt的默认拖拽前记录所有拖拽项的原始状态
drag_info = []
for item in dragged_items:
panda_node = item.data(0, Qt.UserRole)
if not panda_node or panda_node.is_empty():
continue # 跳过无效节点
dragged_node = dragged_item.data(0, Qt.UserRole)
target_node = target_item.data(0, Qt.UserRole)
drag_info.append({
"item": item,
"node": panda_node,
"old_parent_node": item.parent().data(0, Qt.UserRole) if item.parent() else None
})
if not dragged_node or not target_node:
event.ignore()
return
print(f"dragged_node: {dragged_node}, target_node: {target_node}")
# 记录拖拽前的父节点
old_parent_item = dragged_item.parent()
old_parent_node = old_parent_item.data(0, Qt.UserRole) if old_parent_item else None
# 获取拖拽前的缩放值2D GUI节点需要特别处理
is_2d_gui = dragged_item.data(0, Qt.UserRole + 1) in self.gui_2d_types
if is_2d_gui:
# 对于2D GUI直接记录节点自身的缩放不考虑父节点缩放
original_scale = dragged_node.getScale()
else:
# 对于3D节点记录世界缩放
original_scale = dragged_node.getScale(self.world.render)
# 执行Qt默认拖拽
# 3. 执行Qt的默认拖拽让UI树先行更新
# 这一步会自动处理移动或复制,并将项目从旧父节点移除,添加到新父节点
super().dropEvent(event)
# 检查拖拽后的父节点
new_parent_item = dragged_item.parent()
new_parent_node = new_parent_item.data(0, Qt.UserRole) if new_parent_item else None
# 同步Panda3D场景图的父子关系
# 4. 遍历记录下的信息同步每一个Panda3D节点的状态
try:
# 检查是否是跨层级拖拽(父节点发生变化)
if old_parent_node != new_parent_node:
print(f"跨层级拖拽:从 {old_parent_node} 移动到 {new_parent_node}")
for info in drag_info:
dragged_item = info["item"]
dragged_node = info["node"]
old_parent_node = info["old_parent_node"]
# 保存世界坐标位置
world_pos = dragged_node.getPos(self.world.render)
world_hpr = dragged_node.getHpr(self.world.render)
# 获取拖拽后的新父节点
new_parent_item = dragged_item.parent()
new_parent_node = new_parent_item.data(0, Qt.UserRole) if new_parent_item else None
# 重新父化到新的父节点
if new_parent_node:
if is_2d_gui:
# 2D GUI元素需要特殊处理
if hasattr(new_parent_node, 'getTag') and new_parent_node.getTag("is_gui_element") == "1":
# 目标是GUI元素直接重新父化
dragged_node.reparentTo(new_parent_node)
# 仅当父节点实际发生变化时才执行重新父化
if old_parent_node != new_parent_node:
print(f"跨层级拖拽:从 {old_parent_node} 移动到 {new_parent_node}")
# # 保存世界坐标位置
# world_pos = dragged_node.getPos(self.world.render)
# world_hpr = dragged_node.getHpr(self.world.render)
# world_scale = dragged_node.getScale(self.world.render)
# 检查是否是2D GUI元素
dragged_type = dragged_item.data(0, Qt.UserRole + 1)
is_2d_gui = dragged_type in self.gui_2d_types
# 重新父化到新的父节点
if new_parent_node:
if is_2d_gui:
# 2D GUI元素需要特殊处理
if hasattr(new_parent_node, 'getTag') and new_parent_node.getTag("is_gui_element") == "1":
# 目标是GUI元素直接重新父化
dragged_node.wrtReparentTo(new_parent_node)
else:
# 目标是3D节点保持GUI特性重新父化到aspect2d
# from direct.showbase.ShowBase import aspect2d
dragged_node.wrtReparentTo(self.world.aspect2d)
print(f"2D GUI元素 {dragged_item.text(0)} 保持在aspect2d下")
else:
# 目标是3D节点保持GUI特性重新父化到aspect2d
from direct.showbase.ShowBase import aspect2d
dragged_node.reparentTo(aspect2d)
print(f"2D GUI元素 {dragged_item.text(0)} 保持在aspect2d下")
# 非GUI元素正常重新父化
dragged_node.wrtReparentTo(new_parent_node)
else:
# 非GUI元素正常重新父化
dragged_node.reparentTo(new_parent_node)
# 如果新父节点为None根据元素类型决定父节点
if is_2d_gui:
# from direct.showbase.ShowBase import aspect2d
dragged_node.wrtReparentTo(self.world.aspect2d)
print(f"2D GUI元素 {dragged_item.text(0)} 重新父化到aspect2d")
else:
dragged_node.wrtReparentTo(self.world.render)
# # 恢复世界坐标位置对于2D GUI可能需要调整
# if is_2d_gui:
# # 2D GUI元素使用屏幕坐标系可能需要特殊处理
# dragged_node.setPos(world_pos)
# dragged_node.setHpr(world_hpr)
# dragged_node.setScale(world_scale)
# else:
# dragged_node.setPos(self.world.render, world_pos)
# dragged_node.setHpr(self.world.render, world_hpr)
# dragged_node.setScale(self.world.render, world_scale)
print(f"✅ Panda3D父子关系已更新")
else:
# 如果新父节点为None根据元素类型决定父节点
if is_2d_gui:
from direct.showbase.ShowBase import aspect2d
dragged_node.reparentTo(aspect2d)
print(f"2D GUI元素 {dragged_item.text(0)} 重新父化到aspect2d")
else:
dragged_node.reparentTo(self.world.render)
# 恢复世界坐标位置和方向
dragged_node.setPos(self.world.render, world_pos)
dragged_node.setHpr(self.world.render, world_hpr)
# 恢复缩放值
if is_2d_gui:
# 对于2D GUI直接使用原始缩放值不考虑父节点缩放
dragged_node.setScale(original_scale)
print(f"✅ 2D GUI {dragged_item.text(0)} 缩放已恢复为原始值: {original_scale}")
else:
# 对于3D节点使用世界缩放恢复
dragged_node.setScale(self.world.render, original_scale)
print(f"✅ Panda3D父子关系已更新")
else:
print(f"同层级移动父节点未变化跳过Panda3D重新父化")
print(f"同层级移动父节点未变化跳过Panda3D重新父化")
except Exception as e:
print(f"⚠️ 同步Panda3D场景图失败: {e}")
@ -2176,7 +2175,7 @@ class CustomTreeWidget(QTreeWidget):
# 检查是否有GUI标签
if hasattr(node, 'getTag'):
return node.getTag("is_gui_element") == "1"
return node.getTag("is_gui_element") == "1" or node.getTag("gui_type") in ["info_panel", "button", "label", "entry", "3d_text", "virtual_screen"]
# 检查是否是DirectGUI对象
try: