From 37b3cc30dc3d33133293364731ab83a2f8af4cad Mon Sep 17 00:00:00 2001 From: Hector <145347438+hudomn@users.noreply.github.com> Date: Sat, 28 Feb 2026 17:39:04 +0800 Subject: [PATCH] =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E5=AF=BC=E5=85=A5=E6=88=90?= =?UTF-8?q?=E9=BB=91=E8=89=B2=E9=97=AE=E9=A2=98=E4=BF=AE=E5=A4=8D=E3=80=82?= =?UTF-8?q?=E6=89=93=E5=BC=80=E9=A1=B9=E7=9B=AE=E4=BD=BF=E7=94=A8SSBO?= =?UTF-8?q?=E5=AF=BC=E5=85=A5=E6=A8=A1=E5=9E=8B=E9=80=BB=E8=BE=91=E6=B5=81?= =?UTF-8?q?=E7=95=85=E8=BF=90=E8=A1=8C=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- EG | 1 - core/InfoPanelManager.py | 1715 ----------- core/resource_manager.py | 6 + core/selection.py | 34 + imgui.ini | 10 +- main.py | 4 +- project/project_manager.py | 1 - scene/scene_manager_io_mixin.py | 247 +- scene/scene_manager_model_mixin.py | 92 +- scene/util.py | 35 +- ssbo_component/ssbo_editor.py | 621 +++- templates/main_template.py | 5 - ui/panels/animation_tools.py | 13 +- ui/panels/app_actions.py | 46 +- ui/panels/editor_panels.py | 5 +- ui/panels/object_factory.py | 45 - ui/widgets.py | 4319 ---------------------------- 17 files changed, 955 insertions(+), 6244 deletions(-) delete mode 160000 EG delete mode 100644 core/InfoPanelManager.py delete mode 100644 ui/widgets.py diff --git a/EG b/EG deleted file mode 160000 index 69e2bda4..00000000 --- a/EG +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 69e2bda47e9713705ad5c45a08b6fc643a2b51f6 diff --git a/core/InfoPanelManager.py b/core/InfoPanelManager.py deleted file mode 100644 index 68ea363f..00000000 --- a/core/InfoPanelManager.py +++ /dev/null @@ -1,1715 +0,0 @@ -# 修改后的 InfoPanelManager.py -from xml.sax.handler import property_encoding - -from PyQt5.QtCore import Qt -from PyQt5.QtWidgets import QMessageBox -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"{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=0, # 设置一个非常大的值,几乎不自动换行 - 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位置编辑 - panel_node.setTag("is_gui_element",'1') - panel_node.setTag("tree_item_type","INFO_PANEL") - panel_node.setTag("supports_3d_position_editing","1") - # 如果有背景图片,保存背景图片路径 - if bg_image: - panel_node.setTag("image_path", bg_image) - - - if not visible: - panel_node.hide() - - # 将面板添加到场景树 - #self._addPanelToSceneTree(panel_node, panel_id) - if hasattr(self.world, 'gui_elements'): - self.world.gui_elements.append(panel_node) - - return panel_node - - def _addPanelToSceneTree(self, panel_node, panel_id): - """ - 将信息面板添加到场景树中 - """ - try: - # 获取树形控件 - if hasattr(self.world, 'interface_manager') and hasattr(self.world.interface_manager, 'treeWidget'): - tree_widget = self.world.interface_manager.treeWidget - 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: - # 使用现有的 add_node_to_tree_widget 方法添加节点 - qt_item = tree_widget.add_node_to_tree_widget(panel_node, root_item, "INFO_PANEL") - if qt_item: - print(f"✅ 信息面板 {panel_id} 已添加到场景树") - # 选中创建的节点 - tree_widget.setCurrentItem(qt_item) - # 更新选择和属性面板 - tree_widget.update_selection_and_properties(panel_node, qt_item) - else: - print(f"⚠️ 信息面板 {panel_id} 添加到场景树失败") - else: - print("⚠️ 未找到场景树根节点,无法添加信息面板") - else: - print("⚠️ 无法访问场景树控件,信息面板未添加到场景树") - except Exception as e: - print(f"❌ 添加信息面板到场景树时出错: {e}") - import traceback - traceback.print_exc() - - 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 - - # 更新 registerDataSource 方法以更好地处理不同类型面板 - def registerDataSource(self, panel_id, data_callback, update_interval=1.0): - """ - 注册数据源,定期更新面板内容 - 改进版 - """ - 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, - 'panel_type': '3d' if self._is3DPanel(panel_id) else '2d' if self._is2DPanel(panel_id) else 'unknown' - } - - self.data_sources[panel_id] = data_source - - # 启动数据更新线程 - thread = threading.Thread(target=self._updateDataThread, args=(panel_id,), daemon=True) - thread.start() - - return True - - # 在 InfoPanelManager 类中修复 _updateDataThread 方法 - 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: - panel_type = self.data_sources[panel_id].get('panel_type', 'unknown') - - if panel_type == '2d': - self.updatePanelContent(panel_id, content=data) - elif panel_type == '3d': - self.update3DPanelContent(panel_id, content=data) - else: - # 尝试自动检测 - if self._is2DPanel(panel_id): - self.updatePanelContent(panel_id, content=data) - elif self._is3DPanel(panel_id): - self.update3DPanelContent(panel_id, content=data) - - # 等待下次更新 - interval = self.data_sources[panel_id]['interval'] - time.sleep(interval) - - except Exception as e: - print(f"更新面板 {panel_id} 数据时出错: {e}") - import traceback - traceback.print_exc() - time.sleep(1.0) # 出错时等待1秒再重试 - - # 在 InfoPanelManager 类中添加以下方法 - def _is3DPanel(self, panel_id): - """ - 判断面板是否为3D面板 - """ - if panel_id not in self.panels: - return False - panel_data = self.panels[panel_id] - return 'content_node' in panel_data and 'content_label' not in panel_data - - def _is2DPanel(self, panel_id): - """ - 判断面板是否为2D面板 - """ - if panel_id not in self.panels: - return False - panel_data = self.panels[panel_id] - return 'content_label' in panel_data and 'content_node' not in panel_data - - 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'] = 0 - - # 如果有背景图片,也需要更新其大小 - 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'] - current_size = props.get('size',(1.0,0.6)) - # 当字体大小改变时,仍然保持较大的换行值 - panel_data['content_label']['text_wordwrap'] = 0 - - # 更新背景图片 - 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 - - def create3DInfoPanel(self, panel_id, position=(0, 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): - """ - 创建简化版3D信息面板 - 只显示文字,无面板背景,避免闪烁 - """ - # 如果面板已存在,先移除它 - if panel_id in self.panels: - self.removePanel(panel_id) - - # 确保父节点存在 - parent_node = self.parent if self.parent else self.world.render - - # 根据面板ID确定标题和内容 - title, content = self._getPanelContent(panel_id) - - # 创建主节点,便于统一管理 - panel_node = parent_node.attachNewNode(f"{panel_id}") - panel_node.setPos(position[0], position[1], position[2]) - - # 直接创建文字节点,不创建面板背景和边框 - from panda3d.core import TextNode - - # 创建标题文本 - title_text_node = TextNode(f'title_{panel_id}') - title_text_node.setText(title) - title_text_node.setTextColor(*title_color) - title_text_node.setAlign(TextNode.ACenter) - if font: - title_text_node.setFont(font) - - title_text = panel_node.attachNewNode(title_text_node) - title_text.setScale(0.06) - title_text.setPos(0, 0, size[1] / 4) # 将标题放在上方 - - # 创建内容文本 - content_text_node = TextNode(f'content_{panel_id}') - content_text_node.setText(content) - content_text_node.setTextColor(*content_color) - content_text_node.setAlign(TextNode.ALeft) - content_text_node.setWordwrap(size[0] * 2) # 根据面板宽度设置换行 - if font: - content_text_node.setFont(font) - - content_text = panel_node.attachNewNode(content_text_node) - content_text.setScale(0.045) - content_text.setPos(-size[0] / 2, 0, size[1] / 4 - 0.1) # 将内容放在标题下方 - - # 保存引用 - self.panels[panel_id] = { - 'node': panel_node, - 'title_text': title_text, - 'content_text': content_text, - 'title_node': title_text_node, - 'content_node': content_text_node, - 'properties': { - 'size': size, - 'position': position, - 'title_color': title_color, - 'content_color': content_color, - 'font': font - } - } - - # 设置GUI类型标记和支持3D编辑的标记 - panel_node.setTag("gui_type", "info_panel_3d") - panel_node.setTag("panel_id", panel_id) - panel_node.setTag("is_gui_element", "1") # 添加此标记确保节点被识别为GUI元素 - panel_node.setTag("is_scene_element", "1") # 添加此标记确保节点被识别为场景元素 - panel_node.setTag("supports_3d_position_editing", "1") # 支持3D位置编辑 - panel_node.setTag("tree_item_type", "INFO_PANEL_3D") # 添加树节点类型标记 - - # 如果有背景图片,保存背景图片路径 - if bg_image: - panel_node.setTag("bg_image_path", bg_image) - - if not visible: - panel_node.hide() - - # 将面板添加到场景树 - #self._addPanelToSceneTree(panel_node, panel_id) - if hasattr(self.world, 'gui_elements'): - self.world.gui_elements.append(panel_node) - - return panel_node - - def update3DPanelContent(self, panel_id, title=None, content=None): - """ - 更新3D面板内容 - """ - 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 'title_node' in panel_data: - panel_data['title_node'].setText(title) - - if content is not None and 'content_node' in panel_data: - panel_data['content_node'].setText(content) - - return True - - def update3DPanelProperties(self, panel_id, **properties): - """ - 更新3D面板属性 - """ - 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], pos[1], pos[2]) - props['position'] = pos - - # 更新大小 - if 'size' in properties: - size = properties['size'] - props['size'] = size - - # 由于3D面板使用CardMaker创建,需要重新创建几何体 - print("注意:3D面板大小调整需要重新创建面板几何体") - - # 更新背景颜色 - if 'bg_color' in properties: - bg_color = properties['bg_color'] - if 'panel_bg' in panel_data: - from panda3d.core import Material - material = Material() - material.setDiffuse(Vec4(*bg_color)) - material.setAmbient(Vec4(*bg_color[:3], 1.0)) - panel_data['panel_bg'].setMaterial(material, 1) - props['bg_color'] = bg_color - - # 更新边框颜色 - if 'border_color' in properties: - border_color = properties['border_color'] - from panda3d.core import Material - border_mat = Material() - border_mat.setDiffuse(Vec4(*border_color)) - for border in panel_data['borders'].values(): - border.setMaterial(border_mat, 1) - props['border_color'] = border_color - - # 更新标题颜色 - if 'title_color' in properties: - title_color = properties['title_color'] - if 'title_node' in panel_data: - panel_data['title_node'].setTextColor(*title_color) - props['title_color'] = title_color - - # 更新内容颜色 - if 'content_color' in properties: - content_color = properties['content_color'] - if 'content_node' in panel_data: - panel_data['content_node'].setTextColor(*content_color) - props['content_color'] = content_color - - # 更新标题 - if 'title' in properties: - if 'title_node' in panel_data: - panel_data['title_node'].setText(properties['title']) - - # 更新内容 - if 'content' in properties: - if 'content_node' in panel_data: - panel_data['content_node'].setText(properties['content']) - - # 更新字体大小 - if 'title_size' in properties: - if 'title_text' in panel_data: - panel_data['title_text'].setScale(properties['title_size']) - props['title_size'] = properties['title_size'] - - if 'content_size' in properties: - if 'content_text' in panel_data: - panel_data['content_text'].setScale(properties['content_size']) - props['content_size'] = properties['content_size'] - - return True - - def create3DHTTPInfoPanel(self, panel_id, url, method="GET", headers=None, data=None, - position=(0, 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): - """ - 创建3D HTTP信息面板 - """ - # 创建面板 - domain = urlparse(url).netloc or url[:30] - title = f"HTTP数据: {domain}" - - panel_node = self.create3DInfoPanel( - 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.update3DPanelContent(panel_id, title=title) - - # 立即获取并显示数据 - content = fetchHTTPData(url, method, headers, data) - self.update3DPanelContent(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 - - # 在 add_methods_to_property_panel 函数中添加以下方法 - - def add_methods_to_property_panel(property_panel_instance): - # ... (原有代码保持不变) - import types - - 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 - - def create3DRealtimeDataPanel(self, data_callback=None, update_interval=1.0): - """创建3D实时数据面板""" - try: - # 确保父节点已设置 - if self.info_panel_manager.parent is None and hasattr(self, 'world'): - self.info_panel_manager.setParent(self.world.render) - - # 创建3D实时数据面板 - panel_node = self.info_panel_manager.create3DInfoPanel( - panel_id="realtime_data_3d", - position=(0, 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_3d") - 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.update3DPanelContent("realtime_data_3d", content=initial_data) - - # 注册数据源 - self.info_panel_manager.registerDataSource("realtime_data_3d", data_callback, update_interval) - else: - # 使用默认数据 - default_data = "等待数据..." - self.info_panel_manager.update3DPanelContent("realtime_data_3d", content=default_data) - - print("✓ 已创建3D实时数据面板") - - return panel_node - - except Exception as e: - print(f"✗ 创建3D实时数据面板失败: {e}") - return None - - def create3DModelInfoPanel(self, model): - """为模型创建3D信息面板""" - 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.create3DInfoPanel( - panel_id="model_info_3d", - position=(2, 0, 0), # 默认放在模型旁边 - 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.update3DPanelContent("model_info_3d", - title="模型信息", - content=content) - - # 设置标签 - panel_node.setTag("element_type", "info_panel_3d") - panel_node.setTag("is_scene_element", "1") - panel_node.setTag("supports_3d_position_editing", "1") # 支持3D位置编辑 - - print(f"✓ 已创建3D模型信息面板: {model_name}") - - return panel_node - - except Exception as e: - print(f"✗ 创建3D模型信息面板失败: {e}") - return None - - # 在绑定方法的部分添加3D面板方法 - property_panel_instance.create3DRealtimeDataPanel = types.MethodType(create3DRealtimeDataPanel, - property_panel_instance) - property_panel_instance.create3DModelInfoPanel = types.MethodType(create3DModelInfoPanel, - property_panel_instance) - - # 将新方法绑定到实例 - # ... (原有绑定保持不变) - property_panel_instance.createHTTPInfoPanel = types.MethodType(createHTTPInfoPanel, property_panel_instance) - property_panel_instance.updateHTTPInfoPanel = types.MethodType(updateHTTPInfoPanel, property_panel_instance) - - def serializePanelData(self, panel_id): - """序列化面板数据用于保存""" - if panel_id not in self.panels: - return None - - panel_data = self.panels[panel_id] - props = panel_data['properties'] - - # 获取面板类型 - panel_type = "2d" - if panel_data['node'].hasTag("gui_type"): - gui_type = panel_data['node'].getTag("gui_type") - if "3d" in gui_type.lower(): - panel_type = "3d" - - # 构建序列化数据 - serialized_data = { - 'panel_id': panel_id, - 'panel_type': panel_type, - 'position': props.get('position', (0, 0) if panel_type == "2d" else (0, 0, 0)), - 'size': props.get('size', (1.0, 0.6)), - 'bg_color': props.get('bg_color', (0.15, 0.15, 0.15, 0.9)), - 'border_color': props.get('border_color', (0.3, 0.3, 0.3, 1.0)), - 'title_color': props.get('title_color', (1.0, 1.0, 1.0, 1.0)), - 'content_color': props.get('content_color', (0.9, 0.9, 0.9, 1.0)), - 'title': panel_data['title_label'].getText() if 'title_label' in panel_data else "信息面板", - 'content': panel_data[ - 'content_label'].getText() if 'content_label' in panel_data else "" if 'content_node' not in panel_data else - panel_data['content_node'].getText(), - 'font_path': props.get('font', None), - 'bg_image': props.get('bg_image', None), - 'visible': not panel_data['node'].isHidden() - } - - # 添加HTTP面板特有数据 - if panel_id in self.data_sources and 'http_info' in self.data_sources[panel_id]: - serialized_data['http_info'] = self.data_sources[panel_id]['http_info'] - - return serialized_data - - def getAllPanelData(self): - """获取所有面板的序列化数据""" - panel_data_list = [] - for panel_id in self.panels: - data = self.serializePanelData(panel_id) - if data: - panel_data_list.append(data) - return panel_data_list - - def recreatePanelFromData(self, panel_data): - """从序列化数据重新创建面板""" - try: - panel_id = panel_data['panel_id'] - panel_type = panel_data['panel_type'] - position = panel_data['position'] - size = panel_data['size'] - - # 重建面板 - if panel_type == "3d": - panel_node = self.create3DInfoPanel( - panel_id=panel_id, - position=position, - size=size, - bg_color=panel_data.get('bg_color', (0.15, 0.15, 0.15, 0.9)), - border_color=panel_data.get('border_color', (0.3, 0.3, 0.3, 1.0)), - title_color=panel_data.get('title_color', (1.0, 1.0, 1.0, 1.0)), - content_color=panel_data.get('content_color', (0.9, 0.9, 0.9, 1.0)), - visible=panel_data.get('visible', True), - font=panel_data.get('font_path', None), - bg_image=panel_data.get('bg_image', None) - ) - - # 更新内容 - self.update3DPanelContent( - panel_id, - title=panel_data.get('title', '信息面板'), - content=panel_data.get('content', '') - ) - else: - panel_node = self.createInfoPanel( - panel_id=panel_id, - position=(position[0], position[1]) if len(position) >= 2 else (0, 0), - size=size, - bg_color=panel_data.get('bg_color', (0.15, 0.15, 0.15, 0.9)), - border_color=panel_data.get('border_color', (0.3, 0.3, 0.3, 1.0)), - title_color=panel_data.get('title_color', (1.0, 1.0, 1.0, 1.0)), - content_color=panel_data.get('content_color', (0.9, 0.9, 0.9, 1.0)), - visible=panel_data.get('visible', True), - font=panel_data.get('font_path', None), - bg_image=panel_data.get('bg_image', None) - ) - - # 更新内容 - self.updatePanelContent( - panel_id, - title=panel_data.get('title', '信息面板'), - content=panel_data.get('content', '') - ) - - # 设置标签 - if panel_node: - panel_node.setTag("element_type", "info_panel") - panel_node.setTag("is_scene_element", "1") - panel_node.setTag("supports_3d_position_editing", "1") - - # 如果是HTTP面板,重新注册数据源 - if 'http_info' in panel_data: - http_info = panel_data['http_info'] - self.createHTTPInfoPanel( - panel_id=panel_id, - url=http_info['url'], - method=http_info.get('method', 'GET'), - headers=http_info.get('headers', None), - data=http_info.get('data', None), - position=position, - size=size, - update_interval=self.data_sources.get(panel_id, {}).get('interval', - 30.0) if panel_id in self.data_sources else 30.0 - ) - - print(f"✓ 信息面板 {panel_id} 已重建") - return panel_node - - except Exception as e: - print(f"✗ 重建信息面板 {panel_data.get('panel_id', 'unknown')} 失败: {e}") - import traceback - traceback.print_exc() - return None - - def onCreateSampleInfoPanel(self): - """创建示例天气信息面板(模拟数据)""" - try: - # 获取中文字体 - from panda3d.core import TextNode - font = self.world.getChineseFont() if self.world.getChineseFont() else None - - # 使用唯一的面板ID - import time - unique_id = f"weather_info_{int(time.time())}" - - # 创建示例面板 - weather_panel = self.createInfoPanel( - panel_id=unique_id, # 使用唯一ID - position=(1.32, 0.68), - size=(1, 0.6), - 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, - bg_image="/home/tiger/图片/内部信息框2@2x.png" - ) - weather_panel.setTag("name",unique_id) - - # 更新面板标题 - self.updatePanelContent(unique_id, title="北京天气") - - self._addPanelToSceneTree(weather_panel, unique_id) - # 立即显示加载中信息 - self.updatePanelContent(unique_id, content="正在获取天气数据...") - - self.registerDataSource(unique_id, self.getRealWeatherData, update_interval=5.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://api.open-meteo.com/v1/forecast?latitude=39.9042&longitude=116.4074¤t=temperature_2m,apparent_temperature,relative_humidity_2m,surface_pressure,wind_speed_10m,wind_direction_10m,weather_code&timezone=Asia%2FShanghai" - response = requests.get(url, timeout=10) - response.raise_for_status() - - # 解析JSON数据 - weather_data = response.json() - - current_condition = weather_data.get('current', {}) - if not current_condition: - return "错误: 天气数据格式不正确 (缺少 current 字段)" - - weather_code = current_condition.get('weather_code') - weather_desc_map = { - 0: "晴朗", - 1: "大部晴朗", - 2: "局部多云", - 3: "阴天", - 45: "雾", - 48: "冻雾", - 51: "毛毛雨", - 53: "小雨", - 55: "中雨", - 61: "小雨", - 63: "中雨", - 65: "大雨", - 71: "小雪", - 73: "中雪", - 75: "大雪", - 80: "阵雨", - 81: "较强阵雨", - 82: "强阵雨", - 95: "雷暴", - 96: "雷暴夹小冰雹", - 99: "雷暴夹大冰雹", - } - weather_desc = weather_desc_map.get(weather_code, f"天气代码 {weather_code}") - - temp_c = current_condition.get('temperature_2m', 'N/A') - feels_like = current_condition.get('apparent_temperature', 'N/A') - humidity = current_condition.get('relative_humidity_2m', 'N/A') - pressure = current_condition.get('surface_pressure', 'N/A') - wind_speed = current_condition.get('wind_speed_10m', 'N/A') - wind_dir = current_condition.get('wind_direction_10m', 'N/A') - visibility = "N/A" - air_quality = "N/A" - - update_time = current_condition.get('time') - if not update_time: - 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}\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 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 信息面板支持 - """ - import types - - # 添加信息面板管理器作为类属性 - 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/resource_manager.py b/core/resource_manager.py index 257f5fb5..5d9df317 100644 --- a/core/resource_manager.py +++ b/core/resource_manager.py @@ -389,6 +389,12 @@ class ResourceManager: shutil.copytree(src, dst) else: shutil.copy2(src, dst) + if src.suffix.lower() == '.fbx': + fbm_src = src.with_name(src.stem + '.fbm') + if fbm_src.exists() and fbm_src.is_dir(): + fbm_dst = destination_root / fbm_src.name + if not fbm_dst.exists(): + shutil.copytree(fbm_src, fbm_dst) imported.append(dst) except Exception as e: errors.append(f"导入失败 {src}: {e}") diff --git a/core/selection.py b/core/selection.py index b6586f76..a13df628 100644 --- a/core/selection.py +++ b/core/selection.py @@ -2198,6 +2198,40 @@ class SelectionSystem: """获取当前选中的节点""" return self.selectedNode + def deleteSelectedNode(self): + """兼容旧接口:删除当前选中节点。""" + node = self.selectedNode + if not node or node.isEmpty(): + return False + + try: + if node.getName() == "render": + return False + except Exception: + return False + + self._deleting_node = True + try: + # 优先走应用层统一删除链路(命令系统/SSBO清理/消息等)。 + if hasattr(self.world, "_delete_node") and callable(self.world._delete_node): + deleted = bool(self.world._delete_node(node)) + else: + deleted = False + scene_manager = getattr(self.world, "scene_manager", None) + if scene_manager and hasattr(scene_manager, "models") and node in scene_manager.models: + scene_manager.models.remove(node) + try: + node.removeNode() + deleted = True + except Exception: + deleted = False + + if deleted: + self.clearSelection() + return deleted + finally: + self._deleting_node = False + def hasSelection(self): """检查是否有选中的节点""" return self.selectedNode is not None diff --git a/imgui.ini b/imgui.ini index 8e1aeaf6..34f02042 100644 --- a/imgui.ini +++ b/imgui.ini @@ -48,8 +48,8 @@ Collapsed=0 DockId=0x00000008,0 [Window][脚本管理] -Pos=1540,20 -Size=380,390 +Pos=1653,20 +Size=267,390 Collapsed=0 DockId=0x00000003,1 @@ -84,7 +84,7 @@ Size=400,300 Collapsed=0 [Window][选择路径] -Pos=660,245 +Pos=660,254 Size=600,500 Collapsed=0 @@ -150,8 +150,8 @@ Size=101,226 Collapsed=0 [Window][LUI编辑器] -Pos=1628,412 -Size=292,597 +Pos=1653,412 +Size=267,597 Collapsed=0 DockId=0x00000004,0 diff --git a/main.py b/main.py index 6a0bc305..d656757d 100644 --- a/main.py +++ b/main.py @@ -26,7 +26,6 @@ from core.Command_System import CommandManager from core.terrain_manager import TerrainManager from scene.scene_manager import SceneManager from project.project_manager import ProjectManager -from core.InfoPanelManager import InfoPanelManager from core.collision_manager import CollisionManager from core.CustomMouseController import CustomMouseController from core.resource_manager import ResourceManager @@ -78,6 +77,7 @@ class MyWorld(PanelDelegates, CoreWorld): self.accept("F", self.onFocusKeyPressed) # 大写F self.use_ssbo_mouse_picking = True + self.use_ssbo_scene_import = True if not self.use_ssbo_mouse_picking: self.accept("mouse1", self.onMouseClick) # Keep release/move bindings even in SSBO mode so gizmo drag can work. @@ -117,8 +117,6 @@ class MyWorld(PanelDelegates, CoreWorld): self.project_manager = ProjectManager(self) # print(f"[DEBUG] 项目管理系统初始化完成") - self.info_panel_manager = InfoPanelManager(self) - self.command_manager = CommandManager() # 初始化碰撞管理器 diff --git a/project/project_manager.py b/project/project_manager.py index d10bf13b..44561355 100644 --- a/project/project_manager.py +++ b/project/project_manager.py @@ -478,7 +478,6 @@ class ProjectManager: def _copyScriptSystemToBuild(self,build_dir): core_files = [ "script_system.py", - "InfoPanelManager.py", "CustomMouseController.py" ] diff --git a/scene/scene_manager_io_mixin.py b/scene/scene_manager_io_mixin.py index 20c96fe0..f6ebb7b9 100644 --- a/scene/scene_manager_io_mixin.py +++ b/scene/scene_manager_io_mixin.py @@ -74,8 +74,8 @@ class SceneManagerIOMixin: gui_info["parent_name"] = parent_name # 收集所有标签(仅对NodePath类型的对象) - if hasattr(gui_node, 'getTagNames'): - for tag in gui_node.getTagNames(): + if hasattr(gui_node, 'getTagKeys'): + for tag in gui_node.getTagKeys(): gui_info["tags"][tag] = gui_node.getTag(tag) elif hasattr(gui_node, 'getTags'): # 对于DirectGUI对象 # DirectGUI对象使用不同的方法存储标签 @@ -219,22 +219,6 @@ class SceneManagerIOMixin: all_nodes.extend(self.Spotlight) all_nodes.extend(self.Pointlight) - # 添加GUI元素节点 - gui_elements = [] - if hasattr(self.world, 'gui_elements'): - # 过滤掉空的或重复的GUI元素 - unique_gui_elements = [] - seen_names = set() - - for elem in self.world.gui_elements: - if elem and not elem.isEmpty(): - if not elem.isEmpty() and elem.getName() not in seen_names: - unique_gui_elements.append(elem) - seen_names.add(elem.getName()) - gui_elements = unique_gui_elements - - print(f"保存时GUI元素列表=>>>>>>>>>>>>{self.world.gui_elements}") - all_nodes.extend(gui_elements) # 创建用于保存GUI信息的JSON文件路径 gui_info_file = filename.replace('.bam', '_gui.json') @@ -321,15 +305,6 @@ class SceneManagerIOMixin: node.setTag(f"python_tag_{tag_name}", str(tag_value)) except: pass - elif node.hasTag("element_type") and node.getTag("element_type") == "info_panel": - # 保存信息面板特定信息 - print(f"保存信息面板信息: {node.getName()}") - panel_id = node.getTag("panel_id") if node.hasTag("panel_id") else node.getName() - if hasattr(self.world, 'info_panel_manager'): - panel_data = self.world.info_panel_manager.serializePanelData(panel_id) - if panel_data: - import json - node.setTag("info_panel_data", json.dumps(panel_data, ensure_ascii=False)) # 保存模型动画信息 elif node.hasTag("is_model_root"): @@ -448,11 +423,7 @@ class SceneManagerIOMixin: max_retries = 3 try: print(f"\n=== 开始加载场景: {filename} (尝试 {retry_count + 1}/{max_retries + 1}) ===") - # print(f"[DEBUG] loadScene 被调用,参数: {filename}") - # print(f"[DEBUG] 当前场景中的模型数量: {len(self.models)}") - # print(f"[DEBUG] 当前场景中的灯光数量: {len(self.Spotlight) + len(self.Pointlight)}") - # print(f"[DEBUG] 当前GUI元素数量: {len(self.world.gui_elements) if hasattr(self.world, 'gui_elements') else 0}") - + # 确保文件路径是规范化的 filename = os.path.normpath(filename) # print(f"[DEBUG] 规范化后的文件路径: {filename}") @@ -553,15 +524,6 @@ class SceneManagerIOMixin: if not light.isEmpty(): light.removeNode() self.Pointlight.clear() - - for gui in self.world.gui_elements: - if not gui.isEmpty(): - gui.removeNode() - self.world.gui_elements.clear() - - if hasattr(self.world,'info_panel_manager'): - self.world.info_panel_manager.removeAllPanels() - # 清理可能存在的辅助节点 self._cleanupAuxiliaryNodes() @@ -587,18 +549,55 @@ class SceneManagerIOMixin: # 4. 使用更安全的加载方式 # print(f"[DEBUG] 尝试加载BAM文件...") - panda_filename = Filename.fromOsSpecific(filename) - # print(f"[DEBUG] Panda3D文件名: {panda_filename}") - # 设置加载选项 from panda3d.core import LoaderOptions loader_options = LoaderOptions() loader_options.setFlags(loader_options.LFNoCache) # 禁用缓存 - - scene = self.world.loader.loadModel(panda_filename, loader_options) - - if not scene: - print("场景加载失败") + + # 兼容中文路径:优先使用宽字符接口,再回退常规接口。 + candidate_files = [] + for ctor_name in ("fromOsSpecificW", "from_os_specific_w", "fromOsSpecific", "from_os_specific"): + ctor = getattr(Filename, ctor_name, None) + if not ctor: + continue + try: + fn = ctor(filename) + key = fn.get_fullpath() + if key not in [c.get_fullpath() for c in candidate_files]: + candidate_files.append(fn) + except Exception: + continue + if not candidate_files: + candidate_files = [Filename(filename)] + + scene = None + last_error = None + for panda_filename in candidate_files: + try: + scene = self.world.loader.loadModel(panda_filename, loader_options) + if scene and not scene.isEmpty(): + break + except Exception as e: + last_error = e + scene = None + + # 极端回退:把场景复制到ASCII路径再加载(规避少数编码问题) + if (not scene or scene.isEmpty()) and os.path.exists(filename): + try: + fallback_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "_scene_load_cache") + os.makedirs(fallback_dir, exist_ok=True) + fallback_file = os.path.join(fallback_dir, "scene_fallback.bam") + shutil.copy2(filename, fallback_file) + scene = self.world.loader.loadModel(Filename.fromOsSpecific(fallback_file), loader_options) + if scene and not scene.isEmpty(): + print("[SceneLoad] Fallback loaded from ASCII cache path.") + except Exception as e: + last_error = e + + if not scene or scene.isEmpty(): + print(f"场景加载失败: {filename}") + if last_error: + print(f"[SceneLoad] 最后错误: {last_error}") return False # print(f"[DEBUG] BAM文件加载成功") @@ -640,6 +639,26 @@ class SceneManagerIOMixin: #存储所有加载的节点,用于后续处理父子关系 loaded_nodes = {} #name->nodePath映射 + use_ssbo_scene_import = bool( + getattr(self.world, "use_ssbo_mouse_picking", False) and + getattr(self.world, "use_ssbo_scene_import", False) and + getattr(self.world, "ssbo_editor", None) and + callable(getattr(self.world, "_import_model_for_runtime", None)) + ) + ssbo_scene_model = None + if use_ssbo_scene_import: + try: + print(f"[SSBO] 打开项目使用统一导入链路: {filename}") + ssbo_scene_model = self.world._import_model_for_runtime(filename) + if ssbo_scene_model and not ssbo_scene_model.isEmpty(): + if ssbo_scene_model not in self.models: + self.models.append(ssbo_scene_model) + else: + print("[SSBO] 统一导入未返回有效模型,回退旧流程。") + use_ssbo_scene_import = False + except Exception as e: + print(f"[SSBO] 统一导入失败,回退旧流程: {e}") + use_ssbo_scene_import = False # 遍历场景中的所有节点 def processNode(nodePath, depth=0): @@ -700,6 +719,7 @@ class SceneManagerIOMixin: if is_scene_element or is_potential_gui: print(f"{indent}找到场景元素节点: {nodePath.getName()}") + is_model_root = nodePath.hasTag("is_model_root") # 如果是潜在的GUI元素但没有标签,添加基本标签 if is_potential_gui and not (nodePath.hasTag("gui_type") or nodePath.hasTag("is_gui_element")): @@ -721,9 +741,7 @@ class SceneManagerIOMixin: else: nodePath.setTag("gui_type", "unknown") - # 清除现有材质状态 - nodePath.clearMaterial() - nodePath.clearColor() + # 保留原始材质/颜色状态,避免在场景恢复阶段造成黑模。 # 恢复变换信息 def parseVec3(vec_str): @@ -769,7 +787,11 @@ class SceneManagerIOMixin: else: nodePath.hide() - if nodePath.hasTag("has_scripts") and nodePath.getTag("has_scripts") == "true": + if ( + nodePath.hasTag("has_scripts") + and nodePath.getTag("has_scripts") == "true" + and not (use_ssbo_scene_import and is_model_root) + ): if hasattr(self.world,'script_manager') and self.world.script_manager: try: import json @@ -824,40 +846,41 @@ class SceneManagerIOMixin: except: return Vec4(1, 1, 1, 1) - # 创建并恢复材质 - material = Material() - material_changed = False + if not is_model_root: + # 创建并恢复材质 + material = Material() + material_changed = False - if nodePath.hasTag("material_ambient"): - material.setAmbient(parseColor(nodePath.getTag("material_ambient"))) - material_changed = True + if nodePath.hasTag("material_ambient"): + material.setAmbient(parseColor(nodePath.getTag("material_ambient"))) + material_changed = True - if nodePath.hasTag("material_diffuse"): - material.setDiffuse(parseColor(nodePath.getTag("material_diffuse"))) - material_changed = True + if nodePath.hasTag("material_diffuse"): + material.setDiffuse(parseColor(nodePath.getTag("material_diffuse"))) + material_changed = True - if nodePath.hasTag("material_specular"): - material.setSpecular(parseColor(nodePath.getTag("material_specular"))) - material_changed = True + if nodePath.hasTag("material_specular"): + material.setSpecular(parseColor(nodePath.getTag("material_specular"))) + material_changed = True - if nodePath.hasTag("material_emission"): - material.setEmission(parseColor(nodePath.getTag("material_emission"))) - material_changed = True + if nodePath.hasTag("material_emission"): + material.setEmission(parseColor(nodePath.getTag("material_emission"))) + material_changed = True - if nodePath.hasTag("material_shininess"): - material.setShininess(float(nodePath.getTag("material_shininess"))) - material_changed = True + if nodePath.hasTag("material_shininess"): + material.setShininess(float(nodePath.getTag("material_shininess"))) + material_changed = True - if nodePath.hasTag("material_basecolor"): - material.setBaseColor(parseColor(nodePath.getTag("material_basecolor"))) - material_changed = True + if nodePath.hasTag("material_basecolor"): + material.setBaseColor(parseColor(nodePath.getTag("material_basecolor"))) + material_changed = True - if material_changed: - nodePath.setMaterial(material) + if material_changed: + nodePath.setMaterial(material) - # 恢复颜色属性 - if nodePath.hasTag("color"): - nodePath.setColor(parseColor(nodePath.getTag("color"))) + # 恢复颜色属性 + if nodePath.hasTag("color"): + nodePath.setColor(parseColor(nodePath.getTag("color"))) # 处理特定类型的节点 if nodePath.hasTag("light_type"): @@ -893,32 +916,47 @@ class SceneManagerIOMixin: # 这里我们先保持原挂载关系 pass else: + if use_ssbo_scene_import and is_model_root: + # SSBO 模式下模型节点由新导入链路接管,这里不直接挂载。 + pass # 其他节点确保挂载到render下 - if nodePath.getParent() != self.world.render and not nodePath.getName() in ["render", - "aspect2d", - "render2d"]: + elif nodePath.getParent() != self.world.render and not nodePath.getName() in ["render", + "aspect2d", + "render2d"]: nodePath.wrtReparentTo(self.world.render) # 为模型节点设置碰撞检测 - if nodePath.hasTag("is_model_root"): + if is_model_root: print(f"J{indent}处理模型节点{nodePath.getName()}") - - #self._validateAndFixAllTransforms(nodePath) - - self._fixModelStructure(nodePath) - - # 恢复模型动画信息 - self._restoreModelAnimationInfo(nodePath) - - # 检测并处理模型动画(类似property_panel.py中的逻辑) - self._processModelAnimations(nodePath) - - # if self.world.property_panel._hasCollision(nodePath): - # print(f"{indent}模型{nodePath.getName()}已有碰撞体,跳过碰撞体设置") - # else: - # print(f"{indent}为模型{nodePath.getName()}设置碰撞检测") - # self.setupCollision(nodePath) - self.models.append(nodePath) + if use_ssbo_scene_import: + # SSBO 模式下整个 scene.bam 已通过统一导入链路载入, + # 这里跳过逐模型旧导入逻辑,避免与菜单导入路径不一致。 + pass + else: + #self._validateAndFixAllTransforms(nodePath) + self._fixModelStructure(nodePath) + self._restoreModelAnimationInfo(nodePath) + self._processModelAnimations(nodePath) + try: + ssbo_editor = getattr(self.world, "ssbo_editor", None) + repair_fn = getattr(ssbo_editor, "_repair_missing_textures", None) if ssbo_editor else None + if callable(repair_fn): + model_path = "" + for tag_name in ("model_path", "saved_model_path", "original_path", "file"): + if nodePath.hasTag(tag_name): + candidate = nodePath.getTag(tag_name).strip() + if candidate: + model_path = candidate + break + repair_fn(nodePath, model_path or filename) + except Exception as e: + print(f"[SceneLoad] 贴图修复失败: {e}") + # if self.world.property_panel._hasCollision(nodePath): + # print(f"{indent}模型{nodePath.getName()}已有碰撞体,跳过碰撞体设置") + # else: + # print(f"{indent}为模型{nodePath.getName()}设置碰撞检测") + # self.setupCollision(nodePath) + self.models.append(nodePath) # 递归处理子节点 for child in nodePath.getChildren(): @@ -927,6 +965,8 @@ class SceneManagerIOMixin: print("\n开始处理场景节点...") processNode(scene) + # SSBO 模式下模型已在前面统一导入;若失败已自动回退旧流程。 + #处理父子关系 - 在所有节点加载完成后设置正确的父子关系 print("\n开始重建父子关系...") self._rebuildParentChildRelationships(loaded_nodes) @@ -964,13 +1004,6 @@ class SceneManagerIOMixin: #self.updateSceneTree() #self._get_tree_widget().create_model_items(scene) - print(f"加载完成,GUI元素数量: {len(self.world.gui_elements)}") - if len(self.world.gui_elements) > 0: - print("GUI元素列表:") - for i, elem in enumerate(self.world.gui_elements): - print( - f" {i + 1}. {elem.getName()} (类型: {elem.getTag('gui_type') if elem.hasTag('gui_type') else 'unknown'})") - print("=== 场景加载完成 ===\n") return True diff --git a/scene/scene_manager_model_mixin.py b/scene/scene_manager_model_mixin.py index 940e54db..1afc5d61 100644 --- a/scene/scene_manager_model_mixin.py +++ b/scene/scene_manager_model_mixin.py @@ -61,11 +61,8 @@ class SceneManagerModelMixin: model_name = "imported_model" model.setName(model_name) - # 设置默认颜色以避免get_color警告 - try: - model.setColor(0.8, 0.8, 0.8, 1.0) # 设置为中性灰 - except: - pass # 如果设置颜色失败,继续执行 + # 移除统一设置颜色的代码,因为它可能覆盖PBR纹理并导致模型在RenderPipeline中渲染异常纯黑 + # # model.setColor(0.8, 0.8, 0.8, 1.0) # 移除以防覆盖PBR纹理 # 将模型添加到场景 model.reparentTo(self.world.render) @@ -140,7 +137,7 @@ class SceneManagerModelMixin: # 创建并设置基础材质 #print("\n=== 开始设置材质 ===") - #self._applyMaterialsToModel(model) + self._fixBlackMaterials(model) # 设置碰撞检测(重要!用于选择功能) print("\n=== 设置碰撞检测 ===") @@ -467,6 +464,71 @@ class SceneManagerModelMixin: print(f"应用材质时出错: {e}") print("=== 材质设置完成 ===\n") + + def _fixBlackMaterials(self, model): + # 修复模型中全黑的材质问题,同时为缺失材质的几何体添加默认材质,保留原有纹理 + try: + from panda3d.core import MaterialAttrib, Material, GeomNode + + for geom_path in model.findAllMatches('**/+GeomNode'): + geom_node = geom_path.node() + + # 级联节点状态 + node_state = geom_path.getState() + if node_state.hasAttrib(MaterialAttrib.getClassType()): + mat_attrib = node_state.getAttrib(MaterialAttrib.getClassType()) + mat = mat_attrib.getMaterial() + if mat: + is_black = False + if mat.hasBaseColor(): + c = mat.getBaseColor() + if c.x <= 0.05 and c.y <= 0.05 and c.z <= 0.05: + is_black = True + elif mat.hasDiffuse(): + c = mat.getDiffuse() + if c.x <= 0.05 and c.y <= 0.05 and c.z <= 0.05: + is_black = True + + if is_black or not (mat.hasBaseColor() or mat.hasDiffuse()): + new_mat = Material(mat) + new_mat.setBaseColor((0.8, 0.8, 0.8, 1.0)) + new_mat.setDiffuse((0.8, 0.8, 0.8, 1.0)) + geom_path.setState(node_state.setAttrib(MaterialAttrib.make(new_mat))) + + # 几何体状态 + for i in range(geom_node.getNumGeoms()): + geom_state = geom_node.getGeomState(i) + if geom_state.hasAttrib(MaterialAttrib.getClassType()): + mat_attrib = geom_state.getAttrib(MaterialAttrib.getClassType()) + mat = mat_attrib.getMaterial() + if mat: + is_black = False + if mat.hasBaseColor(): + c = mat.getBaseColor() + if c.x <= 0.05 and c.y <= 0.05 and c.z <= 0.05: + is_black = True + elif mat.hasDiffuse(): + c = mat.getDiffuse() + if c.x <= 0.05 and c.y <= 0.05 and c.z <= 0.05: + is_black = True + + if is_black or not (mat.hasBaseColor() or mat.hasDiffuse()): + new_mat = Material(mat) + new_mat.setBaseColor((0.8, 0.8, 0.8, 1.0)) + new_mat.setDiffuse((0.8, 0.8, 0.8, 1.0)) + geom_node.setGeomState(i, geom_state.setAttrib(MaterialAttrib.make(new_mat))) + else: + new_mat = Material() + new_mat.setBaseColor((0.8, 0.8, 0.8, 1.0)) + new_mat.setDiffuse((0.8, 0.8, 0.8, 1.0)) + new_mat.setSpecular((0.2, 0.2, 0.2, 1.0)) + new_mat.setRoughness(0.8) + geom_node.setGeomState(i, geom_state.addAttrib(MaterialAttrib.make(new_mat))) + + model.clearColor() + except Exception as e: + print(f'修复黑色模型材质时出错: {e}') + def _adjustModelToGround(self, model): """智能调整模型到地面,但保持原有缩放结构""" try: @@ -917,15 +979,27 @@ class SceneManagerModelMixin: from direct.actor.Actor import Actor from panda3d.core import Filename - candidate_paths = [model_path] - candidate_paths.append(Filename.from_os_specific(model_path).get_fullpath()) + candidate_paths = [] + # Always prefer normalized Panda paths; avoid raw Windows path fallback, + # which triggers noisy loader(error) logs for some absolute/Unicode paths. try: normalized = util.normalize_model_path(model_path) - if normalized: + if normalized and normalized != model_path: candidate_paths.append(normalized) except Exception: pass + for ctor_name in ("fromOsSpecificW", "from_os_specific_w", "fromOsSpecific", "from_os_specific"): + ctor = getattr(Filename, ctor_name, None) + if not ctor: + continue + try: + panda_path = ctor(model_path).get_fullpath() + if panda_path: + candidate_paths.append(panda_path) + except Exception: + continue + seen = set() unique_paths = [] for p in candidate_paths: diff --git a/scene/util.py b/scene/util.py index a19193a7..7b42605f 100644 --- a/scene/util.py +++ b/scene/util.py @@ -56,16 +56,29 @@ class CrossPlatformPathHandler: exists = os.path.exists(filepath) return exists - def _panda3d_normalize(self, filepath): - """使用Panda3D标准化路径""" - try: - panda_filename = Filename.from_os_specific(filepath) - normalized_path = panda_filename.get_fullpath() - print(f"✓ Panda3D标准化: {normalized_path}") - return normalized_path - except Exception as e: - print(f"⚠️ Panda3D标准化失败: {e}") - return filepath + def _panda3d_normalize(self, filepath): + """使用Panda3D标准化路径""" + for ctor_name in ("fromOsSpecificW", "from_os_specific_w", "fromOsSpecific", "from_os_specific"): + ctor = getattr(Filename, ctor_name, None) + if not ctor: + continue + try: + panda_filename = ctor(filepath) + normalized_path = panda_filename.get_fullpath() + if normalized_path: + print(f"✓ Panda3D标准化: {normalized_path}") + return normalized_path + except Exception: + continue + try: + panda_filename = Filename(filepath) + normalized_path = panda_filename.get_fullpath() + if normalized_path: + print(f"✓ Panda3D标准化: {normalized_path}") + return normalized_path + except Exception as e: + print(f"⚠️ Panda3D标准化失败: {e}") + return filepath def _attempt_path_fixes(self, filepath): """尝试各种路径修复方法""" @@ -293,4 +306,4 @@ def normalize_model_path(filepath): def suggest_file_placement(filename): """便捷函数:建议文件放置位置""" - return path_handler.suggest_file_placement(filename) \ No newline at end of file + return path_handler.suggest_file_placement(filename) diff --git a/ssbo_component/ssbo_editor.py b/ssbo_component/ssbo_editor.py index 765b7dcd..9946ae2b 100644 --- a/ssbo_component/ssbo_editor.py +++ b/ssbo_component/ssbo_editor.py @@ -11,7 +11,7 @@ from panda3d.core import ( TransparencyAttrib, BoundingSphere, NodePath, GraphicsEngine, WindowProperties, FrameBufferProperties, GraphicsPipe, GraphicsOutput, Camera, DisplayRegion, OrthographicLens, - BoundingBox, BitMask32 + BoundingBox, BitMask32, Material, MaterialAttrib, ColorAttrib, TextureAttrib, PNMImage ) # p3dimgui.backend first tries `from shaders import *`, which can be shadowed by @@ -213,11 +213,87 @@ class SSBOEditor: io.fonts.clear() io.fonts.add_font_default() + def _fixBlackMaterials(self, model): + try: + from panda3d.core import MaterialAttrib, Material, GeomNode + + for geom_path in model.findAllMatches('**/+GeomNode'): + geom_node = geom_path.node() + + node_state = geom_path.getState() + if node_state.hasAttrib(MaterialAttrib.getClassType()): + mat_attrib = node_state.getAttrib(MaterialAttrib.getClassType()) + mat = mat_attrib.getMaterial() + if mat: + is_black = False + if mat.hasBaseColor(): + c = mat.getBaseColor() + if c.x <= 0.05 and c.y <= 0.05 and c.z <= 0.05: + is_black = True + elif mat.hasDiffuse(): + c = mat.getDiffuse() + if c.x <= 0.05 and c.y <= 0.05 and c.z <= 0.05: + is_black = True + + if is_black or not (mat.hasBaseColor() or mat.hasDiffuse()): + new_mat = Material(mat) + new_mat.setBaseColor((0.8, 0.8, 0.8, 1.0)) + new_mat.setDiffuse((0.8, 0.8, 0.8, 1.0)) + geom_path.setState(node_state.setAttrib(MaterialAttrib.make(new_mat))) + + for i in range(geom_node.getNumGeoms()): + geom_state = geom_node.getGeomState(i) + if geom_state.hasAttrib(MaterialAttrib.getClassType()): + mat_attrib = geom_state.getAttrib(MaterialAttrib.getClassType()) + mat = mat_attrib.getMaterial() + if mat: + is_black = False + if mat.hasBaseColor(): + c = mat.getBaseColor() + if c.x <= 0.05 and c.y <= 0.05 and c.z <= 0.05: + is_black = True + elif mat.hasDiffuse(): + c = mat.getDiffuse() + if c.x <= 0.05 and c.y <= 0.05 and c.z <= 0.05: + is_black = True + + if is_black or not (mat.hasBaseColor() or mat.hasDiffuse()): + new_mat = Material(mat) + new_mat.setBaseColor((0.8, 0.8, 0.8, 1.0)) + new_mat.setDiffuse((0.8, 0.8, 0.8, 1.0)) + geom_node.setGeomState(i, geom_state.setAttrib(MaterialAttrib.make(new_mat))) + else: + new_mat = Material() + new_mat.setBaseColor((0.8, 0.8, 0.8, 1.0)) + new_mat.setDiffuse((0.8, 0.8, 0.8, 1.0)) + new_mat.setSpecular((0.2, 0.2, 0.2, 1.0)) + new_mat.setRoughness(0.8) + geom_node.setGeomState(i, geom_state.addAttrib(MaterialAttrib.make(new_mat))) + + model.clearColor() + + except Exception as e: + print(f'修复黑色模型材质时出错: {e}') + def load_model(self, model_path): """Load and process a model using hybrid static/dynamic chunks.""" print(f"[SSBOEditor] Loading model: {model_path}") - fn = Filename.fromOsSpecific(model_path) - source_model = self.base.loader.loadModel(fn) + source_model = None + last_error = None + for fn in self._build_filename_candidates(model_path): + try: + source_model = self.base.loader.loadModel(fn) + if source_model and not source_model.is_empty(): + break + except Exception as e: + last_error = e + source_model = None + if not source_model or source_model.is_empty(): + if last_error: + raise RuntimeError(f"Failed to load model '{model_path}': {last_error}") + raise RuntimeError(f"Failed to load model '{model_path}'") + self._fixBlackMaterials(source_model) + self._repair_missing_textures(source_model, model_path) model_name = os.path.basename(model_path) if model_name: source_model.set_name(model_name) @@ -241,6 +317,545 @@ class SSBOEditor: print(f"[SSBOEditor] Model loaded. Total objects: {count}") + def _build_filename_candidates(self, path_text): + """Build Filename candidates with wide-char first for Windows CJK paths.""" + candidates = [] + seen = set() + for ctor_name in ("fromOsSpecificW", "from_os_specific_w", "fromOsSpecific", "from_os_specific"): + ctor = getattr(Filename, ctor_name, None) + if not ctor: + continue + try: + fn = ctor(path_text) + key = fn.get_fullpath() + if key in seen: + continue + seen.add(key) + candidates.append(fn) + except Exception: + continue + if not candidates: + try: + candidates.append(Filename(path_text)) + except Exception: + pass + return candidates + + def _load_texture_from_path(self, texture_path): + """Load texture with robust path constructors.""" + for fn in self._build_filename_candidates(texture_path): + try: + tex = self.base.loader.loadTexture(fn) + if tex: + return tex + except Exception: + continue + return None + + def _build_texture_search_dirs(self, model_path): + """Build candidate directories for missing texture recovery.""" + dirs = [] + model_dir = os.path.dirname(os.path.abspath(model_path)) + project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + def add_dir(path): + if not path: + return + path = os.path.normpath(path) + if path in dirs: + return + if os.path.isdir(path): + dirs.append(path) + + add_dir(model_dir) + + try: + if os.path.isdir(model_dir): + for item in os.listdir(model_dir): + if item.lower().endswith('.fbm'): + fbm_dir = os.path.join(model_dir, item) + if os.path.isdir(fbm_dir): + add_dir(fbm_dir) + for sub in ("textures", "texture", "tex", "assets", "materials"): + add_dir(os.path.join(fbm_dir, sub)) + except Exception: + pass + + for sub in ("textures", "texture", "tex", "assets", "materials"): + add_dir(os.path.join(model_dir, sub)) + + parent = model_dir + for _ in range(2): + parent = os.path.dirname(parent) + if not parent: + break + for sub in ("textures", "texture", "tex", "assets", "materials"): + add_dir(os.path.join(parent, sub)) + + add_dir(os.path.join(project_root, "Resources")) + add_dir(os.path.join(project_root, "Resources", "textures")) + add_dir(os.path.join(project_root, "Resources", "materials")) + add_dir(os.path.join(project_root, "Resources", "models")) + return dirs + + def _index_texture_files(self, dirs, limit=30000): + """Index texture files by basename for fast lookup.""" + texture_exts = {".png", ".jpg", ".jpeg", ".tga", ".bmp", ".dds", ".ktx", ".ktx2", ".webp"} + index = {} + scanned = 0 + for root_dir in dirs: + try: + for root, _, files in os.walk(root_dir): + for filename in files: + ext = os.path.splitext(filename)[1].lower() + if ext not in texture_exts: + continue + key = filename.lower() + if key not in index: + index[key] = os.path.join(root, filename) + scanned += 1 + if scanned >= limit: + return index + except Exception: + continue + return index + + def _repair_missing_textures(self, model_np, model_path): + """ + Repair broken texture paths by basename search; if unresolved, clear that + missing texture binding to avoid black PBR sampling. + """ + if not model_np or model_np.is_empty(): + return + + search_dirs = self._build_texture_search_dirs(model_path) + texture_index = self._index_texture_files(search_dirs) + + fixed = 0 + cleared = 0 + white_tex = self._get_white_fallback_texture() + nodes = [model_np] + list(model_np.find_all_matches("**")) + for node in nodes: + if not node or node.is_empty(): + continue + + try: + stages = node.find_all_texture_stages() + except Exception: + try: + stages = node.findAllTextureStages() + except Exception: + continue + + try: + stage_count = stages.get_num_texture_stages() + stage_at = stages.get_texture_stage + except Exception: + try: + stage_count = stages.getNumTextureStages() + stage_at = stages.getTextureStage + except Exception: + continue + + for i in range(stage_count): + stage = stage_at(i) + if not stage: + continue + + try: + tex = node.get_texture(stage) + except Exception: + tex = node.getTexture(stage) + if not tex: + continue + + if self._texture_is_valid(tex): + continue + + basename = self._extract_texture_basename(tex) + if not basename: + continue + + replacement = texture_index.get(basename.lower()) + if replacement: + try: + new_tex = self._load_texture_from_path(replacement) + if new_tex: + try: + node.set_texture(stage, new_tex, 1) + except Exception: + node.setTexture(stage, new_tex, 1) + fixed += 1 + continue + except Exception: + pass + + # Missing texture with no replacement: CLEAR the texture binding. + # Do NOT use a white fallback texture, because if this is a Metallic or Roughness + # map, pure white will force Metallic=1.0 and Roughness=1.0, which turns models black! + try: + try: + node.clear_texture(stage) + except Exception: + node.clearTexture(stage) + cleared += 1 + except Exception: + pass + + # GeomState-level texture bindings (common in imported FBX/GLTF): + # inspect and apply the same fallback on this GeomNode path. + try: + gnode = node.node() + get_num_geoms = getattr(gnode, "get_num_geoms", None) or getattr(gnode, "getNumGeoms", None) + get_geom_state = getattr(gnode, "get_geom_state", None) or getattr(gnode, "getGeomState", None) + geom_count = int(get_num_geoms()) if callable(get_num_geoms) else 0 + except Exception: + geom_count = 0 + + for gi in range(geom_count): + try: + state = get_geom_state(gi) + except Exception: + continue + if state is None: + continue + + tattr = None + try: + if state.has_attrib(TextureAttrib.get_class_type()): + tattr = state.get_attrib(TextureAttrib.get_class_type()) + except Exception: + try: + if state.hasAttrib(TextureAttrib.getClassType()): + tattr = state.getAttrib(TextureAttrib.getClassType()) + except Exception: + tattr = None + if not tattr: + continue + + try: + num_on = tattr.get_num_on_stages() + get_stage = tattr.get_on_stage + get_tex = tattr.get_on_texture + except Exception: + try: + num_on = tattr.getNumOnStages() + get_stage = tattr.getOnStage + get_tex = tattr.getOnTexture + except Exception: + continue + + for si in range(int(num_on)): + try: + stage = get_stage(si) + tex = get_tex(stage) + except Exception: + continue + if not stage or not tex: + continue + if self._texture_is_valid(tex): + continue + + basename = self._extract_texture_basename(tex) + replacement = texture_index.get(basename.lower()) if basename else None + if replacement: + try: + new_tex = self._load_texture_from_path(replacement) + if new_tex: + try: + node.set_texture(stage, new_tex, 100000) + except Exception: + node.setTexture(stage, new_tex, 100000) + fixed += 1 + continue + except Exception: + pass + + try: + try: + node.clear_texture(stage) + except Exception: + node.clearTexture(stage) + cleared += 1 + except Exception: + pass + + if fixed or cleared: + print(f"[SSBOEditor] Texture repair: fixed={fixed}, cleared_missing={cleared}") + self._apply_nonblack_material_fallback(model_np) + + def _texture_is_valid(self, tex): + if not tex: + return False + tex_path = self._extract_texture_os_path(tex) + if tex_path and os.path.exists(tex_path): + return True + if tex_path: + # Some valid textures use Panda VFS virtual paths; keep them RAM-checkable. + path_norm = tex_path.replace("\\", "/") + if path_norm.startswith("/$$") or path_norm.startswith("$$"): + pass + else: + # File-backed texture with missing source path should be considered invalid, + # even when Panda keeps a tiny fallback image in RAM. + return False + try: + if tex.has_ram_image(): + return tex.get_ram_image_size() > 0 + except Exception: + try: + if tex.hasRamImage(): + return tex.getRamImageSize() > 0 + except Exception: + pass + return False + + def _extract_texture_os_path(self, tex): + tex_path = "" + try: + if tex.has_fullpath(): + fullpath = tex.get_fullpath() + try: + tex_path = fullpath.to_os_specific() + except Exception: + try: + tex_path = fullpath.toOsSpecific() + except Exception: + tex_path = str(fullpath) + except Exception: + try: + if tex.hasFullpath(): + fullpath = tex.getFullpath() + try: + tex_path = fullpath.toOsSpecific() + except Exception: + tex_path = str(fullpath) + except Exception: + tex_path = "" + + tex_path = str(tex_path or "").strip() + if not tex_path: + return "" + + tex_path = os.path.normpath(tex_path) + if os.path.exists(tex_path): + return tex_path + + # Convert Panda internal drive path (/d/foo/bar) to Windows path if needed. + if len(tex_path) >= 3 and tex_path[0] in ("/", "\\") and tex_path[1].isalpha() and tex_path[2] in ("/", "\\"): + drive_path = f"{tex_path[1]}:{tex_path[2:]}" + drive_path = os.path.normpath(drive_path) + if os.path.exists(drive_path): + return drive_path + return tex_path + + def _extract_texture_basename(self, tex): + tex_path = self._extract_texture_os_path(tex) + if tex_path: + return os.path.basename(tex_path.replace("\\", "/")) + try: + name = tex.get_name() + except Exception: + try: + name = tex.getName() + except Exception: + name = "" + return os.path.basename(str(name).replace("\\", "/")) + + def _get_white_fallback_texture(self): + tex = getattr(self, "_ssbo_white_fallback_tex", None) + if tex: + return tex + try: + img = PNMImage(2, 2, 4) + img.fill(1.0, 1.0, 1.0) + img.alpha_fill(1.0) + tex = Texture("ssbo_white_fallback") + tex.load(img) + tex.set_minfilter(Texture.FT_nearest) + tex.set_magfilter(Texture.FT_nearest) + self._ssbo_white_fallback_tex = tex + return tex + except Exception: + return None + + def _node_has_valid_texture(self, node): + if not node or node.is_empty(): + return False + try: + stages = node.find_all_texture_stages() + stage_count = stages.get_num_texture_stages() + stage_at = stages.get_texture_stage + except Exception: + try: + stages = node.findAllTextureStages() + stage_count = stages.getNumTextureStages() + stage_at = stages.getTextureStage + except Exception: + return False + for i in range(stage_count): + stage = stage_at(i) + if not stage: + continue + try: + tex = node.get_texture(stage) + except Exception: + tex = node.getTexture(stage) + if self._texture_is_valid(tex): + return True + + # GeomState TextureAttrib bindings can hold the effective textures. + try: + gnode = node.node() + get_num_geoms = getattr(gnode, "get_num_geoms", None) or getattr(gnode, "getNumGeoms", None) + get_geom_state = getattr(gnode, "get_geom_state", None) or getattr(gnode, "getGeomState", None) + geom_count = int(get_num_geoms()) if callable(get_num_geoms) else 0 + except Exception: + geom_count = 0 + + for gi in range(geom_count): + try: + state = get_geom_state(gi) + except Exception: + continue + if state is None: + continue + + tattr = None + try: + if state.has_attrib(TextureAttrib.get_class_type()): + tattr = state.get_attrib(TextureAttrib.get_class_type()) + except Exception: + try: + if state.hasAttrib(TextureAttrib.getClassType()): + tattr = state.getAttrib(TextureAttrib.getClassType()) + except Exception: + tattr = None + if not tattr: + continue + + try: + num_on = tattr.get_num_on_stages() + get_stage = tattr.get_on_stage + get_tex = tattr.get_on_texture + except Exception: + try: + num_on = tattr.getNumOnStages() + get_stage = tattr.getOnStage + get_tex = tattr.getOnTexture + except Exception: + continue + + for si in range(int(num_on)): + try: + stage = get_stage(si) + tex = get_tex(stage) + except Exception: + continue + if self._texture_is_valid(tex): + return True + return False + + def _is_node_material_dark(self, node): + """Heuristic: detect near-black material/color state.""" + if not node or node.is_empty(): + return False + try: + state = node.get_state() + except Exception: + try: + state = node.getState() + except Exception: + return True + + def _vec_dark(v): + try: + return max(float(v[0]), float(v[1]), float(v[2])) < 0.08 + except Exception: + try: + return max(float(v.x), float(v.y), float(v.z)) < 0.08 + except Exception: + return False + + # Material color + mat = None + try: + if state.has_attrib(MaterialAttrib.get_class_type()): + mat_attr = state.get_attrib(MaterialAttrib.get_class_type()) + mat = mat_attr.get_material() + except Exception: + try: + if state.hasAttrib(MaterialAttrib.getClassType()): + mat_attr = state.getAttrib(MaterialAttrib.getClassType()) + mat = mat_attr.getMaterial() + except Exception: + mat = None + + if mat: + for has_name, get_name in ( + ("has_base_color", "get_base_color"), + ("has_diffuse", "get_diffuse"), + ("hasBaseColor", "getBaseColor"), + ("hasDiffuse", "getDiffuse"), + ): + has_fn = getattr(mat, has_name, None) + get_fn = getattr(mat, get_name, None) + if callable(has_fn) and callable(get_fn): + try: + if has_fn(): + return _vec_dark(get_fn()) + except Exception: + continue + return False + + # ColorAttrib fallback + try: + if state.has_attrib(ColorAttrib.get_class_type()): + color_attr = state.get_attrib(ColorAttrib.get_class_type()) + if not color_attr.is_off(): + return _vec_dark(color_attr.get_color()) + except Exception: + try: + if state.hasAttrib(ColorAttrib.getClassType()): + color_attr = state.getAttrib(ColorAttrib.getClassType()) + if not color_attr.isOff(): + return _vec_dark(color_attr.getColor()) + except Exception: + pass + return True + + def _apply_nonblack_material_fallback(self, model_np): + """If a geom has no texture and is effectively black, apply a neutral material.""" + if not model_np or model_np.is_empty(): + return + neutral = Material() + neutral.set_base_color((0.75, 0.75, 0.75, 1.0)) if hasattr(neutral, "set_base_color") else neutral.setBaseColor((0.75, 0.75, 0.75, 1.0)) + neutral.set_diffuse((0.75, 0.75, 0.75, 1.0)) if hasattr(neutral, "set_diffuse") else neutral.setDiffuse((0.75, 0.75, 0.75, 1.0)) + neutral.set_ambient((0.22, 0.22, 0.22, 1.0)) if hasattr(neutral, "set_ambient") else neutral.setAmbient((0.22, 0.22, 0.22, 1.0)) + neutral.set_specular((0.1, 0.1, 0.1, 1.0)) if hasattr(neutral, "set_specular") else neutral.setSpecular((0.1, 0.1, 0.1, 1.0)) + neutral.set_shininess(8.0) if hasattr(neutral, "set_shininess") else neutral.setShininess(8.0) + + patched = 0 + for geom_np in model_np.find_all_matches("**/+GeomNode"): + if self._node_has_valid_texture(geom_np): + continue + if not self._is_node_material_dark(geom_np): + continue + try: + try: + geom_np.set_material(neutral, 1) + except Exception: + geom_np.setMaterial(neutral, 1) + try: + geom_np.set_color_scale(1.0, 1.0, 1.0, 1.0) + except Exception: + geom_np.setColorScale(1.0, 1.0, 1.0, 1.0) + patched += 1 + except Exception: + continue + if patched: + print(f"[SSBOEditor] Applied non-black fallback material to {patched} geom nodes.") + # No custom effect needed — RP default rendering for maximum FPS def _inject_ssbo_into_shadow_state(self, effect_path): diff --git a/templates/main_template.py b/templates/main_template.py index f2ba3638..cb226813 100644 --- a/templates/main_template.py +++ b/templates/main_template.py @@ -13,7 +13,6 @@ from direct.actor.Actor import Actor from direct.showbase.ShowBaseGlobal import globalClock from panda3d.core import TextNode, CardMaker, TextureStage, NodePath, Texture, TransparencyAttrib, CollisionTraverser, \ Point3 -from core.InfoPanelManager import InfoPanelManager # 获取渲染管线路径 # 在文件开头添加sys导入(如果还没有的话) import sys @@ -97,8 +96,6 @@ class MainApp(ShowBase): # 加载所有脚本e self.script_manager.load_all_scripts_from_directory() - self.info_panel_manager = InfoPanelManager(self) - try: # 再导入controller模块 from rpcore.util.movement_controller import MovementController @@ -538,8 +535,6 @@ class MainApp(ShowBase): video_path=video_path, size=absolute_scale ) - elif gui_type == "info_panel": - new_element = self.info_panel_manager.onCreateSampleInfoPanel() if "scripts" in gui_info and new_element: self.processScripts(new_element,gui_info["scripts"]) diff --git a/ui/panels/animation_tools.py b/ui/panels/animation_tools.py index 01300dc8..764826e1 100644 --- a/ui/panels/animation_tools.py +++ b/ui/panels/animation_tools.py @@ -707,7 +707,18 @@ class AnimationTools: resolved_source = source if isinstance(source, (str, os.PathLike)): src_text = os.fspath(source) - resolved_source = Filename.from_os_specific(src_text).get_fullpath() + resolved_source = src_text + for ctor_name in ("fromOsSpecificW", "from_os_specific_w", "fromOsSpecific", "from_os_specific"): + ctor = getattr(Filename, ctor_name, None) + if not ctor: + continue + try: + candidate = ctor(src_text).get_fullpath() + if candidate: + resolved_source = candidate + break + except Exception: + continue actor = Actor(resolved_source) # 无论是否已检测到动画名,都显式绑定一次,避免“有名字但无可播放控制” try: diff --git a/ui/panels/app_actions.py b/ui/panels/app_actions.py index cbb85d7c..e5135a7c 100644 --- a/ui/panels/app_actions.py +++ b/ui/panels/app_actions.py @@ -930,6 +930,8 @@ class AppActions: # 检查场景文件 scene_file = os.path.join(project_path, "scenes", "scene.bam") if os.path.exists(scene_file): + if getattr(self, "use_ssbo_mouse_picking", False) and callable(getattr(self, "_import_model_for_runtime", None)): + self.use_ssbo_scene_import = True # 加载场景 try: if self.scene_manager.loadScene(scene_file): @@ -1074,9 +1076,15 @@ class AppActions: model_np = getattr(self.ssbo_editor, 'model', None) # Keep legacy ray-pick fallback usable by adding a collision body. if model_np: + try: + from scene import util as scene_util + normalized_model_path = scene_util.normalize_model_path(file_path) + except Exception: + normalized_model_path = file_path # Apply vital tags manually since SSBO overrides SceneManager loader - model_np.setTag("model_path", file_path) + model_np.setTag("model_path", normalized_model_path) model_np.setTag("original_path", file_path) + model_np.setTag("saved_model_path", normalized_model_path) model_np.setTag("is_model_root", "1") model_np.setTag("is_scene_element", "1") model_np.setTag("file", os.path.basename(file_path)) @@ -1135,28 +1143,30 @@ class AppActions: self.add_error_message("模型导入失败") return None - if hasattr(self.scene_manager, 'processMaterials'): - self.scene_manager.processMaterials(model_node) - if show_info_message: - self.add_info_message("已应用默认材质") - - try: - model_node.clearMaterial() - model_node.clearTexture() - + # SSBO 模式下保留原始材质/纹理,避免模型发黑。 + if not getattr(self, "use_ssbo_mouse_picking", False): if hasattr(self.scene_manager, 'processMaterials'): self.scene_manager.processMaterials(model_node) + if show_info_message: + self.add_info_message("已应用默认材质") try: - color = model_node.getColor() - if color and len(color) >= 4 and color == (1, 1, 1, 1): + model_node.clearMaterial() + model_node.clearTexture() + + if hasattr(self.scene_manager, 'processMaterials'): + self.scene_manager.processMaterials(model_node) + + try: + color = model_node.getColor() + if color and len(color) >= 4 and color == (1, 1, 1, 1): + model_node.setColor(0.8, 0.8, 0.8, 1.0) + elif not color: + model_node.setColor(0.8, 0.8, 0.8, 1.0) + except Exception: model_node.setColor(0.8, 0.8, 0.8, 1.0) - elif not color: - model_node.setColor(0.8, 0.8, 0.8, 1.0) - except Exception: - model_node.setColor(0.8, 0.8, 0.8, 1.0) - except Exception as e: - self.add_warning_message(f"材质处理警告: {e}") + except Exception as e: + self.add_warning_message(f"材质处理警告: {e}") if set_origin: model_node.setPos(0, 0, 0) diff --git a/ui/panels/editor_panels.py b/ui/panels/editor_panels.py index 2d82a66a..de23cdb6 100644 --- a/ui/panels/editor_panels.py +++ b/ui/panels/editor_panels.py @@ -2229,7 +2229,10 @@ class EditorPanels: # 删除对象 imgui.same_line() if imgui.button("删除"): - if hasattr(self, 'selection') and self.selection: + if hasattr(self.app, "_on_delete") and callable(self.app._on_delete): + self.app._on_delete() + elif hasattr(self, 'selection') and self.selection and hasattr(self.selection, "deleteSelectedNode"): + # 兼容旧选择系统接口 self.selection.deleteSelectedNode() diff --git a/ui/panels/object_factory.py b/ui/panels/object_factory.py index 9fb711c7..11798dab 100644 --- a/ui/panels/object_factory.py +++ b/ui/panels/object_factory.py @@ -339,51 +339,6 @@ class ObjectFactory: except Exception as e: print(f"✗ 创建平面失败: {e}") return None - - - def create2DSamplePanel(self): - """创建2D示例面板""" - try: - from core.InfoPanelManager import createSampleInfoPanel - result = createSampleInfoPanel(self.render) - return result - except Exception as e: - print(f"创建2D示例面板失败: {e}") - return None - - - def create3DSamplePanel(self): - """创建3D实例面板""" - try: - if hasattr(self, 'info_panel_manager') and self.info_panel_manager: - # 创建3D信息面板 - panel_id = f"3d_sample_{int(time.time())}" - result = self.info_panel_manager.create3DInfoPanel( - panel_id=panel_id, - position=(0, 0, 2), - size=(1.0, 0.6), - 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) - ) - - # 添加示例内容 - if result: - sample_data = { - "标题": "3D信息面板", - "状态": "运行中", - "创建时间": time.strftime("%Y-%m-%d %H:%M:%S"), - "位置": f"X:0, Y:0, Z:2" - } - self.info_panel_manager.updatePanelContent(panel_id, content=sample_data) - - return result - return None - except Exception as e: - print(f"创建3D示例面板失败: {e}") - return None - def createWebPanel(self, url="https://www.example.com"): """创建Web面板""" diff --git a/ui/widgets.py b/ui/widgets.py deleted file mode 100644 index c667635b..00000000 --- a/ui/widgets.py +++ /dev/null @@ -1,4319 +0,0 @@ -""" -自定义Qt部件模块 - -包含所有自定义的Qt界面组件: -- NewProjectDialog: 新建项目对话框 -- CustomPanda3DWidget: 自定义Panda3D显示部件 -- CustomFileView: 自定义文件浏览器 -- CustomTreeWidget: 自定义场景树部件 -""" - -import os -import re -from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, - QLineEdit, QPushButton, QLabel, - QTreeView, QTreeWidget, QTreeWidgetItem, QWidget, - QFileDialog, QMessageBox, QAbstractItemView, QMenu, QDockWidget, QButtonGroup, QToolButton, QFrame, QSizePolicy) -from PyQt5.QtCore import Qt, QUrl, QMimeData, QPoint, QSize -from PyQt5.QtGui import QDrag, QPainter, QPixmap, QPen, QBrush, QFont -from PyQt5.sip import wrapinstance -from direct.showbase.ShowBaseGlobal import aspect2d -from panda3d.core import ModelRoot, NodePath, CollisionNode - -from QMeta3D.QMeta3DWidget import QMeta3DWidget -from scene import util -from ui.icon_manager import get_icon_manager - -class NewProjectDialog(QDialog): - """新建项目对话框""" - - def __init__(self, parent=None): - super().__init__(parent) - self.setWindowTitle("新建项目") - self.setObjectName("newProjectDialog") - self.resize(508, 274) - self.setMinimumSize(508, 274) - self.setModal(True) - - # 移除默认窗口装饰,使用自定义顶部栏 - self.setWindowFlags(Qt.Dialog | Qt.FramelessWindowHint) - self.setAttribute(Qt.WA_TranslucentBackground, True) - - # 拖拽和窗口状态 - self.dragging = False - self.drag_position = QPoint() - self.icon_manager = get_icon_manager() - self._title_icon_size = QSize(18, 18) - self._icon_close = self.icon_manager.get_icon('close_icon', self._title_icon_size) - - # 设置严格按照Figma设计的样式 - self.setStyleSheet(""" - QDialog#newProjectDialog { - background-color: transparent; - color: #EBEBEB; - border: none; - } - QFrame#baseFrame { - background-color: #000000; - border: 1px solid #3E3E42; - border-radius: 5px; - } - QWidget#titleBar { - background-color: transparent; - border: none; - border-radius: 5px 5px 0px 0px; - min-height: 32px; - max-height: 32px; - } - QWidget#titleBar QWidget { - background-color: transparent; - border: none; - } - QLabel#titleLabel { - color: #FFFFFF; - font-family: 'Inter', 'Microsoft YaHei', sans-serif; - font-size: 14px; - font-weight: 500; - letter-spacing: 0.7px; - } - QWidget#controlButtons QPushButton { - background-color: transparent; - border: none; - color: #EBEBEB; - font-size: 14px; - min-width: 18px; - max-width: 18px; - min-height: 18px; - max-height: 18px; - padding: 0px; - border-radius: 3px; - } - QWidget#controlButtons QPushButton:hover { - background-color: #2A2D2E; - color: #FFFFFF; - } - QPushButton#closeButton { - border-radius: 0px 5px 0px 0px; - } - QPushButton#closeButton:hover { - background-color: #2A2D2E; - color: #FFFFFF; - } - QWidget#contentWidget { - background-color: transparent; - border-radius: 0px 0px 5px 5px; - } - QFrame#contentContainer { - background-color: #19191B; - border: 1px solid #2C2F36; - border-radius: 5px; - } - QLabel[role="sectionTitle"] { - color: #EBEBEB; - font-family: 'Inter', 'Microsoft YaHei', sans-serif; - font-size: 12px; - font-weight: 500; - letter-spacing: 0.6px; - padding: 0px; - margin-bottom: 0px; - } - QLabel[role="hint"] { - color: rgba(235, 235, 235, 0.6); - font-family: 'Inter', 'Microsoft YaHei', sans-serif; - font-size: 11px; - font-weight: 300; - letter-spacing: 0.55px; - margin-top: 0px; - padding: 0px; - } - QLineEdit { - background-color: rgba(89, 100, 113, 0.2); - color: #EBEBEB; - border: 1px solid rgba(76, 92, 110, 0.6); - border-radius: 2px; - padding: 6px 10px; - font-family: 'Inter', 'Microsoft YaHei', sans-serif; - font-size: 11px; - font-weight: 300; - letter-spacing: 0.55px; - min-height: 14px; - max-height: 30px; - } - QLineEdit:focus { - border: 1px solid #3067C0; - background-color: rgba(48, 103, 192, 0.1); - } - QLineEdit:hover { - border: 1px solid #3067C0; - background-color: rgba(89, 100, 113, 0.3); - } - QLineEdit:disabled { - background-color: rgba(89, 100, 113, 0.1); - color: rgba(235, 235, 235, 0.4); - border: 1px solid rgba(76, 92, 110, 0.2); - } - QPushButton { - background-color: rgba(89, 98, 118, 0.5); - color: #EBEBEB; - border: none; - border-radius: 2px; - padding: 0px 0px; - font-family: 'Inter', 'Microsoft YaHei', sans-serif; - font-weight: 300; - font-size: 10px; - letter-spacing: 0.5px; - min-width: 90px; - min-height: 30px; - max-height: 30px; - } - QPushButton:hover { - background-color: #3067C0; - color: #FFFFFF; - } - QPushButton:pressed { - background-color: #2556A0; - color: #FFFFFF; - } - QPushButton:disabled { - background-color: rgba(89, 98, 118, 0.3); - color: rgba(235, 235, 235, 0.4); - } - QPushButton#primaryButton { - background-color: rgba(89, 98, 118, 0.5); - border: none; - color: #EBEBEB; - font-weight: 300; - min-width: 120px; - max-width: 120px; - } - QPushButton#primaryButton:hover { - background-color: #2556A0; - } - QPushButton#primaryButton:pressed { - background-color: #1E4A8C; - } - QPushButton#secondaryButton { - background-color: rgba(89, 98, 118, 0.5); - border: none; - color: #EBEBEB; - min-width: 120px; - max-width: 120px; - } - QPushButton#secondaryButton:hover { - background-color: #3067C0; - color: #FFFFFF; - } - QPushButton#secondaryButton:pressed { - background-color: #2556A0; - color: #FFFFFF; - } - QPushButton#browseButton { - min-width: 90px; - max-width: 90px; - min-height: 28px; - max-height: 28px; - } - """) - - # 创建主布局 - main_layout = QVBoxLayout(self) - main_layout.setContentsMargins(0, 0, 0, 0) - main_layout.setSpacing(0) - - # 创建基础容器,负责绘制圆角和边框 - base_frame = QFrame() - base_frame.setObjectName('baseFrame') - base_frame.setFrameShape(QFrame.NoFrame) - base_frame.setAttribute(Qt.WA_StyledBackground, True) - - base_layout = QVBoxLayout(base_frame) - base_layout.setContentsMargins(0, 0, 0, 0) - base_layout.setSpacing(0) - - # 创建自定义顶部栏 - self.createTitleBar() - base_layout.addWidget(self.title_bar) - - # 创建内容区域 - content_widget = QWidget() - content_widget.setObjectName('contentWidget') - content_layout = QVBoxLayout(content_widget) - content_layout.setContentsMargins(10, 10, 10, 10) - content_layout.setSpacing(0) - - content_container = QFrame() - content_container.setObjectName('contentContainer') - content_container.setFrameShape(QFrame.NoFrame) - content_container.setAttribute(Qt.WA_StyledBackground, True) - content_container.setFixedWidth(488) - content_container.setMinimumHeight(213) - - container_layout = QVBoxLayout(content_container) - container_layout.setContentsMargins(15, 10, 15, 10) - container_layout.setSpacing(10) - - pathLabel = QLabel('项目路径') - pathLabel.setProperty('role', 'sectionTitle') - container_layout.addWidget(pathLabel) - - path_row_widget = QWidget() - path_row_layout = QHBoxLayout(path_row_widget) - path_row_layout.setContentsMargins(0, 0, 0, 0) - path_row_layout.setSpacing(10) - path_row_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) - self.pathEdit = QLineEdit() - self.pathEdit.setReadOnly(True) - self.pathEdit.setPlaceholderText('请选择项目路径') - self.pathEdit.setMinimumWidth(358) - self.pathEdit.setFixedHeight(30) - path_row_layout.addWidget(self.pathEdit) - - browseButton = QPushButton('浏览...') - browseButton.setObjectName('browseButton') - browseButton.clicked.connect(self.browsePath) - path_row_layout.addWidget(browseButton) - container_layout.addWidget(path_row_widget) - - nameLabel = QLabel('项目名称') - nameLabel.setProperty('role', 'sectionTitle') - container_layout.addWidget(nameLabel) - - self.nameEdit = QLineEdit() - self.nameEdit.setText('新项目') - self.nameEdit.setPlaceholderText('请输入项目名称') - self.nameEdit.selectAll() - self.nameEdit.setFixedHeight(30) - container_layout.addWidget(self.nameEdit) - - self.tipLabel = QLabel('项目名称只能包含字母、数字、下划线、中划线和中文') - self.tipLabel.setProperty('role', 'hint') - container_layout.addWidget(self.tipLabel) - - separator = QFrame() - separator.setFrameShape(QFrame.HLine) - separator.setFrameShadow(QFrame.Plain) - separator.setFixedHeight(1) - separator.setStyleSheet('background-color: #2C2F36; border: none;') - container_layout.addWidget(separator) - - button_row_widget = QWidget() - button_row_layout = QHBoxLayout(button_row_widget) - button_row_layout.setContentsMargins(0, 0, 0, 0) - button_row_layout.setSpacing(10) - button_row_layout.addStretch() - button_row_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) - - self.cancelButton = QPushButton('取消') - self.cancelButton.setObjectName('secondaryButton') - self.cancelButton.clicked.connect(self.reject) - self.cancelButton.setFixedSize(120, 30) - button_row_layout.addWidget(self.cancelButton) - - self.confirmButton = QPushButton('确认') - self.confirmButton.setObjectName('primaryButton') - self.confirmButton.clicked.connect(self.validate) - self.confirmButton.setFixedSize(120, 30) - button_row_layout.addWidget(self.confirmButton) - - container_layout.addWidget(button_row_widget) - - self.confirmButton.setDefault(True) - self.confirmButton.setAutoDefault(True) - self.cancelButton.setAutoDefault(False) - - content_layout.addWidget(content_container, 0, Qt.AlignTop) - base_layout.addWidget(content_widget) - main_layout.addWidget(base_frame) - - self.projectPath = "" - self.projectName = "" - - def createTitleBar(self): - """创建自定义顶部栏""" - self.title_bar = QFrame() - self.title_bar.setObjectName("titleBar") - - title_layout = QHBoxLayout(self.title_bar) - title_layout.setContentsMargins(12, 0, 12, 0) - title_layout.setSpacing(0) - - # Control buttons area - controls = QWidget() - controls.setObjectName("controlButtons") - controls_layout = QHBoxLayout(controls) - controls_layout.setContentsMargins(0, 0, 0, 0) - controls_layout.setSpacing(0) - - self.close_button = QPushButton() - self.close_button.setObjectName("closeButton") - self.close_button.clicked.connect(self.reject) - self.close_button.setFocusPolicy(Qt.NoFocus) - controls_layout.addWidget(self.close_button) - - self._applyTitleBarIcons() - - # Reserve left space equal to control buttons to keep title centered - left_placeholder = QWidget() - left_placeholder.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) - title_layout.addWidget(left_placeholder) - - self.title_label = QLabel(self.windowTitle()) - self.title_label.setObjectName("titleLabel") - self.title_label.setAlignment(Qt.AlignCenter) - title_layout.addWidget(self.title_label, 1) - - title_layout.addWidget(controls) - - left_placeholder.setFixedWidth(controls.sizeHint().width()) - - - def _applyTitleBarIcons(self): - """为标题栏按钮应用图标""" - if self._icon_close: - self.close_button.setIcon(self._icon_close) - self.close_button.setIconSize(self._title_icon_size) - self.close_button.setText("") - self.close_button.setToolTip("关闭") - - - def toggleMaximize(self): - """切换窗口最大化/还原""" - if self.isMaximized(): - self.showNormal() - else: - self.showMaximized() - - - def setWindowTitle(self, title): - super().setWindowTitle(title) - if hasattr(self, "title_label"): - self.title_label.setText(title) - - - def mousePressEvent(self, event): - """鼠标按下事件 - 用于拖拽窗口""" - if event.button() == Qt.LeftButton and self.title_bar.geometry().contains(event.pos()): - if self.isMaximized(): - self.showNormal() - self.dragging = True - self.drag_position = event.globalPos() - self.frameGeometry().topLeft() - event.accept() - super().mousePressEvent(event) - - def mouseDoubleClickEvent(self, event): - """双击标题栏切换最大化""" - if event.button() == Qt.LeftButton and self.title_bar.geometry().contains(event.pos()): - self.toggleMaximize() - event.accept() - return - super().mouseDoubleClickEvent(event) - - - def mouseMoveEvent(self, event): - """鼠标移动事件 - 用于拖拽窗口""" - if event.buttons() == Qt.LeftButton and self.dragging: - self.move(event.globalPos() - self.drag_position) - event.accept() - super().mouseMoveEvent(event) - - def mouseReleaseEvent(self, event): - """鼠标释放事件 - 停止拖拽""" - if event.button() == Qt.LeftButton: - self.dragging = False - super().mouseReleaseEvent(event) - - def browsePath(self): - """浏览选择项目路径""" - path = QFileDialog.getExistingDirectory(self, "选择项目路径") - if path: - self.pathEdit.setText(path) - - def validate(self): - """验证输入并关闭对话框""" - # 获取并验证路径 - self.projectPath = self.pathEdit.text() - if not self.projectPath: - UniversalMessageDialog.show_warning(self, "错误", "请选择项目路径!", show_cancel=False, confirm_text="确认") - return - - # 获取并验证项目名称 - self.projectName = self.nameEdit.text().strip() - if not self.projectName: - UniversalMessageDialog.show_warning(self, "错误", "请输入项目名称!", show_cancel=False, confirm_text="确认") - return - - # 验证项目名称格式 - if not re.match(r'^[a-zA-Z0-9_\-\u4e00-\u9fa5]+$', self.projectName): - UniversalMessageDialog.show_warning(self, "错误", - "项目名称只能包含字母、数字、下划线、中划线和中文!", show_cancel=False, confirm_text="确认") - return - - # 检查项目是否已存在 - full_path = os.path.join(self.projectPath, self.projectName) - if os.path.exists(full_path): - UniversalMessageDialog.show_warning(self, "错误", "项目已存在!", show_cancel=False, confirm_text="确认") - return - - self.accept() - -class CustomMeta3DWidget(QMeta3DWidget): - """自定义Panda3D显示部件""" - - def __init__(self, world, parent=None): - if parent is None: - parent = wrapinstance(0, QWidget) - super().__init__(world, parent) - self.world = world - self.setFocusPolicy(Qt.StrongFocus) # 允许接收键盘焦点 - self.setAcceptDrops(True) # 允许接收拖放 - self.setMouseTracking(True) # 启用鼠标追踪 - - # 让world引用这个widget,以便获取准确的尺寸 - if hasattr(world, 'setQtWidget'): - world.setQtWidget(self) - - def getActualSize(self): - """获取Qt部件的实际渲染尺寸""" - return (self.width(), self.height()) - - def dragEnterEvent(self, event): - """处理拖拽进入事件""" - # 检查是否是文件拖拽 - if event.mimeData().hasUrls(): - # 检查是否包含支持的模型文件 - for url in event.mimeData().urls(): - filepath = url.toLocalFile() - if filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')): - event.acceptProposedAction() - return - event.ignore() - - def dragMoveEvent(self, event): - """处理拖拽移动事件""" - if event.mimeData().hasUrls(): - event.acceptProposedAction() - else: - event.ignore() - - def dropEvent(self, event): - """处理拖放事件""" - if event.mimeData().hasUrls(): - for url in event.mimeData().urls(): - filepath = url.toLocalFile() - if filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')): - # 使用关键字参数确保兼容性 - self.world.importModel(filepath) - event.acceptProposedAction() - else: - event.ignore() - - def wheelEvent(self, event): - """处理滚轮事件""" - if event.angleDelta().y() > 0: - # 滚轮向前滚动 - self.world.wheelForward() - else: - # 滚轮向后滚动 - self.world.wheelBackward() - event.accept() - - def mousePressEvent(self, event): - """处理 Qt 鼠标按下事件""" - if event.button() == Qt.LeftButton: - self.world.mousePressEventLeft({ - 'x': event.x(), - 'y': event.y() - }) - elif event.button() == Qt.RightButton: - self.world.mousePressEventRight({ - 'x': event.x(), - 'y': event.y() - }) - elif event.button() == Qt.MiddleButton: # 添加滑轮按下事件处理 - self.world.mousePressEventMiddle({ - 'x': event.x(), - 'y': event.y() - }) - event.accept() - - def mouseReleaseEvent(self, event): - """处理 Qt 鼠标释放事件""" - if event.button() == Qt.LeftButton: - self.world.mouseReleaseEventLeft({ - 'x': event.x(), - 'y': event.y() - }) - elif event.button() == Qt.RightButton: - self.world.mouseReleaseEventRight({ - 'x': event.x(), - 'y': event.y() - }) - elif event.button() == Qt.MiddleButton: # 添加滑轮释放事件处理 - self.world.mouseReleaseEventMiddle({ - 'x': event.x(), - 'y': event.y() - }) - event.accept() - - def mouseMoveEvent(self, event): - """处理 Qt 鼠标移动事件""" - self.world.mouseMoveEvent({ - 'x': event.x(), - 'y': event.y() - }) - event.accept() - - def cleanup(self): - """清理Panda3D资源""" - try: - print("🧹 清理CustomPanda3DWidget资源...") - - # 清理world资源 - if hasattr(self, 'world') and self.world: - # 如果world有cleanup方法,调用它 - if hasattr(self.world, 'cleanup'): - self.world.cleanup() - # 清理world引用 - self.world = None - - # 调用父类的清理方法(如果存在) - if hasattr(super(), 'cleanup'): - super().cleanup() - - print("✅ CustomPanda3DWidget资源清理完成") - - except Exception as e: - print(f"⚠️ 清理CustomPanda3DWidget资源时出错: {e}") - -class CustomFileView(QTreeView): - """自定义文件浏览器""" - - def __init__(self, world, parent=None): - if parent is None: - parent = wrapinstance(0, QWidget) - super().__init__(parent) - base_font = self.font() - base_font.setFamily("Inter") - base_font.setPointSize(10) - base_font.setWeight(QFont.Normal) - self.setFont(base_font) - if self.model(): - self.model().rowsInserted.connect(self._handle_rows_inserted) - self.world = world - self.setupUI() - self.setupDragDrop() - - def setupUI(self): - """初始化UI设置""" - self.setHeaderHidden(True) - # 启用多选和拖拽 - self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) - self.setDropIndicatorShown(True) # 启用拖放指示线 - - def setupDragDrop(self): - """设置拖拽功能""" - # 使用自定义拖拽模式 - self.setDragDropMode(QTreeView.DragOnly) # 只允许拖出,不允许拖入 - self.setDefaultDropAction(Qt.DropAction.MoveAction) - self.setDragEnabled(True) - self.setAcceptDrops(True) - - def startDrag(self, supportedActions): - """开始拖拽操作""" - # 获取选中的文件 - indexes = self.selectedIndexes() - if not indexes: - return - - # 只处理文件名列 - indexes = [idx for idx in indexes if idx.column() == 0] - - # 创建 MIME 数据 - mimeData = self.model().mimeData(indexes) - - # 检查是否包含支持的模型文件 - urls = [] - for index in indexes: - filepath = self.model().filePath(index) - if filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')): - urls.append(QUrl.fromLocalFile(filepath)) - - if not urls: - return - - # 设置 URL 列表 - mimeData.setUrls(urls) - - # 创建拖拽对象 - drag = QDrag(self) - drag.setMimeData(mimeData) - - # 设置拖拽图标(可选) - pixmap = QPixmap(32, 32) - pixmap.fill(Qt.transparent) - painter = QPainter(pixmap) - painter.drawText(pixmap.rect(), Qt.AlignCenter, str(len(urls))) - painter.end() - drag.setPixmap(pixmap) - - # 执行拖拽 - drag.exec_(supportedActions) - - def mouseDoubleClickEvent(self, event): - """双击标题栏切换最大化""" - index = self.indexAt(event.pos()) - if index.isValid(): - model = self.model() - filepath = model.filePath(index) - - # 检查是否是模型文件 - if filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')): - self.world.importModel(filepath) - else: - print("不支持的文件类型") - super().mouseDoubleClickEvent(event) - -from PyQt5.QtCore import QFileSystemWatcher,QTimer -import os - -class CustomAssetsTreeWidget(QTreeWidget): - def __init__(self, world, parent=None): - if parent is None: - parent = wrapinstance(0, QWidget) - super().__init__(parent) - self.world = world - self.root_path = None - 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() - # 设置右键菜单 - self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) - self.customContextMenuRequested.connect(self.showContextMenu) - - def showContextMenu(self, position): - """显示右键菜单""" - item = self.itemAt(position) - if not item: - return - - filepath = item.data(0, Qt.UserRole) - is_folder = item.data(0, Qt.UserRole + 1) - - menu = QMenu(self) - - if is_folder: - # 文件夹右键菜单 - menu.addAction("📁 新建文件夹", lambda: self.createNewFolder(item)) - menu.addAction("📄 新建文件", lambda: self.createNewFile(item)) - menu.addSeparator() - menu.addAction("✏️ 重命名", lambda: self.renameItem(item)) - menu.addAction("🗑️ 删除", lambda: self.deleteItem(item)) - menu.addSeparator() - menu.addAction("📋 复制路径", lambda: self.copyPath(filepath)) - menu.addAction("🔍 查看属性", lambda: self.showProperties(item)) - else: - # 文件右键菜单 - menu.addAction("📂 打开文件", lambda: self.openFile(filepath)) - menu.addSeparator() - menu.addAction("✏️ 重命名", lambda: self.renameItem(item)) - menu.addAction("🗑️ 删除", lambda: self.deleteItem(item)) - menu.addSeparator() - menu.addAction("📋 复制路径", lambda: self.copyPath(filepath)) - menu.addAction("🔍 查看属性", lambda: self.showProperties(item)) - - # 显示菜单 - menu.exec_(self.mapToGlobal(position)) - - def _saveExpandedState(self): - """保存展开状态""" - expanded_paths = set() - - def collectExpanded(item): - if item.isExpanded(): - path = item.data(0, Qt.UserRole) - if path: - expanded_paths.add(path) - - for i in range(item.childCount()): - collectExpanded(item.child(i)) - - # 遍历所有顶级项目 - for i in range(self.topLevelItemCount()): - collectExpanded(self.topLevelItem(i)) - - return expanded_paths - - def _restoreExpandedState(self, expanded_paths): - """恢复展开状态""" - def restoreExpanded(item): - path = item.data(0, Qt.UserRole) - if path in expanded_paths: - item.setExpanded(True) - - for i in range(item.childCount()): - restoreExpanded(item.child(i)) - - # 遍历所有顶级项目 - for i in range(self.topLevelItemCount()): - restoreExpanded(self.topLevelItem(i)) - - def _refreshWithStatePreservation(self): - """刷新树形视图并保持状态""" - # 保存当前状态 - expanded_paths = self._saveExpandedState() - - # 刷新树形结构 - self.load_file_tree() - - # 恢复展开状态 - self._restoreExpandedState(expanded_paths) - - def createNewFolder(self, parent_item): - """新建文件夹""" - import os - - parent_path = parent_item.data(0, Qt.UserRole) - dialog = StyledTextInputDialog(self, "新建文件夹", "文件夹名称", "请输入文件夹名称") - if dialog.exec_() != QDialog.Accepted: - return - - folder_name = dialog.text() - if not folder_name: - return - - new_folder_path = os.path.join(parent_path, folder_name) - try: - os.makedirs(new_folder_path, exist_ok=True) - self._refreshWithStatePreservation() - print(f"新建文件夹: {new_folder_path}") - except OSError as e: - print(f"新建文件夹失败: {e}") - - - def createNewFile(self, parent_item): - """新建文件""" - import os - - parent_path = parent_item.data(0, Qt.UserRole) - dialog = StyledTextInputDialog(self, "新建文件", "文件名称", "请输入文件名称") - if dialog.exec_() != QDialog.Accepted: - return - - file_name = dialog.text() - if not file_name: - return - - new_file_path = os.path.join(parent_path, file_name) - try: - with open(new_file_path, "w", encoding="utf-8") as f: - f.write("") - self._refreshWithStatePreservation() - print(f"新建文件: {new_file_path}") - except OSError as e: - print(f"新建文件失败: {e}") - - - def renameItem(self, item): - """重命名文件或文件夹""" - import os - - old_path = item.data(0, Qt.UserRole) - old_name = os.path.basename(old_path) - - dialog = StyledTextInputDialog(self, "重命名", "新名称", "请输入新名称", old_name) - if dialog.exec_() != QDialog.Accepted: - return - - new_name = dialog.text() - if not new_name or new_name == old_name: - return - - parent_dir = os.path.dirname(old_path) - new_path = os.path.join(parent_dir, new_name) - - try: - os.rename(old_path, new_path) - item.setText(0, new_name) - item.setData(0, Qt.UserRole, new_path) - self._refreshWithStatePreservation() - except OSError as e: - print(f"重命名失败: {e}") - - - def deleteItem(self, item): - """删除文件或文件夹""" - import os - import shutil - - filepath = item.data(0, Qt.UserRole) - is_folder = item.data(0, Qt.UserRole + 1) - - item_type = "文件夹" if is_folder else "文件" - result = UniversalMessageDialog.show_warning( - self, - "确认删除", - f"确定要删除这个{item_type}吗?\n{filepath}", - show_cancel=True, - confirm_text="删除", - cancel_text="取消" - ) - - if result == QDialog.Accepted: - try: - if is_folder: - shutil.rmtree(filepath) - else: - os.remove(filepath) - self._refreshWithStatePreservation() - print(f"删除{item_type}: {filepath}") - except OSError as e: - print(f"删除{item_type}失败: {e}") - UniversalMessageDialog.show_error( - self, - "错误", - f"删除{item_type}失败: {e}", - show_cancel=False, - confirm_text="确认" - ) - - def copyPath(self, filepath): - """复制路径到剪贴板""" - from PyQt5.QtWidgets import QApplication - - clipboard = QApplication.clipboard() - clipboard.setText(filepath) - print(f"已复制路径: {filepath}") - - def openFile(self, filepath): - """打开文件""" - import os - import subprocess - import platform - - try: - system = platform.system() - if system == "Windows": - os.startfile(filepath) - elif system == "Darwin": # macOS - subprocess.run(["open", filepath]) - else: # Linux - subprocess.run(["xdg-open", filepath]) - print(f"打开文件: {filepath}") - except Exception as e: - print(f"打开文件失败: {e}") - - def showProperties(self, item): - """显示属性面板""" - import os - - filepath = item.data(0, Qt.UserRole) - is_folder = item.data(0, Qt.UserRole + 1) - - try: - stat = os.stat(filepath) - size = stat.st_size - modified = os.path.getmtime(filepath) - - import datetime - modified_str = datetime.datetime.fromtimestamp(modified).strftime('%Y-%m-%d %H:%M:%S') - - item_type = "文件夹" if is_folder else "文件" - size_str = f"{size} 字节" if not is_folder else "文件夹" - - properties = f""" - 路径: {filepath} - 类型: {item_type} - 大小: {size_str} - 修改时间: {modified_str} - """ - - UniversalMessageDialog.show_info( - self, - "属性", - properties.strip(), - show_cancel=False, - confirm_text="确认" - ) - - except OSError as e: - UniversalMessageDialog.show_warning( - self, - "错误", - f"无法获取属性: {e}", - show_cancel=False, - confirm_text="确认" - ) - - - # def mouseDoubleClickEvent(self, event): - # """处理双击事件""" - # item = self.itemAt(event.pos()) - # if item: - # filepath = item.data(0, Qt.UserRole) - # is_folder = item.data(0, Qt.UserRole + 1) - # - # if is_folder: - # # 文件夹:展开/折叠 - # item.setExpanded(not item.isExpanded()) - # else: - # # 文件:检查是否是模型文件 - # if filepath and filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')): - # self.world.importModel(filepath) - # else: - # # 其他文件:用系统默认程序打开 - # self.openFile(filepath) - # - # super().mouseDoubleClickEvent(event) - - def setupUI(self): - """初始化UI设置""" - self.setHeaderHidden(True) - # 启用多选和拖拽 - self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) - self.setDropIndicatorShown(True) # 启用拖放指示线 - - def setupDragDrop(self): - """设置拖拽功能""" - # 使用InternalMove模式以正确显示插入指示线 - self.setDragDropMode(QAbstractItemView.InternalMove) - self.setDefaultDropAction(Qt.DropAction.MoveAction) - self.setDragEnabled(True) - self.setAcceptDrops(True) - self.setDropIndicatorShown(True) - - def getProjectRootPath(self): - """获取项目根路径下的Resources文件夹,考虑跨平台""" - import os - - # 获取当前文件所在目录,然后向上查找项目根目录 - current_dir = os.path.dirname(os.path.abspath(__file__)) - - # 向上查找直到找到项目根目录(包含特定标识文件或文件夹) - project_root = current_dir - max_depth = 10 # 限制向上查找的深度 - depth = 0 - - while depth < max_depth: - # 检查是否是项目根目录(可以根据实际情况调整判断条件) - if (os.path.exists(os.path.join(project_root, "main.py")) or - os.path.exists(os.path.join(project_root, "setup.py")) or - os.path.exists(os.path.join(project_root, ".git"))): - break - parent_dir = os.path.dirname(project_root) - if parent_dir == project_root: # 已经到达文件系统根目录 - # 回退到使用当前工作目录 - project_root = os.getcwd() - break - project_root = parent_dir - depth += 1 - - # 构建Resources文件夹路径(跨平台) - resources_path = os.path.join(project_root, "Resources") - - # 如果Resources文件夹不存在,创建它 - if not os.path.exists(resources_path): - try: - os.makedirs(resources_path, exist_ok=True) - print(f"创建Resources文件夹: {resources_path}") - except OSError as e: - print(f"无法创建Resources文件夹: {e}") - # 如果无法创建,回退到项目根路径 - return project_root - - return resources_path - - # def getProjectRootPath(self): - # """获取项目根路径下的Resources文件夹,考虑跨平台""" - # import os - # - # # 获取项目根路径 - # project_root = os.getcwd() - # - # # 构建Resources文件夹路径(跨平台) - # resources_path = os.path.join(project_root, "Resources") - # - # # 如果Resources文件夹不存在,创建它 - # if not os.path.exists(resources_path): - # try: - # os.makedirs(resources_path, exist_ok=True) - # print(f"创建Resources文件夹: {resources_path}") - # except OSError as e: - # print(f"无法创建Resources文件夹: {e}") - # # 如果无法创建,回退到项目根路径 - # return project_root - # - # return resources_path - - def load_file_tree(self): - """加载树形视图""" - self.clear() - self.current_path = self.getProjectRootPath() - try: - # 创建当前目录的根节点 - root_name = os.path.basename(self.current_path) or self.current_path - root_item = QTreeWidgetItem([f"📁 {root_name}"]) - root_item.setData(0, Qt.UserRole, self.current_path) - root_item.setData(0, Qt.UserRole + 1, True) - font = QFont("Microsoft YaHei", 10, QFont.Light) # 假设您希望字体大小为10 - root_item.setFont(0, font) - self.addTopLevelItem(root_item) - - # 加载当前目录内容 - self.load_directory_tree(self.current_path, root_item) - - #添加目录到监控器 - self.addWatchedDirectory(self.current_path) - - # 展开根节点 - root_item.setExpanded(True) - - except PermissionError: - 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: - return - - try: - items = os.listdir(path) - items.sort() - - # 分别处理文件夹和文件 - folders = [] - files = [] - - for item in items: - # 跳过隐藏文件和系统文件 - if item.startswith('.') or item.startswith('__'): - continue - - item_path = os.path.join(path, item) - if os.path.isdir(item_path): - folders.append(item) - elif os.path.isfile(item_path): - files.append(item) - - # 先添加文件夹 - for folder in folders: - folder_path = os.path.join(path, folder) - folder_item = self.create_simple_tree_item(folder, folder_path, True) - parent_item.addChild(folder_item) - - # 递归加载子目录(限制深度) - if current_depth < max_depth - 1: - self.load_directory_tree(folder_path, folder_item, max_depth, current_depth + 1) - - # 再添加文件(显示重要文件类型,包括图片和模型) - important_extensions = { - # 编程文件 - '.py', '.js', '.html', '.css', '.java', '.cpp', '.c', '.php', '.rb', '.go', '.rs', - # 文档文件 - '.md', '.txt', '.pdf', '.doc', '.docx', '.rtf', - # 配置和数据文件 - '.json', '.xml', '.yaml', '.yml', '.ini', '.cfg', '.toml', - # 图片文件 - '.jpg', '.jpeg', '.png', '.gif', '.bmp', '.svg', '.ico', '.webp', '.tiff', '.tif', - # 3D模型文件 - '.fbx', '.obj', '.3ds', '.max', '.blend', '.dae', '.gltf', '.glb', '.stl', '.ply', - # 音视频文件 - '.mp3', '.wav', '.mp4', '.avi', '.mov', '.wmv', '.flac', '.aac', - # 压缩文件 - '.zip', '.rar', '.7z', '.tar', '.gz', - # 材质和纹理 - '.mtl', '.mat', '.exr', '.hdr', '.hdri', '.dds', - # CAD文件 - '.dwg', '.dxf', '.step', '.stp', '.iges', '.sldprt', '.sldasm', - # 其他重要文件 - '.exe', '.dll', '.bat', '.sh', '.log' - } - - # 重要文件名(不依赖扩展名) - important_files = {'requirements.txt', 'README.md', 'main.py', 'LICENSE', 'CHANGELOG.md', 'INSTALL.md'} - - for file in files: - file_ext = os.path.splitext(file)[1].lower() - if file_ext in important_extensions or file in important_files: - file_path = os.path.join(path, file) - file_item = self.create_simple_tree_item(file, file_path, False) - parent_item.addChild(file_item) - - except (OSError, PermissionError): - pass - - def create_simple_tree_item(self, name, path, is_folder): - """创建简单的树形项目""" - try: - if is_folder: - # 文件夹项目,展开/折叠图标由CSS样式控制 - item = QTreeWidgetItem([f"📁 {name}"]) - item.setData(0, Qt.UserRole + 1, True) # 标记为文件夹 - else: - icon = self.get_file_icon(name) - item = QTreeWidgetItem([f"{icon} {name}"]) - item.setData(0, Qt.UserRole + 1, False) # 标记为文件 - - item.setData(0, Qt.UserRole, path) - - return item - - except (OSError, PermissionError): - item = QTreeWidgetItem([name]) - item.setData(0, Qt.UserRole, path) - return item - - def get_file_icon(self, filename): - """ - 根据文件扩展名获取图标 - - 这个函数根据文件的扩展名返回对应的Unicode图标字符。 - 如果文件类型不在映射表中,返回默认的文档图标。 - - 参数: - filename (str): 文件名(包含扩展名) - - 返回值: - str: 对应的Unicode图标字符 - """ - # 获取文件扩展名并转换为小写 - ext = os.path.splitext(filename)[1].lower() - - # 文件扩展名到图标的映射表 - icon_map = { - # 编程语言文件 - '.py': '🐍', # Python文件 - '.js': '⚡', # JavaScript文件 - '.html': '🌐', # HTML文件 - '.css': '🎨', # CSS样式文件 - '.java': '☕', # Java文件 - '.cpp': '⚙️', # C++文件 - '.c': '⚙️', # C文件 - '.h': '📋', # 头文件 - '.php': '🐘', # PHP文件 - '.rb': '💎', # Ruby文件 - '.go': '🐹', # Go文件 - '.rs': '🦀', # Rust文件 - '.swift': '🐦', # Swift文件 - '.kt': '🎯', # Kotlin文件 - - # 文档文件 - '.txt': '📄', # 纯文本文件 - '.md': '📝', # Markdown文档 - '.rst': '📝', # reStructuredText文档 - '.pdf': '📕', # PDF文档 - '.doc': '📘', # Word文档 - '.docx': '📘', # Word文档 - '.rtf': '📄', # RTF文档 - '.odt': '📄', # OpenDocument文本 - - # 数据文件 - '.json': '📋', # JSON数据文件 - '.xml': '📋', # XML文件 - '.yaml': '📋', # YAML文件 - '.yml': '📋', # YAML文件 - '.csv': '📊', # CSV表格文件 - '.xls': '📗', # Excel表格 - '.xlsx': '📗', # Excel表格 - '.ods': '📊', # OpenDocument表格 - - # 图像文件 - '.jpg': '🖼️', # JPEG图像 - '.jpeg': '🖼️', # JPEG图像 - '.png': '🖼️', # PNG图像 - '.gif': '🖼️', # GIF图像 - '.bmp': '🖼️', # BMP图像 - '.svg': '🎨', # SVG矢量图 - '.ico': '🎯', # 图标文件 - '.webp': '🖼️', # WebP图像 - '.tiff': '🖼️', # TIFF图像 - '.tif': '🖼️', # TIFF图像 - - # 音视频文件 - '.mp4': '🎬', # MP4视频 - '.avi': '🎬', # AVI视频 - '.mov': '🎬', # MOV视频 - '.wmv': '🎬', # WMV视频 - '.flv': '🎬', # FLV视频 - '.webm': '🎬', # WebM视频 - '.mp3': '🎵', # MP3音频 - '.wav': '🎵', # WAV音频 - '.flac': '🎵', # FLAC音频 - '.aac': '🎵', # AAC音频 - '.ogg': '🎵', # OGG音频 - - # 压缩文件 - '.zip': '📦', # ZIP压缩包 - '.rar': '📦', # RAR压缩包 - '.7z': '📦', # 7Z压缩包 - '.tar': '📦', # TAR归档 - '.gz': '📦', # GZIP压缩 - '.bz2': '📦', # BZIP2压缩 - '.xz': '📦', # XZ压缩 - - # 可执行文件 - '.exe': '⚙️', # Windows可执行文件 - '.msi': '📦', # Windows安装包 - '.deb': '📦', # Debian包 - '.rpm': '📦', # RPM包 - '.dmg': '💿', # macOS磁盘镜像 - '.app': '📱', # macOS应用程序 - - # 系统文件 - '.dll': '🔧', # 动态链接库 - '.so': '🔧', # 共享库 - '.dylib': '🔧', # macOS动态库 - '.lib': '📚', # 静态库 - - # 脚本文件 - '.bat': '📜', # Windows批处理 - '.cmd': '📜', # Windows命令脚本 - '.sh': '📜', # Shell脚本 - '.ps1': '💙', # PowerShell脚本 - '.vbs': '📜', # VBScript脚本 - - # 配置文件 - '.ini': '⚙️', # INI配置文件 - '.cfg': '⚙️', # 配置文件 - '.conf': '⚙️', # 配置文件 - '.config': '⚙️', # 配置文件 - '.toml': '⚙️', # TOML配置文件 - - # 3D模型文件 - '.fbx': '🎭', # FBX模型文件 - '.obj': '🎭', # OBJ模型文件 - '.3ds': '🎭', # 3DS Max模型 - '.max': '🎭', # 3DS Max场景 - '.blend': '🎭', # Blender模型 - '.dae': '🎭', # COLLADA模型 - '.gltf': '🎭', # glTF模型 - '.glb': '🎭', # glTF二进制模型 - '.x3d': '🎭', # X3D模型 - '.ply': '🎭', # PLY模型 - '.stl': '🎭', # STL模型(3D打印) - '.off': '🎭', # OFF模型 - '.3mf': '🎭', # 3MF模型 - '.amf': '🎭', # AMF模型 - '.x': '🎭', # DirectX模型 - '.md2': '🎭', # Quake II模型 - '.md3': '🎭', # Quake III模型 - '.mdl': '🎭', # Source引擎模型 - '.mesh': '🎭', # OGRE模型 - '.scene': '🎭', # OGRE场景 - '.ac': '🎭', # AC3D模型 - '.ase': '🎭', # ASCII Scene Export - '.assbin': '🎭', # Assimp二进制 - '.b3d': '🎭', # Blitz3D模型 - '.bvh': '🎭', # BioVision层次 - '.csm': '🎭', # CharacterStudio Motion - '.hmp': '🎭', # 3D GameStudio模型 - '.irr': '🎭', # Irrlicht场景 - '.irrmesh': '🎭', # Irrlicht网格 - '.lwo': '🎭', # LightWave对象 - '.lws': '🎭', # LightWave场景 - '.ms3d': '🎭', # MilkShape 3D - '.nff': '🎭', # Neutral文件格式 - '.q3o': '🎭', # Quick3D对象 - '.q3s': '🎭', # Quick3D场景 - '.raw': '🎭', # RAW三角形 - '.smd': '🎭', # Valve SMD - '.ter': '🎭', # Terragen地形 - '.uc': '🎭', # Unreal模型 - '.vta': '🎭', # Valve VTA - '.xgl': '🎭', # XGL模型 - '.zgl': '🎭', # ZGL模型 - - # 纹理和材质文件 - '.mtl': '🎨', # OBJ材质文件 - '.mat': '🎨', # 材质文件 - '.sbsar': '🎨', # Substance Archive - '.sbs': '🎨', # Substance Designer - '.sbsm': '🎨', # Substance材质 - '.exr': '🖼️', # OpenEXR高动态范围图像 - '.hdr': '🖼️', # HDR图像 - '.hdri': '🖼️', # HDRI环境贴图 - '.dds': '🖼️', # DirectDraw Surface - '.ktx': '🖼️', # Khronos纹理 - '.astc': '🖼️', # ASTC纹理 - '.pvr': '🖼️', # PowerVR纹理 - '.etc1': '🖼️', # ETC1纹理 - '.etc2': '🖼️', # ETC2纹理 - - # 动画文件 - '.anim': '🎬', # 动画文件 - '.fbx': '🎬', # FBX动画(也可以是模型) - '.bip': '🎬', # Character Studio Biped - '.cal3d': '🎬', # Cal3D动画 - '.motion': '🎬', # 动作文件 - '.mocap': '🎬', # 动作捕捉数据 - - # 其他常见文件 - '.log': '📋', # 日志文件 - '.tmp': '🗂️', # 临时文件 - '.bak': '💾', # 备份文件 - '.old': '📦', # 旧文件 - } - - # 返回对应的图标,如果找不到则返回默认文档图标 - return icon_map.get(ext, '📄') - - def startDrag(self, supportedActions): - """开始拖拽操作""" - selected_items = self.selectedItems() - if not selected_items: - return - - # 创建 MIME 数据 - mimeData = QMimeData() - - # 收集文件路径用于向外拖拽 - urls = [] - internal_paths = [] - - for item in selected_items: - filepath = item.data(0, Qt.UserRole) - if filepath: - internal_paths.append(filepath) - # 检查是否是模型文件(用于向外拖拽) - if filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')): - urls.append(QUrl.fromLocalFile(filepath)) - - # 设置内部拖拽数据 - mimeData.setText('\n'.join(internal_paths)) - - # 设置向外拖拽数据 - if urls: - mimeData.setUrls(urls) - - # 创建拖拽对象 - drag = QDrag(self) - drag.setMimeData(mimeData) - - # 设置拖拽图标 - pixmap = QPixmap(32, 32) - pixmap.fill(Qt.transparent) - painter = QPainter(pixmap) - painter.drawText(pixmap.rect(), Qt.AlignCenter, str(len(selected_items))) - painter.end() - drag.setPixmap(pixmap) - - # 执行拖拽 - drag.exec_(supportedActions) - - def dragEnterEvent(self, event): - """处理拖拽进入事件""" - # 检查是否是内部拖拽 - if event.mimeData().hasText(): - event.acceptProposedAction() - else: - event.ignore() - - def dragMoveEvent(self, event): - """处理拖拽移动事件""" - if not event.mimeData().hasText(): - event.ignore() - return - - # 获取目标项 - target_item = self.itemAt(event.pos()) - selected_items = self.selectedItems() - - # 检查是否拖拽到多选区域内的项目 - if target_item and target_item in selected_items: - event.ignore() - return - - # 检查是否拖拽到自己的子级 - if target_item: - for selected_item in selected_items: - if self._isChildOf(target_item, selected_item): - event.ignore() - return - - # 如果拖到文件夹,允许拖到文件夹内部 - if target_item: - is_folder = target_item.data(0, Qt.UserRole + 1) - if is_folder: - # 接受拖放,显示指示框 - event.acceptProposedAction() - return - - - # 调用父类方法处理插入指示线 - event.accept() - super().dragMoveEvent(event) - - def _isChildOf(self, potential_child, potential_parent): - """检查 potential_child 是否是 potential_parent 的子级""" - current = potential_child.parent() - while current: - if current == potential_parent: - return True - current = current.parent() - return False - - def dropEvent(self, event): - """处理拖放事件""" - if not event.mimeData().hasText(): - event.ignore() - return - - drag_paths = event.mimeData().text().split('\n') - if not drag_paths: - event.ignore() - return - - # 获取目标项 - target_item = self.itemAt(event.pos()) - if not target_item: - # 如果拖到空白处,默认放到根目录 - target_path = self.current_path - else: - # 如果是文件夹,就放到里面 - is_folder = target_item.data(0, Qt.UserRole + 1) - if is_folder: - target_path = target_item.data(0, Qt.UserRole) - else: - # 如果是文件,则放到其父目录 - parent_item = target_item.parent() - if parent_item: - target_path = parent_item.data(0, Qt.UserRole) - else: - target_path = self.current_path - - # 执行移动 - self._moveFiles(drag_paths, target_path) - event.acceptProposedAction() - # 让 Qt 更新界面 - super().dropEvent(event) - - def _moveFiles(self, source_paths, target_dir): - """移动文件到目标目录""" - import os - import shutil - from PyQt5.QtWidgets import QMessageBox - - moved_files = [] - failed_files = [] - - for source_path in source_paths: - if not source_path or not os.path.exists(source_path): - continue - - if os.path.isdir(source_path) and target_dir.startswith(source_path): - failed_files.append(f"{source_path} (不能移动到子目录)") - continue - - if os.path.dirname(source_path) == target_dir: - continue - - filename = os.path.basename(source_path) - target_path = os.path.join(target_dir, filename) - - if os.path.exists(target_path): - failed_files.append(f"{filename} (目标已存在)") - continue - - try: - shutil.move(source_path, target_path) - moved_files.append(filename) - print(f"移动文件: {source_path} -> {target_path}") - except Exception as e: - failed_files.append(f"{filename} ({str(e)})") - - # 使用状态保持刷新 - if moved_files: - self._refreshWithStatePreservation() - - if failed_files: - StyledMessageBox.warning( - self, - "移动失败", - f"以下文件移动失败:\n" + "\n".join(failed_files) - ) - - if moved_files: - print(f"成功移动 {len(moved_files)} 个文件") - - # def mouseDoubleClickEvent(self, event): - # """处理双击事件""" - # item = self.itemAt(event.pos()) - # if item: - # filepath = item.data(0, Qt.UserRole) - # is_folder = item.data(0, Qt.UserRole + 1) - # - # if is_folder: - # # 文件夹:展开/折叠 - # item.setExpanded(not item.isExpanded()) - # else: - # # 文件:检查是否是模型文件 - # if filepath and filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')): - # self.world.importModel(filepath) - # else: - # # 其他文件:用系统默认程序打开 - # self.openFile(filepath) - # - # super().mouseDoubleClickEvent(event) - - -class CustomConsoleDockWidget(QWidget): - """自定义控制台停靠部件""" - - def __init__(self, world, parent=None): - if parent is None: - parent = wrapinstance(0, QWidget) - super().__init__(parent) - self.world = world - self.setupUI() - # 注释掉控制台重定向,让输出正常显示在终端 - # self.setupConsoleRedirect() - - def setupUI(self): - """初始化控制台UI""" - layout = QVBoxLayout(self) - - # 控制台工具栏 - toolbar = QHBoxLayout() - - # 清空按钮 - self.clearBtn = QPushButton("清空") - self.clearBtn.setStyleSheet(""" - QPushButton { - background-color: rgba(84, 89, 98, 0.5); - color: #ffffff; - border: none; - padding: 4px 12px; - border-radius: 2px; - font-family: 'Microsoft YaHei', 'Inter', sans-serif; - font-size: 12px; - font-weight: 300; - letter-spacing: 0.7px; - } - QPushButton:hover { - background-color: rgba(84, 89, 98, 0.7); - } - QPushButton:pressed { - background-color: rgba(84, 89, 98, 0.9); - } - """) - self.clearBtn.clicked.connect(self.clearConsole) - toolbar.addWidget(self.clearBtn) - - # 自动滚动开关 - self.autoScrollBtn = QPushButton("自动滚动") - self.autoScrollBtn.setCheckable(True) - self.autoScrollBtn.setChecked(True) - self.autoScrollBtn.setStyleSheet(""" - QPushButton { - background-color: rgba(84, 89, 98, 0.5); - color: #ffffff; - border: none; - padding: 4px 12px; - border-radius: 2px; - font-family: 'Microsoft YaHei', 'Inter', sans-serif; - font-size: 12px; - font-weight: 300; - letter-spacing: 0.7px; - } - QPushButton:checked { - background-color: #3067c0; - color: #ffffff; - } - QPushButton:hover { - background-color: rgba(84, 89, 98, 0.7); - } - QPushButton:pressed { - background-color: rgba(84, 89, 98, 0.9); - } - """) - toolbar.addWidget(self.autoScrollBtn) - - # 时间戳开关 - self.timestampBtn = QPushButton("显示时间") - self.timestampBtn.setCheckable(True) - self.timestampBtn.setChecked(True) - self.timestampBtn.setStyleSheet(""" - QPushButton { - background-color: rgba(84, 89, 98, 0.5); - color: #ffffff; - border: none; - padding: 4px 12px; - border-radius: 2px; - font-family: 'Microsoft YaHei', 'Inter', sans-serif; - font-size: 12px; - font-weight: 300; - letter-spacing: 0.7px; - } - QPushButton:checked { - background-color: #3067c0; - color: #ffffff; - } - QPushButton:hover { - background-color: rgba(84, 89, 98, 0.7); - } - QPushButton:pressed { - background-color: rgba(84, 89, 98, 0.9); - } - """) - toolbar.addWidget(self.timestampBtn) - - self.fpsLabel = QLabel("FPS:0.0") - self.fpsLabel.setStyleSheet(""" - QLabel { - background-color: #3067c0; - color: #ffffff; - border: none; - padding: 4px 12px; - border-radius: 2px; - font-family: 'Microsoft YaHei', 'Inter', sans-serif; - font-size: 12px; - font-weight: 300; - letter-spacing: 0.7px; - } - """) - self.fpsLabel.setMinimumWidth(100) - self.fpsLabel.setAlignment(Qt.AlignCenter) - toolbar.addWidget(self.fpsLabel) - - # 帧率更新定时器 - self.fpsTimer = QTimer() - self.fpsTimer.timeout.connect(self.updateFPS) - self.fpsTimer.start(105) # 每秒更新一次 - - toolbar.addStretch() - layout.addLayout(toolbar) - - # 控制台文本区域 - from PyQt5.QtWidgets import QTextEdit - self.consoleText = QTextEdit() - self.consoleText.setReadOnly(True) - self.consoleText.setStyleSheet(""" - QTextEdit { - background-color: #19191b; - color: #ebebeb; - font-family: 'Consolas', 'Monaco', 'Microsoft YaHei', monospace; - font-size: 10px; - font-weight: 300; - border: none; - letter-spacing: 0.5px; - line-height: 12px; - } - """) - layout.addWidget(self.consoleText) - - # # 命令输入区域 - # inputLayout = QHBoxLayout() - # inputLayout.addWidget(QLabel(">>> ")) - # - # self.commandInput = QLineEdit() - # self.commandInput.setStyleSheet(""" - # QLineEdit { - # background-color: #2d2d2d; - # color: #ffffff; - # font-family: 'Consolas', 'Monaco', monospace; - # font-size: 10pt; - # border: 1px solid #3e3e3e; - # padding: 5px; - # } - # """) - # self.commandInput.returnPressed.connect(self.executeCommand) - # inputLayout.addWidget(self.commandInput) - # - # layout.addLayout(inputLayout) - - # 添加欢迎信息 - self.addMessage("🎮 编辑器控制台已启动", "INFO") - - def updateFPS(self): - try: - if hasattr(self.world,'clock'): - fps = self.world.clock.getAverageFrameRate() - self.fpsLabel.setText(f"FPS:{fps:.1f}") - - # 根据帧率设置颜色 - if fps >= 50: - color = "#80ff80" # 绿色 - 优秀 - elif fps >= 30: - color = "#ffff80" # 黄色 - 一般 - else: - color = "#ff8080" # 红色 - 较差 - - self.fpsLabel.setStyleSheet(f""" - QLabel {{ - background-color: #3067c0; - color: {color}; - border: 1px solid #3a3a4a; - padding: 6px 12px; - border-radius: 4px; - font-weight: 500; - font-family: 'Consolas', 'Monaco', monospace; - }} - """) - except Exception as e: - pass # 静默处理错误,避免影响控制台功能 - - def setupConsoleRedirect(self): - """设置控制台重定向""" - import sys - - # 保存原始的stdout和stderr - self.original_stdout = sys.stdout - self.original_stderr = sys.stderr - - # 创建自定义输出流 - sys.stdout = ConsoleRedirect(self, "STDOUT") - sys.stderr = ConsoleRedirect(self, "STDERR") - - def addMessage(self, message, msg_type="INFO"): - """添加消息到控制台""" - import datetime - - # 获取当前时间 - timestamp = datetime.datetime.now().strftime("%H:%M:%S") - - # 根据消息类型设置颜色 - color_map = { - "INFO": "#ffffff", # 白色 - "WARNING": "#ffaa00", # 橙色 - "ERROR": "#ff4444", # 红色 - "SUCCESS": "#44ff44", # 绿色 - "STDOUT": "#cccccc", # 浅灰色 - "STDERR": "#ff6666", # 浅红色 - } - - color = color_map.get(msg_type, "#ffffff") - - # 构建HTML格式的消息 - if self.timestampBtn.isChecked(): - html_message = f'[{timestamp}] {message}' - else: - html_message = f'{message}' - - # 添加到控制台 - self.consoleText.append(html_message) - - # 自动滚动到底部 - if self.autoScrollBtn.isChecked(): - scrollbar = self.consoleText.verticalScrollBar() - scrollbar.setValue(scrollbar.maximum()) - - def clearConsole(self): - """清空控制台""" - self.consoleText.clear() - self.addMessage("控制台已清空", "INFO") - - def executeCommand(self): - """执行命令""" - command = self.commandInput.text().strip() - if not command: - return - - # 显示输入的命令 - self.addMessage(f">>> {command}", "INFO") - self.commandInput.clear() - - try: - # 执行Python命令 - if hasattr(self.world, 'executeCommand'): - result = self.world.executeCommand(command) - if result: - self.addMessage(str(result), "SUCCESS") - else: - # 简单的eval执行 - result = eval(command) - if result is not None: - self.addMessage(str(result), "SUCCESS") - - except Exception as e: - self.addMessage(f"错误: {str(e)}", "ERROR") - - def cleanup(self): - """清理资源""" - import sys - - # 恢复原始的stdout和stderr - if hasattr(self, 'original_stdout'): - sys.stdout = self.original_stdout - if hasattr(self, 'original_stderr'): - sys.stderr = self.original_stderr - - -class ConsoleRedirect: - """控制台重定向类""" - - def __init__(self, console_widget, stream_type): - self.console_widget = console_widget - self.stream_type = stream_type - # 保存原始输出流的引用 - if stream_type == "STDOUT": - self.original_stream = sys.stdout - else: - self.original_stream = sys.stderr - - def write(self, text): - """重定向写入 - 同时输出到GUI控制台和原始终端""" - # 先输出到原始终端 - self.original_stream.write(text) - - # 然后输出到GUI控制台(仅非空行) - if text.strip(): # 忽略空行 - self.console_widget.addMessage(text.strip(), self.stream_type) - - def flush(self): - """刷新缓冲区 - 同时刷新原始流""" - self.original_stream.flush() - -class StyledMessageBox: - """统一样式的消息框辅助类""" - - MESSAGEBOX_STYLE = """ - QMessageBox { - background-color: #1e1e1f; - color: #ebebeb; - border: 1px solid rgba(77, 116, 189, 0.3); - border-radius: 8px; - padding: 20px; - min-width: 300px; - } - QMessageBox QLabel { - color: #ffffff; - font-family: 'Microsoft YaHei', 'Inter', sans-serif; - font-size: 13px; - font-weight: 400; - line-height: 1.4; - padding: 10px 0px; - } - QMessageBox QPushButton { - background-color: rgba(89, 100, 113, 0.4); - color: rgba(255, 255, 255, 0.8); - border: 1px solid rgba(76, 92, 110, 0.4); - border-radius: 6px; - padding: 8px 16px; - font-family: 'Microsoft YaHei', 'Inter', sans-serif; - font-weight: 400; - font-size: 11px; - min-width: 80px; - min-height: 28px; - } - QMessageBox QPushButton:hover { - background-color: rgba(89, 100, 113, 0.6); - border: 1px solid rgba(77, 116, 189, 0.6); - color: #ffffff; - } - QMessageBox QPushButton:pressed, QMessageBox QPushButton:checked { - background-color: rgba(77, 116, 189, 0.8); - border: 1px solid #4d74bd; - color: #ffffff; - } - QMessageBox QPushButton:disabled { - background-color: rgba(89, 100, 113, 0.2); - color: rgba(235, 235, 235, 0.4); - border: 1px solid rgba(76, 92, 110, 0.2); - } - QMessageBox QPushButton:default { - background-color: rgba(77, 116, 189, 0.8); - border: 1px solid #4d74bd; - color: #ffffff; - font-weight: 500; - } - QMessageBox QPushButton:default:hover { - background-color: rgba(77, 116, 189, 1.0); - border: 1px solid rgba(77, 116, 189, 1.0); - } - QMessageBox QIcon { - padding: 0px 10px 0px 0px; - } - """ - - INPUTDIALOG_STYLE = """ - QInputDialog { - background-color: #1e1e1f; - color: #ebebeb; - border: 1px solid rgba(77, 116, 189, 0.3); - border-radius: 8px; - padding: 20px; - min-width: 350px; - } - QLabel { - color: #ffffff; - font-family: 'Microsoft YaHei', 'Inter', sans-serif; - font-weight: 500; - font-size: 13px; - padding: 0px 0px 10px 0px; - } - QLineEdit { - background-color: rgba(89, 100, 113, 0.15); - color: #ebebeb; - border: 1px solid rgba(76, 92, 110, 0.4); - border-radius: 6px; - padding: 10px 12px; - font-family: 'Microsoft YaHei', 'Inter', sans-serif; - font-size: 11px; - font-weight: 400; - min-height: 16px; - } - QLineEdit:focus { - border: 1px solid #4d74bd; - background-color: rgba(77, 116, 189, 0.1); - } - QLineEdit:hover { - border: 1px solid rgba(77, 116, 189, 0.6); - background-color: rgba(89, 100, 113, 0.2); - } - QPushButton { - background-color: rgba(89, 100, 113, 0.4); - color: rgba(255, 255, 255, 0.8); - border: 1px solid rgba(76, 92, 110, 0.4); - border-radius: 6px; - padding: 8px 16px; - font-family: 'Microsoft YaHei', 'Inter', sans-serif; - font-weight: 400; - font-size: 11px; - min-width: 80px; - min-height: 28px; - } - QPushButton:hover { - background-color: rgba(89, 100, 113, 0.6); - border: 1px solid rgba(77, 116, 189, 0.6); - color: #ffffff; - } - QPushButton:pressed, QPushButton:checked { - background-color: rgba(77, 116, 189, 0.8); - border: 1px solid #4d74bd; - color: #ffffff; - } - QPushButton:disabled { - background-color: rgba(89, 100, 113, 0.2); - color: rgba(235, 235, 235, 0.4); - border: 1px solid rgba(76, 92, 110, 0.2); - } - QPushButton:default { - background-color: rgba(77, 116, 189, 0.8); - border: 1px solid #4d74bd; - color: #ffffff; - font-weight: 500; - } - """ - - BUTTON_STYLE = """ - QPushButton { - background-color: rgba(89, 100, 113, 0.4); - color: rgba(255, 255, 255, 0.8); - border: 1px solid rgba(76, 92, 110, 0.4); - border-radius: 6px; - padding: 8px 16px; - font-family: 'Microsoft YaHei', 'Inter', sans-serif; - font-weight: 400; - font-size: 11px; - min-width: 80px; - min-height: 28px; - } - QPushButton:hover { - background-color: rgba(89, 100, 113, 0.6); - border: 1px solid rgba(77, 116, 189, 0.6); - color: #ffffff; - } - QPushButton:pressed, QPushButton:checked { - background-color: rgba(77, 116, 189, 0.8); - border: 1px solid #4d74bd; - color: #ffffff; - } - QPushButton:disabled { - background-color: rgba(89, 100, 113, 0.2); - color: rgba(235, 235, 235, 0.4); - border: 1px solid rgba(76, 92, 110, 0.2); - } - """ - - @staticmethod - def information(parent, title, message): - """显示信息提示框""" - msg = QMessageBox(QMessageBox.Information, title, message, QMessageBox.Ok, parent) - msg.setStyleSheet(StyledMessageBox.MESSAGEBOX_STYLE) - # 强制设置所有按钮的样式 - for button in msg.buttons(): - button.setStyleSheet(StyledMessageBox.BUTTON_STYLE) - return msg.exec_() - - @staticmethod - def question(parent, title, message, buttons=QMessageBox.Yes | QMessageBox.No, defaultButton=QMessageBox.No): - """显示确认对话框""" - msg = QMessageBox(QMessageBox.Question, title, message, buttons, parent) - msg.setDefaultButton(defaultButton) - msg.setStyleSheet(StyledMessageBox.MESSAGEBOX_STYLE) - # 强制设置所有按钮的样式 - for button in msg.buttons(): - button.setStyleSheet(StyledMessageBox.BUTTON_STYLE) - return msg.exec_() - - @staticmethod - def warning(parent, title, message): - """显示警告对话框""" - msg = QMessageBox(QMessageBox.Warning, title, message, QMessageBox.Ok, parent) - msg.setStyleSheet(StyledMessageBox.MESSAGEBOX_STYLE) - # 强制设置所有按钮的样式 - for button in msg.buttons(): - button.setStyleSheet(StyledMessageBox.BUTTON_STYLE) - return msg.exec_() - - @staticmethod - def critical(parent, title, message): - """显示错误对话框""" - msg = QMessageBox(QMessageBox.Critical, title, message, QMessageBox.Ok, parent) - msg.setStyleSheet(StyledMessageBox.MESSAGEBOX_STYLE) - # 强制设置所有按钮的样式 - for button in msg.buttons(): - button.setStyleSheet(StyledMessageBox.BUTTON_STYLE) - return msg.exec_() - - @staticmethod - def getText(parent, title, label, text=""): - """显示样式统一的文本输入对话框""" - from PyQt5.QtWidgets import QInputDialog - dialog = QInputDialog(parent) - dialog.setWindowTitle(title) - dialog.setLabelText(label) - dialog.setTextValue(text) - dialog.setStyleSheet(StyledMessageBox.INPUTDIALOG_STYLE) - - # 强制设置所有按钮的样式 - for button in dialog.findChildren(QPushButton): - button.setStyleSheet(StyledMessageBox.BUTTON_STYLE) - - ok = dialog.exec_() - return dialog.textValue(), ok - -class UniversalMessageDialog(QDialog): - """通用消息对话框类 - 支持不同图标和按钮配置""" - - # 消息类型枚举 - SUCCESS = "success_icon" - WARNING = "warning_icon" - ERROR = "fail_icon" - INFO = "info" - - def __init__(self, parent, title, message, message_type=INFO, show_cancel=True, - confirm_text="确认", cancel_text="取消", icon_size=QSize(20, 20)): - """ - 初始化通用消息对话框 - - Args: - parent: 父窗口 - title: 对话框标题 - message: 消息内容 - message_type: 消息类型 (SUCCESS, WARNING, ERROR, INFO) - show_cancel: 是否显示取消按钮 - confirm_text: 确认按钮文字 - cancel_text: 取消按钮文字 - icon_size: 图标尺寸 - """ - super().__init__(parent) - self.setWindowTitle(title) - self.setObjectName("universalMessageDialog") - self.setModal(True) - self.resize(508, 134) - self.setMinimumSize(508, 134) - self.setWindowFlags(Qt.Dialog | Qt.FramelessWindowHint) - self.setAttribute(Qt.WA_TranslucentBackground, True) - - # 对话框配置 - self.message_type = message_type - self.show_cancel = show_cancel - self.confirm_text = confirm_text - self.cancel_text = cancel_text - self.icon_size = icon_size - - # 拖拽相关 - self.dragging = False - self.drag_position = QPoint() - - # 图标管理 - self.icon_manager = get_icon_manager() - self._title_icon_size = QSize(18, 18) - self._icon_close = self.icon_manager.get_icon('close_icon', self._title_icon_size) - - # 根据消息类型获取对应图标 - self._message_icon = self._get_message_icon() - - # 设置样式 - self._setup_styles() - - # 创建UI - self._create_ui(message) - - def _get_message_icon(self): - """根据消息类型获取对应图标""" - icon_map = { - self.SUCCESS: 'success_icon', - self.WARNING: 'warning_icon', - self.ERROR: 'fail_icon', - self.INFO: 'success_icon' # 默认使用成功图标 - } - - icon_name = icon_map.get(self.message_type, 'success_icon') - return self.icon_manager.get_icon(icon_name, self.icon_size) - - def _setup_styles(self): - """设置对话框样式""" - self.setStyleSheet(""" - QDialog#universalMessageDialog { - background-color: transparent; - border: none; - } - QFrame#baseFrame { - background-color: #19191B; - border: 1px solid #3E3E42; - border-radius: 5px; - } - QWidget#titleBar { - background-color: transparent; - border: none; - border-radius: 5px 5px 0px 0px; - min-height: 32px; - max-height: 32px; - } - QWidget#titleBar QWidget { - background-color: transparent; - border: none; - } - QLabel#titleLabel { - color: #FFFFFF; - font-family: 'Inter', 'Microsoft YaHei', sans-serif; - font-size: 14px; - font-weight: 500; - letter-spacing: 0.7px; - } - QWidget#controlButtons QPushButton { - background-color: transparent; - border: none; - color: #EBEBEB; - font-size: 14px; - min-width: 18px; - max-width: 18px; - min-height: 18px; - max-height: 18px; - padding: 0px; - border-radius: 3px; - } - QWidget#controlButtons QPushButton:hover { - background-color: #2A2D2E; - color: #FFFFFF; - } - QPushButton#closeButton { - border-radius: 0px 5px 0px 0px; - } - QPushButton#closeButton:hover { - background-color: #2A2D2E; - color: #FFFFFF; - } - QFrame#titleSeparator { - background-color: #3E3E42; - border: none; - min-height: 1px; - max-height: 1px; - } - QWidget#contentWidget { - background-color: transparent; - border: none; - } - QLabel#messageLabel { - color: #EBEBEB; - font-family: 'Inter', 'Microsoft YaHei', sans-serif; - font-size: 13px; - font-weight: 400; - letter-spacing: 0.6px; - line-height: 1.4; - padding: 0px; - margin: 0px; - } - QPushButton { - background-color: rgba(89, 98, 118, 0.5); - color: #EBEBEB; - border: none; - border-radius: 2px; - padding: 0px 12px; - font-family: 'Inter', 'Microsoft YaHei', sans-serif; - font-weight: 300; - font-size: 10px; - letter-spacing: 0.5px; - min-width: 90px; - max-width: 90px; - min-height: 28px; - max-height: 28px; - } - QPushButton:hover { - background-color: #3067C0; - color: #FFFFFF; - } - QPushButton:pressed { - background-color: #2556A0; - color: #FFFFFF; - } - QPushButton#primaryButton { - background-color: rgba(89, 98, 118, 0.5); - border: none; - color: #EBEBEB; - font-weight: 300; - } - QPushButton#primaryButton:hover { - background-color: #2556A0; - } - QPushButton#primaryButton:pressed { - background-color: #1E4A8C; - } - QPushButton#secondaryButton { - background-color: rgba(89, 98, 118, 0.5); - border: none; - color: #EBEBEB; - } - QPushButton#secondaryButton:hover { - background-color: #3067C0; - color: #FFFFFF; - } - QPushButton#secondaryButton:pressed { - background-color: #2556A0; - color: #FFFFFF; - } - """) - - def _create_ui(self, message): - """构建用户界面""" - main_layout = QVBoxLayout(self) - main_layout.setContentsMargins(0, 0, 0, 0) - main_layout.setSpacing(0) - - base_frame = QFrame() - base_frame.setObjectName('baseFrame') - base_frame.setFrameShape(QFrame.NoFrame) - base_frame.setAttribute(Qt.WA_StyledBackground, True) - - base_layout = QVBoxLayout(base_frame) - base_layout.setContentsMargins(25, 14, 25, 14) - base_layout.setSpacing(0) - - self._create_title_bar() - base_layout.addWidget(self.title_bar) - base_layout.addSpacing(4) - - title_separator = QFrame() - title_separator.setObjectName('titleSeparator') - title_separator.setFrameShape(QFrame.HLine) - title_separator.setFrameShadow(QFrame.Plain) - base_layout.addWidget(title_separator) - base_layout.addSpacing(10) - - content_widget = QWidget() - content_widget.setObjectName('contentWidget') - content_layout = QVBoxLayout(content_widget) - content_layout.setContentsMargins(0, 0, 0, 0) - content_layout.setSpacing(10) - - message_area = QHBoxLayout() - message_area.setContentsMargins(0, 0, 0, 0) - message_area.setSpacing(10) - - # 用一个垂直布局包裹icon_label,确保顶部对齐 - icon_vbox = QVBoxLayout() - icon_vbox.setContentsMargins(0, 0, 0, 0) - icon_vbox.setSpacing(0) - icon_label = QLabel() - if self._message_icon and not self._message_icon.isNull(): - icon_label.setPixmap(self._message_icon.pixmap(self.icon_size)) - icon_label.setAlignment(Qt.AlignTop | Qt.AlignLeft) - icon_label.setFixedSize(self.icon_size) - icon_vbox.addWidget(icon_label, alignment=Qt.AlignTop) - icon_vbox.addStretch() - message_area.addLayout(icon_vbox) - - self.message_label = QLabel(message) - self.message_label.setObjectName('messageLabel') - self.message_label.setWordWrap(True) - self.message_label.setAlignment(Qt.AlignTop | Qt.AlignLeft) - self.message_label.setMinimumHeight(self.icon_size.height()) - self.message_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum) - message_area.addWidget(self.message_label, 1) - message_area.addStretch() - - content_layout.addLayout(message_area) - base_layout.addWidget(content_widget) - base_layout.addSpacing(10) - - button_widget = QWidget() - button_layout = QHBoxLayout(button_widget) - button_layout.setContentsMargins(0, 0, 0, 0) - button_layout.setSpacing(10) - button_layout.addStretch() - - if self.show_cancel: - self.cancel_button = QPushButton(self.cancel_text) - self.cancel_button.setObjectName("secondaryButton") - self.cancel_button.clicked.connect(self.reject) - self.cancel_button.setFixedSize(90, 28) - button_layout.addWidget(self.cancel_button) - - self.confirm_button = QPushButton(self.confirm_text) - self.confirm_button.setObjectName("primaryButton") - self.confirm_button.clicked.connect(self.accept) - self.confirm_button.setFixedSize(90, 28) - button_layout.addWidget(self.confirm_button) - - self.confirm_button.setDefault(True) - self.confirm_button.setAutoDefault(True) - if self.show_cancel: - self.cancel_button.setAutoDefault(False) - - base_layout.addWidget(button_widget) - - main_layout.addWidget(base_frame) - - def _create_title_bar(self): - """创建自定义标题栏""" - self.title_bar = QFrame() - self.title_bar.setObjectName("titleBar") - - title_layout = QHBoxLayout(self.title_bar) - title_layout.setContentsMargins(0, 0, 0, 0) - title_layout.setSpacing(0) - - self.title_label = QLabel(self.windowTitle()) - self.title_label.setObjectName("titleLabel") - self.title_label.setAlignment(Qt.AlignVCenter | Qt.AlignLeft) - title_layout.addWidget(self.title_label, 1) - - title_layout.addStretch() - - controls = QWidget() - controls.setObjectName("controlButtons") - controls_layout = QHBoxLayout(controls) - controls_layout.setContentsMargins(0, 0, 0, 0) - controls_layout.setSpacing(0) - - self.close_button = QPushButton() - self.close_button.setObjectName("closeButton") - self.close_button.clicked.connect(self.reject) - self.close_button.setFocusPolicy(Qt.NoFocus) - controls_layout.addWidget(self.close_button) - - self._apply_title_bar_icons() - - title_layout.addWidget(controls) - - - def _apply_title_bar_icons(self): - """应用标题栏图标""" - if self._icon_close: - self.close_button.setIcon(self._icon_close) - self.close_button.setIconSize(self._title_icon_size) - self.close_button.setText("") - self.close_button.setToolTip("关闭") - - def setWindowTitle(self, title): - """设置窗口标题""" - super().setWindowTitle(title) - if hasattr(self, "title_label"): - self.title_label.setText(title) - - def mousePressEvent(self, event): - """鼠标按下事件 - 用于拖拽窗口""" - if event.button() == Qt.LeftButton and self.title_bar.geometry().contains(event.pos()): - self.dragging = True - self.drag_position = event.globalPos() - self.frameGeometry().topLeft() - event.accept() - super().mousePressEvent(event) - - def mouseMoveEvent(self, event): - """鼠标移动事件 - 用于拖拽窗口""" - if event.buttons() == Qt.LeftButton and self.dragging: - self.move(event.globalPos() - self.drag_position) - event.accept() - super().mouseMoveEvent(event) - - def mouseReleaseEvent(self, event): - """鼠标释放事件 - 停止拖拽""" - if event.button() == Qt.LeftButton: - self.dragging = False - super().mouseReleaseEvent(event) - - @staticmethod - def show_success(parent, title, message, show_cancel=False, confirm_text="确认"): - """显示成功消息对话框""" - dialog = UniversalMessageDialog( - parent, title, message, - UniversalMessageDialog.SUCCESS, - show_cancel, confirm_text - ) - return dialog.exec_() - - @staticmethod - def show_warning(parent, title, message, show_cancel=True, confirm_text="确认", cancel_text="取消"): - """显示警告消息对话框""" - dialog = UniversalMessageDialog( - parent, title, message, - UniversalMessageDialog.WARNING, - show_cancel, confirm_text, cancel_text - ) - return dialog.exec_() - - @staticmethod - def show_error(parent, title, message, show_cancel=False, confirm_text="确认"): - """显示错误消息对话框""" - dialog = UniversalMessageDialog( - parent, title, message, - UniversalMessageDialog.ERROR, - show_cancel, confirm_text - ) - return dialog.exec_() - - @staticmethod - def show_info(parent, title, message, show_cancel=True, confirm_text="确认", cancel_text="取消"): - """显示信息消息对话框""" - dialog = UniversalMessageDialog( - parent, title, message, - UniversalMessageDialog.INFO, - show_cancel, confirm_text, cancel_text - ) - return dialog.exec_() - - -class StyledTextInputDialog(QDialog): - """与新建项目样式一致的文本输入对话框""" - - def __init__(self, parent, title, label_text="", placeholder="", default_text=""): - super().__init__(parent) - self.setWindowTitle(title) - self.setObjectName("styledTextInputDialog") - self.setModal(True) - self.resize(420, 186) - self.setWindowFlags(Qt.Dialog | Qt.FramelessWindowHint) - self.setAttribute(Qt.WA_TranslucentBackground, True) - - self.dragging = False - self.drag_position = QPoint() - self.icon_manager = get_icon_manager() - self._title_icon_size = QSize(18, 18) - self._icon_close = self.icon_manager.get_icon('close_icon', self._title_icon_size) - - self.setStyleSheet(""" - QDialog#styledTextInputDialog { - background-color: transparent; - border: none; - } - QFrame#baseFrame { - background-color: #000000; - border: 1px solid #3E3E42; - border-radius: 5px; - } - QWidget#titleBar { - background-color: transparent; - border: none; - border-radius: 5px 5px 0px 0px; - min-height: 32px; - max-height: 32px; - } - QWidget#titleBar QWidget { - background-color: transparent; - border: none; - } - QLabel#titleLabel { - color: #FFFFFF; - font-family: 'Inter', 'Microsoft YaHei', sans-serif; - font-size: 14px; - font-weight: 500; - letter-spacing: 0.7px; - } - QWidget#controlButtons QPushButton { - background-color: transparent; - border: none; - color: #EBEBEB; - font-size: 14px; - min-width: 18px; - max-width: 18px; - min-height: 18px; - max-height: 18px; - padding: 0px; - border-radius: 3px; - } - QWidget#controlButtons QPushButton:hover { - background-color: #2A2D2E; - color: #FFFFFF; - } - QPushButton#closeButton { - border-radius: 0px 5px 0px 0px; - } - QPushButton#closeButton:hover { - background-color: #2A2D2E; - color: #FFFFFF; - } - QWidget#contentWidget { - background-color: transparent; - border-radius: 0px 0px 5px 5px; - } - QFrame#contentContainer { - background-color: #19191B; - border: 1px solid #2C2F36; - border-radius: 5px; - } - QLabel[role="fieldLabel"] { - color: #EBEBEB; - font-family: 'Inter', 'Microsoft YaHei', sans-serif; - font-size: 12px; - font-weight: 400; - letter-spacing: 0.6px; - } - QLineEdit { - background-color: rgba(89, 100, 113, 0.2); - color: #EBEBEB; - border: 1px solid rgba(76, 92, 110, 0.6); - border-radius: 2px; - padding: 0px 10px; - font-family: 'Inter', 'Microsoft YaHei', sans-serif; - font-size: 11px; - font-weight: 300; - letter-spacing: 0.55px; - min-height: 30px; - max-height: 30px; - } - QLineEdit:focus { - border: 1px solid #3067C0; - background-color: rgba(48, 103, 192, 0.1); - } - QLineEdit:hover { - border: 1px solid #3067C0; - background-color: rgba(89, 100, 113, 0.3); - } - QPushButton { - background-color: rgba(89, 98, 118, 0.5); - color: #EBEBEB; - border: none; - border-radius: 2px; - padding: 0px 12px; - font-family: 'Inter', 'Microsoft YaHei', sans-serif; - font-weight: 300; - font-size: 10px; - letter-spacing: 0.5px; - min-width: 120px; - min-height: 30px; - max-height: 30px; - } - QPushButton:hover { - background-color: #3067C0; - color: #FFFFFF; - } - QPushButton:pressed { - background-color: #2556A0; - color: #FFFFFF; - } - """) - - main_layout = QVBoxLayout(self) - main_layout.setContentsMargins(0, 0, 0, 0) - main_layout.setSpacing(0) - - base_frame = QFrame() - base_frame.setObjectName('baseFrame') - base_frame.setFrameShape(QFrame.NoFrame) - base_frame.setAttribute(Qt.WA_StyledBackground, True) - - base_layout = QVBoxLayout(base_frame) - base_layout.setContentsMargins(0, 0, 0, 0) - base_layout.setSpacing(0) - - self._createTitleBar() - base_layout.addWidget(self.title_bar) - base_layout.addSpacing(10) - - content_widget = QWidget() - content_widget.setObjectName('contentWidget') - content_layout = QVBoxLayout(content_widget) - content_layout.setContentsMargins(10, 0, 10, 10) - content_layout.setSpacing(0) - - content_container = QFrame() - content_container.setObjectName('contentContainer') - content_container.setFrameShape(QFrame.NoFrame) - content_container.setAttribute(Qt.WA_StyledBackground, True) - content_container.setFixedWidth(400) - - container_layout = QVBoxLayout(content_container) - container_layout.setContentsMargins(15, 10, 15, 10) - container_layout.setSpacing(10) - - if label_text: - label = QLabel(label_text) - label.setProperty('role', 'fieldLabel') - container_layout.addWidget(label) - - self.line_edit = QLineEdit() - self.line_edit.setPlaceholderText(placeholder) - self.line_edit.setText(default_text) - container_layout.addWidget(self.line_edit) - - separator = QFrame() - separator.setFrameShape(QFrame.HLine) - separator.setFrameShadow(QFrame.Plain) - separator.setFixedHeight(1) - separator.setStyleSheet("background-color: #2C2F36; border: none;") - container_layout.addWidget(separator) - - button_row = QHBoxLayout() - button_row.setContentsMargins(0, 0, 0, 0) - button_row.setSpacing(10) - button_row.addStretch() - - self.confirmButton = QPushButton("确认") - self.confirmButton.setObjectName("primaryButton") - self.confirmButton.clicked.connect(self._onAccept) - button_row.addWidget(self.confirmButton) - - self.cancelButton = QPushButton("取消") - self.cancelButton.setObjectName("secondaryButton") - self.cancelButton.clicked.connect(self.reject) - button_row.addWidget(self.cancelButton) - - container_layout.addLayout(button_row) - - content_layout.addWidget(content_container, 0, Qt.AlignTop) - base_layout.addWidget(content_widget) - main_layout.addWidget(base_frame) - - self.confirmButton.setDefault(True) - self.confirmButton.setAutoDefault(True) - self.line_edit.selectAll() - self.line_edit.setFocus() - - def _createTitleBar(self): - self.title_bar = QFrame() - self.title_bar.setObjectName("titleBar") - - title_layout = QHBoxLayout(self.title_bar) - title_layout.setContentsMargins(12, 6, 12, 6) - title_layout.setSpacing(0) - - controls = QWidget() - controls.setObjectName("controlButtons") - controls_layout = QHBoxLayout(controls) - controls_layout.setContentsMargins(0, 0, 0, 0) - controls_layout.setSpacing(0) - - self.close_button = QPushButton() - self.close_button.setObjectName("closeButton") - self.close_button.clicked.connect(self.reject) - self.close_button.setFocusPolicy(Qt.NoFocus) - self.close_button.setFixedSize(18, 18) - controls_layout.addWidget(self.close_button) - - self._applyTitleBarIcons() - - left_placeholder = QWidget() - left_placeholder.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) - title_layout.addWidget(left_placeholder) - - self.title_label = QLabel(self.windowTitle()) - self.title_label.setObjectName("titleLabel") - self.title_label.setAlignment(Qt.AlignCenter) - title_layout.addWidget(self.title_label, 1) - - title_layout.addWidget(controls) - left_placeholder.setFixedWidth(controls.sizeHint().width()) - - def _applyTitleBarIcons(self): - if self._icon_close: - self.close_button.setIcon(self._icon_close) - self.close_button.setIconSize(self._title_icon_size) - self.close_button.setText("") - self.close_button.setToolTip("关闭") - - def setWindowTitle(self, title): - super().setWindowTitle(title) - if hasattr(self, "title_label"): - self.title_label.setText(title) - - def _onAccept(self): - if self.line_edit.text().strip(): - self.accept() - - def text(self): - return self.line_edit.text().strip() - - def mousePressEvent(self, event): - if event.button() == Qt.LeftButton and self.title_bar.geometry().contains(event.pos()): - self.dragging = True - self.drag_position = event.globalPos() - self.frameGeometry().topLeft() - event.accept() - super().mousePressEvent(event) - - def mouseMoveEvent(self, event): - if event.buttons() == Qt.LeftButton and self.dragging: - self.move(event.globalPos() - self.drag_position) - event.accept() - super().mouseMoveEvent(event) - - def mouseReleaseEvent(self, event): - if event.button() == Qt.LeftButton: - self.dragging = False - super().mouseReleaseEvent(event) - - -class CustomTreeWidget(QTreeWidget): - """自定义场景树部件""" - - def __init__(self, world, parent=None): - if parent is None: - parent = wrapinstance(0, QWidget) - super().__init__(parent) - self.world = world - # self.selectedItems = None - self.initData() - self.setupUI() # 初始化界面 - self.setupContextMenu() # 初始化右键菜单 - self.setupDragDrop() # 设置拖拽功能 - self.original_scales={} - - def initData(self): - """初始化变量""" - # 定义2D GUI元素类型 - self.gui_2d_types = { - "GUI_BUTTON", # GUI 按钮 - "GUI_LABEL", # GUI 标签 - "GUI_ENTRY", # GUI 输入框 - "GUI_IMAGE", # GUI 图片 - "GUI_2D_VIDEO_SCREEN", # GUI 2D视频 - "GUI_SPHERICAL_VIDEO", # GUI 3D球形视频 - "GUI_NODE" # 其他2D GUI容器 - } - - # 定义3D GUI元素类型 - self.gui_3d_types = { - "GUI_3DTEXT", # 3D 文本节点 - "GUI_3DIMAGE", # 3D 图片节点 - "GUI_VIRTUAL_SCREEN", # 3D视频 - "GUI_VirtualScreen" # 3D虚拟视频 - } - - # 定义3D场景节点类型(可以接受3D GUI元素和其他3D场景元素) - self.scene_3d_types = { - "SCENE_ROOT", - "SCENE_NODE", - "LIGHT_NODE", # 灯节点 - "CAMERA_NODE", - "IMPORTED_MODEL_NODE", # 导入模型节点 - "MODEL_NODE", - "TERRAIN_NODE", # 地形节点 - "CESIUM_TILESET_NODE" # 3D Tileset - } - - # 这是一个最佳实践,它让代码的意图变得非常清晰。 - self.valid_3d_parent_types = self.scene_3d_types.union(self.gui_3d_types) - - def setupUI(self): - """初始化UI设置""" - self.setHeaderHidden(True) - # 启用多选和拖拽 - self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) - self.setDropIndicatorShown(True) # 启用拖放指示线 - - def setupDragDrop(self): - """设置拖拽功能""" - # 使用自定义拖拽模式 - self.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove) # 或者使用 DragDrop - self.setDefaultDropAction(Qt.DropAction.MoveAction) - self.setDragEnabled(True) - self.setAcceptDrops(True) - - def setupContextMenu(self): - """设置右键菜单""" - self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) - self.customContextMenuRequested.connect(self.showContextMenu) - - def showContextMenu(self, position): - """显示右键菜单 - 复用主菜单的创建动作""" - if self.selectedItems(): - item = self.selectedItems()[0] - print(f"为项目 '{item.text(0)}' 显示右键菜单") - - # 创建右键菜单 - menu = QMenu(self) - - # 获取主窗口的创建菜单动作 - 关键修改 - if hasattr(self.world, 'main_window'): - main_window = self.world.main_window - create_actions = main_window.getCreateMenuActions() - - # 创建子菜单 - 复用主菜单结构 - createMenu = menu.addMenu('创建') - - # 基础对象 - createMenu.addAction(create_actions['createEmpty']) - - # 3D对象菜单 - create3dObjectMenu = createMenu.addMenu('3D对象') - - # 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']) - createGUIMenu.addSeparator() - createGUIMenu.addAction(create_actions['createVirtualScreen']) - - # 光源菜单 - createLightMenu = createMenu.addMenu('光源') - createLightMenu.addAction(create_actions['createSpotLight']) - createLightMenu.addAction(create_actions['createPointLight']) - - else: - # 备用方案:如果无法获取主窗口动作,显示提示 - createMenu = menu.addMenu('创建') - noActionItem = createMenu.addAction('功能不可用') - noActionItem.setEnabled(False) - - # 添加删除选项 - menu.addSeparator() - deleteAction = menu.addAction('删除') - if hasattr(self, 'delete_items'): - deleteAction.triggered.connect(lambda: self.delete_items(self.selectedItems())) - - # 显示菜单 - global_pos = self.mapToGlobal(position) - action = menu.exec_(global_pos) - - print(f"右键菜单已显示在位置: {global_pos}") - if action: - print(f"用户选择了动作: {action.text()}") - else: - print("用户取消了菜单选择") - - # 在 CustomTreeWidget 类的 dropEvent 方法中替换缩放处理部分 - def dropEvent(self, event): - # 1. 获取所有被拖拽的项 - dragged_items = self.selectedItems() - if not dragged_items: - 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 # 跳过无效节点 - - drag_info.append({ - "item": item, - "node": panda_node, - "old_parent_node": item.parent().data(0, Qt.UserRole) if item.parent() else None - }) - - # 3. 执行Qt的默认拖拽,让UI树先行更新 - # 这一步会自动处理移动或复制,并将项目从旧父节点移除,添加到新父节点 - super().dropEvent(event) - - # 4. 遍历记录下的信息,同步每一个Panda3D节点的状态 - try: - for info in drag_info: - dragged_item = info["item"] - dragged_node = info["node"] - old_parent_node = info["old_parent_node"] - - # 获取拖拽后的新父节点 - new_parent_item = dragged_item.parent() - new_parent_node = new_parent_item.data(0, Qt.UserRole) if new_parent_item else None - - # 仅当父节点实际发生变化时才执行重新父化 - if old_parent_node != new_parent_node: - print(f"跨层级拖拽:从 {old_parent_node} 移动到 {new_parent_node}") - - # # 保存世界坐标位置 - # world_pos = dragged_node.getPos(self.world.render) - # world_hpr = dragged_node.getHpr(self.world.render) - # world_scale = dragged_node.getScale(self.world.render) - - # 检查是否是2D GUI元素 - dragged_type = dragged_item.data(0, Qt.UserRole + 1) - is_2d_gui = dragged_type in self.gui_2d_types - - # 重新父化到新的父节点 - if new_parent_node: - if is_2d_gui: - # 2D GUI元素需要特殊处理 - if hasattr(new_parent_node, 'getTag') and new_parent_node.getTag("is_gui_element") == "1": - # 目标是GUI元素,直接重新父化 - dragged_node.wrtReparentTo(new_parent_node) - else: - # 目标是3D节点,保持GUI特性,重新父化到aspect2d - # from direct.showbase.ShowBase import aspect2d - dragged_node.wrtReparentTo(self.world.aspect2d) - print(f"2D GUI元素 {dragged_item.text(0)} 保持在aspect2d下") - else: - # 非GUI元素正常重新父化 - dragged_node.wrtReparentTo(new_parent_node) - else: - # 如果新父节点为None,根据元素类型决定父节点 - if is_2d_gui: - # from direct.showbase.ShowBase import aspect2d - dragged_node.wrtReparentTo(self.world.aspect2d) - print(f"2D GUI元素 {dragged_item.text(0)} 重新父化到aspect2d") - else: - dragged_node.wrtReparentTo(self.world.render) - - # # 恢复世界坐标位置(对于2D GUI可能需要调整) - # if is_2d_gui: - # # 2D GUI元素使用屏幕坐标系,可能需要特殊处理 - # dragged_node.setPos(world_pos) - # dragged_node.setHpr(world_hpr) - # dragged_node.setScale(world_scale) - # else: - # dragged_node.setPos(self.world.render, world_pos) - # dragged_node.setHpr(self.world.render, world_hpr) - # dragged_node.setScale(self.world.render, world_scale) - - print(f"✅ Panda3D父子关系已更新") - else: - print(f"同层级移动:父节点未变化,跳过Panda3D重新父化") - - except Exception as e: - print(f"⚠️ 同步Panda3D场景图失败: {e}") - # 不影响Qt树的更新,继续执行 - - # 事后验证:确保节点仍在"场景"根节点下 - self._ensureUnderSceneRoot(dragged_item) - self.world.property_panel._syncEffectiveVisibility(dragged_node) - event.accept() - - def _ensureUnderSceneRoot(self, item): - """确保节点在场景根节点下,如果不是则自动修正""" - if not item: - return - - # 检查是否成为了顶级节点 - if not item.parent(): - # 通过数据标识判断是否是场景根节点 - scene_root_marker = item.data(0, Qt.UserRole + 1) - if scene_root_marker != "SCENE_ROOT": - print(f"⚠️ 检测到节点 {item.text(0)} 意外成为顶级节点,正在修正...") - - # 找到场景根节点 - scene_root = None - for i in range(self.topLevelItemCount()): - top_item = self.topLevelItem(i) - if top_item.data(0, Qt.UserRole + 1) == "SCENE_ROOT": - scene_root = top_item - break - - if scene_root: - # 将节点移回场景根节点下 - self.takeTopLevelItem(self.indexOfTopLevelItem(item)) - scene_root.addChild(item) - print(f"✅ 已将节点 {item.text(0)} 移回场景根节点下") - - def isValidParentChild(self, dragged_item, target_item): - """检查是否是有效的父子关系(防止循环)+ GUI类型验证""" - - # 1. 禁止拖放到自身 - if dragged_item == target_item: - return False - - # 2. 禁止拖到根节点之外(根节点本身除外) - target_root = self._getRootNode(target_item) - if not target_root or target_root.data(0, Qt.UserRole + 1) != "SCENE_ROOT": - print(f"❌ 目标节点 {target_item.text(0)} 不在场景下") - return False - - # 3. 禁止拖拽场景根节点 - dragged_root = self._getRootNode(dragged_item) - if (dragged_item.data(0, Qt.UserRole + 1) == "SCENE_ROOT" or - not dragged_root or dragged_root.data(0, Qt.UserRole + 1) != "SCENE_ROOT"): - print(f"❌ 禁止拖拽场景根节点或根节点外的节点") - return False - - # 4. Qt 树循环检查 - current = target_item - while current: - if current == dragged_item: - print(f"❌ Qt 树检测:{target_item.text(0)} 是 {dragged_item.text(0)} 的后代") - return False - current = current.parent() - - # 5. GUI元素类型验证 - 新增功能 - if not self._validateGUITypeCompatibility(dragged_item, target_item): - return False - - return True - - def _validateGUITypeCompatibility(self, dragged_item, target_item): - """验证GUI元素类型兼容性""" - try: - # 获取节点类型标识 - dragged_type = dragged_item.data(0, Qt.UserRole + 1) - target_type = target_item.data(0, Qt.UserRole + 1) - - # 定义2D GUI元素类型 - gui_2d_types = self.gui_2d_types - - # 定义3D GUI元素类型 - gui_3d_types = self.gui_3d_types - - # 定义3D场景节点类型(可以接受3D GUI元素和其他3D场景元素) - scene_3d_types = self.scene_3d_types - - # 检查拖拽元素的类型 - is_dragged_2d_gui = dragged_type in gui_2d_types - is_dragged_3d_gui = dragged_type in gui_3d_types - is_dragged_3d_scene = dragged_type in scene_3d_types - - # 检查目标的类型 - is_target_2d_gui = target_type in gui_2d_types - is_target_3d_gui = target_type in gui_3d_types - is_target_3d_scene = target_type in scene_3d_types - - # === 严格的类型隔离验证逻辑 === - - # 1. 2D GUI元素的拖拽限制 - 只能拖拽到其他2D GUI元素下 - if is_dragged_2d_gui: - if target_type == "SCENE_ROOT": - return True - elif is_target_2d_gui: - print(f"✅ 2D GUI元素 {dragged_item.text(0)} 可以拖拽到2D GUI父节点 {target_item.text(0)}") - return True - elif is_target_3d_gui: - print(f"❌ 2D GUI元素 {dragged_item.text(0)} 不能拖拽到3D GUI元素 {target_item.text(0)} 下") - print(" 💡 提示: 2D GUI元素只能作为其他2D GUI元素的子节点") - return False - elif is_target_3d_scene: - print(f"❌ 2D GUI元素 {dragged_item.text(0)} 不能拖拽到3D场景节点 {target_item.text(0)} 下") - print(" 💡 提示: 2D GUI元素应该保持在2D GUI层级结构中") - return False - else: - print(f"❌ 2D GUI元素 {dragged_item.text(0)} 不能拖拽到未知类型节点 {target_item.text(0)} 下") - return False - - # 2. 3D GUI元素的拖拽限制 - 只能拖拽到3D场景节点下 - elif is_dragged_3d_gui: - if is_target_3d_scene: - print(f"✅ 3D GUI元素 {dragged_item.text(0)} 可以拖拽到3D场景节点 {target_item.text(0)}") - return False - elif is_target_2d_gui: - print(f"❌ 3D GUI元素 {dragged_item.text(0)} 不能拖拽到2D GUI元素 {target_item.text(0)} 下") - print(" 💡 提示: 3D GUI元素不能与2D GUI元素建立父子关系") - return False - elif is_target_3d_gui: - print(f"✅ 3D GUI元素 {dragged_item.text(0)} 可以拖拽到其他3D GUI元素 {target_item.text(0)} 下") - print(" 💡 提示: 允许3D GUI元素之间建立父子关系") - return True - else: - print(f"❌ 3D GUI元素 {dragged_item.text(0)} 不能拖拽到未知类型节点 {target_item.text(0)} 下") - return False - - # 3. 3D场景元素的拖拽限制 - 只能拖拽到3D场景节点或3D GUI元素下 - elif is_dragged_3d_scene: - if is_target_3d_scene: - print(f"✅ 3D场景元素 {dragged_item.text(0)} 可以拖拽到3D场景节点 {target_item.text(0)}") - return True - elif is_target_2d_gui: - print(f"❌ 3D场景元素 {dragged_item.text(0)} 不能拖拽到2D GUI元素 {target_item.text(0)} 下") - print(" 💡 提示: 3D场景元素不能与2D GUI元素建立父子关系") - print(" 💡 建议: 将3D场景元素拖拽到其他3D场景节点或场景根节点下") - return False - elif is_target_3d_gui: - print(f"✅ 3D场景元素 {dragged_item.text(0)} 可以拖拽到3D GUI元素 {target_item.text(0)} 下") - print(" 💡 提示: 允许3D场景元素挂载在3D GUI元素下") - return False - else: - print(f"❌ 3D场景元素 {dragged_item.text(0)} 不能拖拽到未知类型节点 {target_item.text(0)} 下") - return False - - # 4. 其他未分类元素 - 严格禁止拖拽到2D GUI下 - else: - if is_target_2d_gui: - print(f"❌ 元素 {dragged_item.text(0)} 不能拖拽到2D GUI元素 {target_item.text(0)} 下") - print(" 💡 提示: 只有2D GUI元素才能作为其他2D GUI元素的子节点") - return False - elif is_target_3d_gui or is_target_3d_scene: - print(f"✅ 允许元素 {dragged_item.text(0)} 拖拽到3D节点 {target_item.text(0)}") - return True - else: - print(f"❌ 元素 {dragged_item.text(0)} 不能拖拽到未知类型节点 {target_item.text(0)} 下") - return False - - # 默认情况(理论上不应该到达这里) - print(f"⚠️ GUI类型验证遇到未处理的情况: {dragged_type} -> {target_type}") - return False - - except Exception as e: - print(f"❌ GUI类型兼容性验证失败: {str(e)}") - # 出错时采用保守策略,禁止拖拽 - return False - - def _getRootNode(self, item): - """获取树中节点的根节点项""" - if not item: - return None - - current = item - while current.parent(): - current = current.parent() - return current - - def dragEnterEvent(self, event): - """处理拖入事件""" - if event.source() == self: - event.accept() - else: - event.ignore() - - def dragMoveEvent(self, event): - """处理拖动事件""" - indicator_pos = self.dropIndicatorPosition() - indicator_str = "Unknown" - - if indicator_pos == QAbstractItemView.DropIndicatorPosition.OnItem: - indicator_str = "OnItem" - elif indicator_pos == QAbstractItemView.DropIndicatorPosition.AboveItem: - indicator_str = "AboveItem" - elif indicator_pos == QAbstractItemView.DropIndicatorPosition.BelowItem: - indicator_str = "BelowItem" - elif indicator_pos == QAbstractItemView.DropIndicatorPosition.OnViewport: - indicator_str = "OnViewport" - - #print(f'indicator pos: {indicator_str} (value: {int(indicator_pos)})') - - if event.source() != self: - event.ignore() - return - - # 获取当前拖拽的项目和目标位置 - target_item = self.itemAt(event.pos()) - selected_items = self.selectedItems() - - # 检查是否拖拽到多选区域内的项目 - if target_item and target_item in selected_items: - event.ignore() - return - - # 检查其他禁止条件 - if target_item and selected_items: - for dragged_item in selected_items: - if not self.isValidParentChild(dragged_item, target_item): - event.ignore() - return - - super().dragMoveEvent(event) - event.accept() - - def keyPressEvent(self, event): - """处理键盘按键事件""" - if event.key() == Qt.Key_Delete: - # currentItem = self.currentItem() - # if currentItem and currentItem.parent(): - # # 检查是否是模型节点或其子节点 - # if self.world.interface_manager.isModelOrChild(currentItem): - # nodePath = currentItem.data(0, Qt.UserRole) - # if nodePath: - # print("正在删除节点...") - # self.world.interface_manager.deleteNode(nodePath, currentItem) - # print("删除完成") - selected_items = self.selectedItems() - if selected_items: - # 执行删除操作 - # if selected_items.data(0, Qt.UserRole + 1) == "LIGHT_NODE": - # self._preprocess_light_items_for_deletion(selected_items) - self.delete_items(selected_items) - else: - # 没有选中任何项目,执行默认操作 - super().keyPressEvent(event) - else: - super().keyPressEvent(event) - - def _preprocess_light_items_for_deletion(self, selected_items): - """预处理灯光节点删除,特别处理最后一个灯光节点的问题""" - if not selected_items: - return selected_items - - # 检查选中的项目中是否包含灯光节点 - light_items = [] - for item in selected_items: - node_type = item.data(0, Qt.UserRole + 1) - if node_type == "LIGHT_NODE": - light_items.append(item) - - # 如果没有灯光节点,直接返回 - if not light_items: - return selected_items - - # 检查是否只有最后一个灯光节点被选中 - processed_items = list(selected_items) # 创建副本 - - for item in light_items: - panda_node = item.data(0, Qt.UserRole) - if not panda_node: - continue - - # 获取灯光类型 - if hasattr(panda_node, 'getTag'): - light_type = panda_node.getTag("light_type") - - # 检查是否是最后一个Spotlight - if (light_type == "spot_light" and hasattr(self.world, 'Spotlight') and - self.world.Spotlight and self.world.Spotlight[-1] == panda_node and - len(self.world.Spotlight) > 1): - - print(f"⚠️ 检测到选中最后一个Spotlight节点: {item.text(0)}") - # 这里可以添加特殊处理逻辑,比如提示用户或阻止删除 - - # 检查是否是最后一个Pointlight - elif (light_type == "point_light" and hasattr(self.world, 'Pointlight') and - self.world.Pointlight and self.world.Pointlight[-1] == panda_node and - len(self.world.Pointlight) > 1): - - print(f"⚠️ 检测到选中最后一个Pointlight节点: {item.text(0)}") - # 这里可以添加特殊处理逻辑,比如提示用户或阻止删除 - - return processed_items - - def delete_items(self, selected_items): - """删除选中的item - 简化版本""" - if not selected_items: - return - - # 1. 过滤掉不能删除的节点 - deletable_items = [] - for item in selected_items: - node_type = item.data(0, Qt.UserRole + 1) - panda_node = item.data(0, Qt.UserRole) - - # 跳过场景根节点和主相机 - if (node_type == "SCENE_ROOT" or - (panda_node and hasattr(self.world, 'cam') and panda_node == self.world.cam)): - continue - deletable_items.append(item) - - if not deletable_items: - StyledMessageBox.information(self, "提示", "没有可删除的节点") - return - - # 2. 确认删除 - item_count = len(deletable_items) - if item_count == 1: - message = f"确定要删除节点 \"{deletable_items[0].text(0)}\" 吗?" - else: - message = f"确定要删除 {item_count} 个节点吗?" - - dialog = UniversalMessageDialog( - self, - "确认删除", - message, - message_type=UniversalMessageDialog.WARNING, - show_cancel=True, - confirm_text="确认", - cancel_text="取消" - ) - - if dialog.exec_() != QDialog.Accepted: - return - - # 默认选中场景根节点,通常是第一个顶级节点 - #next_item_to_select = self.topLevelItem(0) - - # 3. 执行删除循环 - deleted_count = 0 - for item in deletable_items: - try: - # 在删除前,记录其父节点,作为删除后的新选择 - # 选择最后一个被删除项的父节点作为新的焦点 - if item.parent(): - next_item_to_select = item.parent() - panda_node = item.data(0, Qt.UserRole) - - if panda_node: - # 清理选择状态 - if (hasattr(self.world, 'selection') and - hasattr(self.world.selection, 'selectedNode') and - self.world.selection.selectedNode == panda_node): - self.world.selection.updateSelection(None) - - # 清理特殊资源(参考interface_manager.py的逻辑) - if hasattr(self.world, 'property_panel'): - self.world.property_panel.removeActorForModel(panda_node) - - # 清理灯光 - 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) - - # 从world列表中移除 - if hasattr(self.world, 'gui_elements') and panda_node in self.world.gui_elements: - self.world.gui_elements.remove(panda_node) - 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, 'terrains') and panda_node in self.world.terrains: - self.world.terrains.remove(panda_node) - if hasattr(self.world, 'tilesets') and panda_node in self.world.tilesets: - # self.world.tilesets.remove(panda_node) - # 从 tilesets 列表中移除 - if hasattr(self.world, 'scene_manager'): - tilesets_to_remove = [] - for i, tileset_info in enumerate(self.world.scene_manager.tilesets): - if tileset_info['node'] == panda_node: - tilesets_to_remove.append(i) - - # 从后往前删除,避免索引问题 - for i in reversed(tilesets_to_remove): - del self.world.scene_manager.tilesets[i] - - # 从Panda3D场景中移除 - 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() - if parent_item: - parent_item.removeChild(item) - else: - index = self.indexOfTopLevelItem(item) - if index >= 0: - self.takeTopLevelItem(index) - - deleted_count += 1 - print(f"✅ 删除节点: {item.text(0)}") - - except Exception as e: - print(f"❌ 删除节点 {item.text(0)} 失败: {str(e)}") - import traceback - traceback.print_exc() - - # 最终清理 - # if hasattr(self.world, 'property_panel'): - # self.world.property_panel.clearPropertyPanel() - - # 4. 删除操作完成后,更新UI --- - if deleted_count > 0: - print(f"🎉 成功删除 {deleted_count} 个节点。正在更新UI...") - self.update_selection_and_properties(None, None) - - def delete_item(self, panda_node): - """删除指定节点 panda3D(node)- 优化和修复版本""" - if not panda_node or panda_node.is_empty(): - print("ℹ️ 尝试删除一个空的或无效的节点,操作取消。") - return - - # #如果有命令管理系统,则使用命令系统 - # if hasattr(self.world,'command_manager') and self.world.command_manager: - # from core.Command_System import DeleteNodeCommand - # parent_node = panda_node.getParent() - # command = DeleteNodeCommand(panda_node,parent_node) - # self.world.command_manager.execute_command(command) - # return - - # --- 关键修复:在操作前,安全地获取节点名字 --- - node_name_for_logging = panda_node.getName() - - # 1. 寻找对应的Qt Item - item = self.world.interface_manager.findTreeItem(panda_node, self._findSceneRoot()) - - # 场景清理(无论是否找到item,都应该执行) - self._cleanup_panda_node_resources(panda_node) - panda_node.removeNode() - - # 如果没有找到item,说明UI已经移除或不同步,清理完Panda3D资源后即可退出 - if not item: - print(f"✅ Panda3D节点 '{node_name_for_logging}' 已清理并移除。UI树中未找到对应项。") - return - try: - # 2. 过滤受保护节点 - node_type = item.data(0, Qt.UserRole + 1) - if node_type == "SCENE_ROOT": # 相机检查已包含在panda_node判空中 - print(f"ℹ️ 节点 {item.text(0)} 是受保护节点,无法删除。") - return - - # 3. 从UI树中移除 - parent_for_next_selection = item.parent() - if item.parent(): - item.parent().removeChild(item) - else: - index = self.indexOfTopLevelItem(item) - if index >= 0: - self.takeTopLevelItem(index) - - print(f"✅ 成功删除节点: {node_name_for_logging}") - - # 4. 更新UI - print(f"🔄 正在更新UI...") - if parent_for_next_selection and self.indexFromItem(parent_for_next_selection).isValid(): - new_selection_item = parent_for_next_selection - else: - new_selection_item = self.topLevelItem(0) - - if new_selection_item: - self.setCurrentItem(new_selection_item) - new_panda_node_to_select = new_selection_item.data(0, Qt.UserRole) - self.update_selection_and_properties(new_panda_node_to_select, new_selection_item) - else: - self.update_selection_and_properties(None, None) - - except Exception as e: - print(f"❌ 删除节点 {node_name_for_logging} 时发生意外错误: {str(e)}") - import traceback - traceback.print_exc() - - def clear_tree(self): - """清空UI树""" - print("Clear") - self.clear() - # 创建场景根节点 - sceneRoot = QTreeWidgetItem(self, ['场景']) - sceneRoot.setData(0, Qt.UserRole, self.world.render) - sceneRoot.setData(0, Qt.UserRole + 1, "SCENE_ROOT") - self._apply_font_to_item(sceneRoot) - # 添加相机节点 - cameraItem = QTreeWidgetItem(sceneRoot, ['相机']) - cameraItem.setData(0, Qt.UserRole, self.world.cam) - cameraItem.setData(0, Qt.UserRole + 1, "CAMERA_NODE") - self._apply_font_to_item(cameraItem) - # 添加地板节点 - if hasattr(self.world, 'ground') and self.world.ground: - groundItem = QTreeWidgetItem(sceneRoot, ['地板']) - groundItem.setData(0, Qt.UserRole, self.world.ground) - groundItem.setData(0,Qt.UserRole + 1, "SCENE_NODE") - self._apply_font_to_item(groundItem) - - def _apply_font_to_item(self, item): - """根据节点层级设置字体大小""" - if not item: - return - font = QFont(self.font()) - marker = item.data(0, Qt.UserRole + 1) - if item.parent() is None and (marker == "SCENE_ROOT" or item.text(0) == '场景'): - font = QFont("Microsoft YaHei", 12) - font.setWeight(QFont.Light) - else: - font = QFont("Microsoft YaHei", 10) - font.setWeight(QFont.Light) - item.setFont(0, font) - - def _apply_font_recursively(self, item): - """递归应用字体样式""" - if not item: - return - self._apply_font_to_item(item) - for index in range(item.childCount()): - self._apply_font_recursively(item.child(index)) - - def _handle_rows_inserted(self, parent_index, first, last): - """在插入新节点时自动调整字体""" - model = self.model() - if not model: - return - for row in range(first, last + 1): - index = model.index(row, 0, parent_index) - item = self.itemFromIndex(index) - if item: - self._apply_font_recursively(item) - - def _cleanup_panda_node_resources(self, panda_node): - """一个集中的辅助函数,用于清理与Panda3D节点相关的所有资源。""" - if not panda_node or panda_node.is_empty(): - return - try: - # 清理选择状态 - if hasattr(self.world, 'selection') and self.world.selection.selectedNode == panda_node: - self.world.selection.updateSelection(None) - # 清理属性面板 - if hasattr(self.world, 'property_panel'): - self.world.property_panel.removeActorForModel(panda_node) - # 清理灯光 - 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) - # 从各种world管理列表中移除 - lists_to_check = ['gui_elements', 'models', 'Spotlight', 'Pointlight', 'terrains'] - for list_name in lists_to_check: - if hasattr(self.world, list_name): - world_list = getattr(self.world, list_name) - if panda_node in world_list: - world_list.remove(panda_node) - # 特殊处理tilesets - if hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, 'tilesets'): - tilesets_to_remove = [i for i, info in enumerate(self.world.scene_manager.tilesets) if - info.get('node') == panda_node] - for i in reversed(tilesets_to_remove): - del self.world.scene_manager.tilesets[i] - print(f"🧹 已清理节点 {panda_node.getName()} 的所有关联资源。") - except Exception as e: - # 即便这里出错,也要打印信息,但不要让整个删除流程中断 - print(f"⚠️ 清理节点 {panda_node.getName()} 资源时出错: {e}") - - # def mousePressEvent(self, event): - # """鼠标按下事件""" - # if event.button() == Qt.LeftButton: - # if self.currentItem(): - # print(f"self.currentItem() = {self.currentItem()}") - # else: - # print(f"self.currentItem() = None") - # - # # 调用父类处理其他事件 - # super().mousePressEvent(event) - - def update_item_name(self, text, item): - """ 树节点名字 """ - if not item: - return - try: - # 正确的代码 - node = item.data(0, Qt.UserRole) - - item.setText(0, text) - node.setName(text) - except Exception as e: - print(e) - - def _findSceneRoot(self): - """查找场景根节点""" - for i in range(self.topLevelItemCount()): - top_item = self.topLevelItem(i) - if top_item.data(0, Qt.UserRole + 1) == "SCENE_ROOT": - return top_item - return None - - def create_model_items(self, model: NodePath): - """ - 【此函数保持不变】 - 创建模型项。 - 只寻找模型下一层带有 'is_scene_element' 标签的子节点作为分支的根, - 然后完整地展示这些分支。 - """ - if not model: - print("传入的参数model为空") - return - - # 找到场景树的根节点,我们将把模型节点添加到这里 - root_item = self._findSceneRoot() - if not root_item: - print("错误:未能找到场景根节点项") - return - - # 1. 在模型的第一层子节点中进行筛选 - for child_node in model.getChildren(): - if child_node.hasTag("is_scene_element"): - print(f"找到带标签的根节点:{child_node.getName()}") - if (child_node.hasTag("gui_type")and - child_node.getTag("gui_type") in ["3d_text","3d_image","video_screen"]): - print(f"跳过3dGUI节点{child_node.getName()}") - continue - - # 为这个带标签的节点创建一个树项 - child_item = QTreeWidgetItem(root_item) - child_item.setText(0, child_node.getName() or "Unnamed Tagged Node") - child_item.setData(0, Qt.UserRole, child_node) - child_item.setData(0, Qt.UserRole + 1, child_node.getTag("tree_item_type")) - # self._add_node_info(child_item, child_node) # 可选信息 - - # 2. 对这个节点的所有后代进行“无条件”递归添加 (但会跳过碰撞体) - self._add_all_children_unconditionally(child_item, child_node) - - def _add_all_children_unconditionally(self, parent_item: QTreeWidgetItem, node_path: NodePath): - """ - 【此函数已更新】 - 无条件地、递归地添加一个节点下的所有子节点,但会跳过碰撞节点。 - """ - for child_node in node_path.getChildren(): - - # 新增:检查节点是否为碰撞节点 - if isinstance(child_node.node(), CollisionNode): - # print(f"跳过碰撞节点: {child_node.getName()}") # 用于调试 - continue # 如果是,则跳过此节点及其所有子节点 - - # 创建子项 - child_item = QTreeWidgetItem(parent_item) - child_item.setText(0, child_node.getName() or "Unnamed Child") - child_item.setData(0, Qt.UserRole, child_node) - child_item.setData(0, Qt.UserRole + 1, child_node.getTag("tree_item_type")) - # self._add_node_info(child_item, child_node) # 可选信息 - - # 继续无条件地递归 - if not child_node.is_empty(): - self._add_all_children_unconditionally(child_item, child_node) - - # ==================== 辅助方法 ==================== - def _findSceneRoot(self): - """查找场景根节点""" - for i in range(self.topLevelItemCount()): - top_item = self.topLevelItem(i) - if top_item.data(0, Qt.UserRole + 1) == "SCENE_ROOT": - return top_item - return - - def _generateUniqueNodeName(self, base_name, parent_node): - """生成唯一的节点名称""" - # 获取父节点下所有子节点的名称 - existing_names = set() - for child in parent_node.getChildren(): - existing_names.add(child.getName()) - - # 如果基础名称不存在,直接使用 - if base_name not in existing_names: - return base_name - - # 否则添加数字后缀 - counter = 1 - while f"{base_name}_{counter}" in existing_names: - counter += 1 - - return f"{base_name}_{counter}" - - def add_node_to_tree_widget(self, node, parent_item, node_type): - """将node元素添加到树形控件""" - if hasattr(node, 'getTag'): - if node.hasTag('tree_item_type'): - print(f"node0: {node.getName()},{node.getTag('tree_item_type')}") - tree_type = node.getTag('tree_item_type') - else: - node.setTag('tree_item_type', node_type) - else: - print(f"node2: {node.getName()},{node_type}") - tree_type = node_type - - - # BLACK_LIST 和依赖项导入保持不变 - BLACK_LIST = {'', '**', 'temp', 'collision'} - from panda3d.core import CollisionNode, ModelRoot - from PyQt5.QtWidgets import QTreeWidgetItem - from PyQt5.QtCore import Qt - - # 1. 修改内部函数,让它返回创建的节点 - def addNodeToTree(node, parentItem, force=False): - """内部递归函数,现在会返回创建的顶级节点项""" - if not force and should_skip(node): - return None # 如果跳过,返回None - - nodeItem = QTreeWidgetItem(parentItem, [node.getName()]) - nodeItem.setData(0, Qt.UserRole, node) - nodeItem.setData(0, Qt.UserRole + 1, tree_type) - - for child in node.getChildren(): - # 递归调用,但我们只关心顶级的nodeItem - addNodeToTree(child, nodeItem, force=False) - - return nodeItem # <-- 新增:返回创建的QTreeWidgetItem - - 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 == "" - - # 使用一个变量来确保无论哪个分支都有返回值 - new_qt_item = None - node_name = "" - - try: - if tree_type == "IMPORTED_MODEL_NODE": - # getTag('file') 可能是你自己设置的tag,这里假设它存在 - node_name = node.getTag("file") if hasattr(node, 'getTag') and node.hasTag("file") else node.getName() - - # 2. 接收 addNodeToTree 的返回值 - 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]) - new_qt_item.setData(0, Qt.UserRole, node) - new_qt_item.setData(0, Qt.UserRole + 1, tree_type) - - # 确保 new_qt_item 成功创建后再继续操作 - if new_qt_item: - # 展开父节点 - if hasattr(parent_item, 'setExpanded'): - parent_item.setExpanded(True) - - print(f"✅ Qt树节点添加成功: {node_name}") - return new_qt_item - else: - # 如果 addNodeToTree 因为 should_skip 返回了 None - print(f"ℹ️ 节点 {node_name} 被跳过,未添加到树中。") - return None - - except Exception as e: - import traceback - print(f"❌ 添加node到树形控件失败: {str(e)}") - traceback.print_exc() # 打印更详细的错误堆栈,方便调试 - return None - - def update_selection_and_properties(self, node, qt_item): - """更新选择状态和属性面板""" - try: - # 更新选择状态 - if hasattr(self.world, 'selection'): - self.world.selection.updateSelection(node) - - # 更新属性面板 - if hasattr(self.world, 'property_panel'): - self.world.property_panel.updatePropertyPanel(qt_item) - elif hasattr(self.world, 'updatePropertyPanel'): - self.world.updatePropertyPanel(qt_item) - - print(f"✅ 更新选择和属性面板: {qt_item.text(0)}") - - except Exception as e: - print(f"❌ 更新选择和属性面板失败: {str(e)}") - - # ==================== 3D辅助方法 ==================== - def get_target_parents_for_creation(self): - """获取创建目标的父节点列表""" - from PyQt5.QtCore import Qt - - target_parents = [] - - try: - selected_items = self.selectedItems() - - # 检查选中的项目,找出所有有效的父节点 - for item in selected_items: - if self._isValidParentForNewNode(item): - parent_node = item.data(0, Qt.UserRole) - if parent_node: - target_parents.append((item, parent_node)) - print(f"📍 找到有效父节点: {item.text(0)}") - - # 如果没有有效的选中节点,使用场景根节点 - if not target_parents: - print("⚠️ 没有有效选中节点,使用场景根节点") - scene_root = self._findSceneRoot() - if scene_root: - parent_node = scene_root.data(0, Qt.UserRole) - if parent_node: - target_parents.append((scene_root, parent_node)) - else: - # 如果没有_findSceneRoot方法,使用world.render作为默认父节点 - if hasattr(self.world, 'render'): - # 创建一个虚拟的树项目来表示render节点 - class MockTreeItem: - def text(self, column): - return "render" - - mock_item = MockTreeItem() - target_parents.append((mock_item, self.world.render)) - - print(f"📊 总共找到 {len(target_parents)} 个目标父节点") - return target_parents - - except Exception as e: - print(f"❌ 获取目标父节点失败: {str(e)}") - return [] - - def _isValidParentForNewNode(self, item): - """检查节点是否适合作为新节点的父节点-3d""" - if not item: - return False - - # 获取节点类型标识 - node_type = item.data(0, Qt.UserRole + 1) - - # 场景根节点和普通场景节点可以作为父节点 - if node_type in self.valid_3d_parent_types: - return True - - # # 模型节点也可以作为父节点 - # panda_node = item.data(0, Qt.UserRole) - # if panda_node and panda_node.hasTag("is_model_root"): - # return True - # - # # 其他类型的节点也可以,但排除一些特殊情况 - # if panda_node: - # # # 排除相机节点 - # # if panda_node == self.world.cam: - # # return False - # # 排除碰撞节点 - # from panda3d.core import CollisionNode - # if isinstance(panda_node.node(), CollisionNode): - # return False - # return True - - return False - - - def get_target_parents_for_gui_creation(self): - """获取GUI创建目标的父节点列表 - 支持GUI元素作为父节点""" - from PyQt5.QtCore import Qt - - target_parents = [] - - try: - selected_items = self.selectedItems() - - # 检查选中的项目,找出所有有效的父节点 - for item in selected_items: - if self.isValidParentForGUI(item): - parent_node = item.data(0, Qt.UserRole) - if parent_node: - target_parents.append((item, parent_node)) # 修复:确保添加到列表 - print(f"📍 找到有效GUI父节点: {item.text(0)}") - else: - print(f"⚠️ GUI父节点 {item.text(0)} 的Panda3D数据为空") - - # 如果没有有效的选中节点,使用场景根节点 - if not target_parents: - print("⚠️ 没有有效选中节点,使用场景根节点") - scene_root = self._findSceneRoot() - if scene_root: - parent_node = scene_root.data(0, Qt.UserRole) - if parent_node: - target_parents.append((scene_root, parent_node)) - else: - if hasattr(self.world, 'render'): - class MockTreeItem: - def text(self, column): - return "render" - - mock_item = MockTreeItem() - target_parents.append((mock_item, self.world.render)) - - print(f"📊 总共找到 {len(target_parents)} 个目标父节点") - return target_parents - - except Exception as e: - print(f"❌ 获取目标父节点失败: {str(e)}") - return [] - - def isValidParentForGUI(self, item): - """检查节点是否适合作为GUI元素的父节点""" - if not item: - return False - - # 获取节点类型标识 - node_type = item.data(0, Qt.UserRole + 1) - - # GUI元素可以作为其他GUI元素的父节点 - if node_type in self.gui_2d_types: - return True - - # 场景根节点和普通场景节点也可以作为父节点 - if node_type in ["SCENE_ROOT"]: - return True - - return False - - def is_gui_element(self, node): - """判断节点是否是GUI元素""" - if not node: - return False - - # 检查是否有GUI标签 - if hasattr(node, 'getTag'): - return node.getTag("is_gui_element") == "1" or node.getTag("gui_type") in ["info_panel", "button", "label", "entry", "3d_text", "virtual_screen"] - - # 检查是否是DirectGUI对象 - try: - from direct.gui.DirectGuiBase import DirectGuiBase - return isinstance(node, DirectGuiBase) - except ImportError: - return False - - def calculate_relative_gui_position(self, pos, parent_gui): - """计算相对于GUI父节点的位置""" - try: - # 对于GUI子元素,使用相对坐标 - # 这里使用较小的缩放因子,因为是相对于父GUI的位置 - relative_pos = (pos[0] * 0.05, 0, pos[2] * 0.05) - print(f"📐 计算GUI相对位置: {pos} -> {relative_pos}") - return relative_pos - except Exception as e: - print(f"❌ 计算GUI相对位置失败: {str(e)}") - # 如果计算失败,返回默认的屏幕坐标 - return (pos[0] * 0.1, 0, pos[2] * 0.1) - - #---------------------------暂时无用------------------------------- - - def add_existing_node(self, panda_node, node_type="SCENE_NODE", parent_item=None): - """将已存在的Panda3D节点添加到Qt树形控件中 - - Args: - panda_node: 已创建的Panda3D节点 - node_type: 节点类型标识 - parent_item: 父Qt项目,如果为None则使用当前选中项或根节点 - - Returns: - QTreeWidgetItem: 创建的Qt树项目 - """ - try: - if not panda_node: - print("❌ 传入的Panda3D节点为空") - return None - - # 确定父项目 - 保持简单,只处理单个父节点 - if parent_item is None: - # 优先使用当前选中的第一个有效节点作为父节点 - selected_items = self.selectedItems() - if selected_items: - # 找到第一个有效的父节点 - for potential_parent in selected_items: - if self._isValidParentForNewNode(potential_parent): - parent_item = potential_parent - print(f"📍 使用选中节点作为父节点: {parent_item.text(0)}") - break - - # 如果没有找到有效的选中节点 - if not parent_item: - print("⚠️ 所有选中节点都不适合作为父节点,查找场景根节点") - parent_item = self._findSceneRoot() - else: - # 没有选中任何节点,使用场景根节点 - print("📍 没有选中节点,使用场景根节点作为父节点") - parent_item = self._findSceneRoot() - - # 如果场景根节点也找不到,最后尝试render节点 - if not parent_item: - print("📍 场景根节点未找到,尝试使用render节点") - parent_item = self._findRenderItem() - - if not parent_item: - print("❌ 无法找到合适的父节点") - return None - - # 创建Qt树项目 - node_name = panda_node.getName() - new_qt_item = QTreeWidgetItem(parent_item, [node_name]) - new_qt_item.setData(0, Qt.UserRole, panda_node) - new_qt_item.setData(0, Qt.UserRole + 1, node_type) - - # 展开父节点 - parent_item.setExpanded(True) - - print(f"✅ 成功将现有节点添加到树形控件: {node_name} -> 父节点: {parent_item.text(0)}") - return new_qt_item - - except Exception as e: - print(f"❌ 添加现有节点到树形控件失败: {str(e)}") - import traceback - traceback.print_exc() - return None - - def _findRenderItem(self): - """查找render根节点项目""" - try: - root = self.invisibleRootItem() - for i in range(root.childCount()): - item = root.child(i) - if item.text(0) == "render": - return item - - # 如果没找到render节点,返回第一个子项目 - if root.childCount() > 0: - return root.child(0) - - return None - except Exception as e: - print(f"查找render节点失败: {e}") - return None - - def create_item(self, node_type="empty", selected_items=None): - """创建不同类型的场景节点 - - Args: - node_type: 节点类型 ("empty", "spot_light", "point_light") - selected_items: 选中的父节点项目列表,如果为None则使用当前选中项或根节点 - """ - try: - # 确定父节点 - parent_items = self._determineParentItems(selected_items) - if not parent_items: - print("⚠️ 无法确定父节点") - return [] - - created_nodes = [] - - for parent_item in parent_items: - # 验证父节点的有效性 - if not self._isValidParentForNewNode(parent_item): - print(f"⚠️ 节点 {parent_item.text(0)} 不适合作为父节点") - continue - - # 获取父节点的Panda3D对象 - parent_node = parent_item.data(0, Qt.UserRole) - if not parent_node: - print(f"⚠️ 父节点 {parent_item.text(0)} 没有对应的Panda3D对象") - continue - - # 根据节点类型创建不同的节点 - if node_type == "empty": - new_node = self._createEmptyNode(parent_node, parent_item) - elif node_type == "spot_light": - new_node = self._createSpotLightNode(parent_node, parent_item) - elif node_type == "point_light": - new_node = self._createPointLightNode(parent_node, parent_item) - else: - print(f"❌ 不支持的节点类型: {node_type}") - continue - - if new_node: - created_nodes.append(new_node) - - # 如果只创建了一个节点,自动选中它 - if len(created_nodes) == 1: - _, qt_item = created_nodes[0] - self.setCurrentItem(qt_item) - # 更新选择和属性面板 - if hasattr(self.world, 'selection'): - self.world.selection.updateSelection(qt_item.data(0, Qt.UserRole)) - if hasattr(self.world, 'property_panel'): - self.world.property_panel.updatePropertyPanel(qt_item) - - print(f"✅ 总共创建了 {len(created_nodes)} 个 {node_type} 节点") - return created_nodes - - except Exception as e: - print(f"❌ 创建 {node_type} 节点失败: {str(e)}") - import traceback - traceback.print_exc() - return [] - - def _determineParentItems(self, selected_items): - """确定父节点项目列表""" - if selected_items is not None: - return selected_items - - # 使用当前选中的项目 - current_selected = self.selectedItems() - if current_selected: - return current_selected - - # 如果没有选中任何项目,使用场景根节点 - scene_root = self._findSceneRoot() - if scene_root: - return [scene_root] - - return [] - - def _setupNewNodeDefaults(self, node): - """设置新节点的默认属性""" - # 设置默认位置(相对于父节点) - node.setPos(0, 0, 0) - node.setHpr(0, 0, 0) - node.setScale(1, 1, 1) - - # 设置默认可见性 - node.show() - - # 可以根据需要添加更多默认设置 - # 例如:默认材质、碰撞检测等 - - # ==================== 创建节点 ==================== - def _createEmptyNode(self, parent_node, parent_item): - """创建空节点""" - # 生成唯一的节点名称 - node_name = self._generateUniqueNodeName("空节点", parent_node) - - # 在Panda3D场景中创建新节点 - new_panda_node = parent_node.attachNewNode(node_name) - - # 设置新节点的默认属性 - self._setupNewNodeDefaults(new_panda_node) - - # 设置节点标签 - new_panda_node.setTag("is_scene_element", "1") - new_panda_node.setTag("node_type", "empty_node") - new_panda_node.setTag("created_by_user", "1") - - # 在Qt树中创建对应的项目 - new_qt_item = QTreeWidgetItem(parent_item, [node_name]) - new_qt_item.setData(0, Qt.UserRole, new_panda_node) - new_qt_item.setData(0, Qt.UserRole + 1, "SCENE_NODE") - - # 展开父节点 - parent_item.setExpanded(True) - - print(f"✅ 成功创建空节点: {node_name}") - return (new_panda_node, new_qt_item) - - def _createSpotLightNode(self, parent_node, parent_item): - """创建聚光灯节点""" - from RenderPipelineFile.rpcore import SpotLight - from QMeta3D.Meta3DWorld import get_render_pipeline - from panda3d.core import Vec3, NodePath - - try: - render_pipeline = get_render_pipeline() - - # 生成唯一的节点名称 - light_name = self._generateUniqueNodeName(f"Spotlight_{len(self.world.Spotlight)}", parent_node) - - # 创建挂载节点 - light_np = NodePath(light_name) - light_np.reparentTo(parent_node) - - # 创建聚光灯对象 - light = SpotLight() - light.direction = Vec3(0, 0, -1) - light.fov = 70 - light.set_color_from_temperature(5 * 1000.0) - light.energy = 5000 - light.radius = 1000 - light.casts_shadows = True - light.shadow_map_resolution = 256 - light.setPos(0, 0, 0) # 相对于父节点的位置 - - # 添加到渲染管线 - render_pipeline.add_light(light) - - # 设置节点属性和标签 - light_np.setTag("light_type", "spot_light") - light_np.setTag("is_scene_element", "1") - light_np.setTag("light_energy", str(light.energy)) - light_np.setTag("created_by_user", "1") - - # 保存光源对象引用 - light_np.setPythonTag("rp_light_object", light) - - # 添加到管理列表 - self.world.Spotlight.append(light_np) - - # 在Qt树中创建对应的项目 - new_qt_item = QTreeWidgetItem(parent_item, [light_name]) - new_qt_item.setData(0, Qt.UserRole, light_np) - new_qt_item.setData(0, Qt.UserRole + 1, "LIGHT_NODE") - - # 展开父节点 - parent_item.setExpanded(True) - - print(f"✅ 成功创建聚光灯: {light_name}") - - except Exception as e: - print(f"❌ 创建聚光灯失败: {str(e)}") - return None - - def _createPointLightNode(self, parent_node, parent_item): - """创建点光源节点""" - from RenderPipelineFile.rpcore import PointLight - from QMeta3D.Meta3DWorld import get_render_pipeline - from panda3d.core import Vec3, NodePath - - try: - render_pipeline = get_render_pipeline() - - # 生成唯一的节点名称 - light_name = self._generateUniqueNodeName(f"Pointlight_{len(self.world.Pointlight)}", parent_node) - - # 创建挂载节点 - light_np = NodePath(light_name) - light_np.reparentTo(parent_node) - - # 创建点光源对象 - light = PointLight() - light.setPos(0, 0, 0) # 相对于父节点的位置 - light.energy = 5000 - light.radius = 1000 - light.inner_radius = 0.4 - light.set_color_from_temperature(5 * 1000.0) - light.casts_shadows = True - light.shadow_map_resolution = 256 - - # 添加到渲染管线 - render_pipeline.add_light(light) - - # 设置节点属性和标签 - light_np.setTag("light_type", "point_light") - light_np.setTag("is_scene_element", "1") - light_np.setTag("light_energy", str(light.energy)) - light_np.setTag("created_by_user", "1") - - # 保存光源对象引用 - light_np.setPythonTag("rp_light_object", light) - - # 添加到管理列表 - self.world.Pointlight.append(light_np) - - # 在Qt树中创建对应的项目 - new_qt_item = QTreeWidgetItem(parent_item, [light_name]) - new_qt_item.setData(0, Qt.UserRole, light_np) - new_qt_item.setData(0, Qt.UserRole + 1, "LIGHT_NODE") - - # 展开父节点 - parent_item.setExpanded(True) - - print(f"✅ 成功创建点光源: {light_name}") - return (light_np, new_qt_item) - - except Exception as e: - print(f"❌ 创建点光源失败: {str(e)}") - return None - - -