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
-
-
-