From 605f2bbdcd9b5c6db4f0df4ef2cd9fb85390848a Mon Sep 17 00:00:00 2001 From: Hector <2055590199@qq.com> Date: Wed, 10 Sep 2025 09:11:46 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=A1=E6=81=AF=E9=9D=A2=E6=9D=BFGUI?= =?UTF-8?q?=E5=AE=8C=E6=88=90=EF=BC=8Chttp=E6=95=B0=E6=8D=AE=E8=BF=9E?= =?UTF-8?q?=E6=8E=A5=E6=88=90=E5=8A=9F=EF=BC=8C9.10=E5=90=88=E5=B9=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/EG.iml | 4 +- RenderPipelineFile/config/daytime.yaml | 4 +- .../rpcore/pynative/internal_light_manager.py | 1 + core/InfoPanelManager.py | 1054 +++++++++++++ core/world.py | 28 - gui/gui_manager.py | 16 +- main.py | 11 +- scene/scene_manager.py | 273 +--- ui/main_window.py | 420 ++++- ui/property_panel.py | 1369 ++++++++++++++--- ui/widgets.py | 153 +- 11 files changed, 2797 insertions(+), 536 deletions(-) create mode 100644 core/InfoPanelManager.py diff --git a/.idea/EG.iml b/.idea/EG.iml index bd20c4bd..fa7a6154 100644 --- a/.idea/EG.iml +++ b/.idea/EG.iml @@ -1,9 +1,7 @@ - - - + diff --git a/RenderPipelineFile/config/daytime.yaml b/RenderPipelineFile/config/daytime.yaml index f422041e..731e5beb 100644 --- a/RenderPipelineFile/config/daytime.yaml +++ b/RenderPipelineFile/config/daytime.yaml @@ -17,8 +17,8 @@ control_points: scattering: sun_intensity: [[[0.0000000000,0.0000000000],[0.0041666667,0.0000000000],[0.0083333333,0.0000000000],[0.0125000000,0.0000000000],[0.0166666667,0.0000000000],[0.0208333333,0.0000000000],[0.0250000000,0.0000000000],[0.0291666667,0.0000000000],[0.0333333333,0.0000000000],[0.0375000000,0.0000000000],[0.0416666667,0.0000000000],[0.0458333333,0.0000000000],[0.0500000000,0.0000000000],[0.0541666667,0.0000000000],[0.0583333333,0.0000000000],[0.0625000000,0.0000000000],[0.0666666667,0.0000000000],[0.0708333333,0.0000000000],[0.0750000000,0.0000000000],[0.0791666667,0.0000000000],[0.0833333333,0.0000000000],[0.0875000000,0.0000000000],[0.0916666667,0.0000000000],[0.0958333333,0.0000000000],[0.1000000000,0.0000000000],[0.1041666667,0.0000000000],[0.1083333333,0.0000000000],[0.1125000000,0.0000000000],[0.1166666667,0.0000000000],[0.1208333333,0.0000000000],[0.1250000000,0.0000000000],[0.1291666667,0.0000000000],[0.1333333333,0.0000000000],[0.1375000000,0.0000000000],[0.1416666667,0.0000000000],[0.1458333333,0.0000000000],[0.1500000000,0.0000000000],[0.1541666667,0.0000000000],[0.1583333333,0.0000028805],[0.1625000000,0.0003577724],[0.1666666667,0.0013331400],[0.1708333333,0.0029671803],[0.1750000000,0.0052963381],[0.1791666667,0.0083550556],[0.1833333333,0.0121755589],[0.1875000000,0.0167876159],[0.1916666667,0.0222183530],[0.1958333333,0.0284919947],[0.2000000000,0.0356297193],[0.2041666667,0.0436494349],[0.2083333333,0.0525656099],[0.2125000000,0.0623891610],[0.2166666667,0.0731272461],[0.2208333333,0.0847831708],[0.2250000000,0.0973563167],[0.2291666667,0.1108419698],[0.2333333333,0.1252313631],[0.2375000000,0.1405115250],[0.2416666667,0.1566653434],[0.2458333333,0.1736715009],[0.2500000000,0.1915046014],[0.2541666667,0.2101350464],[0.2583333333,0.2295292930],[0.2625000000,0.2496498145],[0.2666666667,0.2704552670],[0.2708333333,0.2919006662],[0.2750000000,0.3139375192],[0.2791666667,0.3365139497],[0.2833333333,0.3595750662],[0.2875000000,0.3830630359],[0.2916666667,0.4069173972],[0.2958333333,0.4310753462],[0.3000000000,0.4554720417],[0.3041666667,0.4800408236],[0.3083333333,0.5047136020],[0.3125000000,0.5294212108],[0.3166666667,0.5540936424],[0.3208333333,0.5786605298],[0.3250000000,0.6030514553],[0.3291666667,0.6271963182],[0.3333333333,0.6510256858],[0.3375000000,0.6744711982],[0.3416666667,0.6974659988],[0.3458333333,0.7199450163],[0.3500000000,0.7418453485],[0.3541666667,0.7631067095],[0.3583333333,0.7836717291],[0.3625000000,0.8034862953],[0.3666666667,0.8224999302],[0.3708333333,0.8406661079],[0.3750000000,0.8579425235],[0.3791666667,0.8742914270],[0.3833333333,0.8896799131],[0.3875000000,0.9040801386],[0.3916666667,0.9174695289],[0.3958333333,0.9298310650],[0.4000000000,0.9411533765],[0.4041666667,0.9514309312],[0.4083333333,0.9606641691],[0.4125000000,0.9688595571],[0.4166666667,0.9760296330],[0.4208333333,0.9821930708],[0.4250000000,0.9873746114],[0.4291666667,0.9916050060],[0.4333333333,0.9949209310],[0.4375000000,0.9973647924],[0.4416666667,0.9989845508],[0.4458333333,0.9998334497],[0.4500000000,0.9999696949],[0.4541666667,0.9994560801],[0.4583333333,0.9983595429],[0.4625000000,0.9967506613],[0.4666666667,0.9947030614],[0.4708333333,0.9922927758],[0.4750000000,0.9895975125],[0.4791666667,0.9866958610],[0.4833333333,0.9836664262],[0.4875000000,0.9805868867],[0.4916666667,0.9775330316],[0.4958333333,0.9745777179],[0.5000000000,0.9717898417],[0.5041666667,0.9692332877],[0.5083333333,0.9669658924],[0.5125000000,0.9650384806],[0.5089595376,0.9690650222],[0.5208333333,0.9623666659],[0.5250000000,0.9616814371],[0.5291666667,0.9614534423],[0.5333333333,0.9616877089],[0.5375000000,0.9623790807],[0.5416666667,0.9635123329],[0.5458333333,0.9650624244],[0.5500000000,0.9669949804],[0.5541666667,0.9692669864],[0.5583333333,0.9718275065],[0.5625000000,0.9746185969],[0.5666666667,0.9775762863],[0.5708333333,0.9806315864],[0.5750000000,0.9837115661],[0.5791666667,0.9867403433],[0.5833333333,0.9896401655],[0.5875000000,0.9923323562],[0.5916666667,0.9947382579],[0.5958333333,0.9967800977],[0.6000000000,0.9983817820],[0.6041666667,0.9994696263],[0.6083333333,0.9999730028],[0.6125000000,0.9998249266],[0.6166666667,0.9989625601],[0.6208333333,0.9973276624],[0.6250000000,0.9948669567],[0.6291666667,0.9915324664],[0.6333333333,0.9872817545],[0.6375000000,0.9820781426],[0.6416666667,0.9758908775],[0.6458333333,0.9686952146],[0.6500000000,0.9604725211],[0.6541666667,0.9512102537],[0.6583333333,0.9409019858],[0.6625000000,0.9295473441],[0.6666666667,0.9171518878],[0.6708333333,0.9037270619],[0.6750000000,0.8892899902],[0.6791666667,0.8738633008],[0.6833333333,0.8574749656],[0.6875000000,0.8401579787],[0.6916666667,0.8219502453],[0.6958333333,0.8028941798],[0.7000000000,0.7830364456],[0.7041666667,0.7624277344],[0.7083333333,0.7411222520],[0.7125000000,0.7191776044],[0.7166666667,0.6966542563],[0.7208333333,0.6736152714],[0.7250000000,0.6501259629],[0.7291666667,0.6262533880],[0.7333333333,0.6020661121],[0.7375000000,0.5776338043],[0.7416666667,0.5530267796],[0.7458333333,0.5283156992],[0.7500000000,0.5035711751],[0.7541666667,0.4788634341],[0.7583333333,0.4542618347],[0.7625000000,0.4298347613],[0.7666666667,0.4056490351],[0.7708333333,0.3817697830],[0.7750000000,0.3582600107],[0.7791666667,0.3351803495],[0.7833333333,0.3125888445],[0.7875000000,0.2905406366],[0.7916666667,0.2690876955],[0.7958333333,0.2482787388],[0.8000000000,0.2281588906],[0.8041666667,0.2087696425],[0.8083333333,0.1901486315],[0.8125000000,0.1723295359],[0.8166666667,0.1553419918],[0.8208333333,0.1392115328],[0.8250000000,0.1239595144],[0.8291666667,0.1096030703],[0.8333333333,0.0961551918],[0.8375000000,0.0836246599],[0.8416666667,0.0720161369],[0.8458333333,0.0613302273],[0.8500000000,0.0515635598],[0.8541666667,0.0427088803],[0.8583333333,0.0347551990],[0.8625000000,0.0276878920],[0.8666666667,0.0214889271],[0.8708333333,0.0161369711],[0.8750000000,0.0116076130],[0.8791666667,0.0078735477],[0.8833333333,0.0049047927],[0.8875000000,0.0026688977],[0.8916666667,0.0011311782],[0.8958333333,0.0002549473],[0.9000000000,0.0000000000],[0.9041666667,0.0000000000],[0.9083333333,0.0000000000],[0.9125000000,0.0000000000],[0.9166666667,0.0000000000],[0.9208333333,0.0000000000],[0.9250000000,0.0000000000],[0.9291666667,0.0000000000],[0.9333333333,0.0000000000],[0.9375000000,0.0000000000],[0.9416666667,0.0000000000],[0.9458333333,0.0000000000],[0.9500000000,0.0000000000],[0.9541666667,0.0000000000],[0.9583333333,0.0000000000],[0.9625000000,0.0000000000],[0.9666666667,0.0000000000],[0.9708333333,0.0000000000],[0.9750000000,0.0000000000],[0.9791666667,0.0000000000],[0.9833333333,0.0000000000],[0.9875000000,0.0000000000],[0.9916666667,0.0000000000],[0.9958333333,0.0000000000]]] sun_color: [[[0.5010435645,0.5818710306],[0.0433100000,0.8999700000],[0.8635787716,0.9130000000],[0.1785000000,0.8973600000],[0.8099800000,0.8651100000],[0.2360800000,0.7712700000],[0.6583432177,0.8485126184],[0.1266806142,0.9648102053],[0.9558541267,0.9090909091],[0.5568400771,0.7353760446]],[[0.5001318426,0.5160300000],[0.0572700000,0.6541600000],[0.2395000000,0.5976800000],[0.8104600000,0.6009000000],[0.6967400000,0.5483900000]],[[0.0862400000,0.4257800000],[0.4955600000,0.4033000000],[0.8234200000,0.4340200000]]] - sun_azimuth: [[[0.5000000000,0.8527777778]]] - sun_altitude: [[[0.5000000000,1.0000000000]]] + sun_azimuth: [[[0.5000000000,0.0000000000]]] + sun_altitude: [[[0.5000000000,0.9333333333]]] extinction: [[[0.4913294798,0.6378830084]]] volumetrics: fog_ramp_size: [[[0.5510597303,0.7409470752]]] diff --git a/RenderPipelineFile/rpcore/pynative/internal_light_manager.py b/RenderPipelineFile/rpcore/pynative/internal_light_manager.py index 1d8c480d..911ee147 100644 --- a/RenderPipelineFile/rpcore/pynative/internal_light_manager.py +++ b/RenderPipelineFile/rpcore/pynative/internal_light_manager.py @@ -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!") diff --git a/core/InfoPanelManager.py b/core/InfoPanelManager.py new file mode 100644 index 00000000..dbe6dede --- /dev/null +++ b/core/InfoPanelManager.py @@ -0,0 +1,1054 @@ +# 修改后的 InfoPanelManager.py +from xml.sax.handler import property_encoding + +from direct.gui.DirectGui import DirectFrame, DirectLabel +from direct.showbase.ShowBaseGlobal import aspect2d +from panda3d.core import TextNode, Vec4, NodePath +from direct.showbase.DirectObject import DirectObject +import threading +import json +import time +from urllib.parse import urlparse +import requests + + +class InfoPanelManager(DirectObject): + """信息面板管理器,用于创建和管理2D信息面板""" + + def __init__(self, world): + """ + 初始化信息面板管理器 + parent: 父节点,默认为aspect2d + """ + DirectObject.__init__(self) + self.world= world + self.parent = aspect2d + self.panels = {} # 存储所有创建的面板 + self.data_sources = {} # 存储数据源 + self.data_callbacks = {} # 存储数据更新回调 + + def setParent(self, parent): + """设置父节点""" + self.parent = parent if parent else aspect2d + + def createInfoPanel(self, panel_id, position=(0, 0), size=(1.0, 0.6), + bg_color=(0.15, 0.15, 0.15, 0.9), border_color=(0.3, 0.3, 0.3, 1.0), + title_color=(1.0, 1.0, 1.0, 1.0), content_color=(0.9, 0.9, 0.9, 1.0), + visible=True, font=None, bg_image=None): + """ + 创建信息面板 - 返回 NodePath 实例 + """ + # 如果面板已存在,先移除它 + if panel_id in self.panels: + self.removePanel(panel_id) + + # 确保父节点存在 + parent_node = self.parent if self.parent else aspect2d + + # 根据面板ID确定标题和内容 + title, content = self._getPanelContent(panel_id) + + # 创建主节点,便于统一管理 + panel_node = parent_node.attachNewNode(f"info_panel_{panel_id}") + panel_node.setPos(position[0], 0, position[1]) + + # 创建主面板框架(带边框效果) + panel = DirectFrame( + frameSize=(-size[0] / 2, size[0] / 2, -size[1] / 2, size[1] / 2), + frameColor=bg_color, + parent=panel_node + ) + + # 添加背景图片(如果提供) + bg_image_node = None + if bg_image and hasattr(self.world, 'loader'): + try: + from panda3d.core import Texture, TransparencyAttrib + # 使用 world.loader 加载纹理 + texture = self.world.loader.loadTexture(bg_image) + if texture: + # 创建一个覆盖整个面板的图片 + bg_image_node = DirectFrame( + frameSize=(-size[0] / 2, size[0] / 2, -size[1] / 2, size[1] / 2), + frameTexture=texture, + parent=panel_node, + sortOrder=-10 # 确保在最底层 + ) + bg_image_node.setTransparency(TransparencyAttrib.MAlpha) + except Exception as e: + print(f"加载背景图片失败: {e}") + + # 添加边框 + border_thickness = 0.005 + # 上边框 + top_border = DirectFrame( + frameSize=(-size[0] / 2 - border_thickness, size[0] / 2 + border_thickness, + size[1] / 2, size[1] / 2 + border_thickness), + frameColor=border_color, + parent=panel_node + ) + # 下边框 + bottom_border = DirectFrame( + frameSize=(-size[0] / 2 - border_thickness, size[0] / 2 + border_thickness, + -size[1] / 2 - border_thickness, -size[1] / 2), + frameColor=border_color, + parent=panel_node + ) + # 左边框 + left_border = DirectFrame( + frameSize=(-size[0] / 2 - border_thickness, -size[0] / 2, + -size[1] / 2, size[1] / 2), + frameColor=border_color, + parent=panel_node + ) + # 右边框 + right_border = DirectFrame( + frameSize=(size[0] / 2, size[0] / 2 + border_thickness, + -size[1] / 2, size[1] / 2), + frameColor=border_color, + parent=panel_node + ) + + # 创建标题栏背景 + title_bar_height = 0.08 + title_bar = DirectFrame( + frameSize=(-size[0] / 2 + border_thickness, size[0] / 2 - border_thickness, + size[1] / 2 - title_bar_height, size[1] / 2 - border_thickness), + frameColor=(0, 0, 0, 0), + parent=panel_node + ) + + # 创建标题 + title_label = DirectLabel( + text=title, + text_scale=0.06, + text_fg=title_color, + text_align=TextNode.ACenter, + pos=(0, 0, size[1] / 2 - title_bar_height / 2 - border_thickness), + parent=panel_node, + relief=None, + text_font=font or None + ) + + # 创建内容区域背景(稍微深一点) + content_bg = DirectFrame( + frameSize=(-size[0] / 2 + border_thickness * 2, size[0] / 2 - border_thickness * 2, + -size[1] / 2 + border_thickness * 2, size[1] / 2 - title_bar_height - border_thickness * 2), + frameColor=(0, 0, 0, 0), + parent=panel_node + ) + + # 创建内容 - 设置一个非常大的换行值,几乎不换行 + content_label = DirectLabel( + text=content, + text_scale=0.045, + text_fg=content_color, + text_align=TextNode.ALeft, + text_wordwrap=500, # 设置一个非常大的值,几乎不自动换行 + pos=(-size[0] / 2 + 0.03, 0, size[1] / 2 - title_bar_height - 0.05), + parent=panel_node, + relief=None, + text_font=font or None + ) + + # 保存引用 + self.panels[panel_id] = { + 'node': panel_node, + 'panel': panel, + 'title_label': title_label, + 'content_label': content_label, + 'title_bar': title_bar, + 'content_bg': content_bg, + 'borders': { + 'top': top_border, + 'bottom': bottom_border, + 'left': left_border, + 'right': right_border + }, + 'bg_image': bg_image_node, # 保存背景图片节点引用 + 'properties': { + 'size': size, + 'position': position, + 'bg_color': bg_color, + 'border_color': border_color, + 'title_color': title_color, + 'content_color': content_color, + 'font': font, + 'bg_image': bg_image # 保存背景图片路径 + } + } + + # 设置GUI类型标记和支持3D编辑的标记 + panel_node.setTag("gui_type", "info_panel") + panel_node.setTag("panel_id", panel_id) + panel_node.setTag("supports_3d_position_editing", "1") # 支持3D位置编辑 + + if not visible: + panel_node.hide() + + return panel_node + + def setPanelBackgroundImage(self, panel_id, image_path): + """ + 为指定面板设置背景图片 + panel_id: 面板ID + image_path: 图片路径 + """ + if panel_id not in self.panels: + print(f"面板 {panel_id} 不存在") + return False + + try: + panel_data = self.panels[panel_id] + + # 如果image_path为None或空,清除现有的背景图片 + if not image_path: + if 'bg_image' in panel_data and panel_data['bg_image']: + panel_data['bg_image'].destroy() + panel_data['bg_image'] = None + panel_data['properties']['bg_image'] = None + # 清除节点标签 + if panel_data['node'].hasTag("bg_image_path"): + panel_data['node'].clearTag("bg_image_path") + return True + + # 使用 world.loader 加载新图片 + texture = self.world.loader.loadTexture(image_path) + if not texture: + print(f"无法加载图片: {image_path}") + return False + + # 获取面板大小 + size = panel_data['properties']['size'] + + # 如果已存在背景图片,先销毁它 + if 'bg_image' in panel_data and panel_data['bg_image']: + panel_data['bg_image'].destroy() + + from direct.gui.DirectGui import DirectFrame + from panda3d.core import TransparencyAttrib + + bg_image_node = DirectFrame( + frameSize=(-size[0] / 2, size[0] / 2, -size[1] / 2, size[1] / 2), + frameTexture=texture, + parent=panel_data['node'], + sortOrder=-10 # 确保在最底层 + ) + + bg_image_node.setTransparency(TransparencyAttrib.MAlpha) + + # 保存引用 + panel_data['bg_image'] = bg_image_node + panel_data['properties']['bg_image'] = image_path + + # 更新节点标签 + panel_data['node'].setTag("bg_image_path", image_path) + + print(f"成功设置信息面板背景图片: {image_path}") + return True + except Exception as e: + print(f"设置背景图片失败: {e}") + import traceback + traceback.print_exc() + return False + + def _getPanelContent(self, panel_id): + """ + 根据面板ID获取固定的标题和内容 + """ + panel_contents = { + "scene_info": { + "title": "场景信息", + "content": "项目名称: 我的3D项目\n场景节点数: 15\n总材质数: 8\n光源数量: 3" + }, + "controls": { + "title": "控制说明", + "content": "WASD: 移动\n鼠标: 视角\n空格: 跳跃\nESC: 退出" + }, + "model_info": { + "title": "模型信息", + "content": "模型名称: Unknown\n子节点数: 0\n类型: Unknown" + }, + "terrain_info": { + "title": "地形信息", + "content": "地形名称: Unknown\n大小: 0 x 0\n分辨率: 0" + }, + "light_info": { + "title": "光源信息", + "content": "光源类型: Unknown\n投射阴影: Unknown\n阴影分辨率: Unknown" + }, + "sensor_data": { + "title": "传感器数据", + "content": "温度: --°C\n湿度: --%\n压力: -- hPa\n更新时间: --" + }, + "system_status": { + "title": "系统状态", + "content": "CPU使用率: --%\n内存使用: --MB\n网络状态: 断开\n运行时间: --" + }, + "realtime_data": { + "title": "实时数据", + "content": "数据加载中..." + }, + "default": { + "title": "信息面板", + "content": "这是一个信息面板" + } + } + + # 返回对应面板的内容,如果没有则返回默认内容 + panel_data = panel_contents.get(panel_id, panel_contents["default"]) + return panel_data["title"], panel_data["content"] + + def updatePanelContent(self, panel_id, title=None, content=None): + """ + 更新面板内容 + """ + if panel_id not in self.panels: + print(f"面板 {panel_id} 不存在") + return False + + panel_data = self.panels[panel_id] + + if title is not None and panel_data['title_label']: + panel_data['title_label'].setText(title) + + if content is not None and panel_data['content_label']: + panel_data['content_label'].setText(content) + + return True + + def registerDataSource(self, panel_id, data_callback, update_interval=1.0): + """ + 注册数据源,定期更新面板内容 + data_callback: 返回数据的回调函数 + update_interval: 更新间隔(秒) + """ + if panel_id not in self.panels: + print(f"面板 {panel_id} 不存在") + return False + + # 停止现有的数据源(如果存在) + if panel_id in self.data_sources: + self.data_sources[panel_id]['stop'] = True + + # 创建数据源 + data_source = { + 'callback': data_callback, + 'interval': update_interval, + 'stop': False + } + + self.data_sources[panel_id] = data_source + + # 启动数据更新线程 + thread = threading.Thread(target=self._updateDataThread, args=(panel_id,), daemon=True) + thread.start() + + return True + + def _updateDataThread(self, panel_id): + """ + 数据更新线程 + """ + while panel_id in self.data_sources and not self.data_sources[panel_id]['stop']: + try: + # 获取数据 + data = self.data_sources[panel_id]['callback']() + + # 更新面板内容 + if data and panel_id in self.panels: + self.updatePanelContent(panel_id, content=data) + + # 等待下次更新 + interval = self.data_sources[panel_id]['interval'] + time.sleep(interval) + + except Exception as e: + print(f"更新面板 {panel_id} 数据时出错: {e}") + time.sleep(1.0) # 出错时等待1秒再重试 + + def unregisterDataSource(self, panel_id): + """ + 注销数据源 + """ + if panel_id in self.data_sources: + self.data_sources[panel_id]['stop'] = True + del self.data_sources[panel_id] + return True + return False + + def updatePanelProperties(self, panel_id, **properties): + """ + 更新面板属性 + """ + if panel_id not in self.panels: + print(f"面板 {panel_id} 不存在") + return False + + panel_data = self.panels[panel_id] + props = panel_data['properties'] + + # 更新位置 + if 'position' in properties: + pos = properties['position'] + panel_data['node'].setPos(pos[0], 0, pos[1]) + props['position'] = pos + + # 更新大小 + if 'size' in properties: + size = properties['size'] + panel_data['panel']['frameSize'] = (-size[0] / 2, size[0] / 2, -size[1] / 2, size[1] / 2) + props['size'] = size + + # 更新边框 + border_thickness = 0.005 + title_bar_height = 0.08 + + # 更新边框位置和大小 + panel_data['borders']['top']['frameSize'] = ( + -size[0] / 2 - border_thickness, size[0] / 2 + border_thickness, + size[1] / 2, size[1] / 2 + border_thickness + ) + + panel_data['borders']['bottom']['frameSize'] = ( + -size[0] / 2 - border_thickness, size[0] / 2 + border_thickness, + -size[1] / 2 - border_thickness, -size[1] / 2 + ) + + panel_data['borders']['left']['frameSize'] = ( + -size[0] / 2 - border_thickness, -size[0] / 2, + -size[1] / 2, size[1] / 2 + ) + + panel_data['borders']['right']['frameSize'] = ( + size[0] / 2, size[0] / 2 + border_thickness, + -size[1] / 2, size[1] / 2 + ) + + # 更新标题栏 + panel_data['title_bar']['frameSize'] = ( + -size[0] / 2 + border_thickness, size[0] / 2 - border_thickness, + size[1] / 2 - title_bar_height, size[1] / 2 - border_thickness + ) + + # 更新标题位置 + panel_data['title_label'].setPos( + 0, 0, size[1] / 2 - title_bar_height / 2 - border_thickness + ) + + # 更新内容背景 + panel_data['content_bg']['frameSize'] = ( + -size[0] / 2 + border_thickness * 2, size[0] / 2 - border_thickness * 2, + -size[1] / 2 + border_thickness * 2, size[1] / 2 - title_bar_height - border_thickness * 2 + ) + + # 更新内容位置 + panel_data['content_label'].setPos( + -size[0] / 2 + 0.03, 0, size[1] / 2 - title_bar_height - 0.05 + ) + + # 设置一个非常大的换行值,几乎不自动换行 + panel_data['content_label']['text_wordwrap'] = 500 + print(f"更新面板换行: 设置为500(几乎不换行)") + + # 如果有背景图片,也需要更新其大小 + if 'bg_image' in panel_data and panel_data['bg_image']: + panel_data['bg_image']['frameSize'] = (-size[0] / 2, size[0] / 2, -size[1] / 2, size[1] / 2) + + # 更新背景颜色 + if 'bg_color' in properties: + panel_data['panel']['frameColor'] = properties['bg_color'] + props['bg_color'] = properties['bg_color'] + + # 更新边框颜色 + if 'border_color' in properties: + color = properties['border_color'] + for border in panel_data['borders'].values(): + border['frameColor'] = color + props['border_color'] = color + + # 更新标题颜色 + if 'title_color' in properties: + panel_data['title_label']['text_fg'] = properties['title_color'] + props['title_color'] = properties['title_color'] + + # 更新内容颜色 + if 'content_color' in properties: + panel_data['content_label']['text_fg'] = properties['content_color'] + props['content_color'] = properties['content_color'] + + # 更新标题 + if 'title' in properties: + panel_data['title_label'].setText(properties['title']) + + # 更新内容 + if 'content' in properties: + panel_data['content_label'].setText(properties['content']) + + # 更新字体大小 + if 'title_size' in properties: + panel_data['title_label']['text_scale'] = properties['title_size'] + props['title_size'] = properties['title_size'] + + if 'content_size' in properties: + panel_data['content_label']['text_scale'] = properties['content_size'] + props['content_size'] = properties['content_size'] + # 当字体大小改变时,仍然保持较大的换行值 + panel_data['content_label']['text_wordwrap'] = 500 + + # 更新背景图片 + if 'bg_image' in properties: + image_path = properties['bg_image'] + # 无论image_path是None、空字符串还是有效路径,都调用setPanelBackgroundImage处理 + self.setPanelBackgroundImage(panel_id, image_path) + # 同步更新properties中的bg_image值 + props['bg_image'] = image_path + + return True + + def showPanel(self, panel_id): + """显示指定面板""" + if panel_id in self.panels: + self.panels[panel_id]['node'].show() + return True + return False + + def hidePanel(self, panel_id): + """隐藏指定面板""" + if panel_id in self.panels: + self.panels[panel_id]['node'].hide() + return True + return False + + def togglePanel(self, panel_id): + """切换面板显示/隐藏状态""" + if panel_id in self.panels: + node = self.panels[panel_id]['node'] + if node.isHidden(): + node.show() + else: + node.hide() + return True + return False + + def getPanelNode(self, panel_id): + """获取面板的节点""" + if panel_id in self.panels: + return self.panels[panel_id]['node'] + return None + + def removePanel(self, panel_id): + """移除指定面板""" + if panel_id in self.panels: + # 停止数据源更新 + self.unregisterDataSource(panel_id) + + panel_data = self.panels[panel_id] + # 清理所有子元素 + panel_data['node'].removeNode() + # 从字典中移除 + del self.panels[panel_id] + return True + return False + + def removeAllPanels(self): + """移除所有面板""" + panel_ids = list(self.panels.keys()) + for panel_id in panel_ids: + self.removePanel(panel_id) + + def createHTTPInfoPanel(self, panel_id, url, method="GET", headers=None, data=None, + position=(0, 0), size=(1.0, 0.6), + bg_color=(0.15, 0.15, 0.15, 0.9), + border_color=(0.3, 0.3, 0.3, 1.0), + title_color=(1.0, 1.0, 1.0, 1.0), + content_color=(0.9, 0.9, 0.9, 1.0), + update_interval=30.0, font=None): + """ + 创建HTTP信息面板 + panel_id: 面板ID + url: 请求的URL + method: HTTP方法 + headers: 请求头 + data: POST数据 + position: 位置 + size: 大小 + bg_color: 背景颜色 + border_color: 边框颜色 + title_color: 标题颜色 + content_color: 内容颜色 + update_interval: 更新间隔(秒) + font: 字体 + """ + # 创建面板 + domain = urlparse(url).netloc or url[:30] + title = f"HTTP数据: {domain}" + + panel_node = self.createInfoPanel( + panel_id=panel_id, + position=position, + size=size, + bg_color=bg_color, + border_color=border_color, + title_color=title_color, + content_color=content_color, + font=font + ) + + # 更新标题 + self.updatePanelContent(panel_id, title=title) + + # 立即获取并显示数据 + content = fetchHTTPData(url, method, headers, data) + self.updatePanelContent(panel_id, content=content) + + # 注册数据源,定期更新 + def http_data_callback(): + return fetchHTTPData(url, method, headers, data) + + self.registerDataSource(panel_id, http_data_callback, update_interval) + + # 保存HTTP请求信息,便于后续更新 + if panel_id not in self.data_sources: + self.data_sources[panel_id] = {} + self.data_sources[panel_id]['http_info'] = { + 'url': url, + 'method': method, + 'headers': headers, + 'data': data + } + + return panel_node + + def updateHTTPInfoPanel(self, panel_id, url=None, method=None, headers=None, data=None): + """ + 更新HTTP信息面板的请求参数 + """ + if panel_id not in self.panels: + print(f"面板 {panel_id} 不存在") + return False + + # 更新HTTP信息 + if panel_id in self.data_sources and 'http_info' in self.data_sources[panel_id]: + http_info = self.data_sources[panel_id]['http_info'] + if url is not None: + http_info['url'] = url + if method is not None: + http_info['method'] = method + if headers is not None: + http_info['headers'] = headers + if data is not None: + http_info['data'] = data + + # 更新面板标题 + if url is not None: + domain = urlparse(url).netloc or url[:30] + title = f"HTTP数据: {domain}" + self.updatePanelContent(panel_id, title=title) + + # 立即获取并显示新数据 + content = fetchHTTPData( + http_info['url'], + http_info['method'], + http_info['headers'], + http_info['data'] + ) + self.updatePanelContent(panel_id, content=content) + + return True + + # 在 add_methods_to_property_panel 函数中添加以下方法 + + def add_methods_to_property_panel(property_panel_instance): + # ... (原有代码保持不变) + + def createHTTPInfoPanel(self, url, panel_id="http_info", method="GET", headers=None, data=None, + position=(0.8, 0.0), size=(0.4, 0.3), update_interval=30.0): + """ + 为属性面板创建HTTP信息面板 + url: 请求的URL + panel_id: 面板ID + method: HTTP方法 + headers: 请求头 + data: POST数据 + position: 位置 + size: 大小 + update_interval: 更新间隔(秒) + """ + try: + # 确保父节点已设置 + if self.info_panel_manager.parent is None and hasattr(self, 'world'): + self.info_panel_manager.setParent(self.world.render) + + # 创建HTTP信息面板 + panel_node = self.info_panel_manager.createHTTPInfoPanel( + panel_id=panel_id, + url=url, + method=method, + headers=headers, + data=data, + position=position, + size=size, + update_interval=update_interval, + bg_color=(0.15, 0.25, 0.35, 0.95), # 蓝色背景 + 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) + ) + + # 设置标签 + panel_node.setTag("element_type", "info_panel") + panel_node.setTag("is_scene_element", "1") + panel_node.setTag("supports_3d_position_editing", "1") # 支持3D位置编辑 + + print(f"✓ 已创建HTTP信息面板: {url}") + + return panel_node + + except Exception as e: + print(f"✗ 创建HTTP信息面板失败: {e}") + return None + + def updateHTTPInfoPanel(self, panel_id, url=None, method=None, headers=None, data=None): + """ + 更新HTTP信息面板 + """ + try: + if hasattr(self, 'info_panel_manager'): + return self.info_panel_manager.updateHTTPInfoPanel(panel_id, url, method, headers, data) + return False + except Exception as e: + print(f"✗ 更新HTTP信息面板失败: {e}") + return False + + # 将新方法绑定到实例 + import types + # ... (原有绑定保持不变) + property_panel_instance.createHTTPInfoPanel = types.MethodType(createHTTPInfoPanel, property_panel_instance) + property_panel_instance.updateHTTPInfoPanel = types.MethodType(updateHTTPInfoPanel, property_panel_instance) + + +# 示例数据源函数 +def getRealtimeData(): + """ + 获取实时数据的示例函数 + """ + import random + from datetime import datetime + + # 模拟实时数据 + value1 = round(random.uniform(10, 100), 2) + value2 = round(random.uniform(0, 50), 2) + timestamp = datetime.now().strftime("%H:%M:%S") + + return f"实时值1: {value1}\n实时值2: {value2}\n更新时间: {timestamp}" + + +# 使用示例函数 +def createSampleInfoPanel(parent_node): + """创建示例信息面板""" + + # 创建信息面板管理器 + info_manager = InfoPanelManager(parent_node) + + # 创建一个实时数据面板 + info_manager.createInfoPanel( + panel_id="realtime_data", + position=(0.8, 0.0), + size=(0.35, 0.3), + bg_color=(0.15, 0.25, 0.35, 0.95), # 蓝色背景 + 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) + ) + + # 立即显示初始数据 + initial_data = getRealtimeData() + info_manager.updatePanelContent("realtime_data", content=initial_data) + + # 注册数据源,每秒更新一次 + info_manager.registerDataSource("realtime_data", getRealtimeData, update_interval=1.0) + + return info_manager + + +# 集成到您的 property_panel.py 中的方法 +def add_methods_to_property_panel(property_panel_instance): + """ + 为 property_panel 实例添加 DirectGUI 信息面板支持 + """ + + # 添加信息面板管理器作为类属性 + if not hasattr(property_panel_instance, 'info_panel_manager'): + if hasattr(property_panel_instance, 'world') and hasattr(property_panel_instance.world, 'render'): + property_panel_instance.info_panel_manager = InfoPanelManager(property_panel_instance.world.render) + else: + property_panel_instance.info_panel_manager = InfoPanelManager() + + def createRealtimeDataPanel(self, data_callback=None, update_interval=1.0): + """创建实时数据面板""" + try: + # 确保父节点已设置 + if self.info_panel_manager.parent is None and hasattr(self, 'world'): + self.info_panel_manager.setParent(self.world.render) + + # 创建实时数据面板 + panel_node = self.info_panel_manager.createInfoPanel( + panel_id="realtime_data", + position=(0.8, 0.0), + size=(0.35, 0.3), + bg_color=(0.15, 0.25, 0.35, 0.95), # 蓝色背景 + 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) + ) + + # 设置标签 + panel_node.setTag("element_type", "info_panel") + panel_node.setTag("is_scene_element", "1") + panel_node.setTag("supports_3d_position_editing", "1") # 支持3D位置编辑 + + # 如果提供了数据回调函数,则注册数据源 + if data_callback: + # 立即显示初始数据 + initial_data = data_callback() + self.info_panel_manager.updatePanelContent("realtime_data", content=initial_data) + + # 注册数据源 + self.info_panel_manager.registerDataSource("realtime_data", data_callback, update_interval) + else: + # 使用默认数据 + default_data = "等待数据..." + self.info_panel_manager.updatePanelContent("realtime_data", content=default_data) + + print("✓ 已创建实时数据面板") + + return panel_node + + except Exception as e: + print(f"✗ 创建实时数据面板失败: {e}") + return None + + + def createModelInfoPanel(self, model): + """为模型创建信息面板""" + try: + # 确保父节点已设置 + if self.info_panel_manager.parent is None and hasattr(self, 'world'): + self.info_panel_manager.setParent(self.world.render) + + # 获取模型信息 + model_name = model.getName() if hasattr(model, 'getName') else 'Unknown' + num_children = model.getNumChildren() if hasattr(model, 'getNumChildren') else 0 + + # 创建面板内容 + content = f"模型名称: {model_name}\n子节点数: {num_children}\n类型: {type(model).__name__}" + + # 创建或更新面板 + panel_node = self.info_panel_manager.createInfoPanel( + panel_id="model_info", + position=(0.8, 0.7), + size=(0.35, 0.25), + bg_color=(0.15, 0.15, 0.25, 0.95), # 蓝紫色背景 + border_color=(0.3, 0.3, 0.7, 1.0), # 蓝色边框 + title_color=(0.5, 0.8, 1.0, 1.0), # 浅蓝色标题 + content_color=(0.95, 0.95, 0.95, 1.0) + ) + + # 更新面板内容为模型特定信息 + self.info_panel_manager.updatePanelContent("model_info", + title="模型信息", + content=content) + + # 设置标签 + panel_node.setTag("element_type", "info_panel") + panel_node.setTag("is_scene_element", "1") + panel_node.setTag("supports_3d_position_editing", "1") # 支持3D位置编辑 + + print(f"✓ 已创建模型信息面板: {model_name}") + + return panel_node + + except Exception as e: + print(f"✗ 创建模型信息面板失败: {e}") + return None + + def createTerrainInfoPanel(self, terrain_info): + """为地形创建信息面板""" + try: + # 确保父节点已设置 + if self.info_panel_manager.parent is None and hasattr(self, 'world'): + self.info_panel_manager.setParent(self.world.render) + + # 获取地形信息 + terrain_name = terrain_info.get('name', 'Unknown') + width = terrain_info.get('width', 0) + height = terrain_info.get('height', 0) + resolution = terrain_info.get('resolution', 0) + + # 创建面板内容 + content = f"地形名称: {terrain_name}\n大小: {width} x {height}\n分辨率: {resolution}" + + # 创建或更新面板 + panel_node = self.info_panel_manager.createInfoPanel( + panel_id="terrain_info", + position=(0.8, 0.7), + size=(0.35, 0.25), + 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) + ) + + # 更新面板内容为地形特定信息 + self.info_panel_manager.updatePanelContent("terrain_info", + title="地形信息", + content=content) + + panel_node.setTag("element_type", "info_panel") + panel_node.setTag("is_scene_element", "1") + panel_node.setTag("supports_3d_position_editing", "1") # 支持3D位置编辑 + + print(f"✓ 已创建地形信息面板: {terrain_name}") + + return panel_node + + except Exception as e: + print(f"✗ 创建地形信息面板失败: {e}") + return None + + def createLightInfoPanel(self, light_object): + """为光源创建信息面板""" + try: + # 确保父节点已设置 + if self.info_panel_manager.parent is None and hasattr(self, 'world'): + self.info_panel_manager.setParent(self.world.render) + + # 获取光源信息 + light_type = type(light_object).__name__ + casts_shadows = getattr(light_object, 'casts_shadows', 'Unknown') + shadow_res = getattr(light_object, 'shadow_map_resolution', 'Unknown') + + # 创建面板内容 + content = f"光源类型: {light_type}\n投射阴影: {casts_shadows}\n阴影分辨率: {shadow_res}" + + # 创建或更新面板 + panel_node = self.info_panel_manager.createInfoPanel( + panel_id="light_info", + position=(0.8, 0.7), + size=(0.35, 0.25), + bg_color=(0.25, 0.25, 0.15, 0.95), # 黄色背景 + border_color=(0.7, 0.7, 0.3, 1.0), # 黄色边框 + title_color=(1.0, 1.0, 0.5, 1.0), # 浅黄色标题 + content_color=(0.95, 0.95, 0.95, 1.0) + ) + + # 更新面板内容为光源特定信息 + self.info_panel_manager.updatePanelContent("light_info", + title="光源信息", + content=content) + + panel_node.setTag("element_type", "info_panel") + panel_node.setTag("is_scene_element", "1") + panel_node.setTag("supports_3d_position_editing", "1") # 支持3D位置编辑 + + print(f"✓ 已创建光源信息面板") + + return panel_node + + except Exception as e: + print(f"✗ 创建光源信息面板失败: {e}") + return None + + def removeInfoPanel(self, panel_id="model_info"): + """移除信息面板""" + if hasattr(self, 'info_panel_manager'): + self.info_panel_manager.removePanel(panel_id) + + def setupInfoPanelManager(self): + """初始化信息面板管理器""" + if not hasattr(self, 'info_panel_manager'): + if hasattr(self, 'world') and hasattr(self.world, 'render'): + self.info_panel_manager = InfoPanelManager(self.world.render) + else: + self.info_panel_manager = InfoPanelManager() + + # 将方法绑定到实例 + import types + property_panel_instance.createRealtimeDataPanel = types.MethodType(createRealtimeDataPanel, property_panel_instance) + property_panel_instance.createModelInfoPanel = types.MethodType(createModelInfoPanel, property_panel_instance) + property_panel_instance.createTerrainInfoPanel = types.MethodType(createTerrainInfoPanel, property_panel_instance) + property_panel_instance.createLightInfoPanel = types.MethodType(createLightInfoPanel, property_panel_instance) + property_panel_instance.removeInfoPanel = types.MethodType(removeInfoPanel, property_panel_instance) + property_panel_instance.setupInfoPanelManager = types.MethodType(setupInfoPanelManager, property_panel_instance) + + +def fetchHTTPData(url, method="GET", headers=None, data=None, timeout=5): + """ + 获取HTTP数据的通用函数 + url: 请求的URL + method: HTTP方法 (GET, POST, etc.) + headers: 请求头 + data: POST数据 + timeout: 超时时间(秒) + """ + try: + if headers is None: + headers = { + 'User-Agent': 'InfoPanelManager/1.0', + 'Accept': 'application/json,text/plain,*/*' + } + + # 根据方法发送请求 + if method.upper() == "GET": + response = requests.get(url, headers=headers, timeout=timeout) + elif method.upper() == "POST": + response = requests.post(url, headers=headers, data=data, timeout=timeout) + else: + response = requests.request(method, url, headers=headers, data=data, timeout=timeout) + + # 检查响应状态 + response.raise_for_status() + + # 尝试解析JSON + try: + json_data = response.json() + return formatJSONData(json_data) + except: + # 如果不是JSON,返回文本内容 + return response.text[:500] # 限制长度 + + except requests.exceptions.Timeout: + return "错误: 请求超时" + except requests.exceptions.ConnectionError: + return "错误: 连接失败" + except requests.exceptions.HTTPError as e: + return f"HTTP错误: {e}" + except Exception as e: + return f"获取数据失败: {str(e)}" + + +def formatJSONData(data, indent=0): + """ + 格式化JSON数据为易读的文本 + """ + if isinstance(data, dict): + lines = [] + for key, value in data.items(): + if isinstance(value, (dict, list)): + lines.append(f"{' ' * indent}{key}:") + lines.append(formatJSONData(value, indent + 1)) + else: + lines.append(f"{' ' * indent}{key}: {value}") + return "\n".join(lines) + elif isinstance(data, list): + lines = [] + for i, item in enumerate(data): + if isinstance(item, (dict, list)): + lines.append(f"{' ' * indent}[{i}]:") + lines.append(formatJSONData(item, indent + 1)) + else: + lines.append(f"{' ' * indent}[{i}] {item}") + return "\n".join(lines) + else: + return str(data) diff --git a/core/world.py b/core/world.py index 4c6aacd9..4860ad50 100644 --- a/core/world.py +++ b/core/world.py @@ -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加载诊断 ===") diff --git a/gui/gui_manager.py b/gui/gui_manager.py index 2ae54393..babc94c2 100644 --- a/gui/gui_manager.py +++ b/gui/gui_manager.py @@ -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()) diff --git a/main.py b/main.py index 10604e61..137b8566 100644 --- a/main.py +++ b/main.py @@ -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): diff --git a/scene/scene_manager.py b/scene/scene_manager.py index e81325e7..69ee1d49 100644 --- a/scene/scene_manager.py +++ b/scene/scene_manager.py @@ -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) diff --git a/ui/main_window.py b/ui/main_window.py index d44cc169..bce4d014 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -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): diff --git a/ui/property_panel.py b/ui/property_panel.py index 534aae92..fe7c776b 100644 --- a/ui/property_panel.py +++ b/ui/property_panel.py @@ -4,16 +4,20 @@ from traceback import print_exc, print_last from types import new_class from typing import Hashable +from PyQt5.QtGui import QColor from PyQt5.QtWidgets import (QLabel, QLineEdit, QDoubleSpinBox, QPushButton, QTreeWidget, QTreeWidgetItem, QMenu, QCheckBox, QComboBox, QHBoxLayout, QWidget, QVBoxLayout, QGroupBox, QGridLayout, QSpinBox, QFileDialog) from PyQt5.QtCore import Qt from deploy_libs.unicodedata import normalize from direct.actor.Actor import Actor +from direct.gui import DirectGui from idna import check_label from jinja2.compiler import has_safe_repr from panda3d.core import Vec3, Vec4, transpose, TransparencyAttrib, PartGroup from scene import util +from direct.gui.DirectGui import DirectLabel, DirectFrame +from panda3d.core import TextNode class PropertyPanelManager: @@ -535,12 +539,25 @@ class PropertyPanelManager: # 清理变换控件引用 self._cleanupTransformControls() + # 清理信息面板监控 + if hasattr(self, '_info_panel_timer'): + self._info_panel_timer.stop() + self._info_panel_timer.deleteLater() + del self._info_panel_timer + + # 清理信息面板控件引用 + if hasattr(self, '_info_panel_controls'): + self._info_panel_controls.clear() + del self._info_panel_controls + + if hasattr(self, '_current_info_panel'): + del self._current_info_panel + # 清理其他可能的控件引用 other_controls = ['scale_x', 'scale_y', 'scale_z', 'pos_x', 'pos_y', 'pos_z'] for name in other_controls: if hasattr(self, name): setattr(self, name, None) - def _setUserVisible(self,node,visible): node.setPythonTag("user_visible",visible) self._syncEffectiveVisibility(node) @@ -1355,8 +1372,8 @@ class PropertyPanelManager: transform_layout.addWidget(QLabel("宽度"),4,0) self.scale_x = QDoubleSpinBox() - self.scale_x.setRange(0.1,10) - self.scale_x.setSingleStep(0.1) + self.scale_x.setRange(0.1,100) + self.scale_x.setSingleStep(0.01) self.scale_x.setValue(width) self.scale_x.valueChanged.connect(lambda v:self.editGUIScale(gui_element,"x",v)) transform_layout.addWidget(self.scale_x,4,1) @@ -1364,7 +1381,7 @@ class PropertyPanelManager: transform_layout.addWidget(QLabel("高度"),4,2) self.scale_z = QDoubleSpinBox() self.scale_z.setRange(0.1,10) - self.scale_z.setSingleStep(0.1) + self.scale_z.setSingleStep(0.01) self.scale_z.setValue(height) self.scale_z.valueChanged.connect(lambda v:self.editGUIScale(gui_element,"z",v)) transform_layout.addWidget(self.scale_z,4,3) @@ -1372,8 +1389,8 @@ class PropertyPanelManager: else: transform_layout.addWidget(QLabel("缩放"),4,0) scaleSpinBox = QDoubleSpinBox() - scaleSpinBox.setRange(0.01,10) - scaleSpinBox.setSingleStep(0.1) + scaleSpinBox.setRange(0.01,100) + scaleSpinBox.setSingleStep(0.01) scaleSpinBox.setValue(gui_element.getScale().getX()*2) scaleSpinBox.valueChanged.connect(lambda v:self._update2DImageScale(gui_element,v)) transform_layout.addWidget(scaleSpinBox,4,1) @@ -1398,18 +1415,21 @@ class PropertyPanelManager: self.pos_x = QDoubleSpinBox() self.pos_x.setRange(-100, 100) self.pos_x.setValue(pos.getX()) + self.pos_x.setSingleStep(0.01) self.pos_x.valueChanged.connect(lambda v: self.world.gui_manager.editGUI3DPosition(gui_element, "x", v)) transform_layout.addWidget(self.pos_x, 1, 1) self.pos_y = QDoubleSpinBox() self.pos_y.setRange(-100, 100) self.pos_y.setValue(pos.getY()) + self.pos_y.setSingleStep(0.01) self.pos_y.valueChanged.connect(lambda v: self.world.gui_manager.editGUI3DPosition(gui_element, "y", v)) transform_layout.addWidget(self.pos_y, 1, 2) self.pos_z = QDoubleSpinBox() self.pos_z.setRange(-100, 100) self.pos_z.setValue(pos.getZ()) + self.pos_z.setSingleStep(0.01) self.pos_z.valueChanged.connect(lambda v: self.world.gui_manager.editGUI3DPosition(gui_element, "z", v)) transform_layout.addWidget(self.pos_z, 1, 3) @@ -1432,7 +1452,7 @@ class PropertyPanelManager: # 缩放数值输入框 self.scale_x = QDoubleSpinBox() self.scale_x.setRange(0.01, 100) - self.scale_x.setSingleStep(0.1) + self.scale_x.setSingleStep(0.01) self.scale_x.setValue( scale.getX() if hasattr(scale, 'getX') else scale[0] if isinstance(scale, (tuple, list)) else scale) self.scale_x.valueChanged.connect(lambda v: self.world.gui_manager.editGUIScale(gui_element, "x", v)) @@ -1440,7 +1460,7 @@ class PropertyPanelManager: self.scale_y = QDoubleSpinBox() self.scale_y.setRange(0.01, 100) - self.scale_y.setSingleStep(0.1) + self.scale_y.setSingleStep(0.01) self.scale_y.setValue( scale.getY() if hasattr(scale, 'getY') else scale[1] if isinstance(scale, (tuple, list)) and len( scale) > 1 else scale) @@ -1449,7 +1469,7 @@ class PropertyPanelManager: self.scale_z = QDoubleSpinBox() self.scale_z.setRange(0.01, 100) - self.scale_z.setSingleStep(0.1) + self.scale_z.setSingleStep(0.01) self.scale_z.setValue( scale.getZ() if hasattr(scale, 'getZ') else scale[2] if isinstance(scale, (tuple, list)) and len( scale) > 2 else scale) @@ -1461,7 +1481,7 @@ class PropertyPanelManager: self._propertyLayout.addWidget(transform_group) # 为2D图像和视频屏幕添加Sort属性 - if gui_type in ["2d_image","2d_video_screen"]: + if gui_type in ["2d_image","2d_video_screen","info_panel"]: sort_group = QGroupBox("显示顺序") sort_layout = QGridLayout() @@ -1521,14 +1541,19 @@ class PropertyPanelManager: image_group = QGroupBox("图片设置") image_layout = QGridLayout() - # 当前图片路径标签 + # 当前图片路径标签和 current_image_label = QLabel("当前图片:") image_layout.addWidget(current_image_label, 0, 0) # 显示当前贴图路径(简化显示) current_texture_path = gui_element.getTag("texture_path") or "未设置" - texture_label = QLabel(current_texture_path) + if current_texture_path != "未设置": + display_path = current_texture_path if len(current_texture_path) <= 10 else current_texture_path[:7] + "..." + else: + display_path = current_texture_path + texture_label = QLabel(display_path) texture_label.setWordWrap(True) + texture_label.setToolTip(current_texture_path if current_texture_path != "未设置" else "") image_layout.addWidget(texture_label, 0, 1) # 选择图片按钮 @@ -1561,7 +1586,7 @@ class PropertyPanelManager: image_group.setLayout(image_layout) self._propertyLayout.addWidget(image_group) - if gui_type == "2d_image": + if gui_type in [ "2d_image"]: image_group = QGroupBox("2D图片设置") image_layout = QGridLayout() @@ -1571,7 +1596,11 @@ class PropertyPanelManager: # 显示当前贴图路径(简化显示) current_texture_path = gui_element.getTag("texture_path") or gui_element.getTag("image_path") or "未设置" - texture_label = QLabel(current_texture_path) + if current_texture_path != "未设置": + display_path = current_texture_path if len(current_texture_path) <= 10 else current_texture_path[:7] + "..." + else: + display_path = current_texture_path + texture_label = QLabel(display_path) texture_label.setWordWrap(True) image_layout.addWidget(texture_label, 0, 1) @@ -1609,10 +1638,12 @@ class PropertyPanelManager: if gui_type == "video_screen": self._addVideoScreenProperties(gui_element) - if gui_type == "2d_video_screen": + elif gui_type == "2d_video_screen": self._add2DVideoScreenProperties(gui_element) - if gui_type == "spherical_video": + elif gui_type == "spherical_video": self._addSphericalVideoProperties(gui_element) + elif gui_type == 'info_panel': + self._addInfoPanelProperties(gui_element) self._propertyLayout.addStretch() @@ -1623,6 +1654,973 @@ class PropertyPanelManager: if propertyWidget: propertyWidget.update() + def _addInfoPanelProperties(self, info_panel): + """为信息面板添加属性控制面板""" + try: + from PyQt5.QtWidgets import (QGroupBox, QGridLayout, QPushButton, QLabel, + QDoubleSpinBox, QColorDialog, QSpinBox, QTextEdit) + from PyQt5.QtCore import Qt, QTimer + from PyQt5.QtGui import QColor + import os + + panel_id = info_panel.getTag("panel_id") + if not panel_id: + print("无法找到信息面板ID") + return + + # 获取面板当前属性 + current_size = (1.0, 0.6) + current_position = (0, 0) + current_bg_color = (0.15, 0.15, 0.15, 0.9) + current_border_color = (0.3, 0.3, 0.3, 1.0) + current_title_color = (1.0, 1.0, 0.0, 1.0) + current_content_color = (0.9, 0.9, 0.9, 1.0) + current_title_size = 0.06 # 默认值 + current_content_size = 0.045 # 默认值 + current_title_text = "信息面板" + current_content_text = "这是一个信息面板" + current_bg_image = "" + current_sort = 0 + + # 标题和内容位置默认值 + current_title_pos = (0, 0) + current_content_pos = (-0.45, 0) + + # 从info_panel_manager获取当前属性 + if hasattr(self.world, 'info_panel_manager'): + panel_data = self.world.info_panel_manager.panels.get(panel_id) + if panel_data: + # 获取基本属性 + if 'properties' in panel_data: + props = panel_data['properties'] + current_size = props.get('size', current_size) + current_position = props.get('position', current_position) + current_bg_color = props.get('bg_color', current_bg_color) + current_border_color = props.get('border_color', current_border_color) + current_title_color = props.get('title_color', current_title_color) + current_content_color = props.get('content_color', current_content_color) + current_bg_image = props.get('bg_image', current_bg_image) + + # 获取文本内容 + if 'title_label' in panel_data and panel_data['title_label']: + current_title_text = panel_data['title_label']['text'] + # 获取标题位置 + title_pos = panel_data['title_label'].getPos() + current_title_pos = (title_pos.getX(), title_pos.getZ()) + + if 'content_label' in panel_data and panel_data['content_label']: + current_content_text = panel_data['content_label']['text'] + # 获取内容位置 + content_pos = panel_data['content_label'].getPos() + current_content_pos = (content_pos.getX(), content_pos.getZ()) + + # 获取字体大小(确保获取的是数值而不是元组) + if 'title_label' in panel_data and panel_data['title_label']: + # 正确获取DirectLabel的text_scale属性 + try: + title_scale = panel_data['title_label']['text_scale'] + # 如果是元组,取第一个值 + if isinstance(title_scale, (tuple, list)): + current_title_size = title_scale[0] if len(title_scale) > 0 else 0.06 + else: + current_title_size = title_scale if isinstance(title_scale, (int, float)) else 0.06 + except: + current_title_size = 0.06 + + if 'content_label' in panel_data and panel_data['content_label']: + # 正确获取DirectLabel的text_scale属性 + try: + content_scale = panel_data['content_label']['text_scale'] + # 如果是元组,取第一个值 + if isinstance(content_scale, (tuple, list)): + current_content_size = content_scale[0] if len(content_scale) > 0 else 0.045 + else: + current_content_size = content_scale if isinstance(content_scale, + (int, float)) else 0.045 + except: + current_content_size = 0.045 + + # 获取sort值 + if info_panel.hasTag("sort"): + try: + current_sort = int(info_panel.getTag("sort")) + except: + current_sort = 0 + + # 背景图片组 + image_group = QGroupBox("背景图片设置") + image_layout = QGridLayout() + + # 当前背景图片路径标签 + current_image_label = QLabel("当前背景图片:") + image_layout.addWidget(current_image_label, 0, 0) + + # 显示当前背景图片路径(简化显示) + if current_bg_image: + display_path = current_bg_image if len(current_bg_image) <= 12 else current_bg_image[:9] + "..." + else: + display_path = "未设置" + texture_label = QLabel(display_path) + texture_label.setWordWrap(True) + texture_label.setToolTip(current_bg_image if current_bg_image else "") + image_layout.addWidget(texture_label, 0, 1) + + # 选择背景图片按钮 + select_texture_button = QPushButton("选择背景图片...") + image_layout.addWidget(select_texture_button, 1, 0, 1, 2) + + # 清除背景图片按钮 + clear_texture_button = QPushButton("清除背景图片") + clear_texture_button.setStyleSheet("color: red;") + image_layout.addWidget(clear_texture_button, 2, 0, 1, 2) + + def onSelectBackgroundImage(): + """选择背景图片""" + from PyQt5.QtWidgets import QFileDialog + file_path, _ = QFileDialog.getOpenFileName( + None, + "选择背景图片", + "", + "图像文件 (*.png *.jpg *.jpeg *.bmp *.tga *.dds)" + ) + if file_path: + # 应用新背景图片到信息面板 + file_path = util.normalize_model_path(file_path) + success = self.updateInfoPanelBackgroundImage(info_panel, file_path) + if success: + # 保存路径到 Tag + info_panel.setTag("bg_image_path", file_path) + # 更新显示 + display_path = file_path if len(file_path) <= 12 else file_path[:9] + "..." + texture_label.setText(display_path) + texture_label.setToolTip(file_path) + + def onClearBackgroundImage(): + """清除背景图片""" + success = self.updateInfoPanelBackgroundImage(info_panel, None) + if success: + # 清除路径标签 + info_panel.clearTag("bg_image_path") + texture_label.setText("未设置") + texture_label.setToolTip("") + + select_texture_button.clicked.connect(onSelectBackgroundImage) + clear_texture_button.clicked.connect(onClearBackgroundImage) + + image_group.setLayout(image_layout) + self._propertyLayout.addWidget(image_group) + + # 显示顺序组 + sort_group = QGroupBox("显示顺序") + sort_layout = QGridLayout() + + sort_layout.addWidget(QLabel("显示优先度:"), 0, 0) + sort_spin = QSpinBox() + sort_spin.setRange(-1000, 1000) + sort_spin.setValue(current_sort) + + def updateSort(value): + try: + # 设置标签 + info_panel.setTag("sort", str(value)) + # 应用sort到节点 + info_panel.setBin("fixed", value) + print(f"✓ 更新信息面板渲染顺序: {value}") + except Exception as e: + print(f"✗ 更新信息面板渲染顺序失败: {e}") + + sort_spin.valueChanged.connect(updateSort) + sort_layout.addWidget(sort_spin, 0, 1) + + sort_group.setLayout(sort_layout) + self._propertyLayout.addWidget(sort_group) + + # 面板大小设置组 + size_group = QGroupBox("面板大小设置") + size_layout = QGridLayout() + + size_layout.addWidget(QLabel("宽度:"), 0, 0) + width_spin = QDoubleSpinBox() + width_spin.setRange(0.1, 5.0) + width_spin.setSingleStep(0.1) + width_spin.setValue(current_size[0]) + size_layout.addWidget(width_spin, 0, 1) + + size_layout.addWidget(QLabel("高度:"), 0, 2) + height_spin = QDoubleSpinBox() + height_spin.setRange(0.1, 5.0) + height_spin.setSingleStep(0.1) + height_spin.setValue(current_size[1]) + size_layout.addWidget(height_spin, 0, 3) + + # 连接大小变化信号 + def onSizeChanged(): + if hasattr(self.world, 'info_panel_manager'): + panel_width = width_spin.value() + panel_height = height_spin.value() + + # 更新面板大小 + self.world.info_panel_manager.updatePanelProperties( + panel_id, size=(panel_width, panel_height)) + + # 同步更新内容的换行设置 + self._adjustContentWordwrap(panel_id, panel_width, content_size_spin.value()) + + width_spin.valueChanged.connect(onSizeChanged) + height_spin.valueChanged.connect(onSizeChanged) + + size_group.setLayout(size_layout) + self._propertyLayout.addWidget(size_group) + + # 边框属性组 + border_group = QGroupBox("边框属性") + border_layout = QGridLayout() + + border_layout.addWidget(QLabel("边框颜色:"), 0, 0) + + # 边框R分量 + border_layout.addWidget(QLabel("R:"), 1, 0) + border_r_spin = QDoubleSpinBox() + border_r_spin.setRange(0.0, 1.0) + border_r_spin.setSingleStep(0.01) + border_r_spin.setValue(current_border_color[0]) + border_layout.addWidget(border_r_spin, 1, 1) + + # 边框G分量 + border_layout.addWidget(QLabel("G:"), 1, 2) + border_g_spin = QDoubleSpinBox() + border_g_spin.setRange(0.0, 1.0) + border_g_spin.setSingleStep(0.01) + border_g_spin.setValue(current_border_color[1]) + border_layout.addWidget(border_g_spin, 1, 3) + + # 边框B分量 + border_layout.addWidget(QLabel("B:"), 2, 0) + border_b_spin = QDoubleSpinBox() + border_b_spin.setRange(0.0, 1.0) + border_b_spin.setSingleStep(0.01) + border_b_spin.setValue(current_border_color[2]) + border_layout.addWidget(border_b_spin, 2, 1) + + # 边框A分量 + border_layout.addWidget(QLabel("A:"), 2, 2) + border_a_spin = QDoubleSpinBox() + border_a_spin.setRange(0.0, 1.0) + border_a_spin.setSingleStep(0.01) + border_a_spin.setValue(current_border_color[3]) + border_layout.addWidget(border_a_spin, 2, 3) + + # 边框颜色选择按钮 + border_color_button = QPushButton("选择边框颜色") + border_color_button.clicked.connect(lambda: self._selectInfoPanelBorderColor( + info_panel, border_r_spin, border_g_spin, border_b_spin, border_a_spin)) + border_layout.addWidget(border_color_button, 3, 0, 1, 4) + + # 连接边框颜色变化信号 + def onBorderColorChanged(): + if hasattr(self.world, 'info_panel_manager'): + self.world.info_panel_manager.updatePanelProperties( + panel_id, + border_color=( + border_r_spin.value(), + border_g_spin.value(), + border_b_spin.value(), + border_a_spin.value() + ) + ) + + border_r_spin.valueChanged.connect(onBorderColorChanged) + border_g_spin.valueChanged.connect(onBorderColorChanged) + border_b_spin.valueChanged.connect(onBorderColorChanged) + border_a_spin.valueChanged.connect(onBorderColorChanged) + + border_group.setLayout(border_layout) + self._propertyLayout.addWidget(border_group) + + bg_color_group = QGroupBox("背景颜色设置") + bg_color_layout = QGridLayout() + + bg_color_layout.addWidget(QLabel("背景颜色:"), 0, 0) + + bg_color_layout.addWidget(QLabel("R:"), 1, 0) + r_spin = QDoubleSpinBox() + r_spin.setRange(0.0, 1.0) + r_spin.setSingleStep(0.01) + r_spin.setValue(current_bg_color[0]) + bg_color_layout.addWidget(r_spin, 1, 1) + + # G分量 + bg_color_layout.addWidget(QLabel("G:"), 1, 2) + g_spin = QDoubleSpinBox() + g_spin.setRange(0.0, 1.0) + g_spin.setSingleStep(0.01) + g_spin.setValue(current_bg_color[1]) + bg_color_layout.addWidget(g_spin, 1, 3) + + # B分量 + bg_color_layout.addWidget(QLabel("B:"), 2, 0) + b_spin = QDoubleSpinBox() + b_spin.setRange(0.0, 1.0) + b_spin.setSingleStep(0.01) + b_spin.setValue(current_bg_color[2]) + bg_color_layout.addWidget(b_spin, 2, 1) + + # A分量 + bg_color_layout.addWidget(QLabel("A:"), 2, 2) + a_spin = QDoubleSpinBox() + a_spin.setRange(0.0, 1.0) + a_spin.setSingleStep(0.01) + a_spin.setValue(current_bg_color[3]) + bg_color_layout.addWidget(a_spin, 2, 3) + + # 颜色选择按钮 + color_button = QPushButton("选择颜色") + color_button.clicked.connect(lambda: self._selectInfoPanelBackgroundColor( + info_panel, r_spin, g_spin, b_spin, a_spin)) + bg_color_layout.addWidget(color_button, 3, 0, 1, 4) + + # 连接颜色数值变化信号,自动更新背景颜色 + def onBackgroundColorChanged(): + self._applyInfoPanelBackgroundColor( + info_panel, r_spin.value(), g_spin.value(), b_spin.value(), a_spin.value()) + + r_spin.valueChanged.connect(onBackgroundColorChanged) + g_spin.valueChanged.connect(onBackgroundColorChanged) + b_spin.valueChanged.connect(onBackgroundColorChanged) + a_spin.valueChanged.connect(onBackgroundColorChanged) + + bg_color_group.setLayout(bg_color_layout) + self._propertyLayout.addWidget(bg_color_group) + + # 标题属性组 + title_group = QGroupBox("标题属性") + title_layout = QGridLayout() + + # 标题文本 + title_layout.addWidget(QLabel("标题文本:"), 0, 0) + title_text_edit = QTextEdit() + title_text_edit.setMaximumHeight(60) + title_text_edit.setPlainText(current_title_text) + title_layout.addWidget(title_text_edit, 0, 1, 1, 3) + + # 标题颜色 + title_layout.addWidget(QLabel("标题颜色:"), 1, 0) + + # 标题R分量 + title_layout.addWidget(QLabel("R:"), 2, 0) + title_r_spin = QDoubleSpinBox() + title_r_spin.setRange(0.0, 1.0) + title_r_spin.setSingleStep(0.01) + title_r_spin.setValue(current_title_color[0]) + title_layout.addWidget(title_r_spin, 2, 1) + + # 标题G分量 + title_layout.addWidget(QLabel("G:"), 2, 2) + title_g_spin = QDoubleSpinBox() + title_g_spin.setRange(0.0, 1.0) + title_g_spin.setSingleStep(0.01) + title_g_spin.setValue(current_title_color[1]) + title_layout.addWidget(title_g_spin, 2, 3) + + # 标题B分量 + title_layout.addWidget(QLabel("B:"), 3, 0) + title_b_spin = QDoubleSpinBox() + title_b_spin.setRange(0.0, 1.0) + title_b_spin.setSingleStep(0.01) + title_b_spin.setValue(current_title_color[2]) + title_layout.addWidget(title_b_spin, 3, 1) + + # 标题A分量 + title_layout.addWidget(QLabel("A:"), 3, 2) + title_a_spin = QDoubleSpinBox() + title_a_spin.setRange(0.0, 1.0) + title_a_spin.setSingleStep(0.01) + title_a_spin.setValue(current_title_color[3]) + title_layout.addWidget(title_a_spin, 3, 3) + + # 标题颜色选择按钮 + title_color_button = QPushButton("选择标题颜色") + title_color_button.clicked.connect(lambda: self._selectInfoPanelTitleColor( + info_panel, title_r_spin, title_g_spin, title_b_spin, title_a_spin)) + title_layout.addWidget(title_color_button, 4, 0, 1, 4) + + # 标题字体大小 + title_layout.addWidget(QLabel("标题字体大小:"), 5, 0) + title_size_spin = QDoubleSpinBox() + title_size_spin.setRange(0.01, 1.0) + title_size_spin.setSingleStep(0.01) + title_size_spin.setValue(current_title_size) # 现在确保是浮点数 + title_layout.addWidget(title_size_spin, 5, 1, 1, 3) + + # 标题位置控制 + title_layout.addWidget(QLabel("标题位置:"), 6, 0) + title_layout.addWidget(QLabel("X:"), 7, 0) + title_x_spin = QDoubleSpinBox() + title_x_spin.setRange(-1.0, 1.0) + title_x_spin.setSingleStep(0.01) + title_x_spin.setValue(current_title_pos[0]) # 从面板获取的实际位置 + title_layout.addWidget(title_x_spin, 7, 1) + + title_layout.addWidget(QLabel("Y:"), 7, 2) + title_y_spin = QDoubleSpinBox() + title_y_spin.setRange(-1.0, 1.0) + title_y_spin.setSingleStep(0.01) + title_y_spin.setValue(current_title_pos[1]) # 从面板获取的实际位置 + title_layout.addWidget(title_y_spin, 7, 3) + + # 连接标题属性变化信号,自动更新标题属性 + def onTitleTextChanged(): + if hasattr(self.world, 'info_panel_manager'): + self.world.info_panel_manager.updatePanelContent( + panel_id, title=title_text_edit.toPlainText()) + + def onTitlePropertyChanged(): + self._applyInfoPanelTitleProperties( + info_panel, title_r_spin.value(), title_g_spin.value(), title_b_spin.value(), + title_a_spin.value(), title_size_spin.value()) + + def onTitlePositionChanged(): + # 更新标题位置 + if hasattr(self.world, 'info_panel_manager'): + panel_data = self.world.info_panel_manager.panels.get(panel_id) + if panel_data and 'title_label' in panel_data: + # 设置标题位置 + panel_data['title_label'].setPos(title_x_spin.value(), 0, title_y_spin.value()) + + title_text_edit.textChanged.connect(onTitleTextChanged) + title_r_spin.valueChanged.connect(onTitlePropertyChanged) + title_g_spin.valueChanged.connect(onTitlePropertyChanged) + title_b_spin.valueChanged.connect(onTitlePropertyChanged) + title_a_spin.valueChanged.connect(onTitlePropertyChanged) + title_size_spin.valueChanged.connect(onTitlePropertyChanged) + title_x_spin.valueChanged.connect(onTitlePositionChanged) + title_y_spin.valueChanged.connect(onTitlePositionChanged) + + title_group.setLayout(title_layout) + self._propertyLayout.addWidget(title_group) + + # 内容属性组 + content_group = QGroupBox("内容属性") + content_layout = QGridLayout() + + # 内容文本 + content_layout.addWidget(QLabel("内容文本:"), 0, 0) + content_text_edit = QTextEdit() + content_text_edit.setMaximumHeight(100) + content_text_edit.setPlainText(current_content_text) + content_layout.addWidget(content_text_edit, 0, 1, 1, 3) + + # 内容颜色 + content_layout.addWidget(QLabel("内容颜色:"), 1, 0) + + # 内容R分量 + content_layout.addWidget(QLabel("R:"), 2, 0) + content_r_spin = QDoubleSpinBox() + content_r_spin.setRange(0.0, 1.0) + content_r_spin.setSingleStep(0.01) + content_r_spin.setValue(current_content_color[0]) + content_layout.addWidget(content_r_spin, 2, 1) + + # 内容G分量 + content_layout.addWidget(QLabel("G:"), 2, 2) + content_g_spin = QDoubleSpinBox() + content_g_spin.setRange(0.0, 1.0) + content_g_spin.setSingleStep(0.01) + content_g_spin.setValue(current_content_color[1]) + content_layout.addWidget(content_g_spin, 2, 3) + + # 内容B分量 + content_layout.addWidget(QLabel("B:"), 3, 0) + content_b_spin = QDoubleSpinBox() + content_b_spin.setRange(0.0, 1.0) + content_b_spin.setSingleStep(0.01) + content_b_spin.setValue(current_content_color[2]) + content_layout.addWidget(content_b_spin, 3, 1) + + # 内容A分量 + content_layout.addWidget(QLabel("A:"), 3, 2) + content_a_spin = QDoubleSpinBox() + content_a_spin.setRange(0.0, 1.0) + content_a_spin.setSingleStep(0.01) + content_a_spin.setValue(current_content_color[3]) + content_layout.addWidget(content_a_spin, 3, 3) + + # 内容颜色选择按钮 + content_color_button = QPushButton("选择内容颜色") + content_color_button.clicked.connect(lambda: self._selectInfoPanelContentColor( + info_panel, content_r_spin, content_g_spin, content_b_spin, content_a_spin)) + content_layout.addWidget(content_color_button, 4, 0, 1, 4) + + # 内容字体大小 + content_layout.addWidget(QLabel("内容字体大小:"), 5, 0) + content_size_spin = QDoubleSpinBox() + content_size_spin.setRange(0.01, 1.0) + content_size_spin.setSingleStep(0.01) + content_size_spin.setValue(current_content_size) # 现在确保是浮点数 + content_layout.addWidget(content_size_spin, 5, 1, 1, 3) + + # 内容位置控制 + content_layout.addWidget(QLabel("内容位置:"), 6, 0) + content_layout.addWidget(QLabel("X:"), 7, 0) + content_x_spin = QDoubleSpinBox() + content_x_spin.setRange(-1.0, 1.0) + content_x_spin.setSingleStep(0.01) + content_x_spin.setValue(current_content_pos[0]) # 从面板获取的实际位置 + content_layout.addWidget(content_x_spin, 7, 1) + + content_layout.addWidget(QLabel("Y:"), 7, 2) + content_y_spin = QDoubleSpinBox() + content_y_spin.setRange(-1.0, 1.0) + content_y_spin.setSingleStep(0.01) + content_y_spin.setValue(current_content_pos[1]) # 从面板获取的实际位置 + content_layout.addWidget(content_y_spin, 7, 3) + + # 连接内容属性变化信号,自动更新内容属性 + def onContentTextChanged(): + if hasattr(self.world, 'info_panel_manager'): + self.world.info_panel_manager.updatePanelContent( + panel_id, content=content_text_edit.toPlainText()) + + def onContentPropertyChanged(): + self._applyInfoPanelContentProperties( + info_panel, content_r_spin.value(), content_g_spin.value(), content_b_spin.value(), + content_a_spin.value(), content_size_spin.value()) + + def onContentPositionChanged(): + # 更新内容位置 + if hasattr(self.world, 'info_panel_manager'): + panel_data = self.world.info_panel_manager.panels.get(panel_id) + if panel_data and 'content_label' in panel_data: + # 设置内容位置 + panel_data['content_label'].setPos(content_x_spin.value(), 0, content_y_spin.value()) + + # 添加内容换行调整函数 + def adjustContentWordwrap(): + """根据面板宽度和字体大小调整内容换行""" + panel_width = width_spin.value() + font_size = content_size_spin.value() + self._adjustContentWordwrap(panel_id, panel_width, font_size) + + # 连接相关信号 + content_text_edit.textChanged.connect(onContentTextChanged) + content_r_spin.valueChanged.connect(onContentPropertyChanged) + content_g_spin.valueChanged.connect(onContentPropertyChanged) + content_b_spin.valueChanged.connect(onContentPropertyChanged) + content_a_spin.valueChanged.connect(onContentPropertyChanged) + content_size_spin.valueChanged.connect(lambda: [onContentPropertyChanged(), adjustContentWordwrap()]) + content_x_spin.valueChanged.connect(onContentPositionChanged) + content_y_spin.valueChanged.connect(onContentPositionChanged) + width_spin.valueChanged.connect(adjustContentWordwrap) + + content_group.setLayout(content_layout) + self._propertyLayout.addWidget(content_group) + + # 存储控件引用,用于后续更新 + self._current_info_panel = info_panel + self._info_panel_controls = { + 'title_text_edit': title_text_edit, + 'content_text_edit': content_text_edit, + 'title_r_spin': title_r_spin, + 'title_g_spin': title_g_spin, + 'title_b_spin': title_b_spin, + 'title_a_spin': title_a_spin, + 'content_r_spin': content_r_spin, + 'content_g_spin': content_g_spin, + 'content_b_spin': content_b_spin, + 'content_a_spin': content_a_spin, + 'title_size_spin': title_size_spin, + 'content_size_spin': content_size_spin, + 'title_x_spin': title_x_spin, + 'title_y_spin': title_y_spin, + 'content_x_spin': content_x_spin, + 'content_y_spin': content_y_spin, + 'width_spin': width_spin, + 'height_spin': height_spin + } + + # 启动定时器定期检查信息面板内容变化 + self._startInfoPanelMonitoring(info_panel, panel_id) + + # 初始调整内容换行 + adjustContentWordwrap() + + print("✅ 信息面板属性面板已添加") + + except Exception as e: + print(f"❌ 添加信息面板属性面板失败: {e}") + import traceback + traceback.print_exc() + + def _adjustContentWordwrap(self, panel_id, panel_width, font_size): + """调整信息面板内容的换行设置""" + try: + if hasattr(self.world, 'info_panel_manager'): + panel_data = self.world.info_panel_manager.panels.get(panel_id) + if panel_data and 'content_label' in panel_data: + # 计算合适的换行值 - 根据面板宽度和字体大小动态调整 + base_wordwrap = panel_width * 10 # 基础换行值 + # 根据字体大小调整换行值(字体越大,每行字符越少) + size_factor = 0.045 / font_size if font_size > 0 else 1.0 + adjusted_wordwrap = base_wordwrap * size_factor + + # 确保换行值在合理范围内 + wordwrap_value = max(5.0, min(100.0, adjusted_wordwrap)) + + # 应用换行设置 + panel_data['content_label']['text_wordwrap'] = wordwrap_value + print( + f"调整内容换行: 面板宽度={panel_width:.2f}, 字体大小={font_size:.3f}, 换行值={wordwrap_value:.2f}") + except Exception as e: + print(f"调整内容换行失败: {e}") + + def _startInfoPanelMonitoring(self, info_panel, panel_id): + """启动信息面板内容监控""" + try: + # 使用 QTimer 定期检查信息面板内容变化 + if not hasattr(self, '_info_panel_timer'): + from PyQt5.QtCore import QTimer + self._info_panel_timer = QTimer() + self._info_panel_timer.timeout.connect(lambda: self._checkInfoPanelUpdates(panel_id)) + self._info_panel_timer.start(500) # 每500毫秒检查一次 + except Exception as e: + print(f"❌ 启动信息面板监控失败: {e}") + + def _checkInfoPanelUpdates(self, panel_id): + """检查信息面板内容更新""" + try: + # 检查是否仍有有效的控件引用 + if not hasattr(self, '_info_panel_controls') or not self._info_panel_controls: + return + + # 检查是否仍有有效的info_panel_manager + if not hasattr(self.world, 'info_panel_manager'): + return + + # 获取当前面板数据 + panel_data = self.world.info_panel_manager.panels.get(panel_id) + if not panel_data: + return + + # 获取控件引用 + controls = self._info_panel_controls + + # 检查并更新标题文本 + if 'title_label' in panel_data and panel_data['title_label']: + current_title_text = panel_data['title_label']['text'] + title_text_edit = controls.get('title_text_edit') + if title_text_edit and title_text_edit.toPlainText() != current_title_text: + title_text_edit.blockSignals(True) + title_text_edit.setPlainText(current_title_text) + title_text_edit.blockSignals(False) + + # 检查并更新内容文本 + if 'content_label' in panel_data and panel_data['content_label']: + current_content_text = panel_data['content_label']['text'] + content_text_edit = controls.get('content_text_edit') + if content_text_edit and content_text_edit.toPlainText() != current_content_text: + content_text_edit.blockSignals(True) + content_text_edit.setPlainText(current_content_text) + content_text_edit.blockSignals(False) + + # 检查并更新标题位置 + if 'title_label' in panel_data and panel_data['title_label']: + title_pos = panel_data['title_label'].getPos() + current_title_pos = (title_pos.getX(), title_pos.getZ()) + + title_x_spin = controls.get('title_x_spin') + title_y_spin = controls.get('title_y_spin') + + if title_x_spin and abs(title_x_spin.value() - current_title_pos[0]) > 0.001: + title_x_spin.blockSignals(True) + title_x_spin.setValue(current_title_pos[0]) + title_x_spin.blockSignals(False) + + if title_y_spin and abs(title_y_spin.value() - current_title_pos[1]) > 0.001: + title_y_spin.blockSignals(True) + title_y_spin.setValue(current_title_pos[1]) + title_y_spin.blockSignals(False) + + # 检查并更新内容位置 + if 'content_label' in panel_data and panel_data['content_label']: + content_pos = panel_data['content_label'].getPos() + current_content_pos = (content_pos.getX(), content_pos.getZ()) + + content_x_spin = controls.get('content_x_spin') + content_y_spin = controls.get('content_y_spin') + + if content_x_spin and abs(content_x_spin.value() - current_content_pos[0]) > 0.001: + content_x_spin.blockSignals(True) + content_x_spin.setValue(current_content_pos[0]) + content_x_spin.blockSignals(False) + + if content_y_spin and abs(content_y_spin.value() - current_content_pos[1]) > 0.001: + content_y_spin.blockSignals(True) + content_y_spin.setValue(current_content_pos[1]) + content_y_spin.blockSignals(False) + + except Exception as e: + print(f"❌ 检查信息面板更新失败: {e}") + + def _selectInfoPanelBorderColor(self, info_panel, r_spin, g_spin, b_spin, a_spin): + """选择信息面板边框颜色""" + try: + from PyQt5.QtWidgets import QColorDialog + from PyQt5.QtGui import QColor + + # 获取当前颜色值 + current_color = QColor( + int(r_spin.value() * 255), + int(g_spin.value() * 255), + int(b_spin.value() * 255), + int(a_spin.value() * 255) + ) + + # 打开颜色选择对话框 + color = QColorDialog.getColor(current_color, None, "选择边框颜色") + if color.isValid(): + # 更新数值框 + r_spin.setValue(color.redF()) # redF() 返回 0.0-1.0 范围的值 + g_spin.setValue(color.greenF()) + b_spin.setValue(color.blueF()) + a_spin.setValue(color.alphaF()) + + except Exception as e: + print(f"❌ 选择边框颜色失败: {e}") + + def updateInfoPanelBackgroundImage(self, info_panel, image_path): + """更新信息面板背景图片""" + try: + panel_id = info_panel.getTag("panel_id") + if not panel_id: + print("❌ 无法找到信息面板ID") + return False + + # 通过 info_panel_manager 更新背景图片 + if hasattr(self.world, 'info_panel_manager'): + info_panel_manager = self.world.info_panel_manager + + # 直接调用 InfoPanelManager 的方法设置背景图片 + if image_path: + # 设置新的背景图片 + success = info_panel_manager.setPanelBackgroundImage(panel_id, image_path) + if success: + print(f"✅ 成功设置信息面板背景图片: {image_path}") + return True + else: + print(f"❌ 设置信息面板背景图片失败: {image_path}") + return False + else: + # 清除背景图片 + success = info_panel_manager.setPanelBackgroundImage(panel_id, None) + if success: + print("✅ 成功清除信息面板背景图片") + return True + else: + print("❌ 清除信息面板背景图片失败") + return False + else: + print("❌ 未找到 info_panel_manager") + return False + + except Exception as e: + print(f"❌ 更新信息面板背景图片失败: {e}") + import traceback + traceback.print_exc() + return False + + def _selectInfoPanelBackgroundColor(self, info_panel, r_spin, g_spin, b_spin, a_spin): + """选择信息面板背景颜色""" + try: + from PyQt5.QtWidgets import QColorDialog + from PyQt5.QtGui import QColor + + # 获取当前颜色值 + current_color = QColor( + int(r_spin.value() * 255), + int(g_spin.value() * 255), + int(b_spin.value() * 255), + int(a_spin.value() * 255) + ) + + # 打开颜色选择对话框 + color = QColorDialog.getColor(current_color, None, "选择背景颜色") + if color.isValid(): + # 更新数值框 + r_spin.setValue(color.redF()) # redF() 返回 0.0-1.0 范围的值 + g_spin.setValue(color.greenF()) + b_spin.setValue(color.blueF()) + a_spin.setValue(color.alphaF()) + + except Exception as e: + print(f"❌ 选择背景颜色失败: {e}") + + def _applyInfoPanelBackgroundColor(self,info_panel,r,g,b,a): + """应用信息面部背景颜色""" + try: + panel_id = info_panel.getTag("panel_id") + if not panel_id: + print("❌ 找不到信息面板ID") + return + + if hasattr(self.world,'info_panel_manager'): + info_panel_manager = self.world.info_panel_manager + success = info_panel_manager.updatePanelProperties(panel_id,bg_color=(r,g,b,a)) + if success: + print(f"✅ 成功设置信息面板背景颜色: R={r:.2f}, G={g:.2f}, B={b:.2f}, A={a:.2f}") + return True + else: + print("❌ 设置信息面板背景颜色失败") + else: + print("❌ 未找到 info_panel_manager") + return False + except Exception as e: + print(f"应用背景颜色失败{e}") + return False + + def _selectInfoPanelTitleColor(self, info_panel, r_spin, g_spin, b_spin, a_spin): + """选择信息面板标题颜色""" + try: + from PyQt5.QtWidgets import QColorDialog + from PyQt5.QtGui import QColor + + # 获取当前颜色值 + current_color = QColor( + int(r_spin.value() * 255), + int(g_spin.value() * 255), + int(b_spin.value() * 255), + int(a_spin.value() * 255) + ) + + # 打开颜色选择对话框 + color = QColorDialog.getColor(current_color, None, "选择标题颜色") + if color.isValid(): + # 更新数值框 + r_spin.setValue(color.redF()) # redF() 返回 0.0-1.0 范围的值 + g_spin.setValue(color.greenF()) + b_spin.setValue(color.blueF()) + a_spin.setValue(color.alphaF()) + + except Exception as e: + print(f"❌ 选择标题颜色失败: {e}") + + def _selectInfoPanelContentColor(self, info_panel, r_spin, g_spin, b_spin, a_spin): + """选择信息面板内容颜色""" + try: + from PyQt5.QtWidgets import QColorDialog + from PyQt5.QtGui import QColor + + # 获取当前颜色值 + current_color = QColor( + int(r_spin.value() * 255), + int(g_spin.value() * 255), + int(b_spin.value() * 255), + int(a_spin.value() * 255) + ) + + # 打开颜色选择对话框 + color = QColorDialog.getColor(current_color, None, "选择内容颜色") + if color.isValid(): + # 更新数值框 + r_spin.setValue(color.redF()) # redF() 返回 0.0-1.0 范围的值 + g_spin.setValue(color.greenF()) + b_spin.setValue(color.blueF()) + a_spin.setValue(color.alphaF()) + + except Exception as e: + print(f"❌ 选择内容颜色失败: {e}") + + def _applyInfoPanelTitleProperties(self, info_panel, r, g, b, a, size): + """应用信息面板标题属性""" + try: + panel_id = info_panel.getTag("panel_id") + if not panel_id: + print("❌ 无法找到信息面板ID") + return False + + # 通过 info_panel_manager 更新标题属性 + if hasattr(self.world, 'info_panel_manager'): + info_panel_manager = self.world.info_panel_manager + + # 更新标题颜色和字体大小 + # 注意:DirectGUI中字体大小通过text_scale设置,需要直接操作title_label对象 + panel_data = info_panel_manager.panels.get(panel_id) + if panel_data and 'title_label' in panel_data: + # 更新颜色 + panel_data['title_label']['text_fg'] = (r, g, b, a) + # 更新字体大小 + panel_data['title_label']['text_scale'] = size + # 同时更新属性存储 + if 'properties' in panel_data: + panel_data['properties']['title_color'] = (r, g, b, a) + + print(f"✅ 成功设置信息面板标题属性: 颜色=({r:.2f}, {g:.2f}, {b:.2f}, {a:.2f}), 大小={size}") + return True + else: + # 回退到使用updatePanelProperties方法 + success = info_panel_manager.updatePanelProperties( + panel_id, + title_color=(r, g, b, a) + ) + if success: + print(f"✅ 成功设置信息面板标题颜色: ({r:.2f}, {g:.2f}, {b:.2f}, {a:.2f})") + return True + + print("❌ 设置信息面板标题属性失败") + return False + else: + print("❌ 未找到 info_panel_manager") + return False + + except Exception as e: + print(f"❌ 应用标题属性失败: {e}") + return False + + def _applyInfoPanelContentProperties(self, info_panel, r, g, b, a, size): + """应用信息面板内容属性""" + try: + panel_id = info_panel.getTag("panel_id") + if not panel_id: + print("❌ 无法找到信息面板ID") + return False + + # 通过 info_panel_manager 更新内容属性 + if hasattr(self.world, 'info_panel_manager'): + info_panel_manager = self.world.info_panel_manager + + # 更新内容颜色和字体大小 + # 注意:DirectGUI中字体大小通过text_scale设置,需要直接操作content_label对象 + panel_data = info_panel_manager.panels.get(panel_id) + if panel_data and 'content_label' in panel_data: + # 更新颜色 + panel_data['content_label']['text_fg'] = (r, g, b, a) + # 更新字体大小 + panel_data['content_label']['text_scale'] = size + # 同时更新属性存储 + if 'properties' in panel_data: + panel_data['properties']['content_color'] = (r, g, b, a) + + print(f"✅ 成功设置信息面板内容属性: 颜色=({r:.2f}, {g:.2f}, {b:.2f}, {a:.2f}), 大小={size}") + return True + else: + # 回退到使用updatePanelProperties方法 + success = info_panel_manager.updatePanelProperties( + panel_id, + content_color=(r, g, b, a) + ) + if success: + print(f"✅ 成功设置信息面板内容颜色: ({r:.2f}, {g:.2f}, {b:.2f}, {a:.2f})") + return True + + print("❌ 设置信息面板内容属性失败") + return False + else: + print("❌ 未找到 info_panel_manager") + return False + + except Exception as e: + print(f"❌ 应用内容属性失败: {e}") + return False + def _addSphericalVideoProperties(self, spherical_video): """为球形视频添加属性控制面板""" try: @@ -1994,7 +2992,7 @@ class PropertyPanelManager: # 加载新视频按钮 load_btn = QPushButton("📁 加载新视频...") - load_btn.clicked.connect(lambda: self._loadNew2DVideo(video_screen)) + load_btn.clicked.connect(lambda: self._loadNewVideo(video_screen)) self._propertyLayout.addWidget(load_btn) # 添加URL输入区域 @@ -2006,7 +3004,8 @@ class PropertyPanelManager: url_layout.addWidget(self.url_input) load_url_btn = QPushButton("加载URL") - # 修改: 直接绑定到 _loadVideoFromURLWithOpenCV_3D 方法,并传递URL参数 + self._current_load_url_btn_3d = load_url_btn + load_url_btn.clicked.connect(lambda: self._loadVideoFromURLWithOpenCV_3D(video_screen, self.url_input.text().strip())) url_layout.addWidget(load_url_btn) @@ -2079,7 +3078,7 @@ class PropertyPanelManager: # 停止按钮 stop_btn = QPushButton("⏹️ 停止") - stop_btn.clicked.connect(lambda: self.stop2DVideo(video_screen)) + stop_btn.clicked.connect(lambda: self._stop2DVideo(video_screen)) video_control_layout.addWidget(stop_btn) video_control_group.setLayout(video_control_layout) @@ -2099,8 +3098,9 @@ class PropertyPanelManager: url_layout.addWidget(self.url_input) load_url_btn = QPushButton("加载URL") - # 修复:直接调用 _loadVideoFromURL 方法并传递 video_screen 和 URL 文本 - load_url_btn.clicked.connect(lambda: self._loadVideoFromURL(video_screen, self.url_input.text().strip())) + self._current_load_url_btn_2d = load_url_btn + + load_url_btn.clicked.connect(lambda: self._loadVideoFromURLWithOpenCV(video_screen, self.url_input.text().strip())) url_layout.addWidget(load_url_btn) url_group.setLayout(url_layout) @@ -2109,30 +3109,6 @@ class PropertyPanelManager: except Exception as e: print(f"添加2D视频屏幕属性失败: {e}") - def _loadVideoFromURL(self, video_screen, url): - """从URL加载视频流并在2D视频屏幕上显示(使用MovieTexture)""" - if not url: - print("❌ URL不能为空") - return False - - try: - # 停止之前可能正在播放的视频 - self._stop2DVideo(video_screen) - - # 检查是本地文件还是网络URL - if url.startswith("http://") or url.startswith("https://"): - # 对于网络流,我们仍然使用OpenCV方式 - return self._loadVideoFromURLWithOpenCV(video_screen, url) - else: - # 对于本地文件,使用与_loadNew2DVideo相同的方式 - return self.world.gui_manager.load2DVideoFile(video_screen, url) - - except Exception as e: - print(f"❌ 加载视频失败: {e}") - import traceback - traceback.print_exc() - return False - def _loadVideoFromURLWithOpenCV(self, video_screen, url): """使用OpenCV从URL加载视频流并在2D视频屏幕上显示(推荐版)""" try: @@ -2141,6 +3117,37 @@ class PropertyPanelManager: from panda3d.core import Texture, PNMImage import numpy as np import time + from PyQt5.QtWidgets import QMessageBox + + load_url_btn = None + if hasattr(self, '_current_load_url_btn_2d'): + load_url_btn = self._current_load_url_btn_2d + + # 安全地更新按钮状态 + def update_button_text(text): + try: + if load_url_btn and load_url_btn.isVisible(): + load_url_btn.setText(text) + return True + except RuntimeError: + # 按钮已被删除,忽略错误 + return False + + def update_button_enabled(enabled): + try: + if load_url_btn and load_url_btn.isVisible(): + load_url_btn.setEnabled(enabled) + return True + except RuntimeError: + # 按钮已被删除,忽略错误 + return False + + if load_url_btn: + load_url_btn.setText("⏳ 加载中...") + load_url_btn.setEnabled(False) + + from PyQt5.QtWidgets import QApplication + QApplication.processEvents() # 停止之前可能正在播放的视频 self._stop2DVideo(video_screen) @@ -2148,7 +3155,14 @@ class PropertyPanelManager: # 使用 OpenCV 打开视频流 cap = cv2.VideoCapture(url) if not cap.isOpened(): + error_msg = f"无法打开视频流:\n{url}\n\n可能的原因:\n1. 网络连接问题\n2. 视频流地址无效\n3. 视频服务器不可用\n4. 防火墙阻止访问" + QMessageBox.critical(None, "视频流错误", error_msg) print(f"❌ 无法打开视频流: {url}") + + #恢复按钮状态 + if load_url_btn: + load_url_btn.setText("加载URL") + load_url_btn.setEnabled(True) return False # 设置视频参数以提高性能 @@ -2175,11 +3189,10 @@ class PropertyPanelManager: # 启动视频播放线程 def update_video_texture(): - target_fps = 25 # 降低目标帧率以减少CPU使用 + target_fps = 60 # 降低目标帧率以减少CPU使用 frame_time = 1.0 / target_fps frame_count = 0 - skip_frames = 2 # 每隔几帧处理一帧以降低CPU使用率 while True: try: @@ -2198,15 +3211,11 @@ class PropertyPanelManager: continue frame_count += 1 - # 跳过一些帧以降低CPU使用率 - if frame_count % skip_frames != 0: - time.sleep(frame_time) - continue # 调整帧大小以降低处理负担 frame_height, frame_width = frame.shape[:2] - if frame_width > 640: # 限制最大宽度 - scale = 640.0 / frame_width + if frame_width > 256: # 限制最大宽度 + scale = 256.0 / frame_width new_width = int(frame_width * scale) new_height = int(frame_height * scale) frame = cv2.resize(frame, (new_width, new_height)) @@ -2248,15 +3257,42 @@ class PropertyPanelManager: thread.start() print(f"✅ 开始在2D视频屏幕上播放网络视频流: {url}") + + if load_url_btn: + load_url_btn.setText("加载URL") + load_url_btn.setEnabled(True) return True + except ImportError: + error_msg = "缺少必要的库,请安装: pip install opencv-python" + QMessageBox.critical(None, "依赖库缺失", error_msg) print("❌ 缺少必要的库,请安装: pip install opencv-python") + + # 恢复按钮状态 + try: + if load_url_btn: + load_url_btn.setText("加载URL") + load_url_btn.setEnabled(True) + except RuntimeError: + pass # 按钮已被删除,忽略 + return False except Exception as e: + error_msg = f"加载网络视频失败:\n{str(e)}" + QMessageBox.critical(None, "视频加载错误", error_msg) print(f"❌ 加载网络视频失败: {e}") import traceback traceback.print_exc() + + # 恢复按钮状态 + try: + if load_url_btn: + load_url_btn.setText("加载URL") + load_url_btn.setEnabled(True) + except RuntimeError: + pass # 按钮已被删除,忽略 + return False def play2DVideo(self, video_screen): @@ -2417,114 +3453,6 @@ class PropertyPanelManager: video_screen["frameColor"] = (0, 0, 0, 0) # 透明背景 return False - def _addVideoScreenProperties(self, video_screen): - """添加视频屏幕属性面板""" - try: - from PyQt5.QtWidgets import (QGroupBox, QGridLayout, QPushButton, QLabel, - QFileDialog, QHBoxLayout, QLineEdit) - from PyQt5.QtCore import Qt - import os - - video_info_group = QGroupBox("视频信息") - video_info_layout = QGridLayout() - - # 显示当前视频文件路径 - 改进版本 - video_path = video_screen.getTag("video_path") if video_screen.hasTag("video_path") else "" - if video_path: - if video_path.startswith("http://") or video_path.startswith("https://"): - # 显示URL信息 - video_info_layout.addWidget(QLabel("视频流URL:"), 0, 0) - path_label = QLabel(video_path) - path_label.setWordWrap(True) - path_label.setStyleSheet("color: #00AAFF;") - video_info_layout.addWidget(path_label, 0, 1) - elif os.path.exists(video_path): - # 显示本地文件信息 - video_info_layout.addWidget(QLabel("视频文件:"), 0, 0) - path_label = QLabel(os.path.basename(video_path)) - path_label.setWordWrap(True) - path_label.setStyleSheet("color: #00AAFF;") - video_info_layout.addWidget(path_label, 0, 1) - else: - # 文件不存在 - video_info_layout.addWidget(QLabel("状态:"), 0, 0) - status_label = QLabel("文件不存在") - status_label.setStyleSheet("color: red;") - video_info_layout.addWidget(status_label, 0, 1) - else: - # 无视频 - video_info_layout.addWidget(QLabel("状态:"), 0, 0) - status_label = QLabel("无视频文件") - status_label.setStyleSheet("color: red;") - video_info_layout.addWidget(status_label, 0, 1) - - video_info_group.setLayout(video_info_layout) - self._propertyLayout.addWidget(video_info_group) - - video_control_group = QGroupBox("视频控制") - video_control_layout = QHBoxLayout() - - # 播放按钮 - play_btn = QPushButton("▶️ 播放") - play_btn.clicked.connect(lambda: self.world.gui_manager.playVideo(video_screen)) - video_control_layout.addWidget(play_btn) - - # 暂停按钮 - pause_btn = QPushButton("⏸️ 暂停") - pause_btn.clicked.connect(lambda: self.world.gui_manager.pauseVideo(video_screen)) - video_control_layout.addWidget(pause_btn) - - # 停止按钮 - stop_btn = QPushButton("⏹️ 停止") - stop_btn.clicked.connect(lambda: self.world.gui_manager.stopVideo(video_screen)) - video_control_layout.addWidget(stop_btn) - - video_control_group.setLayout(video_control_layout) - self._propertyLayout.addWidget(video_control_group) - - # 加载新视频按钮 - load_btn = QPushButton("📁 加载新视频...") - load_btn.clicked.connect(lambda: self._loadNew2DVideo(video_screen)) - self._propertyLayout.addWidget(load_btn) - - # 添加URL输入区域 - url_group = QGroupBox("网络视频") - url_layout = QHBoxLayout() - - self.url_input = QLineEdit() - self.url_input.setPlaceholderText("输入视频流URL") - url_layout.addWidget(self.url_input) - - load_url_btn = QPushButton("加载URL") - # 修改: 直接绑定到 _loadVideoFromURLWithOpenCV_3D_direct 方法 - load_url_btn.clicked.connect(lambda: self._loadVideoFromURLWithOpenCV_3D_direct(video_screen)) - url_layout.addWidget(load_url_btn) - - url_group.setLayout(url_layout) - self._propertyLayout.addWidget(url_group) - - except Exception as e: - print(f"添加视频屏幕属性失败: {e}") - - def _loadVideoFromURLWithOpenCV_3D_direct(self, video_screen): - """直接从URL输入框加载视频流 - 修复版""" - try: - # 从输入框获取URL - url = self.url_input.text().strip() if hasattr(self, 'url_input') else "" - - if not url: - print("❌ URL不能为空") - return False - - # 直接调用OpenCV处理方法 - return self._loadVideoFromURLWithOpenCV_3D(video_screen, url) - - except Exception as e: - print(f"❌ 直接加载视频失败: {e}") - import traceback - traceback.print_exc() - return False - def _loadVideoFromURLWithOpenCV_3D(self, video_screen, url): """使用OpenCV从URL加载视频流并在3D视频屏幕上显示""" try: @@ -2532,6 +3460,45 @@ class PropertyPanelManager: import threading from panda3d.core import Texture, PNMImage, TextureStage import time + from PyQt5.QtWidgets import QMessageBox + + is_first_load = not video_screen.hasTag("video_path") or not video_screen.getTag("video_path") + + needs_second_call = False + if is_first_load and not hasattr(self,'_first_load_processed'): + needs_second_call = True + self._first_load_processed = True + + load_url_btn = None + if hasattr(self, '_current_load_url_btn_3d'): + load_url_btn = self._current_load_url_btn_3d + + # 安全地更新按钮状态 + def update_button_text(text): + try: + if load_url_btn and load_url_btn.isVisible(): + load_url_btn.setText(text) + return True + except RuntimeError: + # 按钮已被删除,忽略错误 + return False + + def update_button_enabled(enabled): + try: + if load_url_btn and load_url_btn.isVisible(): + load_url_btn.setEnabled(enabled) + return True + except RuntimeError: + # 按钮已被删除,忽略错误 + return False + + # 如果有按钮引用,改变按钮状态为加载中 + if load_url_btn: + update_button_text("⏳ 加载中...") + update_button_enabled(False) + + from PyQt5.QtWidgets import QApplication + QApplication.processEvents() # 停止之前可能正在播放的视频 self._stop3DVideo(video_screen) @@ -2539,7 +3506,13 @@ class PropertyPanelManager: # 使用 OpenCV 打开视频流 cap = cv2.VideoCapture(url) if not cap.isOpened(): + error_msg = f"无法打开视频流:\n{url}\n\n可能的原因:\n1. 网络连接问题\n2. 视频流地址无效\n3. 视频服务器不可用\n4. 防火墙阻止访问" + QMessageBox.critical(None, "视频流错误", error_msg) print(f"❌ 无法打开视频流: {url}") + + # 恢复按钮状态 + update_button_text("加载URL") + update_button_enabled(True) return False # 设置视频参数以提高性能 @@ -2574,10 +3547,9 @@ class PropertyPanelManager: # 启动视频播放线程 def update_video_texture(): - target_fps = 25 # 降低目标帧率以减少CPU使用 + target_fps = 60 # 降低目标帧率以减少CPU使用 frame_time = 1.0 / target_fps frame_count = 0 - skip_frames = 2 # 每隔几帧处理一帧以降低CPU使用率 while True: try: @@ -2596,15 +3568,11 @@ class PropertyPanelManager: continue frame_count += 1 - # 跳过一些帧以降低CPU使用率 - if frame_count % skip_frames != 0: - time.sleep(frame_time) - continue # 调整帧大小以降低处理负担 frame_height, frame_width = frame.shape[:2] - if frame_width > 640: # 限制最大宽度 - scale = 640.0 / frame_width + if frame_width > 256: # 限制最大宽度 + scale = 256.0 / frame_width new_width = int(frame_width * scale) new_height = int(frame_height * scale) frame = cv2.resize(frame, (new_width, new_height)) @@ -2646,17 +3614,66 @@ class PropertyPanelManager: thread.start() print(f"✅ 开始在3D视频屏幕上播放网络视频流: {url}") + + if needs_second_call: + print("检测到首次加载,再次调用_loadVideoFromURLWithOpenCV_3D以确保正确显示") + from PyQt5.QtCore import QTimer + QTimer.singleShot(100, lambda: self._loadVideoFromURLWithOpenCV_3D(video_screen, url)) + QTimer.singleShot(200,lambda :setattr(self,'_first_load_processed',False)if hasattr(self,'_first_load_processed')else None) + + # 恢复按钮状态 + update_button_text("加载URL") + update_button_enabled(True) return True except ImportError: + error_msg = "缺少必要的库,请安装: pip install opencv-python" + QMessageBox.critical(None, "依赖库缺失", error_msg) print("❌ 缺少必要的库,请安装: pip install opencv-python") + + # 恢复按钮状态 + try: + if load_url_btn: + load_url_btn.setText("加载URL") + load_url_btn.setEnabled(True) + except RuntimeError: + pass # 按钮已被删除,忽略 + return False except Exception as e: + error_msg = f"加载3D网络视频失败:\n{str(e)}" + QMessageBox.critical(None, "视频加载错误", error_msg) print(f"❌ 加载3D网络视频失败: {e}") import traceback traceback.print_exc() + + # 恢复按钮状态 + try: + if load_url_btn: + load_url_btn.setText("加载URL") + load_url_btn.setEnabled(True) + except RuntimeError: + pass # 按钮已被删除,忽略 + return False + def _verifyVideoDisplay(self,video_screen,texture): + try: + applied_texture = video_screen.getTexture() + if applied_texture and applied_texture.getName() == texture.getName(): + print("视频纹理已正确应用到3d模型") + return True + else: + print("视频纹理未正确应用到3d模型") + return False + except Exception as e: + print(f"验证视频显示时出错:{e}") + return False + + def _retryLoadVideoFromURLWithOpenCV_3D(self,video_screen,url): + print("重新加载3D视频流") + return self._loadVideoFromURLWithOpenCV_3D(video_screen,url) + def _stop3DVideo(self, video_screen): """停止3D视频(内部方法)""" try: @@ -3625,17 +4642,6 @@ class PropertyPanelManager: material_group.setLayout(material_layout) self._propertyLayout.addWidget(material_group) - # # 添加太阳方位角控制面板(只在第一个材质时添加,避免重复) - # # if i == 0: - # # self._addSunAzimuthPanel() - # - # - # # 分隔线 - # if i < len(materials) - 1: - # separator = QLabel("─" * 30) - # separator.setStyleSheet("color: lightgray;") - # self._propertyLayout.addRow(separator) - def _updateMaterialBaseColor(self, material, component, value): """更新材质基础颜色(智能版本)""" try: @@ -6641,7 +7647,6 @@ class PropertyPanelManager: if actor and actor.getAnimNames(): self._buildSkeletalUI(origin_model, actor, animation_layout) has_animation = True - has_skeletal_anim = True print(f"[信息] 检测到骨骼动画: {actor.getAnimNames()}") except Exception as actor_error: # 忽略 Actor 加载错误,很多模型都不是角色动画 @@ -7318,8 +8323,6 @@ except Exception as e: origin_model.setPythonTag("anim_speed",speed) print(f"[动画] 速度设为: {speed} ({display_name})") - - def _dispatchAnimCommand(self,origin_model,cmd): cache = self._actor_cache.get(origin_model) if not cache: diff --git a/ui/widgets.py b/ui/widgets.py index f52c0437..c84deb42 100644 --- a/ui/widgets.py +++ b/ui/widgets.py @@ -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: