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/QPanda3D/Panda3DWorld.py b/QPanda3D/Panda3DWorld.py index a19f52c0..78193075 100644 --- a/QPanda3D/Panda3DWorld.py +++ b/QPanda3D/Panda3DWorld.py @@ -53,7 +53,7 @@ class Panda3DWorld(ShowBase): Panda3DWorld : A class to handle all panda3D world manipulation """ - def __init__(self, width=1380, height=729, is_fullscreen=False, size=1.0, clear_color=LVecBase4f(0, 0.5, 0, 1), + def __init__(self, width=1380, height=750, is_fullscreen=False, size=1.0, clear_color=LVecBase4f(0, 0.5, 0, 1), name="qpanda3D"): global _global_world_instance diff --git a/RenderPipelineFile/config/daytime.yaml b/RenderPipelineFile/config/daytime.yaml index af9861ed..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.5000000000]]] - 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/Resources/c/a b/Resources/c/a new file mode 100644 index 00000000..e69de29b diff --git a/Resources/c/b/a.txt b/Resources/c/b/a.txt new file mode 100644 index 00000000..e69de29b diff --git a/Resources/c/b/b b/Resources/c/b/b new file mode 100644 index 00000000..e69de29b diff --git a/Resources/c/b/b.txt b/Resources/c/b/b.txt new file mode 100644 index 00000000..e69de29b 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/event_handler.py b/core/event_handler.py index 5b35345d..ff7b17c2 100644 --- a/core/event_handler.py +++ b/core/event_handler.py @@ -114,7 +114,7 @@ class EventHandler: def mousePressEventLeft(self, evt): """处理鼠标左键按下事件""" print("\n=== 开始处理鼠标左键事件 ===") - print(f"当前工具: {self.world.currentTool}") + #print(f"当前工具: {self.world.currentTool}") if not evt: print("事件为空") @@ -126,7 +126,7 @@ class EventHandler: # 获取鼠标点击的位置 x = evt.get('x', 0) y = evt.get('y', 0) - print(f"鼠标点击位置: ({x}, {y})") + #print(f"鼠标点击位置: ({x}, {y})") # 获取准确的窗口尺寸 winWidth, winHeight = self.world.getWindowSize() @@ -134,20 +134,20 @@ class EventHandler: # 直接使用 x, y 创建鼠标位置 mx = 2.0 * x / float(winWidth) - 1.0 my = 1.0 - 2.0 * y / float(winHeight) - print(f"转换后的坐标: ({mx}, {my})") + #print(f"转换后的坐标: ({mx}, {my})") # 创建射线 nearPoint = Point3() farPoint = Point3() self.world.cam.node().getLens().extrude(Point2(mx, my), nearPoint, farPoint) - print(f"相机坐标系射线起点: {nearPoint}") - print(f"相机坐标系射线终点: {farPoint}") + #print(f"相机坐标系射线起点: {nearPoint}") + #print(f"相机坐标系射线终点: {farPoint}") # 将相机坐标系的点转换到世界坐标系 worldNearPoint = self.world.render.getRelativePoint(self.world.cam, nearPoint) worldFarPoint = self.world.render.getRelativePoint(self.world.cam, farPoint) - print(f"世界坐标系射线起点: {worldNearPoint}") - print(f"世界坐标系射线终点: {worldFarPoint}") + #print(f"世界坐标系射线起点: {worldNearPoint}") + #print(f"世界坐标系射线终点: {worldFarPoint}") # 进行射线检测 picker = CollisionTraverser() diff --git a/core/selection.py b/core/selection.py index b9c15de4..7cef719c 100644 --- a/core/selection.py +++ b/core/selection.py @@ -63,7 +63,49 @@ class SelectionSystem: "z": (1.0, 1.0, 0.0, 1.0) # 黄色高亮 } + self._current_cursor = None + self._default_cursor = None + print("✓ 选择和变换系统初始化完成") + # ==================== 光标设置 ==================== + def _setCursor(self,cursor_type): + try: + from PyQt5.QtCore import Qt + if self._current_cursor == cursor_type: + return + if hasattr(self.world,'main_window') and self.world.main_window: + main_window = self.world.main_window + else: + from PyQt5.QtWidgets import QApplication + main_window = QApplication.activeWindow() + if not main_window: + windows = QApplication.topLevelWindows() + for window in windows: + if hasattr(window,'isVisible') and window.isVisible(): + main_window = window + break + if main_window: + if cursor_type == "crosshair": + main_window.setCursor(Qt.CrossCursor) + elif cursor_type == "size_hor": + main_window.setCursor(Qt.SizeHorCursor) + elif cursor_type == "size_ver": + main_window.setCursor(Qt.SizeVerCursor) + elif cursor_type == "size_all": + main_window.setCursor(Qt.SizeAllCursor) + elif cursor_type == "pointing_hand": + main_window.setCursor(Qt.PointingHandCursor) + else: + main_window.unsetCursor() + self._current_cursor = cursor_type + #print(f"光标已设置:{cursor_type}") + self._current_cursor = cursor_type + else: + print("警告:无法获取主窗口,光标设置失败") + except Exception as e: + print(f"设置光标失败{e}") + def _resetCursor(self): + self._setCursor("default") # ==================== 选择框系统 ==================== @@ -108,6 +150,26 @@ class SelectionSystem: if not self.selectionBox or not self.selectionBoxTarget: return + if self.selectionBoxTarget.isEmpty(): + return + + minPoint = Point3() + maxPoint = Point3() + + try: + has_bounds = self.selectionBoxTarget.calcTightBounds(minPoint, maxPoint, self.world.render) + if not has_bounds: + return + except: + return + + # 检查边界框的有效性 + if (minPoint.x > maxPoint.x or minPoint.y > maxPoint.y or minPoint.z > maxPoint.z or + abs(minPoint.x) > 1e10 or abs(minPoint.y) > 1e10 or abs(minPoint.z) > 1e10 or + abs(maxPoint.x) > 1e10 or abs(maxPoint.y) > 1e10 or abs(maxPoint.z) > 1e10): + print("警告: 检测到无效的边界框,跳过选择框更新") + return + # 检查是否需要重新计算边界框 if not hasattr(self, '_bounds_cache'): self._bounds_cache = {} @@ -337,19 +399,24 @@ class SelectionSystem: is_scale_tool = self.world.tool_manager.isScaleTool() if self.world.tool_manager else False is_rotate_tool = self.world.tool_manager.isRotateTool() if self.world.tool_manager else False + import os + base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + if is_scale_tool: model_paths = [ - "core/UniformScaleHandle.fbx", + os.path.join(base_dir,"core/UniformScaleHandle.fbx"), ] elif is_rotate_tool: model_paths = [ - "core/RotationHandleQuarter.fbx", + os.path.join(base_dir,"core/RotationHandleQuarter.fbx"), ] else: model_paths = [ - "core/TranslateArrowHandle.fbx", + os.path.join(base_dir, "core/TranslateArrowHandle.fbx"), ] + arrow_path = os.path.join(base_dir, "core/TranslateArrowHandle.fbx") + # model_paths = [ # "core/TranslateArrowHandle.fbx", # "EG/core/TranslateArrowHandle.fbx", @@ -359,7 +426,7 @@ class SelectionSystem: for path in model_paths: try: if is_rotate_tool: - gizmo_model = self.world.loader.loadModel("core/TranslateArrowHandle.fbx") + gizmo_model = self.world.loader.loadModel(arrow_path) gizmoRot_model = self.world.loader.loadModel(path) else: gizmo_model = self.world.loader.loadModel(path) @@ -640,12 +707,25 @@ class SelectionSystem: if current_time - self._last_gizmo_bounds_update > 0.2: # 每0.2秒计算一次边界框 minPoint = Point3() maxPoint = Point3() - if self.gizmoTarget.calcTightBounds(minPoint, maxPoint, self.world.render): - # 计算中心点 - center = Point3((minPoint.x + maxPoint.x) * 0.5, - (minPoint.y + maxPoint.y) * 0.5, - (minPoint.z + maxPoint.z) * 0.5) - self.gizmo.setPos(center) + # 添加异常处理 + try: + if self.gizmoTarget.calcTightBounds(minPoint, maxPoint, self.world.render): + # 检查边界框的有效性 + if (abs(minPoint.x) < 1e10 and abs(minPoint.y) < 1e10 and abs(minPoint.z) < 1e10 and + abs(maxPoint.x) < 1e10 and abs(maxPoint.y) < 1e10 and abs(maxPoint.z) < 1e10): + # 计算中心点 + center = Point3((minPoint.x + maxPoint.x) * 0.5, + (minPoint.y + maxPoint.y) * 0.5, + (minPoint.z + maxPoint.z) * 0.5) + self.gizmo.setPos(center) + except Exception as e: + print(f"更新Gizmo位置时出错: {e}") + # if self.gizmoTarget.calcTightBounds(minPoint, maxPoint, self.world.render): + # # 计算中心点 + # center = Point3((minPoint.x + maxPoint.x) * 0.5, + # (minPoint.y + maxPoint.y) * 0.5, + # (minPoint.z + maxPoint.z) * 0.5) + # self.gizmo.setPos(center) self._last_gizmo_bounds_update = current_time is_scale_tool = self.world.tool_manager.isScaleTool() if self.world.tool_manager else False @@ -703,7 +783,7 @@ class SelectionSystem: self.gizmo.setScale(scale_factor) # 限制缩放范围,避免过大或过小 - min_scale = 0.08 + min_scale = 0.001 max_scale = 100.0 final_scale = max(min_scale, min(max_scale, scale_factor)) @@ -733,6 +813,7 @@ class SelectionSystem: self.dragStartMousePos = None self.gizmoTargetStartPos = None self.gizmoStartPos = None + self._resetCursor() # def setGizmoAxisColor(self, axis, color): @@ -889,15 +970,18 @@ class SelectionSystem: axis_node = axis_nodes[axis] + if axis_node.isEmpty(): + return + handle_node = None handle_node = axis_node.find("x_handle") if axis == "x" else handle_node handle_node = axis_node.find("y_handle") if axis == "y" else handle_node handle_node = axis_node.find("z_handle") if axis == "z" else handle_node - #如果找不到特定名称的节点,尝试查找任何子节点 - if not handle_node: + # 如果找不到特定名称的节点,尝试查找任何子节点 + if not handle_node or handle_node.isEmpty(): children = axis_node.getChildren() - if children.getNumPath()>0: + if children.getNumPaths() > 0: handle_node = children[0] if not handle_node: @@ -907,15 +991,6 @@ class SelectionSystem: # 创建或获取材质 mat = Material() - # # 设置材质属性 - 使用自发光确保在RenderPipeline下可见 - # mat.setBaseColor(Vec4(color[0], color[1], color[2], color[3])) - # mat.setDiffuse(Vec4(0, 0, 0, 1)) - # #mat.setEmission(Vec4(color[0], color[1], color[2], 1.0)) # 自发光 - # mat.setEmission(Vec4(1,1,1,1.0)) # 自发光 - # mat.set_roughness(1) - - # 设置材质属性 - 使用更自然的颜色,避免过亮的自发光 - # 将颜色值控制在合理范围内 adjusted_color = Vec4( min(color[0], 1.0), min(color[1], 1.0), @@ -924,16 +999,11 @@ class SelectionSystem: ) mat.setBaseColor(adjusted_color) - #mat.setDiffuse(adjusted_color * 1) # 稍微降低漫反射亮度 - #mat.setAmbient(adjusted_color * 0.4) # 设置环境光反射 - # mat.setSpecular(Vec4(0.4, 0.4, 0.4, 1.0)) # 适度的镜面反射 - # mat.setShininess(1.0) # 适中的高光强度 mat.setEmission(Vec4(1, 1, 1, 1.0)) # 自发光 # 应用材质 handle_node.setMaterial(mat, 1) - # 设置透明度 if color[3] < 1.0: handle_node.setTransparency(TransparencyAttrib.MAlpha) @@ -1336,6 +1406,7 @@ class SelectionSystem: def updateGizmoHighlight(self, mouseX, mouseY): """更新坐标轴高亮状态""" if not self.gizmo or self.isDraggingGizmo: + self._resetCursor() return # 使用碰撞检测方法 @@ -1357,15 +1428,19 @@ class SelectionSystem: # 高亮新的轴 if hoveredAxis: self.setGizmoAxisColor(hoveredAxis, self.gizmo_highlight_colors[hoveredAxis]) + self._setCursor("pointing_hand") else: # 如果没有悬停在任何轴上,确保所有轴都恢复原始颜色 for axis_name in ["x", "y", "z"]: if axis_name != self.dragGizmoAxis: # 不要改变正在拖拽的轴的颜色 self.setGizmoAxisColor(axis_name, self.gizmo_colors[axis_name]) + self._resetCursor() self.gizmoHighlightAxis = hoveredAxis self._last_detected_axis = hoveredAxis + elif hoveredAxis is None: + self._resetCursor() def _detectHoveredAxis(self, mouseX, mouseY): """检测鼠标悬停的轴 - 提取为独立方法""" @@ -1445,6 +1520,13 @@ class SelectionSystem: # self.dragGizmoAxis = axis # # self.gizmoHighlightAxis = self.dragGizmoAxis + # 设置拖拽光标 + if self.dragGizmoAxis == "x": + self._setCursor("size_all") # 水平调整光标 + elif self.dragGizmoAxis == "y": + self._setCursor("size_all") # 垂直调整光标 + elif self.dragGizmoAxis == "z": + self._setCursor("size_all") # 全向调整光标 print( f"开始拖拽 {self.dragGizmoAxis} 轴 - 目标起始位置: {self.gizmoTargetStartPos}, 坐标轴位置: {self.gizmoStartPos}, 鼠标: ({mouseX}, {mouseY})") @@ -1487,6 +1569,9 @@ class SelectionSystem: if is_scale_tool: scale_factor = 1.0 + (mouseDeltaX + mouseDeltaY) * 0.01 + + scale_factor = max(0.001, scale_factor) + start_scale = getattr(self,'gizmoTargetStartScale',Vec3(1,1,1)) if is_gui_element: @@ -1514,6 +1599,12 @@ class SelectionSystem: start_scale.y * scale_factor, start_scale.z * scale_factor) + new_scale = Vec3( + max(0.001,new_scale.x), + max(0.001,new_scale.y), + max(0.001,new_scale.z) + ) + # 应用新缩放值 self.gizmoTarget.setScale(new_scale) # 安全地更新属性面板 @@ -1528,7 +1619,7 @@ class SelectionSystem: if self.dragGizmoAxis == "x": new_hpr = Vec3(start_hpr.x+rotation_amount,start_hpr.y,start_hpr.z) elif self.dragGizmoAxis == "y": - new_hpr = Vec3(start_hpr.x,start_hpr.y+rotation_amount,start_hpr.z) + new_hpr = Vec3(start_hpr.x,start_hpr.y-rotation_amount,start_hpr.z) elif self.dragGizmoAxis == "z": new_hpr = Vec3(start_hpr.x,start_hpr.y,start_hpr.z+rotation_amount) else: @@ -1776,6 +1867,7 @@ class SelectionSystem: # 重置高亮轴 self.gizmoHighlightAxis = None + self._resetCursor() # ==================== 选择管理 ==================== def updateSelection(self, nodePath): diff --git a/core/terrain_manager.py b/core/terrain_manager.py index dd940091..cfd85590 100644 --- a/core/terrain_manager.py +++ b/core/terrain_manager.py @@ -13,6 +13,15 @@ class TerrainManager: self.terrains = [] # core/terrain_manager.py + def _get_tree_widget(self): + """安全获取树形控件""" + try: + if (hasattr(self.world, 'interface_manager') and + hasattr(self.world.interface_manager, 'treeWidget')): + return self.world.interface_manager.treeWidget + except AttributeError: + pass + return None def createTerrainFromHeightMap(self, heightmap_path, scale=(1, 1, 1)): """从高度图创建地形""" @@ -381,9 +390,6 @@ class TerrainManager: print(f"✓ 地形高度已修改: 位置({x}, {y}), 半径{radius}, 操作{operation}") print(f"✓ 重新设置了地形碰撞体") - # 更新场景树 - if hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, 'updateSceneTree'): - self.world.scene_manager.updateSceneTree() return modified diff --git a/core/world.py b/core/world.py index d987d256..4860ad50 100644 --- a/core/world.py +++ b/core/world.py @@ -33,38 +33,71 @@ class CoreWorld(Panda3DWorld): self.mouseRightPressed = False # 初始化世界 + self._setupResourcePaths() self._setupCamera() self._setupLighting() self._setupGround() self._loadFont() - #self.load_and_play_glb_model() - def load_and_play_glb_model(self): - """加载 glTF 模型并播放动画""" + def _setupResourcePaths(self): + """设置Panda3D资源搜索路径,确保能正确找到Resources文件夹中的模型和贴图""" 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 绑定到节点 - - + import os + from panda3d.core import getModelPath, DSearchPath, Filename + + # 获取项目根目录 + project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + resources_dir = os.path.join(project_root, "Resources") + + # 确保Resources目录存在 + if not os.path.exists(resources_dir): + os.makedirs(resources_dir, exist_ok=True) + print(f"✓ 创建Resources目录: {resources_dir}") + + # 添加Resources目录到Panda3D模型搜索路径 + model_path = getModelPath() + resources_filename = Filename.from_os_specific(resources_dir) + + # 检查路径是否已存在,避免重复添加 + if not model_path.findFile(resources_filename): + model_path.appendDirectory(resources_filename) + print(f"✓ 添加Resources到模型搜索路径: {resources_dir}") + + # 同时添加各个子目录到搜索路径 + subdirs = ['models', 'textures', 'animations', 'icons', 'materials'] + for subdir in subdirs: + subdir_path = os.path.join(resources_dir, subdir) + if os.path.exists(subdir_path): + subdir_filename = Filename.from_os_specific(subdir_path) + if not model_path.findFile(subdir_filename): + model_path.appendDirectory(subdir_filename) + print(f"✓ 添加子目录到搜索路径: {subdir}") + else: + # 创建不存在的子目录 + os.makedirs(subdir_path, exist_ok=True) + subdir_filename = Filename.from_os_specific(subdir_path) + model_path.appendDirectory(subdir_filename) + print(f"✓ 创建并添加子目录: {subdir}") + + # 设置纹理搜索路径 + from panda3d.core import getTexturePath + texture_path = getTexturePath() + if not texture_path.findFile(resources_filename): + texture_path.appendDirectory(resources_filename) + + for subdir in ['textures', 'materials', 'icons']: + subdir_path = os.path.join(resources_dir, subdir) + if os.path.exists(subdir_path): + subdir_filename = Filename.from_os_specific(subdir_path) + if not texture_path.findFile(subdir_filename): + texture_path.appendDirectory(subdir_filename) + + print(f"✓ 资源路径设置完成") + print(f" 项目根目录: {project_root}") + print(f" Resources目录: {resources_dir}") + except Exception as e: - print(f"模型加载失败: {e}") - return None + print(f"⚠️ 设置资源路径失败: {e}") def diagnose_fbx_loading(self, fbx_path): """诊断FBX加载状态""" @@ -399,14 +432,14 @@ class CoreWorld(Panda3DWorld): def mousePressEventRight(self, evt): """处理鼠标右键按下事件""" - print("右键按下") + #print("右键按下") self.mouseRightPressed = True self.lastMouseX = evt['x'] self.lastMouseY = evt['y'] def mouseReleaseEventRight(self, evt): """处理鼠标右键释放事件""" - print("右键释放") + #print("右键释放") self.mouseRightPressed = False def mouseMoveEvent(self, evt): diff --git a/gui/gui_manager.py b/gui/gui_manager.py index 1f1c921e..babc94c2 100644 --- a/gui/gui_manager.py +++ b/gui/gui_manager.py @@ -24,6 +24,78 @@ except ImportError: WEB_ENGINE_AVAILABLE = False print("⚠️ QtWebEngineWidgets 不可用,Cesium 集成功能将被禁用") + def createGUI3DImage(self, pos=(0, 0, 0), image_path=None, size=1.0): + from panda3d.core import CardMaker, Material, LColor,TransparencyAttrib + + # 参数类型检查和转换 + if isinstance(size, (list, tuple)): + if len(size) >= 2: + x_size, y_size = float(size[0]), float(size[1]) + else: + x_size = y_size = float(size[0]) if size else 1.0 + else: + x_size = y_size = float(size) + + # 创建卡片 + cm = CardMaker('gui_3d_image') + cm.setFrame(-x_size/2, x_size/2, -y_size/2, y_size/2) + + # 创建3D图像节点 + image_node = self.world.render.attachNewNode(cm.generate()) + image_node.setPos(*pos) + + # 为3D图像创建独立的材质 + material = Material(f"image-material-{len(self.gui_elements)}") + material.setBaseColor(LColor(1, 1, 1, 1)) + material.setDiffuse(LColor(1, 1, 1, 1)) + material.setAmbient(LColor(0.5, 0.5, 0.5, 1)) + material.setSpecular(LColor(0.1, 0.1, 0.1, 1.0)) + material.setShininess(10.0) + material.setEmission(LColor(0, 0, 0, 1)) # 无自发光 + image_node.setMaterial(material, 1) + + image_node.setTransparency(TransparencyAttrib.MAlpha) + + # 如果提供了图像路径,则加载纹理 + if image_path: + self.update3DImageTexture(image_node, image_path) + + # 应用PBR效果(如果可用) + try: + if hasattr(self, 'render_pipeline') and self.render_pipeline: + self.render_pipeline.set_effect( + image_node, + "effects/default.yaml", + { + "normal_mapping": True, + "render_gbuffer": True, + "alpha_testing": False, + "parallax_mapping": False, + "render_shadow": False, + "render_envmap": True, + "disable_children_effects": True + }, + 50 + ) + print("✓ GUI 3D图像PBR效果已应用") + except Exception as e: + print(f"⚠️ GUI 3D图像PBR效果应用失败: {e}") + + # 为GUI元素添加标识(效仿3D文本方法) + image_node.setTag("gui_type", "3d_image") + image_node.setTag("gui_id", f"3d_image_{len(self.gui_elements)}") + if image_path: + image_node.setTag("gui_image_path", image_path) + image_node.setTag("is_gui_element", "1") + + self.gui_elements.append(image_node) + + # 更新场景树 + if hasattr(self.world, 'updateSceneTree'): + self.world.updateSceneTree() + + print(f"✓ 3D图像创建完成: {image_path or '无纹理'} (世界位置: {pos})") + return image_node class GUIManager: """GUI元素管理系统类""" @@ -85,17 +157,23 @@ class GUIManager: # 父节点是GUI元素 - 作为子GUI挂载 gui_pos = tree_widget.calculate_relative_gui_position(pos, parent_node) parent_gui_node = parent_node # 直接挂载到GUI元素 - print(f"📎 挂载到GUI父节点: {parent_node.getName()}") + parent_scale=parent_node.getScale() + relative_scale = ( + size/parent_scale[0] if parent_scale[0]!=0 else size, + size/parent_scale[1] if parent_scale[1]!=0 else size, + size/parent_scale[2] if parent_scale[2]!=0 else size + ) else: # 父节点是普通3D节点 - 使用屏幕坐标 gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1) parent_gui_node = None # 使用默认的aspect2d + relative_scale = size print(f"📎 挂载到3D父节点: {parent_item.text(0)}") button = DirectButton( text=text, pos=gui_pos, - scale=size, + scale=relative_scale, command=self.onGUIButtonClick, extraArgs=[f"button_{len(self.gui_elements)}"], frameColor=(0.2, 0.6, 0.8, 1), @@ -196,17 +274,22 @@ class GUIManager: # 父节点是GUI元素 - 作为子GUI挂载 gui_pos = tree_widget.calculate_relative_gui_position(pos, parent_node) parent_gui_node = parent_node - print(f"📎 挂载到GUI父节点: {parent_node.getName()}") + + parent_scale = parent_node.getScale() + relative_scale = ( + size/parent_scale[0] if parent_scale[0]!= 0 else size, + size/parent_scale[1] if parent_scale[1]!= 0 else size, + size/parent_scale[2] if parent_scale[2]!= 0 else size + ) else: # 父节点是普通3D节点 - 使用屏幕坐标 gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1) parent_gui_node = None - print(f"📎 挂载到3D父节点: {parent_item.text(0)}") - + relative_scale = size label = DirectLabel( text=text, pos=gui_pos, - scale=size, + scale=relative_scale, frameColor=(0, 0, 0, 0), # 透明背景 text_fg=(1, 1, 1, 1), text_font=self.world.getChineseFont() if self.world.getChineseFont() else None, @@ -272,211 +355,6 @@ class GUIManager: return None def createGUIEntry(self, pos=(0, 0, 0), placeholder="输入文本...", size=0.08): - """创建2D GUI文本输入框""" - from direct.gui.DirectGui import DirectEntry - - # 将3D坐标转换为2D屏幕坐标 - gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1) - - entry = DirectEntry( - text="", - pos=gui_pos, - scale=size, - command=self.onGUIEntrySubmit, - extraArgs=[f"entry_{len(self.gui_elements)}"], - initialText=placeholder, - numLines=1, - width=12, - focus=0 - ) - - # 为GUI元素添加标识 - entry.setTag("gui_type", "entry") - entry.setTag("gui_id", f"entry_{len(self.gui_elements)}") - entry.setTag("gui_placeholder", placeholder) - entry.setTag("is_gui_element", "1") - - self.gui_elements.append(entry) - # 安全地调用updateSceneTree - if hasattr(self.world, 'updateSceneTree'): - self.world.updateSceneTree() - - print(f"✓ 创建GUI输入框: {placeholder} (逻辑位置: {pos}, 屏幕位置: {gui_pos})") - return entry - def createGUI2DImage(self, pos=(0, 0, 0), image_path=None, size=0.2): - """创建2D GUI图片""" - from direct.gui.DirectGui import DirectFrame - from panda3d.core import TransparencyAttrib,Texture,CardMaker,CardMaker,Vec3 - - # 将3D坐标转换为2D屏幕坐标 - gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1) - - #使用CardMaker创建一个更可靠的图片框架 - cm = CardMaker('gui-2d-image') - cm.setFrame(-size,size,-size,size) - - image_node = self.world.aspect2d.attachNewNode(cm.generate()) - image_node.setPos(gui_pos) - image_node.setBin('fixed',0) - image_node.setDepthWrite(False) - image_node.setDepthTest(False) - image_node.setColor(1,1,1,1) - - # 设置透明度支持 - image_node.setTransparency(TransparencyAttrib.MAlpha) - - # 如果提供了图像路径,则加载纹理 - if image_path: - try: - texture = self.world.loader.loadTexture(image_path) - if texture: - image_node.setTexture(texture,1) - texture.setWrapU(Texture.WM_clamp) - texture.setWrapV(Texture.WM_clamp) - texture.setMinfilter(Texture.FT_linear) - texture.setMagfilter(Texture.FT_linear) - image_node.setColor(1,1,1,1) - else: - print(f"⚠️ 无法加载2D图片纹理: {image_path}") - except Exception as e: - print(f"❌ 加载2D图片纹理失败: {e}") - - # 为GUI元素添加标识 - image_node.setTag("gui_type", "2d_image") - image_node.setTag("gui_id", f"2d_image_{len(self.gui_elements)}") - image_node.setTag("gui_text", f"2D图片_{len(self.gui_elements)}") - if image_path: - image_node.setTag("image_path", image_path) - image_node.setTag("is_gui_element", "1") - - self.gui_elements.append(image_node) - - # 安全地调用updateSceneTree - if hasattr(self.world, 'updateSceneTree'): - self.world.updateSceneTree() - - print(f"✓ 创建2D GUI图片 (逻辑位置: {pos}, 屏幕位置: {gui_pos})") - return image_node - - def constrain2DPosition(self,gui_element,new_x=None,new_z=None): - """限制2dGUI元素位置在屏幕范围内""" - try: - from panda3d.core import Vec3 - - bounds = gui_element.getTightBounds() - element_width=0 - element_height=0 - - if bounds: - min_point,max_point = bounds - element_width = (max_point.getX() - min_point.getX())/2 - element_height = (max_point.getZ()-min_point.getZ())/2 - - #获取当前缩放 - scale = gui_element.getScale() - if hasattr(scale,'getX'): - scale_x = scale.getX() - scale_z = scale.getZ() if hasattr(scale,'getZ') else scale_x - else: - scale_x = scale_z = scale if isinstance(scale,(int,float)) else 1.0 - - actual_width = element_width * scale_x - actual_height = element_height * scale_z - - screen_width = 1.9 - screen_height = 0.9 - - min_x = -screen_width + actual_width - max_x = screen_width - actual_width - min_z = -screen_height + actual_height - max_z = screen_height - actual_height - - #获取当前位置 - current_pos = gui_element.getPos() - x = new_x if new_x is not None else current_pos.getX() - z = new_z if new_z is not None else current_pos.getZ() - - #应用边界限制 - x = max(min_x,min(max_x,x)) - z = max(min_z,min(max_z,z)) - - return Vec3(x,current_pos.getY(),z) - except Exception as e: - print(f"约束2D位置时出错: {e}") - # 出错时返回原始值 - current_pos = gui_element.getPos() - x = new_x if new_x is not None else current_pos.getX() - z = new_z if new_z is not None else current_pos.getZ() - return Vec3(x, current_pos.getY(), z) - - - def createGUI3DText(self, pos=(0, 0, 0), text="3D文本", size=1): - """创建3D空间文本""" - from panda3d.core import TextNode,Material,Vec4,ColorAttrib,TransparencyAttrib - - textNode = TextNode(f'3d-text-{len(self.gui_elements)}') - textNode.setText(text) - textNode.setAlign(TextNode.ACenter) - if self.world.getChineseFont(): - textNode.setFont(self.world.getChineseFont()) - - textNode.setTextColor(Vec4(1,1,1,1)) - - textNodePath = self.world.render.attachNewNode(textNode) - textNodePath.setPos(*pos) - textNodePath.setScale(size,size,size) - #textNodePath.setBillboardAxis() # 让文本总是面向相机 - - # 为3D文本创建默认材质 - material = Material(f"text-material-{len(self.gui_elements)}") - material.setBaseColor(Vec4(1, 1, 1, 1)) # 白色 - material.setDiffuse(Vec4(1, 1, 1, 1)) - material.setAmbient(Vec4(0.5, 0.5, 0.5, 1)) - material.setSpecular(Vec4(0.1, 0.1, 0.1, 1.0)) - material.setShininess(10.0) - #material.setEmission(0,0,0,1) - - textNodePath.setMaterial(material, 1) - - textNodePath.setTransparency(TransparencyAttrib.MAlpha) - textNodePath.setAttrib(ColorAttrib.makeFlat(Vec4(1, 1, 1, 1))) - textNodePath.setLightOff() - - # 为GUI元素添加标识 - textNodePath.setTag("gui_type", "3d_text") - textNodePath.setTag("gui_id", f"3d_text_{len(self.gui_elements)}") - textNodePath.setTag("gui_text", text) - textNodePath.setTag("is_gui_element", "1") - - textNodePath.setDepthWrite(True) # 确保深度写入 - textNodePath.setDepthTest(True) # 启用深度测试 - textNodePath.setBin("fixed", 0) # 设置渲染层级,避免被遮挡 - - # if hasattr(self, 'render_pipeline') and self.render_pipeline: - # try: - # self.render_pipeline.set_effect( - # textNodePath, - # "effects/default.yaml", - # { - # "normal_mapping": False, - # "render_gbuffer": False, - # "alpha_testing": True, - # "parallax_mapping": False, - # "render_shadow": False, - # "render_envmap": False - # }, - # 50 - # ) - # except Exception as e: - # print(f"⚠️ PBR效果应用失败: {e}") - - self.gui_elements.append(textNodePath) - # 安全地调用updateSceneTree - if hasattr(self.world, 'updateSceneTree'): - self.world.updateSceneTree() - - print(f"✓ 创建3D文本: {text} (世界位置: {pos})") - return textNodePath """创建2D GUI文本输入框 - 支持多选创建和GUI父子关系,优化版本""" try: from direct.gui.DirectGui import DirectEntry @@ -587,6 +465,185 @@ class GUIManager: traceback.print_exc() return None + def createGUI2DImage(self, pos=(0, 0, 0), image_path=None, size=0.2): + """创建2D GUI图片""" + try: + from direct.gui.DirectGui import DirectButton + from PyQt5.QtCore import Qt + + print(f"🔘 开始创建GUI按钮,位置: {pos}, 图片路径: {image_path}, 尺寸: {size}") + + # 获取树形控件 + tree_widget = self._get_tree_widget() + if not tree_widget: + print("❌ 无法访问树形控件") + return None + + # 使用CustomTreeWidget的方法获取目标父节点列表 + target_parents = tree_widget.get_target_parents_for_gui_creation() + if not target_parents: + print("❌ 没有找到有效的父节点") + return None + + created_2dimage = [] + + # 为每个有效的父节点创建GUI按钮 + for parent_item, parent_node in target_parents: + try: + # 生成唯一名称 + image_name = f"GUIImage_{len(self.gui_elements)}" + + # 使用CustomTreeWidget的方法判断父节点类型并设置相应的挂载方式 + if tree_widget.is_gui_element(parent_node): + # 父节点是GUI元素 - 作为子GUI挂载 + gui_pos = tree_widget.calculate_relative_gui_position(pos, parent_node) + parent_gui_node = parent_node # 直接挂载到GUI元素 + print(f"📎 挂载到GUI父节点: {parent_node.getName()}") + else: + # 父节点是普通3D节点 - 使用屏幕坐标 + gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1) + parent_gui_node = None # 使用默认的aspect2d + print(f"📎 挂载到3D父节点: {parent_item.text(0)}") + + # 使用CardMaker创建一个更可靠的图片框架 + cm = CardMaker('gui-2d-image') + cm.setFrame(-size, size, -size, size) + + image_node = self.world.aspect2d.attachNewNode(cm.generate()) + image_node.setPos(gui_pos) + image_node.setBin('fixed', 0) + image_node.setDepthWrite(False) + image_node.setDepthTest(False) + image_node.setColor(1, 1, 1, 1) + + # 设置透明度支持 + image_node.setTransparency(TransparencyAttrib.MAlpha) + + # 如果提供了图像路径,则加载纹理 + if image_path: + try: + texture = self.world.loader.loadTexture(image_path) + if texture: + image_node.setTexture(texture, 1) + texture.setWrapU(Texture.WM_clamp) + texture.setWrapV(Texture.WM_clamp) + texture.setMinfilter(Texture.FT_linear) + texture.setMagfilter(Texture.FT_linear) + image_node.setColor(1, 1, 1, 1) + else: + print(f"⚠️ 无法加载2D图片纹理: {image_path}") + except Exception as e: + print(f"❌ 加载2D图片纹理失败: {e}") + + # 设置节点标签 + image_node.setTag("gui_type", "2d_image") + image_node.setTag("gui_id", f"2d_image_{len(self.gui_elements)}") + image_node.setTag("gui_text", f"2D图片_{len(self.gui_elements)}") + image_node.setTag("is_gui_element", "1") + image_node.setTag("is_scene_element", "1") + image_node.setTag("created_by_user", "1") + image_node.setTag("gui_parent_type", "gui" if parent_gui_node else "3d") + image_node.setName(image_name) + + # 如果有GUI父节点,建立引用关系 + if parent_gui_node: + parent_id = parent_gui_node.getTag("gui_id") if hasattr(parent_gui_node, 'getTag') else "" + image_node.setTag("gui_parent_id", parent_id) + + # 添加到GUI元素列表 + self.gui_elements.append(image_node) + + print(f"✅ 为 {parent_item.text(0)} 创建GUI按钮成功: {image_name}") + + # 使用CustomTreeWidget的方法在Qt树形控件中添加对应节点 + qt_item = tree_widget.add_node_to_tree_widget(image_node, parent_item, "GUI_IMAGE") + if qt_item: + created_2dimage.append((image_node, qt_item)) + else: + created_2dimage.append((image_node, None)) + print("⚠️ Qt树节点添加失败,但GUI对象已创建") + + except Exception as e: + print(f"❌ 为 {parent_item.text(0)} 创建GUI按钮失败: {str(e)}") + continue + + # 处理创建结果 + if not created_2dimage: + print("❌ 没有成功创建任何GUI按钮") + return None + + # 选中最后创建的按钮并更新场景树 + if created_2dimage: + last_button, last_qt_item = created_2dimage[-1] + if last_qt_item: + tree_widget.setCurrentItem(last_qt_item) + tree_widget.update_selection_and_properties(last_button, last_qt_item) + + print(f"🎉 总共创建了 {len(created_2dimage)} 个GUI按钮") + + # 返回值处理 + if len(created_2dimage) == 1: + return created_2dimage[0][0] + else: + return [button for button, _ in created_2dimage] + + except Exception as e: + print(f"❌ 创建GUI按钮过程失败: {str(e)}") + import traceback + traceback.print_exc() + return None + + def constrain2DPosition(self,gui_element,new_x=None,new_z=None): + """限制2dGUI元素位置在屏幕范围内""" + try: + from panda3d.core import Vec3 + + bounds = gui_element.getTightBounds() + element_width=0 + element_height=0 + + if bounds: + min_point,max_point = bounds + element_width = (max_point.getX() - min_point.getX())/2 + element_height = (max_point.getZ()-min_point.getZ())/2 + + #获取当前缩放 + scale = gui_element.getScale() + if hasattr(scale,'getX'): + scale_x = scale.getX() + scale_z = scale.getZ() if hasattr(scale,'getZ') else scale_x + else: + scale_x = scale_z = scale if isinstance(scale,(int,float)) else 1.0 + + actual_width = element_width * scale_x + actual_height = element_height * scale_z + + screen_width = 1.9 + screen_height = 0.9 + + min_x = -screen_width + actual_width + max_x = screen_width - actual_width + min_z = -screen_height + actual_height + max_z = screen_height - actual_height + + #获取当前位置 + current_pos = gui_element.getPos() + x = new_x if new_x is not None else current_pos.getX() + z = new_z if new_z is not None else current_pos.getZ() + + #应用边界限制 + x = max(min_x,min(max_x,x)) + z = max(min_z,min(max_z,z)) + + return Vec3(x,current_pos.getY(),z) + except Exception as e: + print(f"约束2D位置时出错: {e}") + # 出错时返回原始值 + current_pos = gui_element.getPos() + x = new_x if new_x is not None else current_pos.getX() + z = new_z if new_z is not None else current_pos.getZ() + return Vec3(x, current_pos.getY(), z) + def createGUI3DText(self, pos=(0, 0, 0), text="3D文本", size=0.5): """创建3D空间文本 - 支持多选创建,优化版本""" try: @@ -626,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) # 设置节点标签 @@ -680,6 +737,1209 @@ class GUIManager: traceback.print_exc() return None + def createGUI3DImage(self, pos=(0, 0, 0), image_path=None, size=1.0): + """创建3D空间图片""" + try: + from panda3d.core import TextNode + from PyQt5.QtCore import Qt + + print(f"📄 开始创建3D文本,位置: {pos}, 3D图片位置: {image_path}, 尺寸: {size}") + + # 获取树形控件 + tree_widget = self._get_tree_widget() + if not tree_widget: + print("❌ 无法访问树形控件") + return None + + # 获取目标父节点列表 + target_parents = tree_widget.get_target_parents_for_creation() + if not target_parents: + print("❌ 没有找到有效的父节点") + return None + + created_3dimage = [] + + # 为每个有效的父节点创建3D文本 + for parent_item, parent_node in target_parents: + try: + # 生成唯一名称 + image_name = f"GUI3DImage_{len(self.gui_elements)}" + + # 参数类型检查和转换 + if isinstance(size, (list, tuple)): + if len(size) >= 2: + x_size, y_size = float(size[0]), float(size[1]) + else: + x_size = y_size = float(size[0]) if size else 1.0 + else: + x_size = y_size = float(size) + + # 创建卡片 + cm = CardMaker('gui_3d_image') + cm.setFrame(-x_size / 2, x_size / 2, -y_size / 2, y_size / 2) + + # 创建3D图像节点 + image_node = self.world.render.attachNewNode(cm.generate()) + image_node.setPos(*pos) + + # 为3D图像创建独立的材质 + material = Material(f"image-material-{len(self.gui_elements)}") + material.setBaseColor(LColor(1, 1, 1, 1)) + material.setDiffuse(LColor(1, 1, 1, 1)) + material.setAmbient(LColor(0.5, 0.5, 0.5, 1)) + material.setSpecular(LColor(0.1, 0.1, 0.1, 1.0)) + material.setShininess(10.0) + material.setEmission(LColor(0, 0, 0, 1)) # 无自发光 + image_node.setMaterial(material, 1) + + image_node.setTransparency(TransparencyAttrib.MAlpha) + + # 如果提供了图像路径,则加载纹理 + if image_path: + self.update3DImageTexture(image_node, image_path) + + # 应用PBR效果(如果可用) + try: + if hasattr(self, 'render_pipeline') and self.render_pipeline: + self.render_pipeline.set_effect( + image_node, + "effects/default.yaml", + { + "normal_mapping": True, + "render_gbuffer": True, + "alpha_testing": False, + "parallax_mapping": False, + "render_shadow": False, + "render_envmap": True, + "disable_children_effects": True + }, + 50 + ) + print("✓ GUI 3D图像PBR效果已应用") + except Exception as e: + print(f"⚠️ GUI 3D图像PBR效果应用失败: {e}") + image_node.setName(image_name) + + # 设置节点标签 + image_node.setTag("gui_type", "3d_image") + image_node.setTag("gui_id", f"3d_image_{len(self.gui_elements)}") + if image_path: + image_node.setTag("gui_image_path", image_path) + image_node.setTag("is_gui_element", "1") + image_node.setTag("is_scene_element", "1") + image_node.setTag("created_by_user", "1") + + # 添加到GUI元素列表 + self.gui_elements.append(image_node) + + print(f"✅ 为 {parent_item.text(0)} 创建3D文本成功: {image_name}") + + # 在Qt树形控件中添加对应节点 + qt_item = tree_widget.add_node_to_tree_widget(image_node, parent_item, "GUI_3DIMAGE") + if qt_item: + created_3dimage.append((image_node, qt_item)) + else: + created_3dimage.append((image_node, None)) + print("⚠️ Qt树节点添加失败,但GUI对象已创建") + + except Exception as e: + print(f"❌ 为 {parent_item.text(0)} 创建3D文本失败: {str(e)}") + continue + + # 处理创建结果 + if not created_3dimage: + print("❌ 没有成功创建任何3D文本") + return None + + # 选中最后创建的文本并更新场景树 + if created_3dimage: + last_image, last_qt_item = created_3dimage[-1] + if last_qt_item: + tree_widget.setCurrentItem(last_qt_item) + tree_widget.update_selection_and_properties(last_image, last_qt_item) + + print(f"🎉 总共创建了 {len(created_3dimage)} 个3D文本") + + # 返回值处理 + if len(created_3dimage) == 1: + return created_3dimage[0][0] + else: + return [text_np for text_np, _ in created_3dimage] + + except Exception as e: + print(f"❌ 创建3D文本过程失败: {str(e)}") + import traceback + traceback.print_exc() + return None + + def createVideoScreen(self, pos=(0, 0, 0), size=0.2, video_path=None): + """创建3D视频播放屏幕 - 添加占位符纹理支持""" + try: + from panda3d.core import CardMaker, TransparencyAttrib, Texture, TextureStage + import os + + # 确保 pos 是有效的三维坐标元组 + if not isinstance(pos, (tuple, list)) or len(pos) != 3: + print(f"⚠️ 位置参数无效,使用默认值 (0, 0, 0),原始值: {pos}") + pos = (0, 0, 0) + else: + # 确保所有坐标都是数值类型 + try: + pos = (float(pos[0]), float(pos[1]), float(pos[2])) + except (ValueError, TypeError): + print(f"⚠️ 位置参数包含非数值,使用默认值 (0, 0, 0),原始值: {pos}") + pos = (0, 0, 0) + + # 确保 size 是有效数值 + try: + size = float(size) + except (ValueError, TypeError): + print(f"⚠️ 尺寸参数无效,使用默认值 0.2,原始值: {size}") + size = 0.2 + + print(f"📺 开始创建视频屏幕,位置: {pos}, 尺寸: {size}, 视频路径: {video_path}") + + # 获取树形控件 + tree_widget = self._get_tree_widget() + if not tree_widget: + print("❌ 无法访问树形控件") + return None + + # 获取目标父节点列表 + target_parents = tree_widget.get_target_parents_for_creation() + if not target_parents: + print("❌ 没有找到有效的父节点") + return None + + created_videoscreens = [] + + # 为每个有效的父节点创建视频屏幕 + for parent_item, parent_node in target_parents: + try: + # 生成唯一名称 + screen_name = f"VideoScreen_{len(self.gui_elements)}" + + # 使用CardMaker创建视频屏幕框架 + cm = CardMaker('video-screen') + cm.setFrame(-size, size, -size, size) + + # 创建挂载节点 - 挂载到选中的父节点 + video_screen = parent_node.attachNewNode(cm.generate()) + video_screen.setPos(*pos) + video_screen.setName(screen_name) + video_screen.setBin('fixed', 10) + + # 设置透明度支持 + video_screen.setTransparency(TransparencyAttrib.MAlpha) + + # 设置初始颜色为白色,确保纹理能正确显示 + video_screen.setColor(1, 1, 1, 1) + + # 确保视频屏幕有正确的材质 + self._ensureVideoScreenMaterial(video_screen) + + # 设置节点标签 + video_screen.setTag("gui_type", "video_screen") + video_screen.setTag("gui_id", f"video_screen_{len(self.gui_elements)}") + video_screen.setTag("gui_text", f"视频屏幕_{len(self.gui_elements)}") + video_screen.setTag("is_gui_element", "1") + video_screen.setTag("is_scene_element", "1") + video_screen.setTag("created_by_user", "1") + + # 设置视频路径标签 + if video_path and os.path.exists(video_path): + video_screen.setTag("video_path", video_path) + else: + video_screen.setTag("video_path", "") + + # 关键修改:预先创建一个占位符纹理,为后续视频播放做准备 + placeholder_texture = Texture(f"placeholder_video_texture_3d_{len(self.gui_elements)}") + placeholder_texture.setup2dTexture(1, 1, Texture.TUnsignedByte, Texture.FRgb) + placeholder_data = b'\x19\x19\x4c' # 深蓝色占位符颜色 (25, 25, 76) + placeholder_texture.setRamImage(placeholder_data) + + # 创建纹理阶段并应用占位符纹理到视频屏幕 + texture_stage = TextureStage("video_placeholder") + texture_stage.setSort(0) + texture_stage.setMode(TextureStage.MModulate) + video_screen.setTexture(texture_stage, placeholder_texture) + + # 保存占位符纹理引用 + video_screen.setPythonTag("placeholder_texture", placeholder_texture) + + print(f"🔧 为3D视频屏幕创建了占位符纹理环境: {screen_name}") + + # 如果提供了视频路径,则加载视频纹理 + movie_texture = None + if video_path and os.path.exists(video_path): + try: + print(f"🔍 尝试加载视频纹理: {video_path}") + # 加载视频纹理 + movie_texture = self._loadMovieTexture(video_path) + if movie_texture: + # 创建纹理阶段 + texture_stage = TextureStage("video") + texture_stage.setSort(0) + texture_stage.setMode(TextureStage.MModulate) + video_screen.setTexture(texture_stage, movie_texture) + + print(f"✅ 视频纹理加载成功: {video_path}") + + # 保存视频纹理引用以便后续控制 + video_screen.setPythonTag("movie_texture", movie_texture) + + # 尝试自动播放视频(如果支持) + try: + if hasattr(movie_texture, 'play'): + movie_texture.play() + print("▶️ 视频已开始播放") + except Exception as play_error: + print(f"⚠️ 视频自动播放失败: {play_error}") + else: + print(f"⚠️ 无法加载视频纹理: {video_path}") + # 使用默认颜色作为占位符 + video_screen.setColor(0.1, 0.1, 0.3, 0.8) + except Exception as e: + print(f"❌ 加载视频纹理失败: {e}") + import traceback + traceback.print_exc() + # 使用默认颜色作为占位符 + video_screen.setColor(0.1, 0.1, 0.3, 0.8) + else: + # 没有视频文件时显示默认颜色 + video_screen.setColor(0.1, 0.1, 0.3, 0.8) + if video_path: + print(f"⚠️ 视频文件不存在: {video_path}") + else: + print("ℹ️ 未提供视频文件,显示默认占位符") + + # 保存视频纹理引用以便后续控制 + if movie_texture: + video_screen.setPythonTag("movie_texture", movie_texture) + + # 添加到GUI元素列表 + self.gui_elements.append(video_screen) + + print(f"✅ 为 {parent_item.text(0)} 创建视频屏幕成功: {screen_name}") + + # 在Qt树形控件中添加对应节点 + qt_item = tree_widget.add_node_to_tree_widget(video_screen, parent_item, "GUI_VIDEO_SCREEN") + if qt_item: + created_videoscreens.append((video_screen, qt_item)) + else: + created_videoscreens.append((video_screen, None)) + print("⚠️ Qt树节点添加失败,但Panda3D对象已创建") + + except Exception as e: + print(f"❌ 为 {parent_item.text(0)} 创建视频屏幕失败: {str(e)}") + import traceback + traceback.print_exc() + continue + + # 处理创建结果 + if not created_videoscreens: + print("❌ 没有成功创建任何视频屏幕") + return None + + # 选中最后创建的视频屏幕 + if created_videoscreens: + last_screen_np, last_qt_item = created_videoscreens[-1] + if last_qt_item: + tree_widget.setCurrentItem(last_qt_item) + # 更新选择和属性面板 + tree_widget.update_selection_and_properties(last_screen_np, last_qt_item) + + print(f"🎉 总共创建了 {len(created_videoscreens)} 个视频屏幕") + + # 返回值处理 + if len(created_videoscreens) == 1: + return created_videoscreens[0][0] # 单个屏幕返回NodePath + else: + return [screen_np for screen_np, _ in created_videoscreens] # 多个屏幕返回列表 + + except Exception as e: + print(f"❌ 创建视频屏幕过程失败: {str(e)}") + import traceback + traceback.print_exc() + return None + + def _ensureVideoScreenMaterial(self, video_screen): + """确保视频屏幕有正确的材质设置""" + try: + from panda3d.core import Material, LColor + + # 如果还没有材质,则创建一个 + if not video_screen.hasMaterial(): + material = Material(f"video-material-{video_screen.getName()}") + material.setBaseColor(LColor(1, 1, 1, 1)) + material.setDiffuse(LColor(1, 1, 1, 1)) + material.setAmbient(LColor(1, 1, 1, 1)) # 确保环境光为白色 + material.setEmission(LColor(0, 0, 0, 1)) + material.setSpecular(LColor(0, 0, 0, 1)) + material.setShininess(0) + video_screen.setMaterial(material, 1) + print(f"✅ 为视频屏幕创建了新材质: {video_screen.getName()}") + else: + # 更新现有材质确保正确设置 + material = video_screen.getMaterial() + material.setBaseColor(LColor(1, 1, 1, 1)) + material.setAmbient(LColor(1, 1, 1, 1)) # 确保环境光为白色 + video_screen.setMaterial(material, 1) + print(f"✅ 更新了视频屏幕材质: {video_screen.getName()}") + + except Exception as e: + print(f"⚠️ 设置视频屏幕材质时出错: {e}") + + def _debugVideoScreenTextures(self, video_screen): + """调试视频屏幕的纹理状态""" + try: + print(f"调试视频屏幕 {video_screen.getName()}:") + + # 检查PythonTag + movie_texture = video_screen.getPythonTag("movie_texture") + if movie_texture: + print(f" - PythonTag movie_texture: {type(movie_texture)}") + if hasattr(movie_texture, 'is_playable'): + print(f" - is_playable: {movie_texture.is_playable()}") + else: + print(" - PythonTag movie_texture: None") + + # 检查所有纹理阶段 + texture_stages = video_screen.findAllTextureStages() + print(f" - 纹理阶段数: {texture_stages.getNumStages()}") + for i in range(texture_stages.getNumStages()): + stage = texture_stages.getStage(i) + texture = video_screen.getTexture(stage) + print(f" - 阶段 {i}: {stage.getName()}, 纹理: {texture.getName() if texture else 'None'}") + + except Exception as e: + print(f"调试视频屏幕纹理时出错: {e}") + + def playVideo(self, video_screen): + """播放视频 - 改进版本,支持从暂停处继续播放""" + try: + # 获取视频纹理 + movie_texture = self._getMovieTextureFromScreen(video_screen) + + if movie_texture: + # 检查是否有播放方法 + if hasattr(movie_texture, 'play'): + try: + movie_texture.play() + print(f"▶️ 继续播放视频: {video_screen.getName()}") + return True + except Exception as play_error: + print(f"⚠️ 播放视频时出错: {play_error}") + return False + else: + print(f"⚠️ 纹理对象没有播放方法: {video_screen.getName()}") + return False + else: + print(f"❌ 视频屏幕没有关联的视频纹理: {video_screen.getName()}") + self._debugVideoScreenTextures(video_screen) + return False + except Exception as e: + print(f"❌ 播放视频失败: {e}") + import traceback + traceback.print_exc() + return False + + def _getMovieTextureFromScreen(self, video_screen): + """从视频屏幕获取视频纹理""" + try: + # 方法1: 从PythonTag获取 + movie_texture = video_screen.getPythonTag("movie_texture") + if movie_texture: + return movie_texture + + # 方法2: 从纹理阶段获取 + from panda3d.core import TextureStage + texture_stage = video_screen.findTextureStage("video") + if texture_stage: + movie_texture = video_screen.getTexture(texture_stage) + if movie_texture: + return movie_texture + + # 方法3: 获取第一个纹理 + if video_screen.hasTexture(): + movie_texture = video_screen.getTexture() + if movie_texture: + return movie_texture + + return None + except Exception as e: + print(f"获取视频纹理时出错: {e}") + return None + + def pauseVideo(self, video_screen): + """暂停视频""" + try: + movie_texture = self._getMovieTextureFromScreen(video_screen) + + if movie_texture: + # 检查是否有暂停方法 + if hasattr(movie_texture, 'stop'): # MovieTexture使用stop来暂停 + try: + movie_texture.stop() + print(f"⏸️ 视频已暂停: {video_screen.getName()}") + return True + except Exception as stop_error: + print(f"⚠️ 暂停视频时出错: {stop_error}") + return False + elif hasattr(movie_texture, 'set play rate'): # 某些版本支持设置播放速率 + try: + movie_texture.setPlayRate(0.0) + print(f"⏸️ 视频已暂停(播放速率设为0): {video_screen.getName()}") + return True + except Exception as rate_error: + print(f"⚠️ 设置播放速率时出错: {rate_error}") + return False + else: + print(f"⚠️ 纹理对象没有暂停方法: {video_screen.getName()}") + return False + else: + print(f"❌ 视频屏幕没有关联的视频纹理: {video_screen.getName()}") + return False + except Exception as e: + print(f"❌ 暂停视频失败: {e}") + import traceback + traceback.print_exc() + return False + + def stopVideo(self, video_screen): + """停止视频(回到开头)""" + try: + movie_texture = self._getMovieTextureFromScreen(video_screen) + + if movie_texture: + # 停止并重置到开头 + if hasattr(movie_texture, 'stop'): + try: + movie_texture.stop() + # 如果有重置方法,调用它 + if hasattr(movie_texture, 'setTime'): + movie_texture.setTime(0.0) + print(f"⏹️ 视频已停止: {video_screen.getName()}") + return True + except Exception as stop_error: + print(f"⚠️ 停止视频时出错: {stop_error}") + return False + else: + print(f"⚠️ 纹理对象没有停止方法: {video_screen.getName()}") + return False + else: + print(f"❌ 视频屏幕没有关联的视频纹理: {video_screen.getName()}") + return False + except Exception as e: + print(f"❌ 停止视频失败: {e}") + import traceback + traceback.print_exc() + return False + def setVideoTime(self, video_screen, time_seconds): + """设置视频播放时间""" + try: + movie_texture = video_screen.getPythonTag("movie_texture") + + # 备用获取方法 + if not movie_texture: + from panda3d.core import TextureStage + texture_stage = video_screen.findTextureStage("video") + if texture_stage: + movie_texture = video_screen.getTexture(texture_stage) + + if not movie_texture and video_screen.hasTexture(): + movie_texture = video_screen.getTexture() + + if movie_texture and hasattr(movie_texture, 'set_time'): + movie_texture.set_time(time_seconds) + print(f"🕒 设置视频时间 {time_seconds}s: {video_screen.getName()}") + return True + else: + print(f"❌ 视频屏幕没有关联的视频纹理: {video_screen.getName()}") + return False + except Exception as e: + print(f"❌ 设置视频时间失败: {e}") + return False + + def loadVideoFile(self, video_screen, video_path): + """为视频屏幕加载新的视频文件""" + try: + from panda3d.core import Texture, TextureStage + import os + + if not os.path.exists(video_path): + print(f"❌ 视频文件不存在: {video_path}") + return False + + # 加载新的视频纹理 + movie_texture = self._loadMovieTexture(video_path) + if movie_texture: + # 清除现有的纹理 + video_screen.clearTexture() + + # 设置视频纹理属性 + movie_texture.setWrapU(Texture.WM_clamp) + movie_texture.setWrapV(Texture.WM_clamp) + movie_texture.setMinfilter(Texture.FT_linear) + movie_texture.setMagfilter(Texture.FT_linear) + + # 如果视频纹理支持循环播放,设置循环 + if hasattr(movie_texture, 'set_loop'): + movie_texture.set_loop(True) + + # 如果视频纹理支持播放速率控制,设置正常速率 + if hasattr(movie_texture, 'set_play_rate'): + movie_texture.set_play_rate(1.0) + + # 重要:为视频纹理创建专用的纹理阶段 + texture_stage = TextureStage("video") + texture_stage.setSort(0) # 使用第一个纹理槽 + texture_stage.setMode(TextureStage.MModulate) + + # 应用纹理到视频屏幕 + video_screen.setTexture(texture_stage, movie_texture) + + # 保存新的视频纹理引用到PythonTag + video_screen.setPythonTag("movie_texture", movie_texture) + video_screen.setTag("video_path", video_path) + + # 确保视频屏幕有正确的材质 + self._ensureVideoScreenMaterial(video_screen) + + print(f"✅ 成功加载新视频: {video_path}") + return True + else: + print(f"❌ 无法加载视频文件: {video_path}") + return False + + except Exception as e: + print(f"❌ 加载视频文件失败: {e}") + import traceback + traceback.print_exc() + return False + + def _loadMovieTexture(self, video_path): + """加载视频纹理的兼容方法""" + try: + from panda3d.core import Texture, MovieTexture + import os + + # 检查文件是否存在 + if not os.path.exists(video_path): + print(f"❌ 视频文件不存在: {video_path}") + return None + + print(f"🔍 尝试加载视频文件: {video_path}") + + # 方法1: 尝试使用 MovieTexture(专门用于视频) + try: + movie_texture = MovieTexture(video_path) + if movie_texture.read(video_path): + print("✅ 使用 MovieTexture 成功加载视频") + self._configureVideoTexture(movie_texture) + return movie_texture + else: + print("⚠️ MovieTexture.read() 返回失败") + except Exception as e: + print(f"⚠️ MovieTexture 方法失败: {e}") + + # 方法2: 尝试使用 loader.loadTexture + try: + movie_texture = self.world.loader.loadTexture(video_path) + if movie_texture and hasattr(movie_texture, 'is_playable') and movie_texture.is_playable(): + print("✅ 使用 loader.loadTexture 成功加载视频纹理") + self._configureVideoTexture(movie_texture) + return movie_texture + else: + print("⚠️ loader.loadTexture 加载的不是可播放的视频纹理") + except Exception as e: + print(f"⚠️ loader.loadTexture 方法失败: {e}") + + # 方法3: 尝试使用 Texture.read(作为最后备选) + try: + texture = Texture() + if texture.read(video_path): + print("✅ 使用 Texture.read 成功加载(可能作为静态纹理)") + self._configureVideoTexture(texture) + return texture + except Exception as e: + print(f"⚠️ Texture.read 方法失败: {e}") + + print("❌ 所有视频纹理加载方法都失败") + return None + + except Exception as e: + print(f"❌ 加载视频纹理时发生未知错误: {e}") + import traceback + traceback.print_exc() + return None + + def _configureVideoTexture(self, texture): + """配置视频纹理属性""" + try: + from panda3d.core import Texture + + # 设置纹理属性 + texture.setWrapU(Texture.WM_clamp) + texture.setWrapV(Texture.WM_clamp) + texture.setMinfilter(Texture.FT_linear) + texture.setMagfilter(Texture.FT_linear) + + # 如果是可播放的视频纹理,设置播放属性 + if hasattr(texture, 'set_loop') and hasattr(texture, 'set_play_rate'): + texture.set_loop(True) + texture.set_play_rate(1.0) + + print(f"✅ 视频纹理配置完成: {texture.getName()}") + + except Exception as e: + print(f"⚠️ 配置视频纹理时出错: {e}") + + def createGUI2DVideoScreen(self, pos=(0, 0), size=0.2, video_path=None): + """创建2D视频播放屏幕 - 使用2D坐标""" + try: + from direct.gui.DirectGui import DirectFrame + from panda3d.core import TransparencyAttrib, Texture, TextureStage + from PyQt5.QtCore import Qt + import os + + # 确保 pos 是有效的二维坐标元组 + if pos is None or pos is False or not isinstance(pos, (tuple, list)) or len(pos) != 2: + print(f"⚠️ 位置参数无效,使用默认值 (0, 0),原始值: {pos}") + pos = (0, 0) + else: + # 确保所有坐标都是数值类型 + try: + pos = (float(pos[0]), float(pos[1])) + except (ValueError, TypeError, IndexError) as e: + print(f"⚠️ 位置参数包含非数值或索引错误,使用默认值 (0, 0),原始值: {pos}, 错误: {e}") + pos = (0, 0) + + # 确保 size 是有效数值 + try: + size = float(size) + except (ValueError, TypeError) as e: + print(f"⚠️ 尺寸参数无效,使用默认值 0.2,原始值: {size}, 错误: {e}") + size = 0.2 + + print(f"📺 开始创建2D视频屏幕,位置: {pos}, 尺寸: {size}, 视频路径: {video_path}") + + # 获取树形控件 + tree_widget = self._get_tree_widget() + if not tree_widget: + print("❌ 无法访问树形控件") + return None + + # 获取目标父节点列表 + target_parents = tree_widget.get_target_parents_for_gui_creation() + if not target_parents: + print("❌ 没有找到有效的父节点") + return None + + created_videoscreens = [] + + # 为每个有效的父节点创建2D视频屏幕 + for parent_item, parent_node in target_parents: + try: + # 生成唯一名称 + screen_name = f"GUI2DVideoScreen_{len(self.gui_elements)}" + + # 使用DirectFrame创建2D视频屏幕 + video_screen = DirectFrame( + frameSize=(-size, size, -size, size), + frameColor=(1, 1, 1, 1), # 默认背景色 + pos=(pos[0] * 0.1, 0, pos[1] * 0.1), # 转换为屏幕坐标 + parent=parent_node if tree_widget.is_gui_element(parent_node) else self.world.aspect2d, + suppressMouse=True + ) + + video_screen.setName(screen_name) + + # 设置透明度支持 + video_screen.setTransparency(TransparencyAttrib.MAlpha) + + # 设置2D视频屏幕特有的标签 + video_screen.setTag("gui_type", "2d_video_screen") + video_screen.setTag("gui_id", f"2d_video_screen_{len(self.gui_elements)}") + video_screen.setTag("gui_text", f"2D视频屏幕_{len(self.gui_elements)}") + video_screen.setTag("is_gui_element", "1") + video_screen.setTag("is_scene_element", "1") + video_screen.setTag("created_by_user", "1") + + # 设置视频路径标签 + if video_path and os.path.exists(video_path): + video_screen.setTag("video_path", video_path) + else: + video_screen.setTag("video_path", "") + + # 关键修改:预先创建一个占位符纹理,为后续视频播放做准备 + placeholder_texture = Texture(f"placeholder_video_texture_{len(self.gui_elements)}") + placeholder_texture.setup2dTexture(1, 1, Texture.TUnsignedByte, Texture.FRgb) + placeholder_data = b'\x19\x19\x4c' # 深蓝色占位符颜色 (25, 25, 76) + placeholder_texture.setRamImage(placeholder_data) + + # 应用占位符纹理到视频屏幕 + video_screen["frameTexture"] = placeholder_texture + + # 保存占位符纹理引用 + video_screen.setPythonTag("placeholder_texture", placeholder_texture) + + print(f"🔧 为2D视频屏幕创建了占位符纹理环境: {screen_name}") + + # 如果提供了视频路径,则加载视频纹理 + movie_texture = None + if video_path and os.path.exists(video_path): + try: + print(f"🔍 尝试加载2D视频纹理: {video_path}") + # 加载视频纹理 + movie_texture = self._loadMovieTexture(video_path) + if movie_texture: + # 应用纹理到视频屏幕(替换占位符) + video_screen["frameTexture"] = movie_texture + print(f"✅ 2D视频纹理加载成功: {video_path}") + + # 保存视频纹理引用以便后续控制 + video_screen.setPythonTag("movie_texture", movie_texture) + + # 尝试自动播放视频(如果支持) + try: + if hasattr(movie_texture, 'play'): + movie_texture.play() + print("▶️ 2D视频已开始播放") + except Exception as play_error: + print(f"⚠️ 2D视频自动播放失败: {play_error}") + else: + print(f"⚠️ 无法加载2D视频纹理: {video_path}") + except Exception as e: + print(f"❌ 加载2D视频纹理失败: {e}") + import traceback + traceback.print_exc() + else: + if video_path: + print(f"⚠️ 2D视频文件不存在: {video_path}") + + # 添加到GUI元素列表 + self.gui_elements.append(video_screen) + + print(f"✅ 为 {parent_item.text(0)} 创建2D视频屏幕成功: {screen_name}") + + # 在Qt树形控件中添加对应节点 + qt_item = tree_widget.add_node_to_tree_widget(video_screen, parent_item, "GUI_2D_VIDEO_SCREEN") + if qt_item: + created_videoscreens.append((video_screen, qt_item)) + else: + created_videoscreens.append((video_screen, None)) + print("⚠️ Qt树节点添加失败,但Panda3D对象已创建") + + except Exception as e: + print(f"❌ 为 {parent_item.text(0)} 创建2D视频屏幕失败: {str(e)}") + import traceback + traceback.print_exc() + continue + + # 处理创建结果 + if not created_videoscreens: + print("❌ 没有成功创建任何2D视频屏幕") + return None + + # 选中最后创建的视频屏幕 + if created_videoscreens: + last_screen_np, last_qt_item = created_videoscreens[-1] + if last_qt_item: + tree_widget.setCurrentItem(last_qt_item) + # 更新选择和属性面板 + tree_widget.update_selection_and_properties(last_screen_np, last_qt_item) + + print(f"🎉 总共创建了 {len(created_videoscreens)} 个2D视频屏幕") + + # 返回值处理 + if len(created_videoscreens) == 1: + return created_videoscreens[0][0] # 单个屏幕返回NodePath + else: + return [screen_np for screen_np, _ in created_videoscreens] # 多个屏幕返回列表 + + except Exception as e: + print(f"❌ 创建2D视频屏幕过程失败: {str(e)}") + import traceback + traceback.print_exc() + return None + + def load2DVideoFile(self, video_screen, video_path): + """为2D视频屏幕加载新的视频文件""" + try: + import os + + if not os.path.exists(video_path): + print(f"❌ 2D视频文件不存在: {video_path}") + return False + + # 加载新的视频纹理 + movie_texture = self._loadMovieTexture(video_path) + if movie_texture: + # 应用纹理到2D视频屏幕 + video_screen["frameTexture"] = movie_texture + + # 保存视频纹理引用 + video_screen.setPythonTag("movie_texture", movie_texture) + video_screen.setTag("video_path", video_path) + + print(f"✅ 成功加载新2D视频: {video_path}") + return True + else: + print(f"❌ 无法加载2D视频文件: {video_path}") + return False + + except Exception as e: + print(f"❌ 加载2D视频文件失败: {e}") + import traceback + traceback.print_exc() + return False + + def play2DVideo(self, video_screen): + """播放2D视频""" + try: + if video_screen.hasPythonTag("video_capture"): + print("视频已在播放中") + return True + + # 获取视频路径并重新开始播放 + video_path = video_screen.getTag("video_path") + if video_path: + return self.world.property_manager._loadVideoFromURL(video_screen, video_path) + else: + print("❌ 没有找到视频源") + return False + + except Exception as e: + print(f"❌ 播放视频失败: {e}") + return False + + def pause2DVideo(self, video_screen): + """暂停2D视频""" + try: + # 在OpenCV中没有直接的暂停功能,我们通过停止线程来模拟暂停 + if video_screen.hasPythonTag("video_capture"): + cap = video_screen.getPythonTag("video_capture") + if cap: + cap.release() + video_screen.clearPythonTag("video_capture") + print("⏸️ 视频已暂停") + return True + return False + except Exception as e: + print(f"❌ 暂停视频失败: {e}") + return False + + def stop2DVideo(self, video_screen): + """停止2D视频""" + try: + # 停止视频捕获 + if video_screen.hasPythonTag("video_capture"): + cap = video_screen.getPythonTag("video_capture") + if cap: + cap.release() + video_screen.clearPythonTag("video_capture") + + # 清理纹理 + if video_screen.hasPythonTag("video_texture"): + video_screen.clearPythonTag("video_texture") + + # 清理视频路径标签 + video_screen.clearTag("video_path") + + print("⏹️ 视频已停止") + return True + except Exception as e: + print(f"❌ 停止视频失败: {e}") + return False + + def createSphericalVideo(self, pos=(0, 0, 0), radius=5.0, video_path=None): + """创建球形视频(360度视频)- 支持无初始视频文件""" + try: + from panda3d.core import GeomVertexFormat, GeomVertexData, GeomVertexWriter + from panda3d.core import Geom, GeomTriangles, GeomNode + from panda3d.core import TextureStage, Texture + import math + import os + + # 确保 pos 是有效的三维坐标元组 + if not isinstance(pos, (tuple, list)) or len(pos) != 3: + print(f"⚠️ 位置参数无效,使用默认值 (0, 0, 0),原始值: {pos}") + pos = (0, 0, 0) + else: + # 确保所有坐标都是数值类型 + try: + pos = (float(pos[0]), float(pos[1]), float(pos[2])) + except (ValueError, TypeError): + print(f"⚠️ 位置参数包含非数值,使用默认值 (0, 0, 0),原始值: {pos}") + pos = (0, 0, 0) + + # 确保 radius 是有效数值 + try: + radius = float(radius) + except (ValueError, TypeError): + print(f"⚠️ 半径参数无效,使用默认值 5.0,原始值: {radius}") + radius = 5.0 + + print(f"🌍 开始创建球形视频,位置: {pos}, 半径: {radius}, 视频路径: {video_path}") + + # 不再强制检查视频文件是否存在,允许创建空的球形视频 + if video_path and not os.path.exists(video_path): + print(f"⚠️ 视频文件不存在,将创建空的球形视频: {video_path}") + + # 获取树形控件 + tree_widget = self._get_tree_widget() + if not tree_widget: + print("❌ 无法访问树形控件") + return None + + # 获取目标父节点列表 + target_parents = tree_widget.get_target_parents_for_creation() + if not target_parents: + print("❌ 没有找到有效的父节点") + return None + + created_spherical_videos = [] + + # 为每个有效的父节点创建球形视频 + for parent_item, parent_node in target_parents: + try: + # 生成唯一名称 + sphere_name = f"SphericalVideo_{len(self.gui_elements)}" + + # 创建球形几何体 + sphere_geom = self._createSphereGeometry(radius, segments=32) + + # 创建几何节点 + sphere_node = GeomNode('sphere_video') + sphere_node.addGeom(sphere_geom) + + # 创建节点路径并挂载 + sphere_np = parent_node.attachNewNode(sphere_node) + sphere_np.setPos(*pos) # 现在 pos 已经是有效的元组 + sphere_np.setName(sphere_name) + + # 翻转法线,使视频在球体内部显示 + sphere_np.setTwoSided(True) + sphere_np.setBin('background', 0) # 确保在背景层 + sphere_np.setDepthWrite(False) # 不写入深度缓冲 + + # 设置初始颜色为占位符颜色 + sphere_np.setColor(0.1, 0.1, 0.3, 0.8) # 深蓝色占位符 + + # 如果提供了视频路径且文件存在,则加载视频纹理 + movie_texture = None + if video_path and os.path.exists(video_path): + try: + # 加载视频纹理 + movie_texture = self._loadMovieTexture(video_path) + if movie_texture: + # 设置视频纹理属性 + movie_texture.setWrapU(Texture.WM_clamp) + movie_texture.setWrapV(Texture.WM_clamp) + movie_texture.setMinfilter(Texture.FT_linear) + movie_texture.setMagfilter(Texture.FT_linear) + + # 为视频纹理创建专用的纹理阶段 + texture_stage = TextureStage("spherical_video") + texture_stage.setMode(TextureStage.MModulate) + + # 应用纹理到球体 + sphere_np.setTexture(texture_stage, movie_texture) + + # 设置为白色以正确显示视频 + sphere_np.setColor(1, 1, 1, 1) + + print(f"✅ 视频纹理已应用到球形视频: {video_path}") + + # 尝试自动播放 + try: + if hasattr(movie_texture, 'play'): + movie_texture.play() + print("▶️ 球形视频已开始播放") + except Exception as play_error: + print(f"⚠️ 球形视频自动播放失败: {play_error}") + else: + print(f"⚠️ 无法加载视频纹理: {video_path}") + # 保持占位符颜色 + except Exception as e: + print(f"❌ 加载视频纹理失败: {e}") + import traceback + traceback.print_exc() + # 保持占位符颜色 + else: + if video_path: + print(f"⚠️ 视频文件不存在: {video_path}") + else: + print("ℹ️ 未提供视频文件,创建空的球形视频") + + # 设置标签以便识别和管理 + sphere_np.setTag("gui_type", "spherical_video") + sphere_np.setTag("is_gui_element", "1") + sphere_np.setTag("video_path", video_path or "") + sphere_np.setTag("original_radius", str(radius)) + + # 保存视频纹理引用 + if movie_texture: + sphere_np.setPythonTag("movie_texture", movie_texture) + + # 添加到GUI元素列表 + self.gui_elements.append(sphere_np) + + print(f"✅ 为 {parent_item.text(0)} 创建球形视频成功: {sphere_name}") + + # 在Qt树形控件中添加对应节点 + qt_item = tree_widget.add_node_to_tree_widget(sphere_np, parent_item, "GUI_SPHERICAL_VIDEO") + if qt_item: + created_spherical_videos.append((sphere_np, qt_item)) + else: + created_spherical_videos.append((sphere_np, None)) + print("⚠️ Qt树节点添加失败,但Panda3D对象已创建") + + except Exception as e: + print(f"❌ 为 {parent_item.text(0)} 创建球形视频失败: {str(e)}") + import traceback + traceback.print_exc() + continue + + # 处理创建结果 + if not created_spherical_videos: + print("❌ 没有成功创建任何球形视频") + return None + + # 选中最后创建的球形视频 + if created_spherical_videos: + last_sphere_np, last_qt_item = created_spherical_videos[-1] + if last_qt_item: + tree_widget.setCurrentItem(last_qt_item) + # 更新选择和属性面板 + tree_widget.update_selection_and_properties(last_sphere_np, last_qt_item) + + print(f"🎉 总共创建了 {len(created_spherical_videos)} 个球形视频") + + # 返回值处理 + if len(created_spherical_videos) == 1: + return created_spherical_videos[0][0] # 单个球形视频返回NodePath + else: + return [sphere_np for sphere_np, _ in created_spherical_videos] # 多个球形视频返回列表 + + except Exception as e: + print(f"❌ 创建球形视频过程失败: {str(e)}") + import traceback + traceback.print_exc() + return None + + def _createSphereGeometry(self, radius=5.0, segments=32): + """创建球形几何体""" + try: + from panda3d.core import GeomVertexFormat, GeomVertexData, GeomVertexWriter + from panda3d.core import Geom, GeomTriangles + import math + + # 创建顶点数据 + format = GeomVertexFormat.getV3n3t2() + vdata = GeomVertexData('sphere', format, Geom.UHStatic) + + vertex = GeomVertexWriter(vdata, 'vertex') + normal = GeomVertexWriter(vdata, 'normal') + texcoord = GeomVertexWriter(vdata, 'texcoord') + + # 生成球体顶点 + vertices = [] + for i in range(segments + 1): + phi = math.pi * i / segments # 从0到π + for j in range(segments * 2 + 1): + theta = 2 * math.pi * j / (segments * 2) # 从0到2π + + x = radius * math.sin(phi) * math.cos(theta) + y = radius * math.cos(phi) + z = radius * math.sin(phi) * math.sin(theta) + + # 纹理坐标 + u = j / (segments * 2) + v = i / segments + + # 修复:将坐标作为一个元组添加到列表中 + vertices.append((x, y, z, u, v)) + + # 添加顶点数据 + for vert in vertices: + vertex.addData3f(vert[0], vert[1], vert[2]) + # 法线(标准化顶点位置) + length = math.sqrt(vert[0] ** 2 + vert[1] ** 2 + vert[2] ** 2) + if length > 0: + normal.addData3f(vert[0] / length, vert[1] / length, vert[2] / length) + else: + normal.addData3f(0, 1, 0) + texcoord.addData2f(vert[3], vert[4]) + + # 创建几何体 + geom = Geom(vdata) + prim = GeomTriangles(Geom.UHStatic) + + # 生成三角形面 + for i in range(segments): + for j in range(segments * 2): + # 计算顶点索引 + v1 = i * (segments * 2 + 1) + j + v2 = (i + 1) * (segments * 2 + 1) + j + v3 = (i + 1) * (segments * 2 + 1) + (j + 1) + v4 = i * (segments * 2 + 1) + (j + 1) + + # 添加两个三角形 + prim.addVertices(v1, v2, v3) + prim.addVertices(v1, v3, v4) + + prim.closePrimitive() + geom.addPrimitive(prim) + + return geom + + except Exception as e: + print(f"❌ 创建球形几何体失败: {e}") + import traceback + traceback.print_exc() + raise + + def playSphericalVideo(self,spherical_video_node): + try: + if not spherical_video_node: + return False + texture = spherical_video_node.getTexture() + if texture and hasattr(texture,'play'): + texture.play() + return True + else: + return False + except Exception as e: + return False + + def pauseSphericalVideo(self,spherical_video_node): + try: + if not spherical_video_node: + return False + texture = spherical_video_node.getTexture() + if texture and hasattr(texture,'stop'): + texture.stop() + return True + else: + return False + except Exception as e: + return False + + def setSphericalVideoTime(self,spherical_video_node,time_seconds): + try: + if not spherical_video_node: + return False + texture = spherical_video_node.getTexture() + if texture and hasattr(texture,'set_time'): + texture.setTime(time_seconds) + return True + else: + return False + except Exception as e: + return False + def createGUIVirtualScreen(self, pos=(0, 0, 0), size=(2, 1), text="虚拟屏幕"): """创建3D虚拟屏幕 - 支持多选创建,优化版本""" try: @@ -1028,7 +2288,7 @@ class GUIManager: if hasattr(gui_element, 'getScale'): scale = gui_element.getScale() scaleEdit = QDoubleSpinBox() - scaleEdit.setRange(0.01, 10) + scaleEdit.setRange(0.01, 100) scaleEdit.setSingleStep(0.1) scaleEdit.setValue(scale.getX()) form.addRow("缩放:", scaleEdit) @@ -1463,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()): @@ -1494,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) @@ -1565,13 +2825,13 @@ class GUIManager: transform_layout.addWidget(z_label, 0, 2) xPos = QDoubleSpinBox() - xPos.setRange(-50, 50) + xPos.setRange(-1000, 1000) xPos.setValue(logical_x) xPos.valueChanged.connect(lambda v: self.world.gui_manager.editGUI2DPosition(gui_element, "x", v)) transform_layout.addWidget(xPos, 1, 1) zPos = QDoubleSpinBox() - zPos.setRange(-50, 50) + zPos.setRange(-1000, 1000) zPos.setValue(logical_z) zPos.valueChanged.connect(lambda v: self.world.gui_manager.editGUI2DPosition(gui_element, "z", v)) transform_layout.addWidget(zPos, 1, 2) @@ -1634,7 +2894,7 @@ class GUIManager: # 宽度控件 transform_layout.addWidget(QLabel("宽度"), 4, 0) widthSpinBox = QDoubleSpinBox() - widthSpinBox.setRange(0.1, 10) + widthSpinBox.setRange(0.01, 100) widthSpinBox.setSingleStep(0.1) widthSpinBox.setValue(width) widthSpinBox.valueChanged.connect( @@ -1644,7 +2904,7 @@ class GUIManager: # 高度控件 transform_layout.addWidget(QLabel("高度"), 4, 2) heightSpinBox = QDoubleSpinBox() - heightSpinBox.setRange(0.1, 10) + heightSpinBox.setRange(0.01, 100) heightSpinBox.setSingleStep(0.1) heightSpinBox.setValue(height) heightSpinBox.valueChanged.connect( @@ -1704,7 +2964,7 @@ class GUIManager: # 缩放数值输入框 scale_x = QDoubleSpinBox() - scale_x.setRange(0.01, 10) + scale_x.setRange(0.01, 100) scale_x.setSingleStep(0.1) scale_x.setValue( scale.getX() if hasattr(scale, 'getX') else scale[0] if isinstance(scale, (tuple, list)) else scale) @@ -1712,7 +2972,7 @@ class GUIManager: transform_layout.addWidget(scale_x, 3, 1) scale_y = QDoubleSpinBox() - scale_y.setRange(0.01, 10) + scale_y.setRange(0.01, 100) scale_y.setSingleStep(0.1) scale_y.setValue( scale.getY() if hasattr(scale, 'getY') else scale[1] if isinstance(scale, (tuple, list)) and len( @@ -1721,7 +2981,7 @@ class GUIManager: transform_layout.addWidget(scale_y, 3, 2) scale_z = QDoubleSpinBox() - scale_z.setRange(0.01, 10) + scale_z.setRange(0.01, 100) scale_z.setSingleStep(0.1) scale_z.setValue( scale.getZ() if hasattr(scale, 'getZ') else scale[2] if isinstance(scale, (tuple, list)) and len( @@ -1736,7 +2996,7 @@ class GUIManager: scale = gui_element.getScale() scaleSpinBox = QDoubleSpinBox() - scaleSpinBox.setRange(0.01, 10) + scaleSpinBox.setRange(0.01, 100) scaleSpinBox.setSingleStep(0.1) scaleSpinBox.setValue(scale.getX()) scaleSpinBox.valueChanged.connect(lambda v: self.editGUIElement(gui_element, "scale", v)) @@ -1778,28 +3038,9 @@ class GUIManager: if color.isValid(): r, g, b = color.red() / 255.0, color.green() / 255.0, color.blue() / 255.0 self.editGUIElement(gui_element, "color", [r, g, b, 1.0]) - - # def editGUI2DPosition(self, gui_element, axis, value): - # """编辑2D GUI元素位置""" - # try: - # current_pos = gui_element.getPos() - # - # if axis == "x": - # # 将逻辑坐标转换为屏幕坐标 - # new_screen_x = value * 0.1 - # gui_element.setPos(new_screen_x, current_pos.getY(), current_pos.getZ()) - # elif axis == "z": - # # 将逻辑坐标转换为屏幕坐标 - # new_screen_z = value * 0.1 - # gui_element.setPos(current_pos.getX(), current_pos.getY(), new_screen_z) - # - # print(f"更新2D GUI位置: {axis}轴 = {value} (屏幕坐标: {gui_element.getPos()})") - # - # except Exception as e: - # print(f"编辑2D GUI位置失败: {str(e)}") def update3DImageTexture(self, model_nodepath, image_path): - from panda3d.core import Texture + from panda3d.core import Texture, TextureStage try: # 加载新纹理 @@ -1809,14 +3050,21 @@ class GUIManager: new_texture.setMagfilter(Texture.FT_linear) new_texture.setMinfilter(Texture.FT_linear_mipmap_linear) - # 应用纹理到模型 - model_nodepath.setTexture(new_texture, 1) + model_nodepath.clearTexture() + + # 为3D图像创建独立的纹理阶段 + image_stage = TextureStage("3d_image_texture") + image_stage.setSort(0) # 使用第一个纹理槽 + image_stage.setMode(TextureStage.MModulate) # 使用调制模式 + + # 应用纹理到模型,使用独立的纹理阶段 + model_nodepath.setTexture(image_stage, new_texture) # 更新标签 model_nodepath.setTag("gui_image_path", image_path) # 确保材质设置正确 - if not model_nodepath.has_material(): + if not model_nodepath.hasMaterial(): from panda3d.core import Material, LColor mat = Material() mat.setName(f"image-material-{id(model_nodepath)}") @@ -1827,11 +3075,72 @@ class GUIManager: mat.setShininess(10.0) model_nodepath.setMaterial(mat, 1) + # 保护子节点的纹理(特别是3D文本) + self._preserveChildNodeTextures(model_nodepath) + print(f"✅ 3D图像纹理已更新为: {image_path}") + return True else: print(f"❌ 无法加载纹理: {image_path}") + return False except Exception as e: print(f"❌ 更新纹理时出错: {e}") + return False + + def _preserveChildNodeTextures(self, parent_node): + """保护子节点的纹理不被父节点纹理影响""" + try: + # 遍历所有直接子节点 + for i in range(parent_node.getNumChildren()): + child = parent_node.getChild(i) + + # 检查子节点是否为3D文本或其他需要特殊处理的节点 + if self._is3DTextElement(child): + # 为3D文本创建独立的纹理阶段 + self._restore3DTextTexture(child) + elif self._isOtherSpecialElement(child): + # 为其他特殊元素恢复纹理 + self._restoreSpecialElementTexture(child) + + except Exception as e: + print(f"保护子节点纹理时出错: {e}") + + def _is3DTextElement(self, node): + """检查节点是否为3D文本元素""" + try: + return (hasattr(node, 'getTag') and + node.getTag("gui_type") == "3d_text") + except: + return False + + def _isOtherSpecialElement(self, node): + """检查节点是否为其他需要特殊处理的元素""" + try: + return (hasattr(node, 'getTag') and + node.getTag("gui_type") in ["3d_image", "button", "label"]) + except: + return False + + def _restore3DTextTexture(self, text_node): + """恢复3D文本的纹理设置""" + try: + from panda3d.core import TextureStage + + # 如果3D文本已经有字体纹理,确保它使用正确的纹理阶段 + if hasattr(text_node, 'getTexture') and text_node.getTexture(): + # 创建专门用于文本的纹理阶段 + text_stage = TextureStage("text_texture") + text_stage.setSort(10) # 使用较高的纹理槽索引 + text_stage.setMode(TextureStage.MModulate) + + # 重新应用文本纹理 + current_texture = text_node.getTexture() + text_node.setTexture(text_stage, current_texture) + + print(f"已恢复3D文本纹理: {text_node.getName()}") + + except Exception as e: + print(f"恢复3D文本纹理失败: {e}") def update2DImageTexture(self, gui_element, image_path): """更新2D图片纹理""" @@ -1869,26 +3178,20 @@ class GUIManager: try: gui_type = gui_element.getTag("gui_type") - if gui_type in ["button", "label", "entry", "2d_image"]: + if gui_type in ["button", "label", "entry", "2d_image","2d_video_screen"]: # 2D元素使用屏幕坐标,需要转换 current_pos = gui_element.getPos() if axis == "x": # 转换逻辑坐标到屏幕坐标 screen_x = value * 0.1 - # 应用边界约束 - constrained_pos = self.constrain2DPosition(gui_element, new_x=screen_x) - new_pos = (constrained_pos.getX(), constrained_pos.getY(), constrained_pos.getZ()) + new_pos = (screen_x,current_pos[1],current_pos[2]) elif axis == "z": screen_z = value * 0.1 - # 应用边界约束 - constrained_pos = self.constrain2DPosition(gui_element, new_z=screen_z) - new_pos = (constrained_pos.getX(), constrained_pos.getY(), constrained_pos.getZ()) + new_pos = (current_pos[0],current_pos[1],screen_z) else: return False - gui_element.setPos(*new_pos) - print(f"✓ 更新2D GUI元素位置: {axis}={value} (约束后位置: {new_pos})") return True else: print(f"✗ 不支持的GUI类型进行2D位置编辑: {gui_type}") @@ -1904,7 +3207,7 @@ class GUIManager: try: gui_type = gui_element.getTag("gui_type") - if gui_type in ["3d_text", "3d_image"]: + if gui_type in ["3d_text", "3d_image", "video_screen","info_panel"]: current_pos = gui_element.getPos() if axis == "x": @@ -1938,7 +3241,7 @@ class GUIManager: if value == 0: value = 0.01 - if gui_type in ["3d_text", "3d_image"]: + 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()) @@ -1984,7 +3287,6 @@ class GUIManager: # 对于其他2D元素,使用统一缩放 gui_element.setScale(value) - print(f"✓ 更新GUI元素缩放: {axis}={value}") return True except Exception as e: print(f"✗ 更新GUI元素缩放失败: {e}") diff --git a/main.py b/main.py index ed81d628..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) @@ -234,6 +237,27 @@ class MyWorld(CoreWorld): """创建2D GUI图片""" return self.gui_manager.createGUI2DImage(pos, image_path, size) + def createVideoScreen(self,pos=(0,0,0),size=1,video_path=None): + """创建视频屏幕""" + return self.gui_manager.createVideoScreen(pos,size,video_path) + + def create2DVideoScreen(self,pos=(0,0,0),size=0.2,video_path=None): + """创建2D视频屏幕""" + return self.gui_manager.createGUI2DVideoScreen(pos,size,video_path) + + def createSphericalVideo(self,pos=(0,0,0),radius=5.0,video_path=None): + """创建360度视频""" + return self.gui_manager.createSphericalVideo(pos,radius,video_path) + + def playSphericalVideo(self,spherical_video_node): + return self.gui_manager.playSphericalVideo(spherical_video_node) + + def pauseSphericalVideo(self,spherical_video_node): + return self.gui_manager.pauseSphericalVideo(spherical_video_node) + + def setSphericalVideoTime(self,spherical_video_node,time_seconds): + return self.gui_manager.setSphericalVideoTime(spherical_video_node,time_seconds) + def createSpotLight(self,pos=(0,0,5)): """创建聚光灯""" return self.scene_manager.createSpotLight(pos) diff --git a/scene/scene_manager.py b/scene/scene_manager.py index 472b2f16..69ee1d49 100644 --- a/scene/scene_manager.py +++ b/scene/scene_manager.py @@ -87,10 +87,10 @@ class SceneManager: print("✓ 场景管理系统初始化完成") # ==================== 模型导入和处理 ==================== - + def importModel(self, filepath, apply_unit_conversion=False, normalize_scales=True, auto_convert_to_glb=True): - """导入模型到场景 - 只在根节点下创建 - + """导入模型到场景 + Args: filepath: 模型文件路径 apply_unit_conversion: 是否应用单位转换(主要针对FBX文件) @@ -98,16 +98,14 @@ class SceneManager: auto_convert_to_glb: 是否自动将非GLB格式转换为GLB以获得更好的动画支持 """ try: - 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 '关闭'}") - # 预处理文件路径和转换 filepath = util.normalize_model_path(filepath) original_filepath = filepath - - # 检查是否需要转换为GLB + + # 检查是否需要转换为GLB以获得更好的动画支持 if auto_convert_to_glb and self._shouldConvertToGLB(filepath): print(f"🔄 检测到需要转换的格式,尝试转换为GLB...") converted_path = self._convertToGLBWithProgress(filepath) @@ -118,268 +116,206 @@ class SceneManager: try: from PyQt5.QtWidgets import QMessageBox original_ext = os.path.splitext(original_filepath)[1].upper() - QMessageBox.information(None, "转换成功", - f"已将 {original_ext} 格式自动转换为 GLB 格式\n以获得更好的动画支持!") + QMessageBox.information(None, "转换成功", + f"已将 {original_ext} 格式自动转换为 GLB 格式\n以获得更好的动画支持!") except: pass else: print(f"⚠️ 转换失败,使用原始文件") - # 直接在render根节点下创建模型 - print("--- 在根节点下创建模型实例 ---") - - # 加载模型 + # 总是重新加载模型以确保材质信息完整 + # 不使用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") - - # 应用处理选项 + + # 可选的单位转换(主要针对FBX) if apply_unit_conversion and filepath.lower().endswith('.fbx'): print("应用FBX单位转换(厘米到米)...") self._applyUnitConversion(model, 0.01) - model.setTag("unit_conversion_applied", "true") + # 智能缩放标准化(处理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 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""" + def apply_material(node_path, depth=0): indent = " " * depth - print(f"{indent}处理节点: {node_path.getName()}") - print(f"{indent}节点类型: {node_path.node().__class__.__name__}") + try: + print(f"{indent}处理节点: {node_path.getName()}") + print(f"{indent}节点类型: {node_path.node().__class__.__name__}") - if isinstance(node_path.node(), GeomNode): - print(f"{indent}发现GeomNode,处理材质") - geom_node = node_path.node() + if isinstance(node_path.node(), GeomNode): + print(f"{indent}发现GeomNode,处理材质") + geom_node = node_path.node() - # 检查所有几何体的状态 - has_color = False - color = None + # 检查所有几何体的状态 + has_color = False + color = None - # 首先检查节点自身的状态 - node_state = node_path.getState() - if node_state.hasAttrib(MaterialAttrib.getClassType()): - mat_attrib = node_state.getAttrib(MaterialAttrib.getClassType()) - node_material = mat_attrib.getMaterial() - if node_material and node_material.hasDiffuse(): - color = node_material.getDiffuse() - has_color = True - print(f"{indent}从节点材质获取颜色: {color}") + # 首先检查节点自身的状态 + node_state = node_path.getState() + if node_state.hasAttrib(MaterialAttrib.getClassType()): + mat_attrib = node_state.getAttrib(MaterialAttrib.getClassType()) + node_material = mat_attrib.getMaterial() + if node_material and node_material.hasDiffuse(): + color = node_material.getDiffuse() + has_color = True + print(f"{indent}从节点材质获取颜色: {color}") - # 检查FBX特有的属性 - for tag_key in node_path.getTagKeys(): - print(f"{indent}发现标签: {tag_key}") - if "color" in tag_key.lower() or "diffuse" in tag_key.lower(): - tag_value = node_path.getTag(tag_key) - print(f"{indent}颜色相关标签: {tag_key} = {tag_value}") + # 检查FBX特有的属性 + for tag_key in node_path.getTagKeys(): + print(f"{indent}发现标签: {tag_key}") + if "color" in tag_key.lower() or "diffuse" in tag_key.lower(): + tag_value = node_path.getTag(tag_key) + print(f"{indent}颜色相关标签: {tag_key} = {tag_value}") - # 如果还没找到颜色,检查几何体 - if not has_color: - for i in range(geom_node.getNumGeoms()): - geom = geom_node.getGeom(i) - state = geom_node.getGeomState(i) + # 如果还没找到颜色,检查几何体 + if not has_color: + for i in range(geom_node.getNumGeoms()): + try: + geom = geom_node.getGeom(i) + state = geom_node.getGeomState(i) - # 检查顶点颜色 - vdata = geom.getVertexData() - format = vdata.getFormat() - for j in range(format.getNumColumns()): - column = format.getColumn(j) - # InternalName对象需要使用getName()转换为字符串 - column_name = column.getName().getName() - if "color" in column_name.lower(): - print(f"{indent}发现顶点颜色数据: {column_name}") - # 这里可以读取顶点颜色,但先记录发现 + # 检查顶点颜色 + vdata = geom.getVertexData() + if vdata: + format = vdata.getFormat() + if format: + for j in range(format.getNumColumns()): + try: + column = format.getColumn(j) + # InternalName对象需要使用getName()转换为字符串 + column_name = column.getName().getName() + if "color" in column_name.lower(): + print(f"{indent}发现顶点颜色数据: {column_name}") + except Exception: + continue - # 检查材质属性 - if state.hasAttrib(MaterialAttrib.getClassType()): - mat_attrib = state.getAttrib(MaterialAttrib.getClassType()) - orig_material = mat_attrib.getMaterial() - if orig_material: - if orig_material.hasBaseColor(): - color = orig_material.getBaseColor() - has_color = True - print(f"{indent}从基础颜色获取: {color}") - break - elif orig_material.hasDiffuse(): - color = orig_material.getDiffuse() - has_color = True - print(f"{indent}从漫反射颜色获取: {color}") - break + # 检查材质属性 + if state.hasAttrib(MaterialAttrib.getClassType()): + mat_attrib = state.getAttrib(MaterialAttrib.getClassType()) + orig_material = mat_attrib.getMaterial() + if orig_material: + if orig_material.hasBaseColor(): + color = orig_material.getBaseColor() + has_color = True + print(f"{indent}从基础颜色获取: {color}") + break + elif orig_material.hasDiffuse(): + color = orig_material.getDiffuse() + has_color = True + print(f"{indent}从漫反射颜色获取: {color}") + break - # 检查颜色属性 - if not has_color and state.hasAttrib(ColorAttrib.getClassType()): - color_attrib = state.getAttrib(ColorAttrib.getClassType()) - if not color_attrib.isOff(): - color = color_attrib.getColor() - has_color = True - print(f"{indent}从颜色属性获取: {color}") - break + # 检查颜色属性 + if not has_color and state.hasAttrib(ColorAttrib.getClassType()): + color_attrib = state.getAttrib(ColorAttrib.getClassType()) + if not color_attrib.isOff(): + color = color_attrib.getColor() + has_color = True + print(f"{indent}从颜色属性获取: {color}") + break + except Exception as geom_error: + print(f"{indent}处理几何体 {i} 时出错: {geom_error}") + continue - # 创建新材质 - material = Material() - if has_color: - print(f"{indent}应用找到的颜色: {color}") - material.setDiffuse(color) - material.setBaseColor(color) # 同时设置基础颜色 - node_path.setColor(color) - else: - print(f"{indent}使用默认颜色") - material.setDiffuse((0.8, 0.8, 0.8, 1.0)) + # 创建新材质 + material = Material() + if has_color: + print(f"{indent}应用找到的颜色: {color}") + try: + material.setDiffuse(color) + material.setBaseColor(color) # 同时设置基础颜色 + node_path.setColor(color) + except Exception as color_error: + print(f"{indent}设置颜色时出错: {color_error}") + material.setDiffuse((0.8, 0.8, 0.8, 1.0)) + else: + print(f"{indent}使用默认颜色") + material.setDiffuse((0.8, 0.8, 0.8, 1.0)) - # 设置其他材质属性 - material.setAmbient((0.2, 0.2, 0.2, 1.0)) - material.setSpecular((0.5, 0.5, 0.5, 1.0)) - material.setShininess(32.0) - #material.set_metallic(1) - #material.set_roughness(0) + # 设置其他材质属性 + material.setAmbient((0.2, 0.2, 0.2, 1.0)) + material.setSpecular((0.5, 0.5, 0.5, 1.0)) + material.setShininess(32.0) - # 应用材质 - node_path.setMaterial(material) - print(f"{indent}几何体数量: {geom_node.getNumGeoms()}") + # 应用材质 + node_path.setMaterial(material) + print(f"{indent}几何体数量: {geom_node.getNumGeoms()}") + + except Exception as node_error: + print(f"{indent}处理节点 {node_path.getName()} 时出错: {node_error}") # 递归处理子节点 child_count = node_path.getNumChildren() print(f"{indent}子节点数量: {child_count}") for i in range(child_count): - child = node_path.getChild(i) - apply_material(child, depth + 1) + try: + child = node_path.getChild(i) + apply_material(child, depth + 1) + except Exception as child_error: + print(f"{indent}处理子节点 {i} 时出错: {child_error}") + continue # 应用材质 print("\n开始递归应用材质...") - apply_material(model) + try: + apply_material(model) + except Exception as e: + print(f"应用材质时出错: {e}") print("=== 材质设置完成 ===\n") def _adjustModelToGround(self, model): @@ -429,6 +365,8 @@ class SceneManager: print("模型已应用过单位转换,跳过") return + + # 获取当前边界用于后续位置调整 original_bounds = model.getBounds() @@ -734,7 +672,7 @@ class SceneManager: # 根据调试设置决定是否显示碰撞体 if hasattr(self.world, 'debug_collision') and self.world.debug_collision: - cNodePath.show() + cNodePath.hide() else: cNodePath.hide() @@ -768,6 +706,27 @@ class SceneManager: try: print(f"\n=== 开始保存场景到: {filename} ===") + # 存储需要临时隐藏的节点,以便保存后恢复 + nodes_to_restore = [] + + # 查找并隐藏所有坐标轴和选择框节点 + gizmo_nodes = self.world.render.findAllMatches("**/gizmo*") + selection_box_nodes = self.world.render.findAllMatches("**/selectionBox*") + + # 隐藏坐标轴节点 + for node in gizmo_nodes: + if not node.isHidden(): + nodes_to_restore.append((node, True)) # (节点, 原先是否可见) + node.hide() + print(f"临时隐藏坐标轴节点: {node.getName()}") + + # 隐藏选择框节点 + for node in selection_box_nodes: + if not node.isHidden(): + nodes_to_restore.append((node, True)) + node.hide() + print(f"临时隐藏选择框节点: {node.getName()}") + # 遍历所有模型,保存材质状态和变换信息 for model in self.models: # 保存变换信息(关键!) @@ -802,12 +761,34 @@ class SceneManager: if not color_attrib.isOff(): model.setTag("color", str(color_attrib.getColor())) - # 保存场景 - success = self.world.render.writeBamFile(filename) - return success + try: + print("--- 打印当前场景图 (render) ---") + self.world.render.ls() + print("---------------------------------") + # 保存场景 + success = self.world.render.writeBamFile(filename) + + if success: + print(f"✓ 场景保存成功: {filename}") + else: + print("✗ 场景保存失败") + + return success + finally: + # 恢复之前隐藏的节点 + for item in nodes_to_restore: + node, was_visible = item + if was_visible and not node.isEmpty(): + node.show() + print(f"恢复显示节点: {node.getName()}") + + if nodes_to_restore: + print(f"已恢复 {len(nodes_to_restore)} 个辅助节点的显示") except Exception as e: print(f"保存场景时发生错误: {str(e)}") + import traceback + traceback.print_exc() return False def loadScene(self, filename): @@ -821,6 +802,9 @@ class SceneManager: model.removeNode() self.models.clear() + # 清理可能存在的辅助节点(坐标轴、选择框等) + self._cleanupAuxiliaryNodes() + # 加载场景 scene = self.world.loader.loadModel(filename) if not scene: @@ -846,6 +830,16 @@ class SceneManager: print(f"{indent}跳过相机节点: {nodePath.getName()}") return + # 跳过辅助节点(坐标轴和选择框) + if nodePath.getName().startswith(("gizmo", "selectionBox")): + print(f"{indent}跳过辅助节点: {nodePath.getName()}") + return + + if nodePath.getName() in ['SceneRoot'] or \ + any(keyword in nodePath.getName() for keyword in ["Skybox","skybox"]): + print(f"{indent}跳过环境节点:{nodePath.getName()}") + return + if isinstance(nodePath.node(), ModelRoot): print(f"{indent}找到模型根节点!") @@ -940,8 +934,36 @@ class SceneManager: except Exception as e: print(f"加载场景时发生错误: {str(e)}") + import traceback + traceback.print_exc() return False + def _cleanupAuxiliaryNodes(self): + """清理场景中可能存在的辅助节点""" + try: + # 查找并移除所有坐标轴节点 + gizmo_nodes = self.world.render.findAllMatches("**/gizmo*") + for node in gizmo_nodes: + if not node.isEmpty(): + node.removeNode() + print(f"清理坐标轴节点: {node.getName()}") + + # 查找并移除所有选择框节点 + selection_box_nodes = self.world.render.findAllMatches("**/selectionBox*") + for node in selection_box_nodes: + if not node.isEmpty(): + node.removeNode() + print(f"清理选择框节点: {node.getName()}") + + # 停止相关的更新任务 + from direct.task.TaskManagerGlobal import taskMgr + taskMgr.remove("updateGizmo") + taskMgr.remove("updateSelectionBox") + + print("辅助节点清理完成") + except Exception as e: + print(f"清理辅助节点时出错: {e}") + # ==================== 模型管理 ==================== def deleteModel(self, model): @@ -1052,10 +1074,7 @@ class SceneManager: light.radius = 1000 light.casts_shadows = True light.shadow_map_resolution = 256 - - # 设置光源的世界坐标位置 - world_pos = light_np.getPos(self.world.render) - light.setPos(world_pos) + light.setPos(*pos) # 添加到渲染管线 render_pipeline.add_light(light) @@ -1084,6 +1103,8 @@ class SceneManager: except Exception as e: print(f"❌ 为 {parent_item.text(0)} 创建聚光灯失败: {str(e)}") + import traceback + traceback.print_exc() continue # 处理创建结果 @@ -1152,9 +1173,7 @@ class SceneManager: # 创建点光源对象 light = PointLight() - # 设置光源的世界坐标位置 - world_pos = light_np.getPos(self.world.render) - light.setPos(world_pos) + light.setPos(*pos) light.energy = 5000 light.radius = 1000 diff --git a/terminal b/terminal new file mode 100644 index 00000000..6edcaa70 --- /dev/null +++ b/terminal @@ -0,0 +1,4 @@ +# 检查当前分支和状态 +git status +git branch -v +git log --oneline --graph --all -10 \ No newline at end of file diff --git a/ui/interface_manager.py b/ui/interface_manager.py index 230c23d0..3e802bfb 100644 --- a/ui/interface_manager.py +++ b/ui/interface_manager.py @@ -1,3 +1,4 @@ +from PIL.ImageChops import lighter from PyQt5.QtWidgets import QTreeWidget, QTreeWidgetItem, QMenu, QStyle from PyQt5.QtCore import Qt from PyQt5.sip import delete @@ -79,19 +80,113 @@ class InterfaceManager: deleteAction.triggered.connect(lambda:self.deleteCesiumTileset(nodePath,item)) else: - # 为模型节点或其子节点添加删除选项 - parentItem = item.parent() - if parentItem: - if self.isModelOrChild(item): - deleteAction = menu.addAction("删除") - deleteAction.triggered.connect(lambda: self.deleteNode(nodePath, item)) - else: - deleteAction = menu.addAction("删除") - deleteAction.triggered.connect(lambda: self.deleteNode(nodePath, item)) + #灯光节点添加特殊处理 + if self.isLightNode(nodePath): + deleteAction = menu.addAction("删除灯光") + deleteAction.triggered.connect(lambda:self.deleteLightNode(nodePath,item)) + else: + deleteAction = menu.addAction("删除") + deleteAction.triggered.connect(lambda: self.deleteNode(nodePath, item)) # 显示菜单 menu.exec_(self.treeWidget.viewport().mapToGlobal(position)) + def isLightNode(self, nodePath): + try: + if not nodePath or nodePath.isEmpty(): + return False + + # 修复:统一使用 rp_light_object + if hasattr(nodePath, 'getPythonTag'): + light_object = nodePath.getPythonTag('rp_light_object') + if light_object is not None: + return True + + if hasattr(nodePath, 'getTag'): + light_type = nodePath.getTag('light_type') + if light_type in ["spot_light", "point_light"]: + return True + + if hasattr(self.world, 'Spotlight') and nodePath in self.world.Spotlight: + return True + if hasattr(self.world, 'Pointlight') and nodePath in self.world.Pointlight: + return True + + return False + except Exception as e: + print(f"判断节点是否是灯光节点失败: {str(e)}") + return False + + def deleteLightNode(self, nodePath, item): + """专门处理灯光节点的删除""" + try: + print(f"开始删除灯光节点: {nodePath.getName()}") + + # 从RenderPipeline中移除灯光(如果存在) + if hasattr(nodePath, 'getPythonTag'): + light_object = nodePath.getPythonTag('rp_light_object') + if light_object and hasattr(self.world, 'render_pipeline'): + print("从RenderPipeline移除灯光") + self.world.render_pipeline.remove_light(light_object) + nodePath.clearPythonTag('rp_light_object') + + if hasattr(self.world,'Spotlight') and nodePath in self.world.Spotlight: + self.world.Spotlight.remove(nodePath) + print("从Spotlight列表中删除") + if hasattr(self.world,'Pointlight') and nodePath in self.world.Pointlight: + self.world.Pointlight.remove(nodePath) + print("从Pointlight列表中移除") + + if hasattr(self.world,'selection'): + if self.world.selection.selectedNode == nodePath: + self.world.selection.clearSelectionBox() + self.world.selection.clearGizmo() + self.world.selection.selectedNode = None + self.world.selection.selectedObject = None + + print(f"移除节点{nodePath.getName()}") + nodePath.removeNode() + + parentItem = item.parent() + if parentItem: + parentItem.removeChild(item) + + print(f"成功删除灯光节点{nodePath.getName()}") + + if hasattr(self.world,'property_panel'): + self.world.property_panel.clearPropertyPanel() + if hasattr(self.world,'selection'): + self.world.selection.updateSelection(None) + + except Exception as e: + print(f"删除灯光节点失败: {str(e)}") + + def _recursiveRemoveLights(self, nodePath): + """递归删除节点及其子节点中的所有灯光""" + if nodePath.isEmpty(): + return + + # 先递归处理所有子节点 + for child in nodePath.getChildren(): + self._recursiveRemoveLights(child) + + # 然后处理当前节点 + if self.isLightNode(nodePath): + print(f"删除子灯光节点: {nodePath.getName()}") + + # 从RenderPipeline中移除灯光 + if hasattr(nodePath, 'getPythonTag'): + light_object = nodePath.getPythonTag('rp_light_object') + if light_object and hasattr(self.world, 'render_pipeline'): + self.world.render_pipeline.remove_light(light_object) + nodePath.clearPythonTag('rp_light_object') + + # 从灯光列表中移除 + if hasattr(self.world, 'Spotlight') and nodePath in self.world.Spotlight: + self.world.Spotlight.remove(nodePath) + if hasattr(self.world, 'Pointlight') and nodePath in self.world.Pointlight: + self.world.Pointlight.remove(nodePath) + def deleteCesiumTileset(self, nodePath, item): """删除 Cesium tileset""" try: @@ -137,9 +232,14 @@ class InterfaceManager: def deleteNode(self, nodePath, item): """删除节点""" try: - item_data = item.data(0,Qt.UserRole+1) + # 如果是灯光节点,直接调用灯光删除方法 + if self.isLightNode(nodePath): + self.deleteLightNode(nodePath, item) + return + + item_data = item.data(0, Qt.UserRole + 1) if item_data == "terrain": - if hasattr(self.world,'terrain_manager') and self.world.terrain_manager.terrains: + if hasattr(self.world, 'terrain_manager') and self.world.terrain_manager.terrains: terrain_to_remove = None for terrain_info in self.world.terrain_manager.terrains: if terrain_info['node'] == nodePath: @@ -153,34 +253,38 @@ class InterfaceManager: self.world.selection.updateSelection(None) return + # 先递归删除所有子节点中的灯光 + self._recursiveRemoveLights(nodePath) + # 从场景中移除 self.world.property_panel.removeActorForModel(nodePath) - if hasattr(nodePath,'getPythonTag'): - light_object = nodePath.getPythonTag('rp_light_object') - if light_object and hasattr(self.world,'render_pipeline'): - self.world.render_pipeline.remove_light(light_object) - - if hasattr(self.world,'selection'): + # 清除选择状态 + if hasattr(self.world, 'selection'): if self.world.selection.selectedNode == nodePath: self.world.selection.clearSelectionBox() self.world.selection.clearGizmo() self.world.selection.selectedNode = None self.world.selection.selectedObject = None - if nodePath in self.world.Spotlight: - self.world.Spotlight.remove(nodePath) - if nodePath in self.world.Pointlight: - self.world.Pointlight.remove(nodePath) + # 强制删除节点及其所有子节点 + if not nodePath.isEmpty(): + # 先递归删除所有子节点 + children = list(nodePath.getChildren()) + for child in children: + try: + if not child.isEmpty(): + child.removeNode() + except Exception as e: + print(f"删除子节点失败: {str(e)}") - nodePath.removeNode() + # 再删除节点本身 + nodePath.removeNode() - if hasattr(self.world,'selection'): + if hasattr(self.world, 'selection'): self.world.selection.checkAndClearIfTargetDeleted() - # 如果是模型根节点,从模型列表中移除 - #if item.parent().text(0) == "模型": - if nodePath in self.world.models: + if hasattr(self.world, 'models') and nodePath in self.world.models: self.world.models.remove(nodePath) # 从树形控件中移除 @@ -196,6 +300,51 @@ class InterfaceManager: except Exception as e: print(f"删除节点失败: {str(e)}") + import traceback + traceback.print_exc() + + def _cleanupAllLightsInSubtree(self,parentNode): + try: + if parentNode.isEmpty(): + return + #收集所有子节点 + all_nodes = [] + + def collect_all_nodes(node): + if not node.isEmpty(): + all_nodes.append(node) + for child in node.getChildren(): + collect_all_nodes(child) + + collect_all_nodes(parentNode) + + ligths_processed = 0 + for node in all_nodes: + if self.isLightNode(node): + if hasattr(node,'getPythonTag'): + light_object = node.getPythonTag('rp_light_object') + if light_object and hasattr(self.world,'render_pipeline'): + try: + self.world.render_pipeline.remove_light(light_object) + print(f"✓ 从渲染管线移除灯光: {node.getName()}") + except Exception as e: + print(f"⚠ 从渲染管线移除灯光失败: {str(e)}") + #从灯光列表中移除 + try: + if node in self.world.Spotlight: + self.world.Spotlight.remove(node) + print(f"从Spotlight列表移除{node.getName()}") + if node in self.world.Pointlight: + self.world.Pointlight.remove(node) + print(f"从pointlight列表移除{node.getName()}") + except Exception as e: + print(f"从灯列表移除失败{str(e)}") + ligths_processed += 1 + if ligths_processed>0: + print(f"清理{ligths_processed}个灯光节点") + except Exception as e: + print(f"清理节点树中的灯光失败{str(e)}") + def updateSceneTree(self): """更新场景树显示 - 实际实现""" @@ -282,9 +431,6 @@ class InterfaceManager: terrain_item.setData(0,Qt.UserRole+1,"terrain") # 标记为地形节点 addNodeToTree(terrain_node,terrain_item,force=True) - - - # 展开所有节点 #self.treeWidget.expandAll() self._restore_expanded() diff --git a/ui/main_window.py b/ui/main_window.py index 5946ccc3..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): @@ -41,6 +43,11 @@ class MainWindow(QMainWindow): self.updateTimer.timeout.connect(self.updateScriptPanel) self.updateTimer.start(500) # 每500毫秒更新一次 + self.toolbarDragging = False + self.dragStartPos = QPoint(0, 0) + self.toolbarStartPos = QPoint(0, 0) + + def setupCenterWidget(self): """设置窗口基本属性""" self.setWindowTitle("引擎编辑器") @@ -178,21 +185,27 @@ class MainWindow(QMainWindow): def toolbarMouseMoveEvent(self, event): """工具栏鼠标移动事件""" - if self.toolbarDragging and event.buttons() == Qt.LeftButton: - # 计算新位置 - delta = event.globalPos() - self.dragStartPos - new_pos = self.toolbarStartPos + delta + try: + if self.toolbarDragging and event.buttons() == Qt.LeftButton: + # 计算新位置 + delta = event.globalPos() - self.dragStartPos + new_pos = self.toolbarStartPos + delta - # 边界检测 - panda_rect = self.pandaWidget.geometry() - toolbar_size = self.embeddedToolbar.size() + # 边界检测 + panda_rect = self.pandaWidget.geometry() + toolbar_size = self.embeddedToolbar.size() - # 限制在Panda3D区域内 - new_pos.setX(max(0, min(new_pos.x(), panda_rect.width() - toolbar_size.width()))) - new_pos.setY(max(0, min(new_pos.y(), panda_rect.height() - toolbar_size.height()))) + # 限制在Panda3D区域内 + new_pos.setX(max(0, min(new_pos.x(), panda_rect.width() - toolbar_size.width()))) + new_pos.setY(max(0, min(new_pos.y(), panda_rect.height() - toolbar_size.height()))) + + self.embeddedToolbar.move(new_pos) + event.accept() + except Exception as e: + print(f"工具栏鼠标移动事件出错") + import traceback + traceback.print_exc() - self.embeddedToolbar.move(new_pos) - event.accept() def toolbarMouseReleaseEvent(self, event): """工具栏鼠标释放事件""" @@ -303,26 +316,26 @@ class MainWindow(QMainWindow): self.createMenu = menubar.addMenu('创建') self.setupCreateMenuActions() # 统一创建菜单动作 - self.createGUIaddMenu = self.createMenu.addMenu('GUI') - self.createButtonAction = self.createGUIaddMenu.addAction('创建按钮') - self.createLabelAction = self.createGUIaddMenu.addAction('创建标签') - self.createEntryAction = self.createGUIaddMenu.addAction('创建输入框') - self.createGUIaddMenu.addSeparator() - self.createVirtualScreenAction = self.createGUIaddMenu.addAction('创建虚拟屏幕') - self.createCesiumViewAction = self.createGUIaddMenu.addAction('创建Cesium地图') - self.toggleCesiumViewAction = self.createGUIaddMenu.addAction('开关地图') - self.refreshCesiumViewAction = self.createGUIaddMenu.addAction('刷新地图') - - self.createLightaddMenu = self.createMenu.addMenu('光源') - self.createSpotLightAction = self.createLightaddMenu.addAction('聚光灯') - self.createPointLightAction = self.createLightaddMenu.addAction('点光源') + # self.createGUIaddMenu = self.createMenu.addMenu('GUI') + # self.createButtonAction = self.createGUIaddMenu.addAction('创建按钮') + # self.createLabelAction = self.createGUIaddMenu.addAction('创建标签') + # self.createEntryAction = self.createGUIaddMenu.addAction('创建输入框') + # self.createGUIaddMenu.addSeparator() + # self.createVirtualScreenAction = self.createGUIaddMenu.addAction('创建虚拟屏幕') + # self.createCesiumViewAction = self.createGUIaddMenu.addAction('创建Cesium地图') + # self.toggleCesiumViewAction = self.createGUIaddMenu.addAction('开关地图') + # self.refreshCesiumViewAction = self.createGUIaddMenu.addAction('刷新地图') + # + # self.createLightaddMenu = self.createMenu.addMenu('光源') + # self.createSpotLightAction = self.createLightaddMenu.addAction('聚光灯') + # self.createPointLightAction = self.createLightaddMenu.addAction('点光源') #添加地形菜单 self.createTerrainMenu = self.createMenu.addMenu('地形') self.createFlatTerrainAction = self.createTerrainMenu.addAction('创建平面地形') self.createHeightmapTerrainAction = self.createTerrainMenu.addAction('从高度图创建地形') self.createTerrainMenu.addSeparator() - self.terrainEditModeAction = self.createTerrainMenu.addAction('地形编辑模式') + # self.terrainEditModeAction = self.createTerrainMenu.addAction('地形编辑模式') # GUI菜单 # GUI菜单 - 复用创建菜单的动作 @@ -366,11 +379,42 @@ 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('资源') + self.refreshAssetsAction = self.assetsMenu.addAction('刷新资源') + self.refreshAssetsAction.triggered.connect(self.refreshAssetsView) # 帮助菜单 self.helpMenu = menubar.addMenu('帮助') self.aboutAction = self.helpMenu.addAction('关于') + def refreshAssetsView(self): + """"刷新资源视图""" + if hasattr(self,'fileView') and self.fileView: + self.fileView.refreshView() + print("资源视图已刷新") + def setupCreateMenuActions(self): """统一设置创建菜单的所有动作 - 避免重复代码""" # 基础对象 @@ -383,14 +427,17 @@ class MainWindow(QMainWindow): # 3D GUI子菜单 self.create3dGUIaddMenu = self.createMenu.addMenu('3D GUI') self.create3DTextAction = self.create3dGUIaddMenu.addAction('3D文本') + self.create3DImageAction = self.create3dGUIaddMenu.addAction('3D图片') # GUI子菜单 self.createGUIaddMenu = self.createMenu.addMenu('GUI') self.createButtonAction = self.createGUIaddMenu.addAction('创建按钮') self.createLabelAction = self.createGUIaddMenu.addAction('创建标签') self.createEntryAction = self.createGUIaddMenu.addAction('创建输入框') + self.createImageAction = self.createGUIaddMenu.addAction('创建图片') self.createGUIaddMenu.addSeparator() self.createVideoScreen = self.createGUIaddMenu.addAction('创建视频屏幕') + self.create2DVideoScreen = self.createGUIaddMenu.addAction('创建2D视频屏幕') self.createSphericalVideo = self.createGUIaddMenu.addAction('创建球形视频') self.createGUIaddMenu.addSeparator() self.createVirtualScreenAction = self.createGUIaddMenu.addAction('创建虚拟屏幕') @@ -408,12 +455,15 @@ class MainWindow(QMainWindow): # 连接到world对象的创建方法 # self.createEnptyaddAction.triggered.connect(self.world.createEmptyObject) self.create3DTextAction.triggered.connect(lambda: self.world.createGUI3DText()) + self.create3DImageAction.triggered.connect(lambda: self.world.createGUI3DImage()) self.createButtonAction.triggered.connect(lambda: self.world.createGUIButton()) self.createLabelAction.triggered.connect(lambda: self.world.createGUILabel()) self.createEntryAction.triggered.connect(lambda: self.world.createGUIEntry()) - # self.createVideoScreen.triggered.connect(self.world.createVideoScreen) - # self.createSphericalVideo.triggered.connect(self.world.createSphericalVideo) - # self.createVirtualScreenAction.triggered.connect(self.world.createVirtualScreen) + self.createImageAction.triggered.connect(lambda: self.world.createGUI2DImage()) + self.createVideoScreen.triggered.connect(self.world.createVideoScreen) + self.create2DVideoScreen.triggered.connect(self.world.create2DVideoScreen) + self.createSphericalVideo.triggered.connect(self.world.createSphericalVideo) + self.createVirtualScreenAction.triggered.connect(lambda: self.world.createGUIVirtualScreen()) self.createSpotLightAction.triggered.connect(lambda :self.world.createSpotLight()) self.createPointLightAction.triggered.connect(lambda :self.world.createPointLight()) @@ -430,10 +480,13 @@ class MainWindow(QMainWindow): return { 'createEmpty': self.createEnptyaddAction, 'create3DText': self.create3DTextAction, + 'create3DImage': self.create3DImageAction, 'createButton': self.createButtonAction, 'createLabel': self.createLabelAction, 'createEntry': self.createEntryAction, + 'createImage': self.createImageAction, 'createVideoScreen': self.createVideoScreen, + 'create2DVideoScreen':self.create2DVideoScreen, 'createSphericalVideo': self.createSphericalVideo, 'createVirtualScreen': self.createVirtualScreenAction, 'createSpotLight': self.createSpotLightAction, @@ -553,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): """创建工具栏""" @@ -644,11 +697,6 @@ class MainWindow(QMainWindow): self.createHeightmapTerrainTool.setText("高度图地形") self.toolbar.addWidget(self.createHeightmapTerrainTool) - self.terrainEditTool = QToolButton() - self.terrainEditTool.setText("地形编辑") - self.terrainEditTool.setCheckable(True) - self.toolbar.addWidget(self.terrainEditTool) - # 默认选择"选择"工具 self.selectTool.setChecked(True) self.world.setCurrentTool("选择") @@ -785,13 +833,13 @@ class MainWindow(QMainWindow): self.guiEditModeAction.triggered.connect(lambda: self.world.toggleGUIEditMode()) # 连接创建事件 - 使用菜单动作而不是不存在的工具栏按钮 - self.createSpotLightAction.triggered.connect(lambda: self.world.createSpotLight()) - self.createPointLightAction.triggered.connect(lambda: self.world.createPointLight()) - self.createButtonAction.triggered.connect(lambda: self.world.createGUIButton()) - self.createLabelAction.triggered.connect(lambda: self.world.createGUILabel()) - self.createEntryAction.triggered.connect(lambda: self.world.createGUIEntry()) - self.create3DTextAction.triggered.connect(lambda: self.world.createGUI3DText()) - self.createVirtualScreenAction.triggered.connect(lambda: self.world.createGUIVirtualScreen()) + # self.createSpotLightAction.triggered.connect(lambda: self.world.createSpotLight()) + # self.createPointLightAction.triggered.connect(lambda: self.world.createPointLight()) + # self.createButtonAction.triggered.connect(lambda: self.world.createGUIButton()) + # self.createLabelAction.triggered.connect(lambda: self.world.createGUILabel()) + # self.createEntryAction.triggered.connect(lambda: self.world.createGUIEntry()) + # self.create3DTextAction.triggered.connect(lambda: self.world.createGUI3DText()) + # self.createVirtualScreenAction.triggered.connect(lambda: self.world.createGUIVirtualScreen()) self.createCesiumViewAction.triggered.connect(self.onCreateCesiumView) self.toggleCesiumViewAction.triggered.connect(self.onToggleCesiumView) self.refreshCesiumViewAction.triggered.connect(self.onRefreshCesiumView) @@ -799,7 +847,7 @@ class MainWindow(QMainWindow): # 连接地形创建事件 self.createFlatTerrainAction.triggered.connect(self.onCreateFlatTerrain) self.createHeightmapTerrainAction.triggered.connect(self.onCreateHeightmapTerrain) - self.terrainEditModeAction.triggered.connect(self.onTerrainEditMode) + # self.terrainEditModeAction.triggered.connect(self.onTerrainEditMode) # 连接树节点点击信号 self.treeWidget.itemSelectionChanged.connect( @@ -1045,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): @@ -1426,22 +1864,6 @@ class MainWindow(QMainWindow): else: QMessageBox.warning(self, "错误", "高度图地形创建失败!") - def onTerrainEditMode(self): - """地形编辑模式""" - # 检查当前是否已经处于地形编辑模式 - if self.world.currentTool == "地形编辑": - # 退出地形编辑模式 - self.world.setCurrentTool(None) - self.terrainEditTool.setChecked(False) - self.terrainEditTool.setText("地形编辑") - QMessageBox.information(self, "地形编辑", "已退出地形编辑模式") - else: - # 进入地形编辑模式 - self.world.setCurrentTool("地形编辑") - self.terrainEditTool.setChecked(True) - self.terrainEditTool.setText("退出地形编辑") - QMessageBox.information(self, "地形编辑", - "已进入地形编辑模式\n\n使用鼠标左键抬高地形\n使用鼠标右键降低地形") def setup_main_window(world): """设置主窗口的便利函数""" app = QApplication.instance() diff --git a/ui/property_panel.py b/ui/property_panel.py index b15098d6..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) + 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: @@ -24,6 +28,7 @@ class PropertyPanelManager: self.world = world self._propertyLayout = None self._actor_cache={} + self._spherical_video_controls = {} # 初始化地形编辑参数 if not hasattr(self.world, 'terrain_edit_radius'): @@ -299,20 +304,20 @@ class PropertyPanelManager: transform_layout.addWidget(self.pos_z,1,3) transform_layout.addWidget(QLabel("旋转"),2,0) - self.rot_h = QDoubleSpinBox() - self.rot_p = QDoubleSpinBox() - self.rot_r = QDoubleSpinBox() + self.rot_x = QDoubleSpinBox() + self.rot_y = QDoubleSpinBox() + self.rot_z = QDoubleSpinBox() - for rot_widget in [self.rot_h,self.rot_p,self.rot_r]: + for rot_widget in [self.rot_x,self.rot_y,self.rot_z]: rot_widget.setRange(-360,360) - self.rot_h.setValue(terrain_node.getH()) - self.rot_p.setValue(terrain_node.getP()) - self.rot_r.setValue(terrain_node.getR()) + self.rot_x.setValue(terrain_node.getH()) + self.rot_y.setValue(terrain_node.getP()) + self.rot_z.setValue(terrain_node.getR()) - self.rot_h.valueChanged.connect(lambda v:terrain_node.setH(v)) - self.rot_p.valueChanged.connect(lambda v:terrain_node.setP(v)) - self.rot_r.valueChanged.connect(lambda v:terrain_node.setR(v)) + self.rot_x.valueChanged.connect(lambda v:terrain_node.setH(v)) + self.rot_y.valueChanged.connect(lambda v:terrain_node.setP(v)) + self.rot_z.valueChanged.connect(lambda v:terrain_node.setR(v)) h_label = QLabel("H") p_label = QLabel("P") @@ -324,9 +329,9 @@ class PropertyPanelManager: transform_layout.addWidget(h_label,2,1) transform_layout.addWidget(p_label,2,2) transform_layout.addWidget(r_label,2,3) - transform_layout.addWidget(self.rot_h,3,1) - transform_layout.addWidget(self.rot_p,3,2) - transform_layout.addWidget(self.rot_r,3,3) + transform_layout.addWidget(self.rot_x,3,1) + transform_layout.addWidget(self.rot_y,3,2) + transform_layout.addWidget(self.rot_z,3,3) transform_layout.addWidget(QLabel("缩放"),4,0) self.scale_x = QDoubleSpinBox() @@ -340,9 +345,9 @@ class PropertyPanelManager: self.scale_y.setValue(scale.getY()) self.scale_z.setValue(scale.getZ()) - self.scale_x.valueChanged.connect(lambda v:terrain_node.setScaleX(v)) - self.scale_y.valueChanged.connect(lambda v:terrain_node.setScaleY(v)) - self.scale_z.valueChanged.connect(lambda v:terrain_node.setScaleZ(v)) + self.scale_x.valueChanged.connect(lambda v:self._updateXScale(terrain_node,v)) + self.scale_y.valueChanged.connect(lambda v:self._updateYScale(terrain_node,v)) + self.scale_z.valueChanged.connect(lambda v:self._updateZScale(terrain_node,v)) x_label2 = QLabel("X") y_label2 = QLabel("Y") @@ -534,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) @@ -633,10 +651,16 @@ class PropertyPanelManager: self._safeUpdateSpinBox('pos_x', logical_x) self._safeUpdateSpinBox('pos_z', logical_z) else: - # 3D GUI组件使用世界坐标 - self._safeUpdateSpinBox('pos_x', pos.getX()) - self._safeUpdateSpinBox('pos_y', pos.getY()) - self._safeUpdateSpinBox('pos_z', pos.getZ()) + # 3D GUI组件使用世界坐标(包括 video_screen) + pos = gui_element.getPos() + + # 修复:确保 video_screen 也能正确更新位置控件 + if hasattr(self, 'pos_x') and self.pos_x: + self._safeUpdateSpinBox('pos_x', pos.getX()) + if hasattr(self, 'pos_y') and self.pos_y: + self._safeUpdateSpinBox('pos_y', pos.getY()) + if hasattr(self, 'pos_z') and self.pos_z: + self._safeUpdateSpinBox('pos_z', pos.getZ()) # 更新缩放属性 if hasattr(self, 'scale_x') and self.scale_x: @@ -1107,22 +1131,22 @@ class PropertyPanelManager: # 旋转 (Rotation) transform_layout.addWidget(QLabel("旋转"), 4, 0) - self.rot_h = QDoubleSpinBox() - self.rot_p = QDoubleSpinBox() - self.rot_r = QDoubleSpinBox() + self.rot_x = QDoubleSpinBox() + self.rot_y = QDoubleSpinBox() + self.rot_z = QDoubleSpinBox() # 设置旋转控件属性 - for rot_widget in [self.rot_h, self.rot_p, self.rot_r]: + for rot_widget in [self.rot_x, self.rot_y, self.rot_z]: rot_widget.setRange(-360, 360) - self.rot_h.setValue(model.getH()) - self.rot_p.setValue(model.getP()) - self.rot_r.setValue(model.getR()) + self.rot_x.setValue(model.getH()) + self.rot_y.setValue(model.getP()) + self.rot_z.setValue(model.getR()) # 连接旋转变化事件 - self.rot_h.valueChanged.connect(lambda v: model.setH(v)) - self.rot_p.valueChanged.connect(lambda v: model.setP(v)) - self.rot_r.valueChanged.connect(lambda v: model.setR(v)) + self.rot_x.valueChanged.connect(lambda v: model.setH(v)) + self.rot_y.valueChanged.connect(lambda v: model.setP(v)) + self.rot_z.valueChanged.connect(lambda v: model.setR(v)) # 创建并设置 H, P, R 标签居中 h_label = QLabel("H") @@ -1135,9 +1159,9 @@ class PropertyPanelManager: transform_layout.addWidget(h_label, 4, 1) transform_layout.addWidget(p_label, 4, 2) transform_layout.addWidget(r_label, 4, 3) - transform_layout.addWidget(self.rot_h, 5, 1) - transform_layout.addWidget(self.rot_p, 5, 2) - transform_layout.addWidget(self.rot_r, 5, 3) + transform_layout.addWidget(self.rot_x, 5, 1) + transform_layout.addWidget(self.rot_y, 5, 2) + transform_layout.addWidget(self.rot_z, 5, 3) # 缩放 (Scale) transform_layout.addWidget(QLabel("缩放"), 6, 0) @@ -1153,9 +1177,6 @@ class PropertyPanelManager: current_scale.getZ()])): scale_widget.setRange(-1000, 1000) scale_widget.setSingleStep(0.1) - # 如果缩放值为0,设置为一个很小的非零值 - if scale_value == 0: - scale_value = 0.01 if scale_value >= 0 else -0.01 scale_widget.setValue(scale_value) self.scale_x.valueChanged.connect(lambda value: self._onScaleValueChanged(self.scale_x, value)) @@ -1193,13 +1214,7 @@ class PropertyPanelManager: self._updateModelMaterialPanel(model) def _onScaleValueChanged(self, scale_widget, value): - """确保缩放值不为0""" - if value == 0: - # 设置为一个很小的非零值,保持原有符号 - if hasattr(scale_widget, 'value') and scale_widget.value() > 0: - scale_widget.setValue(0.01) - else: - scale_widget.setValue(-0.01) + pass def _updateXScale(self, model, value): """更新X轴缩放值""" @@ -1262,6 +1277,8 @@ class PropertyPanelManager: def updateGUIPropertyPanel(self, gui_element): """更新GUI元素属性面板""" + self.clearPropertyPanel() + gui_type = gui_element.getTag("gui_type") gui_text = gui_element.getTag("gui_text") @@ -1298,7 +1315,7 @@ class PropertyPanelManager: # 变换属性组(合并位置和变换) if hasattr(gui_element, 'getPos'): # 根据GUI类型设置组名—— - if gui_type in ["button", "label", "entry","2d_image"]: + if gui_type in ["button", "label", "entry","2d_image","2d_video_screen"]: transform_group = QGroupBox("变换 Rect Transform") else: transform_group = QGroupBox("变换 Transform") @@ -1308,7 +1325,7 @@ class PropertyPanelManager: pos = gui_element.getPos() # 根据GUI类型决定位置编辑方式 - if gui_type in ["button", "label", "entry","2d_image"]: + if gui_type in ["button", "label", "entry","2d_image","2d_video_screen"]: # 2D GUI组件使用屏幕坐标 logical_x = pos.getX() / 0.1 # 反向转换为逻辑坐标 logical_z = pos.getZ() / 0.1 @@ -1326,14 +1343,13 @@ class PropertyPanelManager: transform_layout.addWidget(z_label, 0, 2) self.pos_x = QDoubleSpinBox() - self.pos_x.setRange(-50, 50) + self.pos_x.setRange(-1000, 1000) self.pos_x.setValue(logical_x) self.pos_x.valueChanged.connect(lambda v: self.world.gui_manager.editGUI2DPosition(gui_element, "x", v)) - transform_layout.addWidget(self.pos_x, 1, 1) self.pos_z = QDoubleSpinBox() - self.pos_z.setRange(-50, 50) + self.pos_z.setRange(-1000, 1000) self.pos_z.setValue(logical_z) self.pos_z.valueChanged.connect(lambda v: self.world.gui_manager.editGUI2DPosition(gui_element, "z", v)) transform_layout.addWidget(self.pos_z, 1, 2) @@ -1349,18 +1365,15 @@ class PropertyPanelManager: transform_layout.addWidget(actualXLabel, 3, 1) transform_layout.addWidget(actualZLabel, 3, 2) - if gui_type == "2d_image": + if gui_type in ["2d_image","2d_video_screen"]: scale = gui_element.getScale() - width = scale.getX() if hasattr(scale, 'getX') else scale[0] if isinstance(scale, - (tuple, list)) else scale - height = scale.getZ() if hasattr(scale, 'getZ') else scale[1] if isinstance(scale, - (tuple, list)) and len( - scale) > 1 else scale + width = scale.getX() if hasattr(scale, 'getX') else scale[0] if isinstance(scale,(tuple, list)) else scale + height = scale.getZ() if hasattr(scale, 'getZ') else scale[1] if isinstance(scale,(tuple, list)) and len(scale) > 1 else scale 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) @@ -1368,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) @@ -1376,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) @@ -1402,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) @@ -1435,16 +1451,16 @@ class PropertyPanelManager: # 缩放数值输入框 self.scale_x = QDoubleSpinBox() - self.scale_x.setRange(0.01, 10) - self.scale_x.setSingleStep(0.1) + self.scale_x.setRange(0.01, 100) + 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)) transform_layout.addWidget(self.scale_x, 3, 1) self.scale_y = QDoubleSpinBox() - self.scale_y.setRange(0.01, 10) - self.scale_y.setSingleStep(0.1) + self.scale_y.setRange(0.01, 100) + 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) @@ -1452,8 +1468,8 @@ class PropertyPanelManager: transform_layout.addWidget(self.scale_y, 3, 2) self.scale_z = QDoubleSpinBox() - self.scale_z.setRange(0.01, 10) - self.scale_z.setSingleStep(0.1) + self.scale_z.setRange(0.01, 100) + 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) @@ -1464,8 +1480,8 @@ class PropertyPanelManager: transform_group.setLayout(transform_layout) self._propertyLayout.addWidget(transform_group) - # 为2D图像添加Sort属性 - if gui_type == "2d_image": + # 为2D图像和视频屏幕添加Sort属性 + if gui_type in ["2d_image","2d_video_screen","info_panel"]: sort_group = QGroupBox("显示顺序") sort_layout = QGridLayout() @@ -1483,17 +1499,13 @@ class PropertyPanelManager: gui_element.setTag("sort", str(value)) # 应用sort到节点 gui_element.setBin("fixed", value) - print(f"✓ 更新2D图像渲染顺序: {value}") + print(f"✓ 更新{gui_type}渲染顺序: {value}") except Exception as e: - print(f"✗ 更新2D图像渲染顺序失败: {e}") + print(f"✗ 更新{gui_type}渲染顺序失败: {e}") sort_spin.valueChanged.connect(updateSort) sort_layout.addWidget(sort_spin, 0, 1) - # sort_help = QLabel("数值越小越先渲染\n负数在背景,正数在前景") - # sort_help.setStyleSheet("font-size: 10px; color: gray;") - # sort_layout.addWidget(sort_help, 1, 0, 1, 2) - sort_group.setLayout(sort_layout) self._propertyLayout.addWidget(sort_group) @@ -1529,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) # 选择图片按钮 @@ -1553,21 +1570,23 @@ class PropertyPanelManager: ) if file_path: # 应用新纹理到 3D Image + file_path = util.normalize_model_path(file_path) success = self.world.gui_manager.update3DImageTexture(gui_element, file_path) if success: # 保存路径到 Tag gui_element.setTag("texture_path", file_path) + gui_element.setTag("gui_image_path",file_path) # 更新显示 texture_label.setText(file_path) # 可选:刷新场景树或其他 UI - self.world.scene_manager.updateSceneTree() + # self.world.scene_manager.updateSceneTree() select_texture_button.clicked.connect(onSelectTexture) 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() @@ -1577,9 +1596,13 @@ class PropertyPanelManager: # 显示当前贴图路径(简化显示) current_texture_path = gui_element.getTag("texture_path") or gui_element.getTag("image_path") or "未设置" - #texture_label = QLabel(current_texture_path) - #texture_label.setWordWrap(True) - #image_layout.addWidget(texture_label, 0, 1) + 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) # 选择图片按钮 select_texture_button = QPushButton("选择图片...") @@ -1595,15 +1618,16 @@ class PropertyPanelManager: ) if file_path: # 应用新纹理到 2D Image + file_path = util.normalize_model_path(file_path) success = self.update2DImageTexture(gui_element, file_path) if success: # 保存路径到 Tag gui_element.setTag("texture_path", file_path) gui_element.setTag("image_path", file_path) # 更新显示 - #texture_label.setText(file_path) + texture_label.setText(file_path) # 可选:刷新场景树或其他 UI - self.world.scene_manager.updateSceneTree() + # self.world.scene_manager.updateSceneTree() select_texture_button.clicked.connect(onSelect2DTexture) @@ -1611,6 +1635,16 @@ class PropertyPanelManager: self._propertyLayout.addWidget(image_group) # 添加弹性空间 + + if gui_type == "video_screen": + self._addVideoScreenProperties(gui_element) + elif gui_type == "2d_video_screen": + self._add2DVideoScreenProperties(gui_element) + elif gui_type == "spherical_video": + self._addSphericalVideoProperties(gui_element) + elif gui_type == 'info_panel': + self._addInfoPanelProperties(gui_element) + self._propertyLayout.addStretch() # 强制更新布局 @@ -1620,7 +1654,2079 @@ class PropertyPanelManager: if propertyWidget: propertyWidget.update() - # 在gui_manager.py或其他相关文件中添加以下方法 + 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: + from PyQt5.QtWidgets import (QGroupBox, QGridLayout, QPushButton, QLabel, + QDoubleSpinBox, QFileDialog, QHBoxLayout, QSlider) + from PyQt5.QtCore import Qt + import os + + # 视频控制组 + video_group = QGroupBox("视频控制") + video_layout = QGridLayout() + + # 播放控制按钮 + control_layout = QHBoxLayout() + + self.play_button = QPushButton("▶️ 播放") + self.play_button.clicked.connect(lambda: self._toggleSphericalVideoPlayback(spherical_video)) + + self.stop_button = QPushButton("⏹️ 停止") + self.stop_button.clicked.connect(lambda: self._stopSphericalVideo(spherical_video)) + + control_layout.addWidget(self.play_button) + control_layout.addWidget(self.stop_button) + video_layout.addLayout(control_layout, 0, 0, 1, 2) + + # 视频进度控制 + progress_layout = QHBoxLayout() + self.progress_slider = QSlider(Qt.Horizontal) + self.progress_slider.setRange(0, 100) + self.progress_slider.setValue(0) + self.progress_slider.sliderMoved.connect(lambda value: self._seekSphericalVideo(spherical_video, value)) + + self.time_label = QLabel("00:00 / 00:00") + self.time_label.setStyleSheet("font-size: 10px; color: gray;") + + progress_layout.addWidget(self.progress_slider) + progress_layout.addWidget(self.time_label) + video_layout.addLayout(progress_layout, 1, 0, 1, 2) + + # 视频文件选择 + file_layout = QHBoxLayout() + self.select_video_button = QPushButton("选择视频文件") + self.select_video_button.clicked.connect(lambda: self._selectSphericalVideoFile(spherical_video)) + + # 显示当前视频文件 + current_video = spherical_video.getTag("video_path") if spherical_video.hasTag("video_path") else "" + self.video_file_label = QLabel(os.path.basename(current_video) if current_video else "未选择视频") + self.video_file_label.setStyleSheet("font-size: 9px; color: gray;") + self.video_file_label.setWordWrap(True) + + file_layout.addWidget(self.select_video_button) + file_layout.addWidget(self.video_file_label) + video_layout.addLayout(file_layout, 2, 0, 1, 2) + + video_group.setLayout(video_layout) + self._propertyLayout.addWidget(video_group) + + # 球体属性组 + sphere_group = QGroupBox("球体属性") + sphere_layout = QGridLayout() + + # 半径控制 + sphere_layout.addWidget(QLabel("半径:"), 0, 0) + radius_spin = QDoubleSpinBox() + radius_spin.setRange(0.1, 100) + radius_spin.setSingleStep(0.1) + current_radius = float(spherical_video.getTag("original_radius") or "5.0") + radius_spin.setValue(current_radius) + radius_spin.valueChanged.connect(lambda value: self._updateSphericalVideoRadius(spherical_video, value)) + sphere_layout.addWidget(radius_spin, 0, 1) + + sphere_group.setLayout(sphere_layout) + self._propertyLayout.addWidget(sphere_group) + + # 存储控件引用 + self._spherical_video_controls = { + 'play_button': self.play_button, + 'stop_button': self.stop_button, + 'progress_slider': self.progress_slider, + 'time_label': self.time_label, + 'select_button': self.select_video_button, + 'file_label': self.video_file_label + } + + print("✅ 球形视频属性面板已添加") + + except Exception as e: + print(f"❌ 添加球形视频属性面板失败: {e}") + import traceback + traceback.print_exc() + + def _toggleSphericalVideoPlayback(self, spherical_video): + """切换球形视频播放状态""" + try: + # 获取视频纹理 + movie_texture = spherical_video.getPythonTag("movie_texture") + if not movie_texture: + # 尝试从节点获取纹理 + texture = spherical_video.getTexture() + if texture: + movie_texture = texture + + if not movie_texture: + print("❌ 未找到视频纹理") + return + + # 检查当前播放状态 + if not hasattr(self, '_video_playing'): + self._video_playing = {} + + video_id = id(spherical_video) + is_playing = self._video_playing.get(video_id, False) + + if is_playing: + # 暂停视频 + if hasattr(movie_texture, 'stop'): + movie_texture.stop() + if hasattr(self, '_spherical_video_controls') and 'play_button' in self._spherical_video_controls: + play_button = self._spherical_video_controls['play_button'] + play_button.setText("▶️ 播放") + print("⏸️ 球形视频已暂停") + else: + # 播放视频 + if hasattr(movie_texture, 'play'): + movie_texture.play() + if hasattr(self, '_spherical_video_controls') and 'play_button' in self._spherical_video_controls: + play_button = self._spherical_video_controls['play_button'] + play_button.setText("⏸️ 暂停") + print("▶️ 球形视频开始播放") + + # 更新播放状态 + self._video_playing[video_id] = not is_playing + + except Exception as e: + print(f"❌ 切换球形视频播放状态失败: {e}") + + def _stopSphericalVideo(self, spherical_video): + """停止球形视频""" + try: + # 获取视频纹理 + movie_texture = spherical_video.getPythonTag("movie_texture") + if not movie_texture: + # 尝试从节点获取纹理 + texture = spherical_video.getTexture() + if texture: + movie_texture = texture + + if movie_texture: + if hasattr(movie_texture, 'stop'): + movie_texture.stop() + # 重置到开头 + if hasattr(movie_texture, 'setTime'): + movie_texture.setTime(0.0) + + # 更新播放按钮 + if hasattr(self, '_spherical_video_controls') and 'play_button' in self._spherical_video_controls: + play_button = self._spherical_video_controls['play_button'] + play_button.setText("▶️ 播放") + + # 更新播放状态 + if hasattr(self, '_video_playing'): + video_id = id(spherical_video) + self._video_playing[video_id] = False + + print("⏹️ 球形视频已停止") + else: + print("❌ 视频纹理不支持停止操作") + else: + print("❌ 未找到视频纹理") + + except Exception as e: + print(f"❌ 停止球形视频失败: {e}") + + def _seekSphericalVideo(self, spherical_video, slider_value): + """跳转到指定时间位置""" + try: + # 获取视频纹理 + movie_texture = spherical_video.getPythonTag("movie_texture") + if not movie_texture: + # 尝试从节点获取纹理 + texture = spherical_video.getTexture() + if texture: + movie_texture = texture + + if movie_texture and hasattr(movie_texture, 'setTime'): + # 假设视频总时长为100秒(实际应该根据视频时长计算) + time_seconds = slider_value + movie_texture.setTime(time_seconds) + print(f"⏰ 跳转到时间: {time_seconds}秒") + + # 更新时间标签 + if hasattr(self, '_spherical_video_controls') and 'time_label' in self._spherical_video_controls: + time_label = self._spherical_video_controls['time_label'] + current_time = f"{int(time_seconds // 60):02d}:{int(time_seconds % 60):02d}" + total_time = "01:40" # 假设总时长100秒 + time_label.setText(f"{current_time} / {total_time}") + + else: + print("❌ 视频纹理不支持时间控制") + + except Exception as e: + print(f"❌ 球形视频跳转失败: {e}") + + def _updateSphericalVideoRadius(self, spherical_video, new_radius): + """更新球形视频半径""" + try: + from panda3d.core import GeomNode + + # 移除旧的几何体 + spherical_video.node().removeAllGeoms() + + # 创建新的球形几何体 + new_geom = self.world.gui_manager._createSphereGeometry(new_radius, segments=32) + spherical_video.node().addGeom(new_geom) + + # 更新标签 + spherical_video.setTag("original_radius", str(new_radius)) + + print(f"🔄 球形视频半径已更新为: {new_radius}") + + except Exception as e: + print(f"❌ 更新球形视频半径失败: {e}") + + def loadSphericalVideoFile(self, spherical_video, video_path): + """为球形视频加载新的视频文件""" + try: + from panda3d.core import TextureStage, Texture + import os + + if not os.path.exists(video_path): + print(f"❌ 球形视频文件不存在: {video_path}") + return False + + # 加载新的视频纹理 + movie_texture = self.world.gui_manager._loadMovieTexture(video_path) + if movie_texture: + # 清除现有的纹理 + spherical_video.clearTexture() + + # 设置视频纹理属性 + movie_texture.setWrapU(Texture.WM_clamp) + movie_texture.setWrapV(Texture.WM_clamp) + movie_texture.setMinfilter(Texture.FT_linear) + movie_texture.setMagfilter(Texture.FT_linear) + + # 为视频纹理创建专用的纹理阶段 + texture_stage = TextureStage("spherical_video") + texture_stage.setMode(TextureStage.MModulate) + + # 应用纹理到球体 + spherical_video.setTexture(texture_stage, movie_texture) + + # 保存视频纹理引用 + spherical_video.setPythonTag("movie_texture", movie_texture) + spherical_video.setTag("video_path", video_path) + + # 设置为白色以正确显示视频 + spherical_video.setColor(1, 1, 1, 1) + + print(f"✅ 成功加载新球形视频: {video_path}") + return True + else: + print(f"❌ 无法加载球形视频文件: {video_path}") + return False + + except Exception as e: + print(f"❌ 加载球形视频文件失败: {e}") + import traceback + traceback.print_exc() + return False + + def _selectSphericalVideoFile(self, spherical_video): + """选择球形视频文件""" + try: + from PyQt5.QtWidgets import QFileDialog + import os + + # 打开文件选择对话框 + file_path, _ = QFileDialog.getOpenFileName( + None, + "选择360度视频文件", + "", + "视频文件 (*.mp4 *.avi *.mov *.wmv *.flv *.mkv *.webm);;所有文件 (*)" + ) + + if file_path and os.path.exists(file_path): + # 加载新视频文件 - 修复方法调用 + success = self.loadSphericalVideoFile(spherical_video, file_path) + if success: + # 更新文件标签显示 + if hasattr(self, '_spherical_video_controls') and 'file_label' in self._spherical_video_controls: + file_label = self._spherical_video_controls['file_label'] + file_label.setText(os.path.basename(file_path)) + print(f"✅ 球形视频文件已更新为: {file_path}") + else: + print(f"❌ 无法加载球形视频文件: {file_path}") + else: + print("❌ 未选择有效的视频文件") + + except Exception as e: + print(f"❌ 选择球形视频文件失败: {e}") + import traceback + traceback.print_exc() + + 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._loadNewVideo(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") + 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) + + url_group.setLayout(url_layout) + self._propertyLayout.addWidget(url_group) + + except Exception as e: + print(f"添加视频屏幕属性失败: {e}") + + def _add2DVideoScreenProperties(self, video_screen): + """为2D视频屏幕添加属性控制面板""" + try: + from PyQt5.QtWidgets import (QGroupBox, QGridLayout, QPushButton, QLabel, + QFileDialog, QHBoxLayout, QLineEdit) + import os + + # 视频信息组 + video_info_group = QGroupBox("2D视频信息") + video_info_layout = QGridLayout() + + # 显示当前视频文件路径 + # 在显示视频信息时区分本地文件和网络URL + video_path = video_screen.getTag("video_path") + 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_screen.setBin("fixed", 0) + + video_info_group.setLayout(video_info_layout) + self._propertyLayout.addWidget(video_info_group) + + # 视频控制组 + video_control_group = QGroupBox("2D视频控制") + video_control_layout = QHBoxLayout() + + # 播放按钮 + play_btn = QPushButton("▶️ 播放") + play_btn.clicked.connect(lambda: self.play2DVideo(video_screen)) + video_control_layout.addWidget(play_btn) + + # 暂停按钮 + pause_btn = QPushButton("⏸️ 暂停") + pause_btn.clicked.connect(lambda: self.pause2DVideo(video_screen)) + video_control_layout.addWidget(pause_btn) + + # 停止按钮 + stop_btn = QPushButton("⏹️ 停止") + stop_btn.clicked.connect(lambda: self._stop2DVideo(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") + 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) + self._propertyLayout.addWidget(url_group) + + except Exception as e: + print(f"添加2D视频屏幕属性失败: {e}") + + def _loadVideoFromURLWithOpenCV(self, video_screen, url): + """使用OpenCV从URL加载视频流并在2D视频屏幕上显示(推荐版)""" + try: + import cv2 + import threading + 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) + + # 使用 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 + + # 设置视频参数以提高性能 + cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) # 减少缓冲以降低延迟 + + # 创建纹理对象 + texture = Texture("video_texture") + texture.setMagfilter(Texture.FTLinear) + texture.setMinfilter(Texture.FTLinear) + texture.setWrapU(Texture.WMClamp) + texture.setWrapV(Texture.WMClamp) + + # 保存视频信息 + video_info = { + 'capture': cap, + 'texture': texture, + 'playing': True + } + + # 应用纹理到视频屏幕 + video_screen.setTexture(texture, 1) + video_screen.setPythonTag("video_info", video_info) + video_screen.setTag("video_path", url) + + # 启动视频播放线程 + def update_video_texture(): + target_fps = 60 # 降低目标帧率以减少CPU使用 + frame_time = 1.0 / target_fps + + frame_count = 0 + + while True: + try: + # 检查是否应该继续播放 + if not (video_screen.hasPythonTag("video_info") and + video_screen.getPythonTag("video_info").get('playing', False)): + break + + video_info = video_screen.getPythonTag("video_info") + cap = video_info['capture'] + + ret, frame = cap.read() + if not ret: + # 视频结束,重新开始播放 + cap.set(cv2.CAP_PROP_POS_FRAMES, 0) + continue + + frame_count += 1 + + # 调整帧大小以降低处理负担 + frame_height, frame_width = frame.shape[:2] + 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)) + + # 转换颜色格式 (BGR to RGB) + frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) + + # 获取帧尺寸 + height, width = frame_rgb.shape[:2] + + # 创建 PNMImage 并设置数据 + img = PNMImage(width, height, 3) + img.set_maxval(255) + + # 使用逐行设置方法(稳定且兼容性好) + for y in range(height): + for x in range(width): + r, g, b = frame_rgb[y, x] + img.setXelVal(x, y, r, g, b) + + # 更新纹理 + texture = video_info['texture'] + if texture: + texture.load(img) + + # 控制帧率 + time.sleep(frame_time) + + except Exception as e: + print(f"视频帧更新错误: {e}") + import traceback + traceback.print_exc() + break + + print("视频播放线程结束") + + # 在单独线程中处理视频流 + thread = threading.Thread(target=update_video_texture, daemon=True) + 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): + """播放2D视频""" + try: + # 检查是否已经有视频信息(OpenCV方式) + if video_screen.hasPythonTag("video_info"): + video_info = video_screen.getPythonTag("video_info") + if video_info: + video_info['playing'] = True + print("▶️ 视频已恢复播放") + return True + + # 检查是否是 MovieTexture 方式 + movie_texture = video_screen.getPythonTag("movie_texture") + if movie_texture and hasattr(movie_texture, 'play'): + try: + movie_texture.play() + print("▶️ MovieTexture 视频开始播放") + return True + except Exception as play_error: + print(f"⚠️ MovieTexture 播放视频时出错: {play_error}") + + # 如果没有视频信息,尝试从URL加载 + video_path = video_screen.getTag("video_path") + if video_path: + # 根据路径判断是本地文件还是URL + if video_path.startswith("http://") or video_path.startswith("https://"): + return self._loadVideoFromURLWithOpenCV(video_screen, video_path) + else: + # 本地文件使用 MovieTexture 方式 + return self.world.gui_manager.load2DVideoFile(video_screen, video_path) + else: + print("❌ 没有找到视频源") + return False + + except Exception as e: + print(f"❌ 播放视频失败: {e}") + import traceback + traceback.print_exc() + return False + + def pause2DVideo(self, video_screen): + """暂停2D视频""" + try: + if video_screen.hasPythonTag("video_info"): + video_info = video_screen.getPythonTag("video_info") + if video_info: + video_info['playing'] = False + print("⏸️ 视频已暂停") + return True + print("没有正在播放的视频") + return False + except Exception as e: + print(f"❌ 暂停视频失败: {e}") + return False + + def _stop2DVideo(self, video_screen): + """停止2D视频(内部方法)""" + try: + # 停止视频播放 + if video_screen.hasPythonTag("video_info"): + video_info = video_screen.getPythonTag("video_info") + if video_info: + # 停止播放 + video_info['playing'] = False + + # 释放视频捕获资源 + if 'capture' in video_info and video_info['capture']: + try: + video_info['capture'].release() + except: + pass + + # 清理纹理 + try: + video_screen.clearTexture() + except: + pass + + video_screen.clearPythonTag("video_info") + + # 清理视频路径标签 + if video_screen.hasTag("video_path"): + video_screen.clearTag("video_path") + + print("⏹️ 视频已停止") + return True + except Exception as e: + print(f"❌ 停止视频失败: {e}") + return False + def _loadNew2DVideo(self, video_screen): + """为2D视频屏幕加载新视频文件""" + try: + file_path, _ = QFileDialog.getOpenFileName( + None, + "选择视频文件", + "", + "视频文件 (*.mp4 *.avi *.mov *.mkv *.webm *.ogg)" + ) + if file_path: + success = self.world.gui_manager.load2DVideoFile(video_screen, file_path) + if success: + print(f"成功加载新视频: {file_path}") + # 刷新属性面板以显示新视频信息 + self._stop2DVideo(video_screen) + self.updateGUIPropertyPanel(video_screen) + return True + except Exception as e: + print(f"加载新视频失败: {e}") + return False + + def load2DVideoFile(self, video_screen, video_path): + """为2D视频屏幕加载新的视频文件""" + try: + import os + + # 处理空路径情况 - 显示空白 + if not video_path or video_path == "": + print("ℹ️ 空视频路径,显示空白") + video_screen["frameColor"] = (0, 0, 0, 0) # 透明背景 + video_screen.clearPythonTag("movie_texture") + video_screen.setTag("video_path", "") + return True + + # 检查文件是否存在 + if not os.path.exists(video_path): + print(f"❌ 2D视频文件不存在: {video_path}") + # 显示空白而不是红色错误提示 + video_screen["frameColor"] = (0, 0, 0, 0) # 透明背景 + return False + + # 加载新的视频纹理 + movie_texture = self._loadMovieTexture(video_path) + if movie_texture: + # 应用纹理到2D视频屏幕 + video_screen["frameTexture"] = movie_texture + # 设置白色背景以正确显示视频 + video_screen["frameColor"] = (1, 1, 1, 1) + + # 保存视频纹理引用 + video_screen.setPythonTag("movie_texture", movie_texture) + video_screen.setTag("video_path", video_path) + + print(f"✅ 成功加载新2D视频: {video_path}") + return True + else: + print(f"❌ 无法加载2D视频文件: {video_path}") + # 显示空白而不是红色错误提示 + video_screen["frameColor"] = (0, 0, 0, 0) # 透明背景 + return False + + except Exception as e: + print(f"❌ 加载2D视频文件失败: {e}") + import traceback + traceback.print_exc() + # 显示空白而不是红色错误提示 + video_screen["frameColor"] = (0, 0, 0, 0) # 透明背景 + return False + + def _loadVideoFromURLWithOpenCV_3D(self, video_screen, url): + """使用OpenCV从URL加载视频流并在3D视频屏幕上显示""" + try: + import cv2 + 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) + + # 使用 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 + + # 设置视频参数以提高性能 + cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) # 减少缓冲以降低延迟 + + # 创建纹理对象 + texture = Texture("video_texture_3d") + texture.setMagfilter(Texture.FTLinear) + texture.setMinfilter(Texture.FTLinear) + texture.setWrapU(Texture.WMClamp) + texture.setWrapV(Texture.WMClamp) + + # 保存视频信息 + video_info = { + 'capture': cap, + 'texture': texture, + 'playing': True + } + + # 创建纹理阶段并应用到3D视频屏幕 + texture_stage = TextureStage("video_3d") + texture_stage.setSort(0) + texture_stage.setMode(TextureStage.MModulate) + video_screen.setTexture(texture_stage, texture) + + # 设置为白色以正确显示视频 + video_screen.setColor(1, 1, 1, 1) + + # 保存视频信息到PythonTag + video_screen.setPythonTag("video_info", video_info) + video_screen.setTag("video_path", url) + + # 启动视频播放线程 + def update_video_texture(): + target_fps = 60 # 降低目标帧率以减少CPU使用 + frame_time = 1.0 / target_fps + frame_count = 0 + + while True: + try: + # 检查是否应该继续播放 + if not (video_screen.hasPythonTag("video_info") and + video_screen.getPythonTag("video_info").get('playing', False)): + break + + video_info = video_screen.getPythonTag("video_info") + cap = video_info['capture'] + + ret, frame = cap.read() + if not ret: + # 视频结束,重新开始播放 + cap.set(cv2.CAP_PROP_POS_FRAMES, 0) + continue + + frame_count += 1 + + # 调整帧大小以降低处理负担 + frame_height, frame_width = frame.shape[:2] + 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)) + + # 转换颜色格式 (BGR to RGB) + frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) + + # 获取帧尺寸 + height, width = frame_rgb.shape[:2] + + # 创建 PNMImage 并设置数据 + img = PNMImage(width, height, 3) + img.set_maxval(255) + + # 使用逐行设置方法(稳定且兼容性好) + for y in range(height): + for x in range(width): + r, g, b = frame_rgb[y, x] + img.setXelVal(x, y, r, g, b) + + # 更新纹理 + texture = video_info['texture'] + if texture: + texture.load(img) + + # 控制帧率 + time.sleep(frame_time) + + except Exception as e: + print(f"3D视频帧更新错误: {e}") + import traceback + traceback.print_exc() + break + + print("3D视频播放线程结束") + + # 在单独线程中处理视频流 + thread = threading.Thread(target=update_video_texture, daemon=True) + 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: + # 停止视频播放 + if video_screen.hasPythonTag("video_info"): + video_info = video_screen.getPythonTag("video_info") + if video_info: + # 停止播放 + video_info['playing'] = False + + # 释放视频捕获资源 + if 'capture' in video_info and video_info['capture']: + try: + video_info['capture'].release() + except: + pass + + # 清理纹理 + try: + video_screen.clearTexture() + except: + pass + + video_screen.clearPythonTag("video_info") + + # 清理视频路径标签 + if video_screen.hasTag("video_path"): + video_screen.clearTag("video_path") + + print("⏹️ 3D视频已停止") + return True + except Exception as e: + print(f"❌ 停止3D视频失败: {e}") + return False + + + def _loadNewVideo(self,video_screen): + try: + file_path, _ = QFileDialog.getOpenFileName( + None, + "选择视频文件", + "", + "视频文件(*.mp4 *.avi *.mov *.mkv *.webm *.ogg)" + ) + if file_path: + success = self.world.gui_manager.loadVideoFile(video_screen,file_path) + if success: + print(f"成功加载新视频{file_path}") + self.updateGUIPropertyPanel(video_screen) + return True + except Exception as e: + print(f"加载新视频失败{e}") + return False def editGUI2DPosition(self, gui_element, axis, value): """编辑2D GUI元素位置""" @@ -1653,35 +3759,6 @@ class PropertyPanelManager: traceback.print_exc() return False - def editGUI3DPosition(self, gui_element, axis, value): - """编辑3D GUI元素位置""" - try: - gui_type = gui_element.getTag("gui_type") - - if gui_type in ["3d_text", "3d_image"]: - current_pos = gui_element.getPos() - - if axis == "x": - new_pos = (value, current_pos.getY(), current_pos.getZ()) - elif axis == "y": - new_pos = (current_pos.getX(), value, current_pos.getZ()) - elif axis == "z": - new_pos = (current_pos.getX(), current_pos.getY(), value) - else: - return False - - gui_element.setPos(*new_pos) - print(f"✓ 更新3D GUI元素位置: {axis}={value}") - return True - else: - print(f"✗ 不支持的GUI类型进行3D位置编辑: {gui_type}") - return False - except Exception as e: - print(f"✗ 更新3D GUI元素位置失败: {e}") - import traceback - traceback.print_exc() - return False - def editGUIScale(self, gui_element, axis, value): """编辑GUI元素缩放""" try: @@ -1692,7 +3769,7 @@ class PropertyPanelManager: if value == 0: value = 0.01 - if gui_type in ["3d_text", "3d_image","2d_image"]: + if gui_type in ["3d_text", "3d_image","2d_image","video_screen","2d_video_screen"]: # 3D元素处理 if axis == "x": new_scale = (value, current_scale.getY(), current_scale.getZ()) @@ -1923,19 +4000,6 @@ class PropertyPanelManager: except Exception as e: print(f"✗ 更新GUI元素Z轴缩放失败: {e}") - def update3DImageTexture(self,nodepath,texture_path): - try: - tex = self.world.loader.loadTexture(texture_path) - if tex: - nodepath.setTexture(tex,1) - return True - else: - print(f"[警告] 无法加载贴图: {texture_path}") - return False - except Exception as e: - print(f"[错误] 更新 3D 图片纹理失败: {e}") - return False - def update2DImageTexture(self, gui_element, image_path): try: new_texture = self.world.loader.loadTexture(image_path) @@ -2365,6 +4429,9 @@ class PropertyPanelManager: material_layout = QGridLayout() material_layout.addWidget(QLabel("名称:"), 0, 0) + limited_length = 25 + if len(display_name) > limited_length: + display_name = f"{display_name[:limited_length - 3]}..." name_label = QLabel(display_name) # name_label.setStyleSheet("color: #FF6B6B; font-weight: bold;") material_layout.addWidget(name_label, 0, 1, 1, 3) @@ -2575,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: @@ -4943,7 +6999,7 @@ class PropertyPanelManager: if preset["shading_model"]==3: self._apply_transparent_effect() - material._applied_preset = preset_name + #material._applied_preset = preset_name self._refreshMaterialUI() print(f"已应用材质预设: {preset_name}") @@ -4975,38 +7031,6 @@ class PropertyPanelManager: # 触发属性面板更新 self.updatePropertyPanel(current_item) - def _addColorSpacePanel(self, material): - """添加颜色空间选择面板""" - from PyQt5.QtWidgets import QButtonGroup, QRadioButton, QHBoxLayout, QWidget - - # color_space_title = QLabel("颜色空间") - # color_space_title.setStyleSheet("color: #FF9800; font-weight:bold;") - # self._propertyLayout.addRow(color_space_title) - # - # color_space_widget = QWidget() - # color_space_layout = QHBoxLayout(color_space_widget) - # color_space_group = QButtonGroup() - - # rgb_radio = QRadioButton("RGB") - # srgb_radio = QRadioButton("sRGB") - # hsv_radio = QRadioButton("HSV") - # - # rgb_radio.setChecked(True) - # - # color_space_group.addButton(rgb_radio, 0) - # color_space_group.addButton(srgb_radio, 1) - # color_space_group.addButton(hsv_radio, 2) - # - # color_space_layout.addWidget(rgb_radio) - # color_space_layout.addWidget(srgb_radio) - # color_space_layout.addWidget(hsv_radio) - # - # color_space_group.buttonClicked.connect( - # lambda button: self._updateColorSpace(material, color_space_group.id(button)) - # ) - # - # self._propertyLayout.addRow("颜色空间:", color_space_widget) - def _addBatchOperationsPanel(self, model): """添加批量操作面板""" batch_title = QLabel("批量操作") @@ -5623,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 加载错误,很多模型都不是角色动画 @@ -6300,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 207f26d9..c84deb42 100644 --- a/ui/widgets.py +++ b/ui/widgets.py @@ -20,7 +20,7 @@ from PyQt5.sip import wrapinstance from panda3d.core import ModelRoot from QPanda3D.QPanda3DWidget import QPanda3DWidget - +from scene import util class NewProjectDialog(QDialog): """新建项目对话框""" @@ -150,8 +150,8 @@ class CustomPanda3DWidget(QPanda3DWidget): for url in event.mimeData().urls(): filepath = url.toLocalFile() if filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')): + # 使用关键字参数确保兼容性 self.world.importModel(filepath) - #self.world.addAnimationPanel(None,filepath) event.acceptProposedAction() else: event.ignore() @@ -315,6 +315,9 @@ class CustomFileView(QTreeView): print("不支持的文件类型") super().mouseDoubleClickEvent(event) +from PyQt5.QtCore import QFileSystemWatcher,QTimer +import os + class CustomAssetsTreeWidget(QTreeWidget): def __init__(self, world, parent=None): if parent is None: @@ -325,6 +328,18 @@ class CustomAssetsTreeWidget(QTreeWidget): self.setupUI() self.setupDragDrop() + #添加文件系统监控器 + self.file_watcher = QFileSystemWatcher() + self.file_watcher.directoryChanged.connect(self.onDirectoryChanged) + self.file_watcher.fileChanged.connect(self.onFileChanged) + + self.refresh_timer = QTimer() + self.refresh_timer.setSingleShot(True) + self.refresh_timer.timeout.connect(self.refreshView) + + #存储监控的目录 + self.watched_directories = set() + # 默认加载项目根路径 self.load_file_tree() # 设置右键菜单 @@ -624,6 +639,9 @@ class CustomAssetsTreeWidget(QTreeWidget): # 加载当前目录内容 self.load_directory_tree(self.current_path, root_item) + #添加目录到监控器 + self.addWatchedDirectory(self.current_path) + # 展开根节点 root_item.setExpanded(True) @@ -631,6 +649,54 @@ class CustomAssetsTreeWidget(QTreeWidget): error_item = QTreeWidgetItem(["❌ 无权限访问此目录"]) self.addTopLevelItem(error_item) + def addWatchedDirectory(self,directory): + """添加监控目录""" + if os.path.exists(directory) and directory not in self.watched_directories: + if self.file_watcher.addPath(directory): + self.watched_directories.add(directory) + print(f"开始监控目录:{directory}") + try: + for item in os.listdir(directory): + item_path = os.path.join(directory,item) + if os.path.isdir(item_path): + self.addWatchedDirectory(item_path) + except Exception as e: + pass + else: + print(f"无法监控目录:{directory}") + + def removeWatchedDirectory(self,directory): + """移除监控目录""" + if directory in self.watched_directories: + if self.file_watcher.removePath(directory): + self.watched_directories.discard(directory) + print(f"停止监控目录:{directory}") + else: + print(f"无法停止监控目录:{directory}") + + def onDirectoryChanged(self,path): + """目录发生变化时处理""" + print(f"目录变化{path}") + if not self.refresh_timer.isActive(): + self.refresh_timer.start(1000) + + def onFileChanged(self,path): + """目录发生变化时的处理""" + print(f"目录变化{path}") + if not self.refresh_timer.isActive(): + self.refresh_timer.start(1000) + + def refreshView(self): + """刷新视图""" + print("刷新资源视图...") + try: + expanded_paths = self._saveExpandedState() + self.load_file_tree() + self._restoreExpandedState(expanded_paths) + print("资源视图刷新完成") + except Exception as e: + print(f"刷新资源视图失败{e}") + def load_directory_tree(self, path, parent_item, max_depth=3, current_depth=0): """递归加载目录树(类似左侧导航面板)""" if current_depth >= max_depth: @@ -936,6 +1002,7 @@ class CustomAssetsTreeWidget(QTreeWidget): internal_paths.append(filepath) # 检查是否是模型文件(用于向外拖拽) if filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')): + print(f"模型路ing!!!!!!!!!!!!!!!!!{QUrl.fromLocalFile(filepath)}") urls.append(QUrl.fromLocalFile(filepath)) # 设置内部拖拽数据 @@ -1302,11 +1369,42 @@ class CustomTreeWidget(QTreeWidget): parent = wrapinstance(0, QWidget) super().__init__(parent) self.world = world + self.initData() self.setupUI() # 初始化界面 self.setupContextMenu() # 初始化右键菜单 self.setupDragDrop() # 设置拖拽功能 + self.original_scales={} + + def initData(self): + """初始化变量""" + # 定义2D GUI元素类型 + self.gui_2d_types = { + "GUI_BUTTON", # DirectButton + "GUI_LABEL", # DirectLabel + "GUI_ENTRY", # DirectEntry + "GUI_IMAGE", + "GUI_NODE" # 其他2D GUI容器 + } + + # 定义3D GUI元素类型 + self.gui_3d_types = { + "GUI_3DTEXT", # 3D TextNode + "GUI_3DIMAGE", + "GUI_VIRTUAL_SCREEN" # Virtual Screen + } + + # 定义3D场景节点类型(可以接受3D GUI元素和其他3D场景元素) + self.scene_3d_types = { + "SCENE_ROOT", + "SCENE_NODE", + "LIGHT_NODE", + "CAMERA_NODE", + "IMPORTED_MODEL_NODE", + "MODEL_NODE" + } + def setupUI(self): """初始化UI设置""" self.setHeaderHidden(True) @@ -1356,12 +1454,14 @@ class CustomTreeWidget(QTreeWidget): # 3D GUI菜单 create3dGUIMenu = createMenu.addMenu('3D GUI') create3dGUIMenu.addAction(create_actions['create3DText']) + create3dGUIMenu.addAction(create_actions['create3DImage']) # GUI菜单 createGUIMenu = createMenu.addMenu('GUI') createGUIMenu.addAction(create_actions['createButton']) createGUIMenu.addAction(create_actions['createLabel']) createGUIMenu.addAction(create_actions['createEntry']) + createGUIMenu.addAction(create_actions['createImage']) createGUIMenu.addSeparator() createGUIMenu.addAction(create_actions['createVideoScreen']) createGUIMenu.addAction(create_actions['createSphericalVideo']) @@ -1395,106 +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 - - # # 检查是否是有效的父子关系 - # if self.isValidParentChild(dragged_item, target_item): - # # 保存当前的世界坐标 - # world_pos = dragged_node.getPos(self.world.render) - # - # # 更新场景图中的父子关系 - # dragged_node.wrtReparentTo(target_node) - # - # # 接受拖放事件,更新树形控件 - # super().dropEvent(event) - # - # #self.world.property_panel.updateNodeVisibilityAfterDrag(dragged_item) - # # 更新属性面板 - # self.world.updatePropertyPanel(dragged_item) - # self.world.property_panel._syncEffectiveVisibility(dragged_node) - - print(f"dragged_node: {dragged_node}, target_node: {target_node}") - - # 记录拖拽前的父节点 - old_parent_item = dragged_item.parent() - old_parent_node = old_parent_item.data(0, Qt.UserRole) if old_parent_item else None - - # 执行Qt默认拖拽 + # 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) - world_scale = dragged_node.getScale(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 - # 检查是否是2D GUI元素 - dragged_type = dragged_item.data(0, Qt.UserRole + 1) - is_2d_gui = dragged_type in {"GUI_BUTTON", "GUI_LABEL", "GUI_ENTRY", "GUI_NODE"} + # 仅当父节点实际发生变化时才执行重新父化 + if old_parent_node != new_parent_node: + print(f"跨层级拖拽:从 {old_parent_node} 移动到 {new_parent_node}") - # 重新父化到新的父节点 - 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) + # # 保存世界坐标位置 + # 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) - 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) + # 如果新父节点为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) + # # 恢复世界坐标位置(对于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: - print(f"同层级移动:父节点未变化,跳过Panda3D重新父化") + print(f"✅ Panda3D父子关系已更新") + else: + print(f"同层级移动:父节点未变化,跳过Panda3D重新父化") except Exception as e: print(f"⚠️ 同步Panda3D场景图失败: {e}") @@ -1505,28 +1592,6 @@ class CustomTreeWidget(QTreeWidget): self.world.property_panel._syncEffectiveVisibility(dragged_node) event.accept() - # try: - # world_pos = dragged_node.getPos(self.world.render) - # - # parent_of_dragged = dragged_node.getParent() - # target_node.wrtReparentTo(parent_of_dragged) - # - # # 拖动节点到目标节点下 - # dragged_node.wrtReparentTo(target_node) - # dragged_node.setPos(self.world.render, world_pos) - # - # # 更新 Qt 树控件 - # super().dropEvent(event) - # - # # 更新属性面板 - # self.world.updatePropertyPanel(dragged_item) - # - # event.accept() - # - # except Exception as e: - # print(f"重设父节点失败: {e}") - # event.ignore() - def _ensureUnderSceneRoot(self, item): """确保节点在场景根节点下,如果不是则自动修正""" if not item: @@ -1595,28 +1660,13 @@ class CustomTreeWidget(QTreeWidget): target_type = target_item.data(0, Qt.UserRole + 1) # 定义2D GUI元素类型 - gui_2d_types = { - "GUI_BUTTON", # DirectButton - "GUI_LABEL", # DirectLabel - "GUI_ENTRY", # DirectEntry - "GUI_NODE" # 其他2D GUI容器 - } + gui_2d_types = self.gui_2d_types # 定义3D GUI元素类型 - gui_3d_types = { - "GUI_3DTEXT", # 3D TextNode - "GUI_VIRTUAL_SCREEN" # Virtual Screen - } + gui_3d_types = self.gui_3d_types # 定义3D场景节点类型(可以接受3D GUI元素和其他3D场景元素) - scene_3d_types = { - "SCENE_ROOT", - "SCENE_NODE", - "LIGHT_NODE", - "CAMERA_NODE", - "IMPORTED_MODEL_NODE", - "MODEL_NODE" - } + scene_3d_types = self.scene_3d_types # 检查拖拽元素的类型 is_dragged_2d_gui = dragged_type in gui_2d_types @@ -1704,24 +1754,6 @@ class CustomTreeWidget(QTreeWidget): # 出错时采用保守策略,禁止拖拽 return False - def _getNodeTypeDescription(self, node_type): - """获取节点类型的描述文本(用于错误提示)""" - type_descriptions = { - "GUI_BUTTON": "2D按钮", - "GUI_LABEL": "2D标签", - "GUI_ENTRY": "2D输入框", - "GUI_NODE": "2D GUI容器", - "GUI_3DTEXT": "3D文本", - "GUI_VIRTUAL_SCREEN": "虚拟屏幕", - "SCENE_ROOT": "场景根节点", - "SCENE_NODE": "场景节点", - "LIGHT_NODE": "灯光节点", - "CAMERA_NODE": "相机节点", - "IMPORTED_MODEL_NODE": "导入模型", - "MODEL_NODE": "模型节点" - } - return type_descriptions.get(node_type, f"未知类型({node_type})") - def _getRootNode(self, item): """获取树中节点的根节点项""" if not item: @@ -1842,18 +1874,45 @@ class CustomTreeWidget(QTreeWidget): if hasattr(panda_node, 'getPythonTag'): light_object = panda_node.getPythonTag('rp_light_object') if light_object and hasattr(self.world, 'render_pipeline'): - self.world.render_pipeline.remove_light(light_object) + try: + self.world.render_pipeline.remove_light(light_object) + print(f"移除灯光{panda_node.getName()}") + except Exception as e: + print(f"移除灯光失败: {str(e)}") + panda_node.clearPythonTag('rp_light_object') + #self.world.render_pipeline.remove_light(light_object) + + if hasattr(self.world,'gui_manager') and hasattr(self.world.gui_manager,'gui_elements'): + if panda_node in self.world.gui_manager.gui_elements: + self.world.gui_manager.gui_elements.remove(panda_node) + print(f"从gui_elements列表中移除{panda_node.getName()}") # 从world列表中移除 if hasattr(self.world, 'models') and panda_node in self.world.models: self.world.models.remove(panda_node) - if hasattr(self.world, 'Spotlight') and panda_node in self.world.Spotlight: - self.world.Spotlight.remove(panda_node) - if hasattr(self.world, 'Pointlight') and panda_node in self.world.Pointlight: - self.world.Pointlight.remove(panda_node) + + # if hasattr(self.world, 'Spotlight') and panda_node in self.world.Spotlight: + # self.world.Spotlight.remove(panda_node) + + if hasattr(self.world,'Spotlight'): + self.world.Spotlight = [light for light in self.world.Spotlight if light != panda_node] + if panda_node in self.world.Spotlight: + print(f"从Spotlight列表中移除{panda_node.getName()}") + + # if hasattr(self.world, 'Pointlight') and panda_node in self.world.Pointlight: + # self.world.Pointlight.remove(panda_node) + + if hasattr(self.world,'Pointlight'): + self.world.Pointlight = [light for light in self.world.Pointlight if light != panda_node] + if panda_node in self.world.Pointlight: + print(f"从Pointlight列表中移除{panda_node.getName()}") # 从Panda3D场景中移除 - panda_node.removeNode() + try: + if not panda_node.isEmpty(): + panda_node.removeNode() + except Exception as e: + print(f"❌ 删除节点 {item.text(0)} 失败: {str(e)}") # 从Qt树中移除 parent_item = item.parent() @@ -1869,6 +1928,8 @@ class CustomTreeWidget(QTreeWidget): except Exception as e: print(f"❌ 删除节点 {item.text(0)} 失败: {str(e)}") + import traceback + traceback.print_exc() # 最终清理 # if hasattr(self.world, 'property_panel'): @@ -1912,24 +1973,30 @@ class CustomTreeWidget(QTreeWidget): def should_skip(node): name = node.getName() - return name in BLACK_LIST or name.startswith('__') or isinstance(node.node(),CollisionNode) or isinstance(node.node(),ModelRoot) or name=="" + return name in BLACK_LIST or name.startswith('__') or isinstance(node.node(), CollisionNode) or isinstance( + node.node(), ModelRoot) or name == "" - def addNodeToTree(node,parentItem,force = False): + def addNodeToTree(node, parentItem, force=False): if not force and should_skip(node): - return - nodeItem = QTreeWidgetItem(parentItem,[node.getName()]) - nodeItem.setData(0,Qt.UserRole, node) + return None + nodeItem = QTreeWidgetItem(parentItem, [node.getName()]) + nodeItem.setData(0, Qt.UserRole, node) nodeItem.setData(0, Qt.UserRole + 1, node_type) for child in node.getChildren(): - addNodeToTree(child,nodeItem,force = False) + addNodeToTree(child, nodeItem, force=False) + return nodeItem try: from PyQt5.QtWidgets import QTreeWidgetItem from PyQt5.QtCore import Qt + + # 初始化new_qt_item变量 + new_qt_item = None + if node_type == "IMPORTED_MODEL_NODE": node_name = node.getTag("file") if hasattr(node, 'getTag') else "model" - addNodeToTree(node, parent_item, force=True) + new_qt_item = addNodeToTree(node, parent_item, force=True) else: node_name = node.getName() if hasattr(node, 'getName') else "node" new_qt_item = QTreeWidgetItem(parent_item, [node_name]) @@ -2018,10 +2085,7 @@ class CustomTreeWidget(QTreeWidget): node_type = item.data(0, Qt.UserRole + 1) # 场景根节点和普通场景节点可以作为父节点 - if node_type in ["SCENE_ROOT", - "SCENE_NODE", "LIGHT_NODE", "CAMERA_NODE", - "IMPORTED_MODEL_NODE", - "GUI_3DTEXT"]: + if node_type in self.gui_3d_types and self.scene_3d_types: return True # # 模型节点也可以作为父节点 @@ -2095,7 +2159,7 @@ class CustomTreeWidget(QTreeWidget): node_type = item.data(0, Qt.UserRole + 1) # GUI元素可以作为其他GUI元素的父节点 - if node_type in ["GUI_BUTTON", "GUI_LABEL", "GUI_ENTRY", "GUI_NODE"]: + if node_type in self.gui_2d_types: return True # 场景根节点和普通场景节点也可以作为父节点 @@ -2111,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: