diff --git a/QMeta3D/Meta3DWorld.py b/QMeta3D/Meta3DWorld.py
index a6b88ffc..54b319fe 100644
--- a/QMeta3D/Meta3DWorld.py
+++ b/QMeta3D/Meta3DWorld.py
@@ -1,192 +1,188 @@
-import sys
-import os
-
-from core.CustomMouseController import CustomMouseController
-
-# 获取 RenderPipelineFile 的路径
-render_pipeline_path = './RenderPipelineFile'
-# 将该路径添加到 sys.path 中,确保 Python 能够找到它
-project_root = os.path.dirname(os.path.abspath(__file__))
-sys.path.insert(0, project_root)
-sys.path.insert(0, render_pipeline_path)
-
-from RenderPipelineFile.rpcore import RenderPipeline
-_global_render_pipeline = None
-
-from PyQt5.QtCore import *
-from PyQt5.QtGui import *
-from PyQt5.QtWidgets import *
-
-from panda3d.core import *
-from panda3d.core import GraphicsOutput, Texture, ConfigVariableManager, WindowProperties
-
-from direct.showbase.ShowBase import ShowBase
-import platform
-
-from QMeta3D.QMouseWatcherNode import QMouseWatcherNode
-
-from RenderPipelineFile.rpcore.render_target import RenderTarget
-__all__ = ["Meta3DWorld"]
-
-_global_world_instance=None
-import builtins
-
-
-class Meta3DWorld(ShowBase):
- def __init__(self, width=1380, height=750, is_fullscreen=False, size=1.0, clear_color=LVecBase4f(0, 0.5, 0, 1),
- name="qMeta3D"):
-
- global _global_world_instance
- global _global_render_pipeline
-
- _global_world_instance = self
-
- sort = -100
- self.parent = None
-
- # 设置基本配置
- loadPrcFileData("", "show-frame-rate-meter 0")
- loadPrcFileData("", "window-type offscreen") # 设置为离屏渲染
- loadPrcFileData("", f"win-size {width} {height}")
- loadPrcFileData("", "win-fixed-size #f") # 允许窗口调整大小
-
- # 🚀 VR性能优化配置
- loadPrcFileData("", "prefer-single-buffer true") # 减少缓冲区交换开销
- loadPrcFileData("", "gl-force-flush false") # 避免强制glFlush导致的性能损失
- loadPrcFileData("", "sync-video false") # 禁用默认VSync,让OpenVR控制
- loadPrcFileData("", "support-stencil false") # 禁用不必要的模板缓冲区
- loadPrcFileData("", "clock-mode non-real-time")
- # loadPrcFileData("", "gl-debug true")
-
- if (is_fullscreen):
- loadPrcFileData("", "fullscreen #t")
-
- self.render_pipeline = RenderPipeline()
- self.render_pipeline.pre_showbase_init()
-
- ShowBase.__init__(self)
-
- # 初始化渲染管线并设置可调整大小的标志
- self.render_pipeline.create(self)
- _global_render_pipeline = self.render_pipeline
-
- # 创建 Qt 能读的 RGBA8 贴图
- self.qt_output_tex = Texture("qt_output_tex")
- self.qt_output_tex.set_format(Texture.F_rgba8)
- self.qt_output_tex.set_component_type(Texture.T_unsigned_byte)
-
- # # 获取图形管线对象
- # gsg = self.win.getGsg()
- # host = gsg.getEngine().getHost()
-
- self.win.add_render_texture(self.qt_output_tex, GraphicsOutput.RTM_copy_ram)
-
- #self.screenTexture = Texture()
- #self.screenTexture.setMinfilter(Texture.FTLinear)
- #self.screenTexture.setFormat(Texture.FRgba32)
- #self.screenTexture.set_wrap_u(Texture.WM_clamp)
- #self.screenTexture.set_wrap_v(Texture.WM_clamp)
-
- # buff_size_x = int(self.win.get_x_size() * size)
- # buff_size_y = int(self.win.get_y_size() * size)
- # winprops = WindowProperties()
- # winprops.set_size(buff_size_x, buff_size_y)
- #
- #
- # props = FrameBufferProperties()
- # props.set_rgb_color(True)
- # props.set_rgba_bits(8, 8, 8, 8)
- # props.set_depth_bits(8)
-
- # self.buff = self.graphicsEngine.make_output(
- # self.pipe, name, sort,
- # props, winprops,
- # GraphicsPipe.BF_resizeable,
- # self.win.get_gsg(), self.win)
-
- #self.screenTexture = render_pipeline._final_stage.target.color_tex
-
- #self.buff.addRenderTexture(self.screenTexture, GraphicsOutput.RTMCopyRam)
- # self.buff.set_sort(sort)
-
-
-
- #self.cam = self.makeCamera(self.buff)
- #self.render_pipeline._showbase.cam=self.makeCamera(self.buff)
-
- #self.render_pipeline._showbase.cam=self.makeCamera(self.buff)
-
- #self.render_pipeline._showbase.camera.reparentTo(self.render)
- #base.camera.reparentTo(self.render)
- #self.cam.reparentTo(self.render) # 可选同步
-
- #render_pipeline.set_camera(self.cam)
-
- #添加渲染效果
- #self.cam = self.render_pipeline._showbase.cam
- #self.camNode = self.cam.node()
- #self.camLens = self.camNode.get_lens()
- self.render_pipeline._showbase.camera = self.render_pipeline._showbase.cam
- #self.render_pipeline.daytime_mgr.update()
-
-
- # if clear_color is None:
- # self.buff.set_clear_active(GraphicsOutput.RTPColor, False)
- # else:
- # self.buff.set_clear_color(clear_color)
- # self.buff.set_clear_active(GraphicsOutput.RTPColor, True)
-
- # self.disableMouse()
- self.mouse_controller = CustomMouseController(self)
- self.mouse_controller.setUp()
-
- # 添加错误处理钩子
- self.accept("transform_state_error", self._handle_transform_error)
-
- def _handle_transform_error(self):
- """处理TransformState相关的错误"""
- try:
- from panda3d.core import TransformState, RenderState
- TransformState.clear_cache()
- RenderState.clear_cache()
- print("已清理TransformState和RenderState缓存")
- except Exception as e:
- print(f"清理缓存时出错: {e}")
-
- def render_pipeline(self):
- """获取 RenderPipeline 实例"""
- return self._render_pipeline
-
-
- def set_parent(self, parent: QWidget):
- self.parent = parent
- self.mouseWatcherNode = QMouseWatcherNode(parent)
-
- def getAspectRatio(self, win = None):
- if win is None and self.parent is not None:
- return float(self.parent.width()) / float(self.parent.height())
- else:
- return super().getAspectRatio(win)
-
-def get_render_pipeline():
- """获取全局 RenderPipeline 单例"""
- if _global_render_pipeline is None:
- raise RuntimeError(
- "RenderPipeline has not been initialized yet. Please create a 3DWorld instance first.")
- return _global_render_pipeline
-
-def resize_buffer(self, width: int, height: int):
- props = WindowProperties()
- props.set_size(width, height)
- self.win.request_properties(props)
-
- # 重新分配输出贴图的大小
- self.qt_output_tex.set_x_size(width)
- self.qt_output_tex.set_y_size(height)
-
- # 更新 lens 的 aspect ratio
- if self.camLens:
- self.camLens.set_film_size(width, height) # 或 set_aspect_ratio(width / height)
-
- # 强制更新窗口(有时在 Qt 内嵌时需要)
+import sys
+import os
+
+from core.CustomMouseController import CustomMouseController
+
+# 获取 RenderPipelineFile 的路径
+render_pipeline_path = './RenderPipelineFile'
+# 将该路径添加到 sys.path 中,确保 Python 能够找到它
+project_root = os.path.dirname(os.path.abspath(__file__))
+sys.path.insert(0, project_root)
+sys.path.insert(0, render_pipeline_path)
+
+from RenderPipelineFile.rpcore import RenderPipeline
+_global_render_pipeline = None
+
+from PyQt5.QtCore import *
+from PyQt5.QtGui import *
+from PyQt5.QtWidgets import *
+
+from panda3d.core import *
+from panda3d.core import GraphicsOutput, Texture, ConfigVariableManager, WindowProperties
+
+from direct.showbase.ShowBase import ShowBase
+import platform
+
+from QMeta3D.QMouseWatcherNode import QMouseWatcherNode
+
+from RenderPipelineFile.rpcore.render_target import RenderTarget
+__all__ = ["Meta3DWorld"]
+
+_global_world_instance=None
+import builtins
+
+
+class Meta3DWorld(ShowBase):
+ def __init__(self, width=1380, height=750, is_fullscreen=False, size=1.0, clear_color=LVecBase4f(0, 0.5, 0, 1),
+ name="qMeta3D"):
+
+ global _global_world_instance
+ global _global_render_pipeline
+
+ _global_world_instance = self
+
+ sort = -100
+ self.parent = None
+
+ # 设置基本配置
+ loadPrcFileData("", "show-frame-rate-meter 0")
+ loadPrcFileData("", "window-type onscreen") # 改为正常窗口渲染
+ loadPrcFileData("", f"win-size {width} {height}")
+ loadPrcFileData("", "win-fixed-size #f") # 允许窗口调整大小
+
+ # 🚀 VR性能优化配置
+ loadPrcFileData("", "prefer-single-buffer true") # 减少缓冲区交换开销
+ loadPrcFileData("", "gl-force-flush false") # 避免强制glFlush导致的性能损失
+ loadPrcFileData("", "sync-video false") # 禁用默认VSync,让OpenVR控制
+ loadPrcFileData("", "support-stencil false") # 禁用不必要的模板缓冲区
+ loadPrcFileData("", "clock-mode non-real-time")
+ # loadPrcFileData("", "gl-debug true")
+
+ if (is_fullscreen):
+ loadPrcFileData("", "fullscreen #t")
+
+ self.render_pipeline = RenderPipeline()
+ self.render_pipeline.pre_showbase_init()
+
+ ShowBase.__init__(self)
+
+ # 初始化渲染管线并设置可调整大小的标志
+ self.render_pipeline.create(self)
+ _global_render_pipeline = self.render_pipeline
+
+ # 正常窗口渲染不需要额外的输出纹理
+ # 注释掉离屏渲染相关的纹理创建
+ # self.qt_output_tex = Texture("qt_output_tex")
+ # self.qt_output_tex.set_format(Texture.F_rgba8)
+ # self.qt_output_tex.set_component_type(Texture.T_unsigned_byte)
+ # self.win.add_render_texture(self.qt_output_tex, GraphicsOutput.RTM_copy_ram)
+
+ #self.screenTexture = Texture()
+ #self.screenTexture.setMinfilter(Texture.FTLinear)
+ #self.screenTexture.setFormat(Texture.FRgba32)
+ #self.screenTexture.set_wrap_u(Texture.WM_clamp)
+ #self.screenTexture.set_wrap_v(Texture.WM_clamp)
+
+ # buff_size_x = int(self.win.get_x_size() * size)
+ # buff_size_y = int(self.win.get_y_size() * size)
+ # winprops = WindowProperties()
+ # winprops.set_size(buff_size_x, buff_size_y)
+ #
+ #
+ # props = FrameBufferProperties()
+ # props.set_rgb_color(True)
+ # props.set_rgba_bits(8, 8, 8, 8)
+ # props.set_depth_bits(8)
+
+ # self.buff = self.graphicsEngine.make_output(
+ # self.pipe, name, sort,
+ # props, winprops,
+ # GraphicsPipe.BF_resizeable,
+ # self.win.get_gsg(), self.win)
+
+ #self.screenTexture = render_pipeline._final_stage.target.color_tex
+
+ #self.buff.addRenderTexture(self.screenTexture, GraphicsOutput.RTMCopyRam)
+ # self.buff.set_sort(sort)
+
+
+
+ #self.cam = self.makeCamera(self.buff)
+ #self.render_pipeline._showbase.cam=self.makeCamera(self.buff)
+
+ #self.render_pipeline._showbase.cam=self.makeCamera(self.buff)
+
+ #self.render_pipeline._showbase.camera.reparentTo(self.render)
+ #base.camera.reparentTo(self.render)
+ #self.cam.reparentTo(self.render) # 可选同步
+
+ #render_pipeline.set_camera(self.cam)
+
+ #添加渲染效果
+ #self.cam = self.render_pipeline._showbase.cam
+ #self.camNode = self.cam.node()
+ #self.camLens = self.camNode.get_lens()
+ self.render_pipeline._showbase.camera = self.render_pipeline._showbase.cam
+ #self.render_pipeline.daytime_mgr.update()
+
+
+ # if clear_color is None:
+ # self.buff.set_clear_active(GraphicsOutput.RTPColor, False)
+ # else:
+ # self.buff.set_clear_color(clear_color)
+ # self.buff.set_clear_active(GraphicsOutput.RTPColor, True)
+
+ # self.disableMouse()
+ self.mouse_controller = CustomMouseController(self)
+ self.mouse_controller.setUp()
+
+ # 添加错误处理钩子
+ self.accept("transform_state_error", self._handle_transform_error)
+
+ def _handle_transform_error(self):
+ """处理TransformState相关的错误"""
+ try:
+ from panda3d.core import TransformState, RenderState
+ TransformState.clear_cache()
+ RenderState.clear_cache()
+ print("已清理TransformState和RenderState缓存")
+ except Exception as e:
+ print(f"清理缓存时出错: {e}")
+
+ def render_pipeline(self):
+ """获取 RenderPipeline 实例"""
+ return self._render_pipeline
+
+
+ def set_parent(self, parent: QWidget):
+ self.parent = parent
+ self.mouseWatcherNode = QMouseWatcherNode(parent)
+
+ def getAspectRatio(self, win = None):
+ if win is None and self.parent is not None:
+ return float(self.parent.width()) / float(self.parent.height())
+ else:
+ return super().getAspectRatio(win)
+
+def get_render_pipeline():
+ """获取全局 RenderPipeline 单例"""
+ if _global_render_pipeline is None:
+ raise RuntimeError(
+ "RenderPipeline has not been initialized yet. Please create a 3DWorld instance first.")
+ return _global_render_pipeline
+
+def resize_buffer(self, width: int, height: int):
+ props = WindowProperties()
+ props.set_size(width, height)
+ self.win.request_properties(props)
+
+ # 重新分配输出贴图的大小
+ self.qt_output_tex.set_x_size(width)
+ self.qt_output_tex.set_y_size(height)
+
+ # 更新 lens 的 aspect ratio
+ if self.camLens:
+ self.camLens.set_film_size(width, height) # 或 set_aspect_ratio(width / height)
+
+ # 强制更新窗口(有时在 Qt 内嵌时需要)
self.graphicsEngine.open_windows()
\ No newline at end of file
diff --git a/QMeta3D/QMeta3DWidget.py b/QMeta3D/QMeta3DWidget.py
index 287b5565..556bd8be 100644
--- a/QMeta3D/QMeta3DWidget.py
+++ b/QMeta3D/QMeta3DWidget.py
@@ -1,252 +1,236 @@
-from PyQt5 import QtWidgets, QtGui
-from PyQt5.QtCore import *
-from PyQt5.QtGui import *
-from PyQt5.QtWidgets import *
-from direct.task.TaskManagerGlobal import taskMgr
-
-from QMeta3D.QMeta3D_Buttons_Translation import QMeta3D_Button_translation
-from QMeta3D.QMeta3D_Keys_Translation import QMeta3D_Key_translation
-from QMeta3D.QMeta3D_Modifiers_Translation import QMeta3D_Modifier_translation
-
-__all__ = ["QMeta3DWidget"]
-
-
-class QMeta3DSynchronizer(QTimer):
- def __init__(self, qMeta3DWidget, FPS=60):
- QTimer.__init__(self)
- self.qMeta3DWidget = qMeta3DWidget
- dt = 1000 / FPS
- self.setInterval(int(dt))
- self.timeout.connect(self.tick)
-
- def tick(self):
- try:
- # 在渲染前清理可能损坏的TransformState对象
- from panda3d.core import TransformState
- TransformState.clear_cache()
-
- taskMgr.step()
- self.qMeta3DWidget.update()
- except AssertionError as e:
- # 专门处理 TransformState has_mat() 断言错误
- if "has_mat" in str(e):
- print(f"警告: 检测到TransformState断言错误,已静默处理: {e}")
- # 尝试恢复渲染状态
- try:
- # 强制清理缓存并重试
- from panda3d.core import TransformState, RenderState
- TransformState.clear_cache()
- RenderState.clear_cache()
- taskMgr.step()
- self.qMeta3DWidget.update()
- except:
- pass
- else:
- # 重新抛出其他断言错误
- raise
- except Exception as e:
- # 静默处理其他所有异常
- print(f"警告: 检测到异常,已静默处理: {e}")
-
-
-def get_panda_key_modifiers(evt):
- panda_mods = []
- qt_mods = evt.modifiers()
- for qt_mod, panda_mod in QMeta3D_Modifier_translation.items():
- if (qt_mods & qt_mod) == qt_mod:
- panda_mods.append(panda_mod)
- return panda_mods
-
-
-def get_panda_key_modifiers_prefix(evt):
- # join all modifiers (except NoModifier, which is None) with '-'
- prefix = "-".join([mod for mod in get_panda_key_modifiers(evt) if mod is not None])
-
- # if the prefix is not empty, append a '-'
- if prefix:
- prefix += '-'
-
- return prefix
-
-
-class QMeta3DWidget(QWidget):
- def __init__(self, Meta3DWorld, parent=None, FPS=60, debug=False):
- QWidget.__init__(self, parent)
- self.rp_sync_requested = False
- # set fixed geometry
- self.Meta3DWorld = Meta3DWorld
- self.Meta3DWorld.set_parent(self)
- # Setup a timer in Qt that runs taskMgr.step() to simulate Panda's own main loop
- # pandaTimer = QTimer(self)
- # pandaTimer.timeout.connect()
- # pandaTimer.start(0)
-
- self.setFocusPolicy(Qt.StrongFocus)
-
- # Setup another timer that redraws this widget in a specific FPS
- # redrawTimer = QTimer(self)
- # redrawTimer.timeout.connect(self.update)
- # redrawTimer.start(1000/FPS)
-
- self.paintSurface = QPainter()
- self.rotate = QTransform()
- self.rotate.rotate(180)
- self.out_image = QImage()
-
- size = self.Meta3DWorld.cam.node().get_lens().get_film_size()
- self.initial_film_size = QSizeF(size.x, size.y)
- self.initial_size = self.size()
-
- self.synchronizer = QMeta3DSynchronizer(self, FPS)
- self.synchronizer.start()
-
- self.debug = debug
-
- def mousePressEvent(self, evt):
- button = evt.button()
- try:
- b = "{}{}".format(get_panda_key_modifiers_prefix(evt), QMeta3D_Button_translation[button])
- if self.debug:
- print(b)
- messenger.send(b,[{"x":evt.x(),"y":evt.y()}])
- except:
- print("Unimplemented button. Please send an issue on github to fix this problem")
-
- def mouseMoveEvent(self, evt:QtGui.QMouseEvent):
- button = evt.button()
- try:
- b = "mouse-move"
- if self.debug:
- print(b)
- messenger.send(b,[{"x":evt.x(),"y":evt.y()}])
- except:
- print("Unimplemented button. Please send an issue on github to fix this problem")
-
-
- def mouseReleaseEvent(self, evt):
- button = evt.button()
- try:
- b = "{}{}-up".format(get_panda_key_modifiers_prefix(evt), QMeta3D_Button_translation[button])
- if self.debug:
- print(b)
- messenger.send(b,[{"x":evt.x(),"y":evt.y()}])
- except:
- print("Unimplemented button. Please send an issue on github to fix this problem")
-
- def wheelEvent(self, evt):
- delta = evt.angleDelta().y()
- try:
- if self.debug:
- print(f"wheel {delta}")
- messenger.send('wheel',[{"delta":delta}])
- except:
- print("Unimplemented button. Please send an issue on github to fix this problem")
-
- def keyPressEvent(self, evt):
- key = evt.key()
- try:
- k = "{}{}".format(get_panda_key_modifiers_prefix(evt), QMeta3D_Key_translation[key])
- if self.debug:
- print(k)
- messenger.send(k)
- except:
- print("Unimplemented key. Please send an issue on github to fix this problem")
-
- def keyReleaseEvent(self, evt):
- key = evt.key()
- try:
- k = "{}{}-up".format(get_panda_key_modifiers_prefix(evt), QMeta3D_Key_translation[key])
- if self.debug:
- print(k)
- messenger.send(k)
- except:
- print("Unimplemented key. Please send an issue on github to fix this problem")
-
- def resizeEvent(self, evt):
- width = evt.size().width()
- height = evt.size().height()
- #print(f"width:{width}")
- #print(f"height:{height}")
-
- from Meta3DWorld import resize_buffer
-
- lens = self.Meta3DWorld.cam.node().get_lens()
-
- def minimumSizeHint(self):
- return QSize(400, 300)
-
- def paintEvent(self, event):
- tex = self.Meta3DWorld.qt_output_tex
- gsg = base.win.getGsg()
-
- if not gsg:
- self.update()
- return
-
- if not tex.hasRamImage():
- base.graphicsEngine.extractTextureData(tex, gsg)
- self.update()
- return
-
- data = tex.getRamImage().getData()
- tex_width = tex.getXSize()
- tex_height = tex.getYSize()
- expected_len = tex_width * tex_height * 4
-
- if len(data) != expected_len:
- print(f"⚠️ 像素数据长度异常({len(data)} != {expected_len}),跳过绘制")
- self.update()
- return
-
- img = QImage(data, tex_width, tex_height, QImage.Format_ARGB32).mirrored()
-
- widget_width = self.width()
- widget_height = self.height()
-
- painter = QPainter(self)
-
- # 【保持宽高比的缩放】
- scaled_img = img.scaled(widget_width, widget_height, Qt.KeepAspectRatio, Qt.SmoothTransformation)
-
- # 居中绘制
- x_offset = (widget_width - scaled_img.width()) // 2
- y_offset = (widget_height - scaled_img.height()) // 2
-
- painter.drawImage(x_offset, y_offset, scaled_img)
- painter.end()
-
- def sync_Meta3d_window_size(self, width, height):
- try:
- from panda3d.core import WindowProperties, LVecBase2i
-
- # 确保尺寸是4的倍数(RenderPipeline 要求)
- adjusted_width = width - width % 4
- adjusted_height = height - height % 4
-
- # 更新窗口属性
- props = WindowProperties()
- props.setSize(adjusted_width, adjusted_height)
-
- # 对于 offscreen 渲染,直接更新窗口属性
- if self.Meta3DWorld.win:
- self.Meta3DWorld.win.request_properties(props)
-
- # 手动触发 RenderPipeline 的尺寸更新逻辑
- if hasattr(self.Meta3DWorld, 'render_pipeline'):
- # 更新全局分辨率
- from RenderPipelineFile.rpcore.globals import Globals
- Globals.native_resolution = LVecBase2i(adjusted_width, adjusted_height)
-
- # 重新计算渲染分辨率
- self.Meta3DWorld.render_pipeline._compute_render_resolution()
-
- # 通知各个管理器处理尺寸变化
- self.Meta3DWorld.render_pipeline.light_mgr.compute_tile_size()
- self.Meta3DWorld.render_pipeline.stage_mgr.handle_window_resize()
- if hasattr(self.Meta3DWorld.render_pipeline, 'debugger'):
- self.Meta3DWorld.render_pipeline.debugger.handle_window_resize()
-
- # 触发插件的窗口尺寸变化钩子
- self.Meta3DWorld.render_pipeline.plugin_mgr.trigger_hook("window_resized")
- except Exception as e:
+from PyQt5 import QtWidgets, QtGui
+from PyQt5.QtCore import *
+from PyQt5.QtGui import *
+from PyQt5.QtWidgets import *
+from direct.task.TaskManagerGlobal import taskMgr
+
+from QMeta3D.QMeta3D_Buttons_Translation import QMeta3D_Button_translation
+from QMeta3D.QMeta3D_Keys_Translation import QMeta3D_Key_translation
+from QMeta3D.QMeta3D_Modifiers_Translation import QMeta3D_Modifier_translation
+
+__all__ = ["QMeta3DWidget"]
+
+
+class QMeta3DSynchronizer(QTimer):
+ def __init__(self, qMeta3DWidget, FPS=60):
+ QTimer.__init__(self)
+ self.qMeta3DWidget = qMeta3DWidget
+ dt = 1000 / FPS
+ self.setInterval(int(dt))
+ self.timeout.connect(self.tick)
+
+ def tick(self):
+ try:
+ # 在渲染前清理可能损坏的TransformState对象
+ from panda3d.core import TransformState
+ TransformState.clear_cache()
+
+ taskMgr.step()
+ self.qMeta3DWidget.update()
+ except AssertionError as e:
+ # 专门处理 TransformState has_mat() 断言错误
+ if "has_mat" in str(e):
+ print(f"警告: 检测到TransformState断言错误,已静默处理: {e}")
+ # 尝试恢复渲染状态
+ try:
+ # 强制清理缓存并重试
+ from panda3d.core import TransformState, RenderState
+ TransformState.clear_cache()
+ RenderState.clear_cache()
+ taskMgr.step()
+ self.qMeta3DWidget.update()
+ except:
+ pass
+ else:
+ # 重新抛出其他断言错误
+ raise
+ except Exception as e:
+ # 静默处理其他所有异常
+ print(f"警告: 检测到异常,已静默处理: {e}")
+
+
+def get_panda_key_modifiers(evt):
+ panda_mods = []
+ qt_mods = evt.modifiers()
+ for qt_mod, panda_mod in QMeta3D_Modifier_translation.items():
+ if (qt_mods & qt_mod) == qt_mod:
+ panda_mods.append(panda_mod)
+ return panda_mods
+
+
+def get_panda_key_modifiers_prefix(evt):
+ # join all modifiers (except NoModifier, which is None) with '-'
+ prefix = "-".join([mod for mod in get_panda_key_modifiers(evt) if mod is not None])
+
+ # if the prefix is not empty, append a '-'
+ if prefix:
+ prefix += '-'
+
+ return prefix
+
+
+class QMeta3DWidget(QWidget):
+ def __init__(self, Meta3DWorld, parent=None, FPS=60, debug=False):
+ QWidget.__init__(self, parent)
+ self.rp_sync_requested = False
+ # set fixed geometry
+ self.Meta3DWorld = Meta3DWorld
+ self.Meta3DWorld.set_parent(self)
+ # Setup a timer in Qt that runs taskMgr.step() to simulate Panda's own main loop
+ # pandaTimer = QTimer(self)
+ # pandaTimer.timeout.connect()
+ # pandaTimer.start(0)
+
+ self.setFocusPolicy(Qt.StrongFocus)
+
+ # Setup another timer that redraws this widget in a specific FPS
+ # redrawTimer = QTimer(self)
+ # redrawTimer.timeout.connect(self.update)
+ # redrawTimer.start(1000/FPS)
+
+ self.paintSurface = QPainter()
+ self.rotate = QTransform()
+ self.rotate.rotate(180)
+ self.out_image = QImage()
+
+ size = self.Meta3DWorld.cam.node().get_lens().get_film_size()
+ self.initial_film_size = QSizeF(size.x, size.y)
+ self.initial_size = self.size()
+
+ self.synchronizer = QMeta3DSynchronizer(self, FPS)
+ self.synchronizer.start()
+
+ self.debug = debug
+
+ def mousePressEvent(self, evt):
+ button = evt.button()
+ try:
+ b = "{}{}".format(get_panda_key_modifiers_prefix(evt), QMeta3D_Button_translation[button])
+ if self.debug:
+ print(b)
+ messenger.send(b,[{"x":evt.x(),"y":evt.y()}])
+ except:
+ print("Unimplemented button. Please send an issue on github to fix this problem")
+
+ def mouseMoveEvent(self, evt:QtGui.QMouseEvent):
+ button = evt.button()
+ try:
+ b = "mouse-move"
+ if self.debug:
+ print(b)
+ messenger.send(b,[{"x":evt.x(),"y":evt.y()}])
+ except:
+ print("Unimplemented button. Please send an issue on github to fix this problem")
+
+
+ def mouseReleaseEvent(self, evt):
+ button = evt.button()
+ try:
+ b = "{}{}-up".format(get_panda_key_modifiers_prefix(evt), QMeta3D_Button_translation[button])
+ if self.debug:
+ print(b)
+ messenger.send(b,[{"x":evt.x(),"y":evt.y()}])
+ except:
+ print("Unimplemented button. Please send an issue on github to fix this problem")
+
+ def wheelEvent(self, evt):
+ delta = evt.angleDelta().y()
+ try:
+ if self.debug:
+ print(f"wheel {delta}")
+ messenger.send('wheel',[{"delta":delta}])
+ except:
+ print("Unimplemented button. Please send an issue on github to fix this problem")
+
+ def keyPressEvent(self, evt):
+ key = evt.key()
+ try:
+ k = "{}{}".format(get_panda_key_modifiers_prefix(evt), QMeta3D_Key_translation[key])
+ if self.debug:
+ print(k)
+ messenger.send(k)
+ except:
+ print("Unimplemented key. Please send an issue on github to fix this problem")
+
+ def keyReleaseEvent(self, evt):
+ key = evt.key()
+ try:
+ k = "{}{}-up".format(get_panda_key_modifiers_prefix(evt), QMeta3D_Key_translation[key])
+ if self.debug:
+ print(k)
+ messenger.send(k)
+ except:
+ print("Unimplemented key. Please send an issue on github to fix this problem")
+
+ def resizeEvent(self, evt):
+ width = evt.size().width()
+ height = evt.size().height()
+ #print(f"width:{width}")
+ #print(f"height:{height}")
+
+ from Meta3DWorld import resize_buffer
+
+ lens = self.Meta3DWorld.cam.node().get_lens()
+
+ def minimumSizeHint(self):
+ return QSize(400, 300)
+
+ def paintEvent(self, event):
+ # 正常窗口渲染模式下,Panda3D 会直接渲染到窗口
+ # 不需要从纹理复制数据
+ # 只需要确保渲染循环继续运行
+
+ # 触发一次渲染更新
+ if hasattr(self.Meta3DWorld, 'win') and self.Meta3DWorld.win:
+ # 确保窗口大小匹配
+ width = self.width()
+ height = self.height()
+
+ # 如果窗口大小不匹配,更新窗口属性
+ if (self.Meta3DWorld.win.get_x_size() != width or
+ self.Meta3DWorld.win.get_y_size() != height):
+ from panda3d.core import WindowProperties
+ props = WindowProperties()
+ props.setSize(width, height)
+ self.Meta3DWorld.win.request_properties(props)
+
+ # 继续下一次更新
+ self.update()
+
+ def sync_Meta3d_window_size(self, width, height):
+ try:
+ from panda3d.core import WindowProperties
+
+ # 更新窗口属性
+ props = WindowProperties()
+ props.setSize(width, height)
+
+ # 对于正常窗口渲染,直接更新窗口属性
+ if self.Meta3DWorld.win:
+ self.Meta3DWorld.win.request_properties(props)
+
+ # 如果使用 RenderPipeline,更新相关设置
+ if hasattr(self.Meta3DWorld, 'render_pipeline'):
+ try:
+ from RenderPipelineFile.rpcore.globals import Globals
+ from panda3d.core import LVecBase2i
+
+ # 更新全局分辨率
+ Globals.native_resolution = LVecBase2i(width, height)
+
+ # 重新计算渲染分辨率
+ self.Meta3DWorld.render_pipeline._compute_render_resolution()
+
+ # 通知各个管理器处理尺寸变化
+ self.Meta3DWorld.render_pipeline.light_mgr.compute_tile_size()
+ self.Meta3DWorld.render_pipeline.stage_mgr.handle_window_resize()
+
+ if hasattr(self.Meta3DWorld.render_pipeline, 'debugger'):
+ self.Meta3DWorld.render_pipeline.debugger.handle_window_resize()
+
+ # 触发插件的窗口尺寸变化钩子
+ self.Meta3DWorld.render_pipeline.plugin_mgr.trigger_hook("window_resized")
+ except Exception as rp_e:
+ print(f"RenderPipeline 尺寸更新失败: {rp_e}")
+ except Exception as e:
print(f"同步 Meta3D 窗口尺寸失败: {str(e)}")
\ No newline at end of file
diff --git a/main.py b/main.py
index bf0e87ea..459d4eb1 100644
--- a/main.py
+++ b/main.py
@@ -1,818 +1,815 @@
-import warnings
-
-from core.Command_System import CommandManager
-from core.InfoPanelManager import InfoPanelManager
-from core.patrol_system import PatrolSystem
-from demo.video_integration import VideoManager
-
-warnings.filterwarnings("ignore", category=DeprecationWarning)
-
-import sys
-import builtins
-from PyQt5.QtWidgets import (QApplication, QMainWindow, QMenuBar, QMenu, QAction,
- QDockWidget, QTreeWidget, QListWidget, QWidget, QVBoxLayout, QTreeWidgetItem,
- QLabel, QLineEdit, QFormLayout, QDoubleSpinBox, QScrollArea, QTreeView, QInputDialog, QFileDialog, QMessageBox, QDialog, QGroupBox, QHBoxLayout, QPushButton, QDialogButtonBox)
-from PyQt5.QtCore import Qt, QDir, QUrl
-from PyQt5.QtGui import QDrag, QPainter, QPixmap
-from PyQt5.QtWidgets import QFileSystemModel
-from QMeta3D.QMeta3DWidget import QMeta3DWidget
-from panda3d.core import loadPrcFileData
-loadPrcFileData("", "assertions 0")
-from core.world import CoreWorld
-from core.selection import SelectionSystem
-from core.event_handler import EventHandler
-from core.tool_manager import ToolManager
-from core.script_system import ScriptManager
-from core.patrol_system import PatrolSystem
-from core.Command_System import CommandManager
-from gui.gui_manager import GUIManager
-from core.terrain_manager import TerrainManager
-from scene.scene_manager import SceneManager
-from project.project_manager import ProjectManager
-from ui.widgets import CustomMeta3DWidget, CustomFileView, CustomTreeWidget
-from ui.property_panel import PropertyPanelManager
-from ui.interface_manager import InterfaceManager
-from panda3d.core import (CardMaker, Vec4, Vec3, ColorAttrib, MaterialAttrib,
- GeomNode, NodePath, Material, CollisionTraverser,
- CollisionHandlerQueue, CollisionNode, CollisionRay,
- Plane, Point3, Point2, BitMask32, CollisionSphere,
- CollisionPlane, ModelPool, Filename, ModelRoot,
- AmbientLight, DirectionalLight, TextureAttrib, DriveInterface, WindowProperties)
-from panda3d.egg import EggData, EggVertexPool, EggPolygon
-from direct.task import Task
-from direct.task.TaskManagerGlobal import taskMgr
-from direct.showbase.ShowBase import ShowBase
-from direct.showbase.DirectObject import DirectObject
-from direct.showbase.ShowBaseGlobal import globalClock, aspect2d
-import os
-import json
-import datetime
-from direct.actor.Actor import Actor
-from PyQt5.sip import wrapinstance
-#from RenderPipelineFile.toolkit.material_editor.main import MaterialEditor
-
-class MyWorld(CoreWorld):
- def __init__(self):
- super().__init__()
-
- # 初始化选择和变换系统
- self.selection = SelectionSystem(self)
-
- # 绑定F键用于聚焦选中节点
- self.accept("f", self.onFocusKeyPressed)
- self.accept("F", self.onFocusKeyPressed) # 大写F
-
- #初始化巡检系统
- self.patrol_system = PatrolSystem(self)
- self.accept("p",self.onPatrolKeyPressed)
- self.accept("P",self.onPatrolKeyPressed)
-
- # 初始化事件处理系统
- self.event_handler = EventHandler(self)
-
- # 初始化工具管理系统
- self.tool_manager = ToolManager(self)
-
- # 初始化脚本管理系统
- self.script_manager = ScriptManager(self)
-
- # 初始化GUI管理系统
- self.gui_manager = GUIManager(self)
-
- # 初始化视频管理
- self.video_manager = VideoManager(self)
-
- # 初始化场景管理系统
- self.scene_manager = SceneManager(self)
-
- # 初始化项目管理系统
- self.project_manager = ProjectManager(self)
-
- # 初始化属性面板管理系统
- self.property_panel = PropertyPanelManager(self)
-
- # 初始化界面管理系统
- self.interface_manager = InterfaceManager(self)
-
-
- # 启动脚本系统
- self.script_manager.start_system()
-
- #self.material_editor = None
- self.terrain_manager = TerrainManager(self)
-
- self.terrain_edit_radius = 3.0
- self.terrain_edit_strength=0.3
- self.terrain_edit_operation = "add"
-
- self.info_panel_manager = InfoPanelManager(self)
-
- self.command_manager = CommandManager()
-
- # 初始化碰撞管理器
- from core.collision_manager import CollisionManager
- self.collision_manager = CollisionManager(self)
-
- # 初始化VR管理器
- try:
- from core.vr import VRManager
- self.vr_manager = VRManager(self)
- print("✓ VR管理器初始化完成")
- except Exception as e:
- print(f"⚠ VR管理器初始化失败: {e}")
- self.vr_manager = None
-
- # 调试选项
- self.debug_collision = True # 是否显示碰撞体
-
- # 默认启用模型间碰撞检测(可选)
- self.enableModelCollisionDetection(enable=True, frequency=0.1, threshold=0.5)
-
- try:
- self.property_panel = PropertyPanelManager(self)
- print("✓ 属性面板管理器初始化完成")
- except Exception as e:
- print(f"⚠ 属性面板管理器初始化失败: {e}")
- import traceback
- traceback.print_exc()
- self.property_panel = None
-
- print("✓ MyWorld 初始化完成")
- print("✅ 碰撞管理器已初始化")
-
- # ==================== 兼容性属性 ====================
-
- # 保留models属性以兼容现有代码
- @property
- def models(self):
- """模型列表的兼容性属性"""
- return self.scene_manager.models
-
- @models.setter
- def models(self, value):
- """模型列表的兼容性设置器"""
- self.scene_manager.models = value
-
- # 保留gui_elements属性以兼容现有代码
- @property
- def gui_elements(self):
- """GUI元素列表的兼容性属性"""
- return self.gui_manager.gui_elements
-
- @gui_elements.setter
- def gui_elements(self, value):
- """GUI元素列表的兼容性设置器"""
- self.gui_manager.gui_elements = value
-
- # 保留gui_elements属性以兼容现有代码
- @property
- def Spotlight(self):
- """GUI元素列表的兼容性属性"""
- return self.scene_manager.Spotlight
-
- @Spotlight.setter
- def Spotlight(self, value):
- """GUI元素列表的兼容性设置器"""
- self.scene_manager.Spotlight = value
-
- @property
- def Pointlight(self):
- return self.scene_manager.Pointlight
-
- @Pointlight.setter
- def Pointlight(self,value):
- self.scene_manager.Pointlight = value
-
-
- # 保留guiEditMode属性以兼容现有代码
- @property
- def guiEditMode(self):
- """GUI编辑模式的兼容性属性"""
- return self.gui_manager.guiEditMode
-
- @guiEditMode.setter
- def guiEditMode(self, value):
- """GUI编辑模式的兼容性设置器"""
- self.gui_manager.guiEditMode = value
-
- # 保留currentTool属性以兼容现有代码
- @property
- def currentTool(self):
- """当前工具的兼容性属性"""
- return self.tool_manager.currentTool
-
- @currentTool.setter
- def currentTool(self, value):
- """当前工具的兼容性设置器"""
- self.tool_manager.currentTool = value
-
- # 保留treeWidget属性以兼容现有代码
- @property
- def treeWidget(self):
- """树形控件的兼容性属性"""
- return self.interface_manager.treeWidget
-
- @treeWidget.setter
- def treeWidget(self, value):
- """树形控件的兼容性设置器"""
- self.interface_manager.treeWidget = value
-
- #保留terrains属性以兼容现有代码
- @property
- def terrains(self):
- """地形列表的兼容性属性"""
- return self.terrain_manager.terrains
-
- @terrains.setter
- def terrains(self,value):
- """地形列表的兼容性设置器"""
- self.terrain_manager.terrains = value
-
- # ==================== GUI管理功能代理 ====================
-
- # GUI元素创建方法 - 代理到gui_manager
- def createGUIButton(self, pos=(0, 0, 0), text="按钮", size=0.1):
- """创建2D GUI按钮"""
- return self.gui_manager.createGUIButton(pos, text, size)
-
- def createGUILabel(self, pos=(0, 0, 0), text="标签", size=0.08):
- """创建2D GUI标签"""
- return self.gui_manager.createGUILabel(pos, text, size)
-
- def createGUIEntry(self, pos=(0, 0, 0), placeholder="输入文本...", size=0.08):
- """创建2D GUI文本输入框"""
- return self.gui_manager.createGUIEntry(pos, placeholder, size)
-
- def createGUI3DText(self, pos=(0, 0, 0), text="3D文本", size=1):
- """创建3D空间文本"""
- return self.gui_manager.createGUI3DText(pos, text, size)
-
- def createGUI3DImage(self,pos=(0,0,0),text="3D图片",size=(1,1,1)):
- """创建3D图片"""
- return self.gui_manager.createGUI3DImage(pos,text,size)
-
- def createGUI2DImage(self, pos=(0, 0, 0), image_path=None, size=2):
- """创建2D GUI图片"""
- return self.gui_manager.createGUI2DImage(pos, image_path, size)
-
- def createVideoScreen(self,pos=(0,0,0),size=1,video_path=None):
- """创建视频屏幕"""
- return self.gui_manager.createVideoScreen(pos,size,video_path)
-
- def create2DVideoScreen(self,pos=(0,0,0),size=0.2,video_path=None):
- """创建2D视频屏幕"""
- return self.gui_manager.createGUI2DVideoScreen(pos,size,video_path)
-
- def createSphericalVideo(self,pos=(0,0,0),radius=5.0,video_path=None):
- """创建360度视频"""
- return self.gui_manager.createSphericalVideo(pos,radius,video_path)
-
- def playSphericalVideo(self,spherical_video_node):
- return self.gui_manager.playSphericalVideo(spherical_video_node)
-
- def pauseSphericalVideo(self,spherical_video_node):
- return self.gui_manager.pauseSphericalVideo(spherical_video_node)
-
- def setSphericalVideoTime(self,spherical_video_node,time_seconds):
- return self.gui_manager.setSphericalVideoTime(spherical_video_node,time_seconds)
-
- def createSpotLight(self,pos=(0,0,5)):
- """创建聚光灯"""
- return self.scene_manager.createSpotLight(pos)
-
- def createPointLight(self,pos=(0,0,5)):
- """创建点光源"""
- return self.scene_manager.createPointLight(pos)
-
- def createGUIVirtualScreen(self, pos=(0, 0, 0), size=(2, 1), text="虚拟屏幕"):
- """创建3D虚拟屏幕"""
- return self.gui_manager.createGUIVirtualScreen(pos, size, text)
-
- def createGUISlider(self, pos=(0, 0, 0), text="滑块", scale=0.3):
- """创建2D GUI滑块"""
- return self.gui_manager.createGUISlider(pos, text, scale)
-
- # GUI元素管理方法 - 代理到gui_manager
- def deleteGUIElement(self, gui_element):
- """删除GUI元素"""
- return self.gui_manager.deleteGUIElement(gui_element)
-
- def editGUIElement(self, gui_element, property_name, value):
- """编辑GUI元素属性"""
- return self.gui_manager.editGUIElement(gui_element, property_name, value)
-
- def duplicateGUIElement(self, gui_element):
- """复制GUI元素"""
- return self.gui_manager.duplicateGUIElement(gui_element)
-
- def editGUIElementDialog(self, gui_element):
- """显示GUI元素编辑对话框"""
- return self.gui_manager.editGUIElementDialog(gui_element)
-
- # GUI事件处理方法 - 代理到gui_manager
- def onGUIButtonClick(self, button_id):
- """GUI按钮点击事件处理"""
- return self.gui_manager.onGUIButtonClick(button_id)
-
- def onGUIEntrySubmit(self, text, entry_id):
- """GUI输入框提交事件处理"""
- return self.gui_manager.onGUIEntrySubmit(text, entry_id)
-
- # GUI编辑模式方法 - 代理到gui_manager
- def toggleGUIEditMode(self):
- """切换GUI编辑模式"""
- return self.gui_manager.toggleGUIEditMode()
-
- def enterGUIEditMode(self):
- """进入GUI编辑模式"""
- return self.gui_manager.enterGUIEditMode()
-
- def exitGUIEditMode(self):
- """退出GUI编辑模式"""
- return self.gui_manager.exitGUIEditMode()
-
- def createGUIEditPanel(self):
- """创建GUI编辑面板"""
- return self.gui_manager.createGUIEditPanel()
-
- def openGUIPreviewWindow(self):
- """打开独立的GUI预览窗口"""
- return self.gui_manager.openGUIPreviewWindow()
-
- def closeGUIPreviewWindow(self):
- """关闭GUI预览窗口"""
- return self.gui_manager.closeGUIPreviewWindow()
-
- # GUI工具和选择方法 - 代理到gui_manager
- def setGUICreateTool(self, tool_type):
- """设置GUI创建工具"""
- return self.gui_manager.setGUICreateTool(tool_type)
-
- def deleteSelectedGUI(self):
- """删除选中的GUI元素"""
- return self.gui_manager.deleteSelectedGUI()
-
- def copySelectedGUI(self):
- """复制选中的GUI元素"""
- return self.gui_manager.copySelectedGUI()
-
- def handleGUIEditClick(self, hitPos):
- """处理GUI编辑模式下的点击"""
- return self.gui_manager.handleGUIEditClick(hitPos)
-
- def createGUIAtPosition(self, world_pos, gui_type):
- """在指定位置创建GUI元素"""
- return self.gui_manager.createGUIAtPosition(world_pos, gui_type)
-
- def findClickedGUI(self, hitNode):
- """查找被点击的GUI元素"""
- return self.gui_manager.findClickedGUI(hitNode)
-
- def selectGUIInTree(self, gui_element):
- """在树形控件中选中GUI元素"""
- return self.gui_manager.selectGUIInTree(gui_element)
-
- def updateGUISelection(self, gui_element):
- """更新GUI元素选择状态"""
- return self.gui_manager.updateGUISelection(gui_element)
-
- # GUI属性面板方法 - 代理到gui_manager
- def selectGUIColor(self, gui_element):
- """选择GUI元素颜色"""
- return self.gui_manager.selectGUIColor(gui_element)
-
- def editGUI2DPosition(self, gui_element, axis, value):
- """编辑2D GUI元素位置"""
- return self.gui_manager.editGUI2DPosition(gui_element, axis, value)
-
- # ==================== 事件处理代理 ====================
-
- def onTreeItemClicked(self, item, column):
- """处理树形控件项目点击事件 - 代理到interface_manager"""
- return self.interface_manager.onTreeItemClicked(item, column)
-
- def setTreeWidget(self, treeWidget):
- """设置树形控件引用 - 代理到interface_manager"""
- return self.interface_manager.setTreeWidget(treeWidget)
-
- def mousePressEventLeft(self, evt):
- """处理鼠标左键按下事件 - 代理到event_handler"""
- return self.event_handler.mousePressEventLeft(evt)
-
- def mouseReleaseEventLeft(self, evt):
- """处理鼠标左键释放事件 - 代理到event_handler"""
- return self.event_handler.mouseReleaseEventLeft(evt)
-
- def wheelForward(self, data=None):
- """处理滚轮向前滚动(前进) - 代理到event_handler"""
- return self.event_handler.wheelForward(data)
-
- def wheelBackward(self, data=None):
- """处理滚轮向后滚动(后退) - 代理到event_handler"""
- return self.event_handler.wheelBackward(data)
-
- def mousePressEventMiddle(self, evt):
- """处理鼠标中键按下事件 - 代理到event_handler"""
- return self.event_handler.mousePressEventMiddle(evt)
-
- def mouseReleaseEventMiddle(self, evt):
- """处理鼠标中键释放事件 - 代理到event_handler"""
- return self.event_handler.mouseReleaseEventMiddle(evt)
-
- def mouseMoveEvent(self, evt):
- """处理鼠标移动事件 - 代理到event_handler"""
- return self.event_handler.mouseMoveEvent(evt)
-
- # ==================== 射线显示控制 ====================
-
- def toggleRayDisplay(self):
- """切换射线显示状态 - 代理到event_handler"""
- return self.event_handler.toggleRayDisplay()
-
- def setRayDisplay(self, show=True):
- """设置射线显示状态 - 代理到event_handler"""
- self.event_handler.showRay = show
- if not show:
- self.event_handler.clearRay()
- return show
-
- def getRayDisplay(self):
- """获取射线显示状态 - 代理到event_handler"""
- return self.event_handler.showRay
-
- def setRayLifetime(self, seconds):
- """设置射线显示时长(秒) - 代理到event_handler"""
- self.event_handler.rayLifetime = seconds
- print(f"射线显示时长设置为: {seconds}秒")
-
- def getRayLifetime(self):
- """获取射线显示时长 - 代理到event_handler"""
- return self.event_handler.rayLifetime
-
- # ==================== 属性面板代理 ====================
-
- def setPropertyLayout(self, layout):
- """设置属性面板布局引用 - 代理到property_panel"""
- return self.property_panel.setPropertyLayout(layout)
-
- def clearPropertyPanel(self):
- """清空属性面板 - 代理到property_panel"""
- return self.property_panel.clearPropertyPanel()
-
- def updatePropertyPanel(self, item):
- """更新属性面板显示 - 代理到property_panel"""
- return self.property_panel.updatePropertyPanel(item)
-
- def updateGUIPropertyPanel(self, gui_element):
- """更新GUI元素属性面板 - 代理到property_panel"""
- return self.property_panel.updateGUIPropertyPanel(gui_element)
-
- def removeActorForModel(self,model):
- return self.property_panel.removeActorForModel( model)
-
- def updateNodeVisibilityAfterDrag(self,item):
- return self.property_panel.updateNodeVisibilityAfterDrag(item)
-
- # ==================== 工具管理代理 ====================
-
- def setCurrentTool(self, tool):
- """设置当前工具 - 代理到tool_manager"""
- return self.tool_manager.setCurrentTool(tool)
-
- # ==================== 场景管理功能代理 ====================
-
- # 模型导入和处理方法 - 代理到scene_manager
- def importModel(self, filepath):
- """导入模型到场景"""
- # 自动转换FBX等格式为GLB以获得更好的动画支持(不再弹窗询问)
- # 如果你不想自动转换,可以将 auto_convert 改为 False
- auto_convert = False
-
- print(f"[模型导入] 文件: {filepath}, 自动转换GLB: {auto_convert}")
-
- return self.scene_manager.importModel(filepath, auto_convert_to_glb=auto_convert)
-
- def importModelAsync(self, filepath):
- """异步导入模型"""
- return self.scene_manager.importModelAsync(filepath)
-
- # 材质和几何体处理方法 - 代理到scene_manager
- def processMaterials(self, model):
- """处理模型材质"""
- return self.scene_manager.processMaterials(model)
-
- def processModelGeometry(self, model):
- """处理模型几何体"""
- return self.scene_manager.processModelGeometry(model)
-
- # 碰撞系统方法 - 代理到scene_manager
- def setupCollision(self, model):
- """为模型设置碰撞检测"""
- return self.scene_manager.setupCollision(model)
-
- # 场景树管理方法 - 代理到scene_manager
- def updateSceneTree(self):
- """更新场景树显示"""
- return self.scene_manager.updateSceneTree()
-
- # 场景保存和加载方法 - 代理到scene_manager
- def saveScene(self, filename):
- """保存场景到BAM文件"""
- return self.scene_manager.saveScene(filename)
-
- def loadScene(self, filename):
- """从BAM文件加载场景"""
- return self.scene_manager.loadScene(filename)
-
- # 模型管理方法 - 代理到scene_manager
- def deleteModel(self, model):
- """删除模型"""
- return self.scene_manager.deleteModel(model)
-
- def clearAllModels(self):
- """清除所有模型"""
- return self.scene_manager.clearAllModels()
-
- def getModels(self):
- """获取模型列表"""
- return self.scene_manager.getModels()
-
- def getModelCount(self):
- """获取模型数量"""
- return self.scene_manager.getModelCount()
-
- def findModelByName(self, name):
- """根据名称查找模型"""
- return self.scene_manager.findModelByName(name)
-
- # ==================== 脚本系统功能代理 ====================
-
- # 脚本系统控制方法 - 代理到script_manager
- def startScriptSystem(self):
- """启动脚本系统"""
- return self.script_manager.start_system()
-
- def stopScriptSystem(self):
- """停止脚本系统"""
- return self.script_manager.stop_system()
-
- def enableHotReload(self, enabled=True):
- """启用/禁用热重载"""
- self.script_manager.hot_reload_enabled = enabled
- if enabled:
- self.script_manager.start_hot_reload()
- else:
- self.script_manager.stop_hot_reload()
-
- # 脚本创建和加载方法 - 代理到script_manager
- def createScript(self, script_name, template="basic"):
- """创建新脚本文件"""
- return self.script_manager.create_script_file(script_name, template)
-
- def loadScript(self, script_path):
- """从文件加载脚本"""
- return self.script_manager.load_script_from_file(script_path)
-
- def loadAllScripts(self, directory=None):
- """从目录加载所有脚本"""
- return self.script_manager.load_all_scripts_from_directory(directory)
-
- def reloadScript(self, script_name):
- """重新加载脚本"""
- return self.script_manager.reload_script(script_name)
-
- # 脚本挂载和管理方法 - 代理到script_manager
- def addScript(self, game_object, script_name):
- """为游戏对象添加脚本"""
- return self.script_manager.add_script_to_object(game_object, script_name)
-
- def removeScript(self, game_object, script_name):
- """从游戏对象移除脚本"""
- return self.script_manager.remove_script_from_object(game_object, script_name)
-
- def getScripts(self, game_object):
- """获取对象上的所有脚本"""
- return self.script_manager.get_scripts_on_object(game_object)
-
- def getScript(self, game_object, script_name):
- """获取对象上的特定脚本"""
- return self.script_manager.get_script_on_object(game_object, script_name)
-
- # 脚本信息查询方法 - 代理到script_manager
- def getAvailableScripts(self):
- """获取所有可用的脚本名称"""
- return self.script_manager.get_available_scripts()
-
- def getScriptInfo(self, script_name):
- """获取脚本信息"""
- return self.script_manager.get_script_info(script_name)
-
- def listAllScripts(self):
- """列出所有脚本信息"""
- return self.script_manager.list_all_scripts()
-
- def loadCesiumTileset(self,tileset_url,position=(0,0,0)):
- return self.scene_manager.load_cesium_tileset(tileset_url,position)
-
- def addCesiumTileset(self,name,url,position=(0,0,0)):
- if hasattr(self,'gui_manager') and self.gui_manager:
- return self.gui_manager.addCesiumTilesetToScene(name,url,position)
- else:
- return self.scene_manager.load_cesium_tileset(url,position)
-
- # ==================== 地形管理功能代理 ====================
- # 地形创建方法 - 代理到terrain_manager
- def createTerrainFromHeightMap(self, heightmap_path, scale=(1, 1, 1)):
- """从高度图创建地形"""
- return self.terrain_manager.createTerrainFromHeightMap(heightmap_path, scale)
-
- def createFlatTerrain(self, size=(0.3, 0.3), resolution=256):
- """创建平面地形"""
- return self.terrain_manager.createFlatTerrain(size, resolution)
-
- def updateTerrain(self):
- """更新所有地形的LOD"""
- return self.terrain_manager.updateTerrain()
-
- def getTerrainHeight(self, terrain_info, x, y):
- """获取地形上指定点的高度"""
- return self.terrain_manager.getTerrainHeight(terrain_info, x, y)
-
- def setTerrainEditParameters(self,radius=None,strength=None,operation=None):
- if radius is not None:
- self.terrain_edit_radius = float(radius)
- if strength is not None:
- self.terrain_edit_strenght = float(strength)
- if operation is not None:
- self.terrain_edit_operation = operation
- print(f"地形编辑参数已更新: 半径={self.terrain_edit_radius}, 强度={self.terrain_edit_strength}, 操作={self.terrain_edit_operation}")
- def getTerrainEditParameters(self):
- """获取当前地形编辑参数"""
- return {
- 'radius': self.terrain_edit_radius,
- 'strength': self.terrain_edit_strength,
- 'operation': self.terrain_edit_operation
- }
-
- def modifyTerrainHeight(self, terrain_info, x, y, radius, strength, operation="add"):
- """修改地形高度的便捷方法"""
- if hasattr(self, 'terrain_manager'):
- return self.terrain_manager.modifyTerrainHeight(terrain_info, x, y, radius, strength, operation)
- return False
-
- # 添加碰撞管理相关的代理方法
- def enableModelCollisionDetection(self, enable=True, frequency=0.1, threshold=0.5):
- """启用模型间碰撞检测"""
- return self.collision_manager.enableModelCollisionDetection(enable, frequency, threshold)
-
- def detectModelCollisions(self, specific_models=None, log_results=True):
- """检测模型间碰撞"""
- return self.collision_manager.detectModelCollisions(specific_models, log_results)
-
- def getCollisionHistory(self, limit=None):
- """获取碰撞历史"""
- return self.collision_manager.getCollisionHistory(limit)
-
- def getCollisionStatistics(self):
- """获取碰撞统计"""
- return self.collision_manager.getCollisionStatistics()
-
- def setupKeyboardEvents(self):
- """设置键盘事件"""
- try:
- # 绑定 F 键用于聚焦选中节点
- self.accept("f", self.onFocusKeyPressed)
- self.accept("F", self.onFocusKeyPressed) # 大写F
-
- print("✓ 键盘事件绑定完成")
-
- except Exception as e:
- print(f"设置键盘事件失败: {e}")
-
- def onFocusKeyPressed(self):
- """处理 F 键按下事件"""
- try:
- #print("检测到 F 键按下")
-
- # 检查是否有选中的节点
- if hasattr(self, 'selection') and self.selection.selectedNode:
- #print(f"当前选中节点: {self.selection.selectedNode.getName()}")
- # 调用选择系统的聚焦功能(可以选择带动画或不带动画的版本)
- # self.selection.focusCameraOnSelectedNode() # 无动画版本
- self.selection.focusCameraOnSelectedNodeAdvanced() # 带动画版本
- else:
- print("当前没有选中任何节点")
-
- except Exception as e:
- print(f"处理 F 键事件失败: {e}")
-
- def onPatrolKeyPressed(self):
- """处理 P 键按下事件 - 控制巡检系统"""
- try:
- print("检测到 P 键按下")
-
- if not self.patrol_system.is_patrolling:
- # 如果巡检系统没有点,创建默认巡检路线
- if not self.patrol_system.patrol_points:
- self.createDefaultPatrolRoute()
-
- # 开始巡检
- if self.patrol_system.start_patrol():
- print("✓ 巡检已开始")
- else:
- print("✗ 巡检启动失败")
- else:
- # 停止巡检
- if self.patrol_system.stop_patrol():
- print("✓ 巡检已停止")
- else:
- print("✗ 巡检停止失败")
-
- except Exception as e:
- print(f"处理 P 键事件失败: {e}")
-
- def createDefaultPatrolRoute(self):
- """创建默认巡检路线(使用自动朝向)"""
- try:
- # 清空现有巡检点
- self.patrol_system.clear_patrol_points()
-
- # 添加巡检点,使用None表示朝向下一个点
- self.patrol_system.add_patrol_point((0, -10, 2), (0,0,0), 1.5)
- self.patrol_system.add_patrol_point((10, -10, 2), (0,0,0), 1.5)
- self.patrol_system.add_patrol_point((10, 5, 2), (0,0,0), 1.5)
- self.patrol_system.add_patrol_point((10, 0, 5), (0,0,0), 1.5)
-
- # 最后一个点可以指定特定的朝向,或者也设为None继续循环
- self.patrol_system.add_patrol_point((0, -10, 2), None, 2.5)
-
- print("✓ 默认自动朝向巡检路线已创建")
- self.patrol_system.list_patrol_points()
-
- except Exception as e:
- print(f"创建默认自动朝向巡检路线失败: {e}")
-
- def _serializeNode(self, node):
- """序列化节点数据"""
- try:
- return self.world.scene_manager.serializeNode(node)
- except Exception as e:
- print(f"序列化节点失败: {e}")
- return None
-
- def _deserializeNode(self, node_data, parent_node):
- """反序列化节点数据"""
- try:
- return self.world.scene_manager.deserializeNode(node_data, parent_node)
- except Exception as e:
- print(f"反序列化节点失败: {e}")
- return None
-
-
-# ==================== 项目管理功能代理 ====================
-# 以下函数代理到project_manager模块的对应功能
-
-def updateWindowTitle(window, project_name=None):
- """更新窗口标题 - 代理到project_manager"""
- from project.project_manager import updateWindowTitle as pm_updateWindowTitle
- return pm_updateWindowTitle(window, project_name)
-
-def createNewProject(parent_window):
- """创建新项目 - 代理到project_manager"""
- world = parent_window.centralWidget().world
- return world.project_manager.createNewProject(parent_window)
-
-def saveProject(appw):
- """保存项目 - 代理到project_manager"""
-
- world = appw.centralWidget().world
- return world.project_manager.saveProject(appw)
-
-def openProject(appw):
- """打开项目 - 代理到project_manager"""
- world = appw.centralWidget().world
- return world.project_manager.openProject(appw)
-
-def openProjectForPath(project_path, appw):
- """打开项目 - 代理到project_manager"""
- world = appw.centralWidget().world
- return world.project_manager.openProjectForPath(project_path, appw)
-
-def buildPackage(appw):
- """打包项目 - 代理到project_manager"""
- world = appw.centralWidget().world
- return world.project_manager.buildPackage(appw)
-
-def run(args = None):
- world = MyWorld()
-
- # 使用新的UI模块创建主窗口
- from ui.main_window import setup_main_window
- print(f'Path is {args}')
- app, main_window = setup_main_window(world, args)
-
- # 启动应用程序
- sys.exit(app.exec_())
-
-if __name__ == "__main__":
+import warnings
+
+from core.Command_System import CommandManager
+from core.InfoPanelManager import InfoPanelManager
+from core.patrol_system import PatrolSystem
+from demo.video_integration import VideoManager
+
+warnings.filterwarnings("ignore", category=DeprecationWarning)
+
+import sys
+import builtins
+from PyQt5.QtWidgets import (QApplication, QMainWindow, QMenuBar, QMenu, QAction,
+ QDockWidget, QTreeWidget, QListWidget, QWidget, QVBoxLayout, QTreeWidgetItem,
+ QLabel, QLineEdit, QFormLayout, QDoubleSpinBox, QScrollArea, QTreeView, QInputDialog, QFileDialog, QMessageBox, QDialog, QGroupBox, QHBoxLayout, QPushButton, QDialogButtonBox)
+from PyQt5.QtCore import Qt, QDir, QUrl
+from PyQt5.QtGui import QDrag, QPainter, QPixmap
+from PyQt5.QtWidgets import QFileSystemModel
+from QMeta3D.QMeta3DWidget import QMeta3DWidget
+from panda3d.core import loadPrcFileData
+loadPrcFileData("", "assertions 0")
+from core.world import CoreWorld
+from core.selection import SelectionSystem
+from core.event_handler import EventHandler
+from core.tool_manager import ToolManager
+from core.script_system import ScriptManager
+from core.patrol_system import PatrolSystem
+from core.Command_System import CommandManager
+from gui.gui_manager import GUIManager
+from core.terrain_manager import TerrainManager
+from scene.scene_manager import SceneManager
+from project.project_manager import ProjectManager
+from ui.widgets import CustomMeta3DWidget, CustomFileView, CustomTreeWidget
+from ui.property_panel import PropertyPanelManager
+from ui.interface_manager import InterfaceManager
+from panda3d.core import (CardMaker, Vec4, Vec3, ColorAttrib, MaterialAttrib,
+ GeomNode, NodePath, Material, CollisionTraverser,
+ CollisionHandlerQueue, CollisionNode, CollisionRay,
+ Plane, Point3, Point2, BitMask32, CollisionSphere,
+ CollisionPlane, ModelPool, Filename, ModelRoot,
+ AmbientLight, DirectionalLight, TextureAttrib, DriveInterface, WindowProperties)
+from panda3d.egg import EggData, EggVertexPool, EggPolygon
+from direct.task import Task
+from direct.task.TaskManagerGlobal import taskMgr
+from direct.showbase.ShowBase import ShowBase
+from direct.showbase.DirectObject import DirectObject
+from direct.showbase.ShowBaseGlobal import globalClock, aspect2d
+import os
+import json
+import datetime
+from direct.actor.Actor import Actor
+from PyQt5.sip import wrapinstance
+#from RenderPipelineFile.toolkit.material_editor.main import MaterialEditor
+
+class MyWorld(CoreWorld):
+ def __init__(self):
+ super().__init__()
+
+ # 初始化选择和变换系统
+ self.selection = SelectionSystem(self)
+
+ # 绑定F键用于聚焦选中节点
+ self.accept("f", self.onFocusKeyPressed)
+ self.accept("F", self.onFocusKeyPressed) # 大写F
+
+ #初始化巡检系统
+ self.patrol_system = PatrolSystem(self)
+ self.accept("p",self.onPatrolKeyPressed)
+ self.accept("P",self.onPatrolKeyPressed)
+
+ # 初始化事件处理系统
+ self.event_handler = EventHandler(self)
+
+ # 初始化工具管理系统
+ self.tool_manager = ToolManager(self)
+
+ # 初始化脚本管理系统
+ self.script_manager = ScriptManager(self)
+
+ # 初始化GUI管理系统
+ self.gui_manager = GUIManager(self)
+
+ # 初始化视频管理
+ self.video_manager = VideoManager(self)
+
+ # 初始化场景管理系统
+ self.scene_manager = SceneManager(self)
+
+ # 初始化项目管理系统
+ self.project_manager = ProjectManager(self)
+
+ # 初始化属性面板管理系统
+ self.property_panel = PropertyPanelManager(self)
+
+ # 初始化界面管理系统
+ self.interface_manager = InterfaceManager(self)
+
+
+ # 启动脚本系统
+ self.script_manager.start_system()
+
+ #self.material_editor = None
+ self.terrain_manager = TerrainManager(self)
+
+ self.terrain_edit_radius = 3.0
+ self.terrain_edit_strength=0.3
+ self.terrain_edit_operation = "add"
+
+ self.info_panel_manager = InfoPanelManager(self)
+
+ self.command_manager = CommandManager()
+
+ # 初始化碰撞管理器
+ from core.collision_manager import CollisionManager
+ self.collision_manager = CollisionManager(self)
+
+ # 初始化VR管理器
+ try:
+ from core.vr import VRManager
+ self.vr_manager = VRManager(self)
+ print("✓ VR管理器初始化完成")
+ except Exception as e:
+ print(f"⚠ VR管理器初始化失败: {e}")
+ self.vr_manager = None
+
+ # 调试选项
+ self.debug_collision = True # 是否显示碰撞体
+
+ # 默认启用模型间碰撞检测(可选)
+ self.enableModelCollisionDetection(enable=True, frequency=0.1, threshold=0.5)
+
+ try:
+ self.property_panel = PropertyPanelManager(self)
+ print("✓ 属性面板管理器初始化完成")
+ except Exception as e:
+ print(f"⚠ 属性面板管理器初始化失败: {e}")
+ import traceback
+ traceback.print_exc()
+ self.property_panel = None
+
+ print("✓ MyWorld 初始化完成")
+ print("✅ 碰撞管理器已初始化")
+
+ # ==================== 兼容性属性 ====================
+
+ # 保留models属性以兼容现有代码
+ @property
+ def models(self):
+ """模型列表的兼容性属性"""
+ return self.scene_manager.models
+
+ @models.setter
+ def models(self, value):
+ """模型列表的兼容性设置器"""
+ self.scene_manager.models = value
+
+ # 保留gui_elements属性以兼容现有代码
+ @property
+ def gui_elements(self):
+ """GUI元素列表的兼容性属性"""
+ return self.gui_manager.gui_elements
+
+ @gui_elements.setter
+ def gui_elements(self, value):
+ """GUI元素列表的兼容性设置器"""
+ self.gui_manager.gui_elements = value
+
+ # 保留gui_elements属性以兼容现有代码
+ @property
+ def Spotlight(self):
+ """GUI元素列表的兼容性属性"""
+ return self.scene_manager.Spotlight
+
+ @Spotlight.setter
+ def Spotlight(self, value):
+ """GUI元素列表的兼容性设置器"""
+ self.scene_manager.Spotlight = value
+
+ @property
+ def Pointlight(self):
+ return self.scene_manager.Pointlight
+
+ @Pointlight.setter
+ def Pointlight(self,value):
+ self.scene_manager.Pointlight = value
+
+
+ # 保留guiEditMode属性以兼容现有代码
+ @property
+ def guiEditMode(self):
+ """GUI编辑模式的兼容性属性"""
+ return self.gui_manager.guiEditMode
+
+ @guiEditMode.setter
+ def guiEditMode(self, value):
+ """GUI编辑模式的兼容性设置器"""
+ self.gui_manager.guiEditMode = value
+
+ # 保留currentTool属性以兼容现有代码
+ @property
+ def currentTool(self):
+ """当前工具的兼容性属性"""
+ return self.tool_manager.currentTool
+
+ @currentTool.setter
+ def currentTool(self, value):
+ """当前工具的兼容性设置器"""
+ self.tool_manager.currentTool = value
+
+ # 保留treeWidget属性以兼容现有代码
+ @property
+ def treeWidget(self):
+ """树形控件的兼容性属性"""
+ return self.interface_manager.treeWidget
+
+ @treeWidget.setter
+ def treeWidget(self, value):
+ """树形控件的兼容性设置器"""
+ self.interface_manager.treeWidget = value
+
+ #保留terrains属性以兼容现有代码
+ @property
+ def terrains(self):
+ """地形列表的兼容性属性"""
+ return self.terrain_manager.terrains
+
+ @terrains.setter
+ def terrains(self,value):
+ """地形列表的兼容性设置器"""
+ self.terrain_manager.terrains = value
+
+ # ==================== GUI管理功能代理 ====================
+
+ # GUI元素创建方法 - 代理到gui_manager
+ def createGUIButton(self, pos=(0, 0, 0), text="按钮", size=0.1):
+ """创建2D GUI按钮"""
+ return self.gui_manager.createGUIButton(pos, text, size)
+
+ def createGUILabel(self, pos=(0, 0, 0), text="标签", size=0.08):
+ """创建2D GUI标签"""
+ return self.gui_manager.createGUILabel(pos, text, size)
+
+ def createGUIEntry(self, pos=(0, 0, 0), placeholder="输入文本...", size=0.08):
+ """创建2D GUI文本输入框"""
+ return self.gui_manager.createGUIEntry(pos, placeholder, size)
+
+ def createGUI3DText(self, pos=(0, 0, 0), text="3D文本", size=1):
+ """创建3D空间文本"""
+ return self.gui_manager.createGUI3DText(pos, text, size)
+
+ def createGUI3DImage(self,pos=(0,0,0),text="3D图片",size=(1,1,1)):
+ """创建3D图片"""
+ return self.gui_manager.createGUI3DImage(pos,text,size)
+
+ def createGUI2DImage(self, pos=(0, 0, 0), image_path=None, size=2):
+ """创建2D GUI图片"""
+ return self.gui_manager.createGUI2DImage(pos, image_path, size)
+
+ def createVideoScreen(self,pos=(0,0,0),size=1,video_path=None):
+ """创建视频屏幕"""
+ return self.gui_manager.createVideoScreen(pos,size,video_path)
+
+ def create2DVideoScreen(self,pos=(0,0,0),size=0.2,video_path=None):
+ """创建2D视频屏幕"""
+ return self.gui_manager.createGUI2DVideoScreen(pos,size,video_path)
+
+ def createSphericalVideo(self,pos=(0,0,0),radius=5.0,video_path=None):
+ """创建360度视频"""
+ return self.gui_manager.createSphericalVideo(pos,radius,video_path)
+
+ def playSphericalVideo(self,spherical_video_node):
+ return self.gui_manager.playSphericalVideo(spherical_video_node)
+
+ def pauseSphericalVideo(self,spherical_video_node):
+ return self.gui_manager.pauseSphericalVideo(spherical_video_node)
+
+ def setSphericalVideoTime(self,spherical_video_node,time_seconds):
+ return self.gui_manager.setSphericalVideoTime(spherical_video_node,time_seconds)
+
+ def createSpotLight(self,pos=(0,0,5)):
+ """创建聚光灯"""
+ return self.scene_manager.createSpotLight(pos)
+
+ def createPointLight(self,pos=(0,0,5)):
+ """创建点光源"""
+ return self.scene_manager.createPointLight(pos)
+
+ def createGUIVirtualScreen(self, pos=(0, 0, 0), size=(2, 1), text="虚拟屏幕"):
+ """创建3D虚拟屏幕"""
+ return self.gui_manager.createGUIVirtualScreen(pos, size, text)
+
+ def createGUISlider(self, pos=(0, 0, 0), text="滑块", scale=0.3):
+ """创建2D GUI滑块"""
+ return self.gui_manager.createGUISlider(pos, text, scale)
+
+ # GUI元素管理方法 - 代理到gui_manager
+ def deleteGUIElement(self, gui_element):
+ """删除GUI元素"""
+ return self.gui_manager.deleteGUIElement(gui_element)
+
+ def editGUIElement(self, gui_element, property_name, value):
+ """编辑GUI元素属性"""
+ return self.gui_manager.editGUIElement(gui_element, property_name, value)
+
+ def duplicateGUIElement(self, gui_element):
+ """复制GUI元素"""
+ return self.gui_manager.duplicateGUIElement(gui_element)
+
+ def editGUIElementDialog(self, gui_element):
+ """显示GUI元素编辑对话框"""
+ return self.gui_manager.editGUIElementDialog(gui_element)
+
+ # GUI事件处理方法 - 代理到gui_manager
+ def onGUIButtonClick(self, button_id):
+ """GUI按钮点击事件处理"""
+ return self.gui_manager.onGUIButtonClick(button_id)
+
+ def onGUIEntrySubmit(self, text, entry_id):
+ """GUI输入框提交事件处理"""
+ return self.gui_manager.onGUIEntrySubmit(text, entry_id)
+
+ # GUI编辑模式方法 - 代理到gui_manager
+ def toggleGUIEditMode(self):
+ """切换GUI编辑模式"""
+ return self.gui_manager.toggleGUIEditMode()
+
+ def enterGUIEditMode(self):
+ """进入GUI编辑模式"""
+ return self.gui_manager.enterGUIEditMode()
+
+ def exitGUIEditMode(self):
+ """退出GUI编辑模式"""
+ return self.gui_manager.exitGUIEditMode()
+
+ def createGUIEditPanel(self):
+ """创建GUI编辑面板"""
+ return self.gui_manager.createGUIEditPanel()
+
+ def openGUIPreviewWindow(self):
+ """打开独立的GUI预览窗口"""
+ return self.gui_manager.openGUIPreviewWindow()
+
+ def closeGUIPreviewWindow(self):
+ """关闭GUI预览窗口"""
+ return self.gui_manager.closeGUIPreviewWindow()
+
+ # GUI工具和选择方法 - 代理到gui_manager
+ def setGUICreateTool(self, tool_type):
+ """设置GUI创建工具"""
+ return self.gui_manager.setGUICreateTool(tool_type)
+
+ def deleteSelectedGUI(self):
+ """删除选中的GUI元素"""
+ return self.gui_manager.deleteSelectedGUI()
+
+ def copySelectedGUI(self):
+ """复制选中的GUI元素"""
+ return self.gui_manager.copySelectedGUI()
+
+ def handleGUIEditClick(self, hitPos):
+ """处理GUI编辑模式下的点击"""
+ return self.gui_manager.handleGUIEditClick(hitPos)
+
+ def createGUIAtPosition(self, world_pos, gui_type):
+ """在指定位置创建GUI元素"""
+ return self.gui_manager.createGUIAtPosition(world_pos, gui_type)
+
+ def findClickedGUI(self, hitNode):
+ """查找被点击的GUI元素"""
+ return self.gui_manager.findClickedGUI(hitNode)
+
+ def selectGUIInTree(self, gui_element):
+ """在树形控件中选中GUI元素"""
+ return self.gui_manager.selectGUIInTree(gui_element)
+
+ def updateGUISelection(self, gui_element):
+ """更新GUI元素选择状态"""
+ return self.gui_manager.updateGUISelection(gui_element)
+
+ # GUI属性面板方法 - 代理到gui_manager
+ def selectGUIColor(self, gui_element):
+ """选择GUI元素颜色"""
+ return self.gui_manager.selectGUIColor(gui_element)
+
+ def editGUI2DPosition(self, gui_element, axis, value):
+ """编辑2D GUI元素位置"""
+ return self.gui_manager.editGUI2DPosition(gui_element, axis, value)
+
+ # ==================== 事件处理代理 ====================
+
+ def onTreeItemClicked(self, item, column):
+ """处理树形控件项目点击事件 - 代理到interface_manager"""
+ return self.interface_manager.onTreeItemClicked(item, column)
+
+ def setTreeWidget(self, treeWidget):
+ """设置树形控件引用 - 代理到interface_manager"""
+ return self.interface_manager.setTreeWidget(treeWidget)
+
+ def mousePressEventLeft(self, evt):
+ """处理鼠标左键按下事件 - 代理到event_handler"""
+ return self.event_handler.mousePressEventLeft(evt)
+
+ def mouseReleaseEventLeft(self, evt):
+ """处理鼠标左键释放事件 - 代理到event_handler"""
+ return self.event_handler.mouseReleaseEventLeft(evt)
+
+ def wheelForward(self, data=None):
+ """处理滚轮向前滚动(前进) - 代理到event_handler"""
+ return self.event_handler.wheelForward(data)
+
+ def wheelBackward(self, data=None):
+ """处理滚轮向后滚动(后退) - 代理到event_handler"""
+ return self.event_handler.wheelBackward(data)
+
+ def mousePressEventMiddle(self, evt):
+ """处理鼠标中键按下事件 - 代理到event_handler"""
+ return self.event_handler.mousePressEventMiddle(evt)
+
+ def mouseReleaseEventMiddle(self, evt):
+ """处理鼠标中键释放事件 - 代理到event_handler"""
+ return self.event_handler.mouseReleaseEventMiddle(evt)
+
+ def mouseMoveEvent(self, evt):
+ """处理鼠标移动事件 - 代理到event_handler"""
+ return self.event_handler.mouseMoveEvent(evt)
+
+ # ==================== 射线显示控制 ====================
+
+ def toggleRayDisplay(self):
+ """切换射线显示状态 - 代理到event_handler"""
+ return self.event_handler.toggleRayDisplay()
+
+ def setRayDisplay(self, show=True):
+ """设置射线显示状态 - 代理到event_handler"""
+ self.event_handler.showRay = show
+ if not show:
+ self.event_handler.clearRay()
+ return show
+
+ def getRayDisplay(self):
+ """获取射线显示状态 - 代理到event_handler"""
+ return self.event_handler.showRay
+
+ def setRayLifetime(self, seconds):
+ """设置射线显示时长(秒) - 代理到event_handler"""
+ self.event_handler.rayLifetime = seconds
+ print(f"射线显示时长设置为: {seconds}秒")
+
+ def getRayLifetime(self):
+ """获取射线显示时长 - 代理到event_handler"""
+ return self.event_handler.rayLifetime
+
+ # ==================== 属性面板代理 ====================
+
+ def setPropertyLayout(self, layout):
+ """设置属性面板布局引用 - 代理到property_panel"""
+ return self.property_panel.setPropertyLayout(layout)
+
+ def clearPropertyPanel(self):
+ """清空属性面板 - 代理到property_panel"""
+ return self.property_panel.clearPropertyPanel()
+
+ def updatePropertyPanel(self, item):
+ """更新属性面板显示 - 代理到property_panel"""
+ return self.property_panel.updatePropertyPanel(item)
+
+ def updateGUIPropertyPanel(self, gui_element):
+ """更新GUI元素属性面板 - 代理到property_panel"""
+ return self.property_panel.updateGUIPropertyPanel(gui_element)
+
+ def removeActorForModel(self,model):
+ return self.property_panel.removeActorForModel( model)
+
+ def updateNodeVisibilityAfterDrag(self,item):
+ return self.property_panel.updateNodeVisibilityAfterDrag(item)
+
+ # ==================== 工具管理代理 ====================
+
+ def setCurrentTool(self, tool):
+ """设置当前工具 - 代理到tool_manager"""
+ return self.tool_manager.setCurrentTool(tool)
+
+ # ==================== 场景管理功能代理 ====================
+
+ # 模型导入和处理方法 - 代理到scene_manager
+ def importModel(self, filepath):
+ """导入模型到场景"""
+ # 自动转换FBX等格式为GLB以获得更好的动画支持(不再弹窗询问)
+ # 如果你不想自动转换,可以将 auto_convert 改为 False
+ auto_convert = False
+
+ print(f"[模型导入] 文件: {filepath}, 自动转换GLB: {auto_convert}")
+
+ return self.scene_manager.importModel(filepath, auto_convert_to_glb=auto_convert)
+
+ def importModelAsync(self, filepath):
+ """异步导入模型"""
+ return self.scene_manager.importModelAsync(filepath)
+
+ # 材质和几何体处理方法 - 代理到scene_manager
+ def processMaterials(self, model):
+ """处理模型材质"""
+ return self.scene_manager.processMaterials(model)
+
+ def processModelGeometry(self, model):
+ """处理模型几何体"""
+ return self.scene_manager.processModelGeometry(model)
+
+ # 碰撞系统方法 - 代理到scene_manager
+ def setupCollision(self, model):
+ """为模型设置碰撞检测"""
+ return self.scene_manager.setupCollision(model)
+
+ # 场景树管理方法 - 代理到scene_manager
+ def updateSceneTree(self):
+ """更新场景树显示"""
+ return self.scene_manager.updateSceneTree()
+
+ # 场景保存和加载方法 - 代理到scene_manager
+ def saveScene(self, filename):
+ """保存场景到BAM文件"""
+ return self.scene_manager.saveScene(filename)
+
+ def loadScene(self, filename):
+ """从BAM文件加载场景"""
+ return self.scene_manager.loadScene(filename)
+
+ # 模型管理方法 - 代理到scene_manager
+ def deleteModel(self, model):
+ """删除模型"""
+ return self.scene_manager.deleteModel(model)
+
+ def clearAllModels(self):
+ """清除所有模型"""
+ return self.scene_manager.clearAllModels()
+
+ def getModels(self):
+ """获取模型列表"""
+ return self.scene_manager.getModels()
+
+ def getModelCount(self):
+ """获取模型数量"""
+ return self.scene_manager.getModelCount()
+
+ def findModelByName(self, name):
+ """根据名称查找模型"""
+ return self.scene_manager.findModelByName(name)
+
+ # ==================== 脚本系统功能代理 ====================
+
+ # 脚本系统控制方法 - 代理到script_manager
+ def startScriptSystem(self):
+ """启动脚本系统"""
+ return self.script_manager.start_system()
+
+ def stopScriptSystem(self):
+ """停止脚本系统"""
+ return self.script_manager.stop_system()
+
+ def enableHotReload(self, enabled=True):
+ """启用/禁用热重载"""
+ self.script_manager.hot_reload_enabled = enabled
+ if enabled:
+ self.script_manager.start_hot_reload()
+ else:
+ self.script_manager.stop_hot_reload()
+
+ # 脚本创建和加载方法 - 代理到script_manager
+ def createScript(self, script_name, template="basic"):
+ """创建新脚本文件"""
+ return self.script_manager.create_script_file(script_name, template)
+
+ def loadScript(self, script_path):
+ """从文件加载脚本"""
+ return self.script_manager.load_script_from_file(script_path)
+
+ def loadAllScripts(self, directory=None):
+ """从目录加载所有脚本"""
+ return self.script_manager.load_all_scripts_from_directory(directory)
+
+ def reloadScript(self, script_name):
+ """重新加载脚本"""
+ return self.script_manager.reload_script(script_name)
+
+ # 脚本挂载和管理方法 - 代理到script_manager
+ def addScript(self, game_object, script_name):
+ """为游戏对象添加脚本"""
+ return self.script_manager.add_script_to_object(game_object, script_name)
+
+ def removeScript(self, game_object, script_name):
+ """从游戏对象移除脚本"""
+ return self.script_manager.remove_script_from_object(game_object, script_name)
+
+ def getScripts(self, game_object):
+ """获取对象上的所有脚本"""
+ return self.script_manager.get_scripts_on_object(game_object)
+
+ def getScript(self, game_object, script_name):
+ """获取对象上的特定脚本"""
+ return self.script_manager.get_script_on_object(game_object, script_name)
+
+ # 脚本信息查询方法 - 代理到script_manager
+ def getAvailableScripts(self):
+ """获取所有可用的脚本名称"""
+ return self.script_manager.get_available_scripts()
+
+ def getScriptInfo(self, script_name):
+ """获取脚本信息"""
+ return self.script_manager.get_script_info(script_name)
+
+ def listAllScripts(self):
+ """列出所有脚本信息"""
+ return self.script_manager.list_all_scripts()
+
+ def loadCesiumTileset(self,tileset_url,position=(0,0,0)):
+ return self.scene_manager.load_cesium_tileset(tileset_url,position)
+
+ def addCesiumTileset(self,name,url,position=(0,0,0)):
+ if hasattr(self,'gui_manager') and self.gui_manager:
+ return self.gui_manager.addCesiumTilesetToScene(name,url,position)
+ else:
+ return self.scene_manager.load_cesium_tileset(url,position)
+
+ # ==================== 地形管理功能代理 ====================
+ # 地形创建方法 - 代理到terrain_manager
+ def createTerrainFromHeightMap(self, heightmap_path, scale=(1, 1, 1)):
+ """从高度图创建地形"""
+ return self.terrain_manager.createTerrainFromHeightMap(heightmap_path, scale)
+
+ def createFlatTerrain(self, size=(0.3, 0.3), resolution=256):
+ """创建平面地形"""
+ return self.terrain_manager.createFlatTerrain(size, resolution)
+
+ def updateTerrain(self):
+ """更新所有地形的LOD"""
+ return self.terrain_manager.updateTerrain()
+
+ def getTerrainHeight(self, terrain_info, x, y):
+ """获取地形上指定点的高度"""
+ return self.terrain_manager.getTerrainHeight(terrain_info, x, y)
+
+ def setTerrainEditParameters(self,radius=None,strength=None,operation=None):
+ if radius is not None:
+ self.terrain_edit_radius = float(radius)
+ if strength is not None:
+ self.terrain_edit_strenght = float(strength)
+ if operation is not None:
+ self.terrain_edit_operation = operation
+ print(f"地形编辑参数已更新: 半径={self.terrain_edit_radius}, 强度={self.terrain_edit_strength}, 操作={self.terrain_edit_operation}")
+ def getTerrainEditParameters(self):
+ """获取当前地形编辑参数"""
+ return {
+ 'radius': self.terrain_edit_radius,
+ 'strength': self.terrain_edit_strength,
+ 'operation': self.terrain_edit_operation
+ }
+
+ def modifyTerrainHeight(self, terrain_info, x, y, radius, strength, operation="add"):
+ """修改地形高度的便捷方法"""
+ if hasattr(self, 'terrain_manager'):
+ return self.terrain_manager.modifyTerrainHeight(terrain_info, x, y, radius, strength, operation)
+ return False
+
+ # 添加碰撞管理相关的代理方法
+ def enableModelCollisionDetection(self, enable=True, frequency=0.1, threshold=0.5):
+ """启用模型间碰撞检测"""
+ return self.collision_manager.enableModelCollisionDetection(enable, frequency, threshold)
+
+ def detectModelCollisions(self, specific_models=None, log_results=True):
+ """检测模型间碰撞"""
+ return self.collision_manager.detectModelCollisions(specific_models, log_results)
+
+ def getCollisionHistory(self, limit=None):
+ """获取碰撞历史"""
+ return self.collision_manager.getCollisionHistory(limit)
+
+ def getCollisionStatistics(self):
+ """获取碰撞统计"""
+ return self.collision_manager.getCollisionStatistics()
+
+ def setupKeyboardEvents(self):
+ """设置键盘事件"""
+ try:
+ # 绑定 F 键用于聚焦选中节点
+ self.accept("f", self.onFocusKeyPressed)
+ self.accept("F", self.onFocusKeyPressed) # 大写F
+
+ print("✓ 键盘事件绑定完成")
+
+ except Exception as e:
+ print(f"设置键盘事件失败: {e}")
+
+ def onFocusKeyPressed(self):
+ """处理 F 键按下事件"""
+ try:
+ #print("检测到 F 键按下")
+
+ # 检查是否有选中的节点
+ if hasattr(self, 'selection') and self.selection.selectedNode:
+ #print(f"当前选中节点: {self.selection.selectedNode.getName()}")
+ # 调用选择系统的聚焦功能(可以选择带动画或不带动画的版本)
+ # self.selection.focusCameraOnSelectedNode() # 无动画版本
+ self.selection.focusCameraOnSelectedNodeAdvanced() # 带动画版本
+ else:
+ print("当前没有选中任何节点")
+
+ except Exception as e:
+ print(f"处理 F 键事件失败: {e}")
+
+ def onPatrolKeyPressed(self):
+ """处理 P 键按下事件 - 控制巡检系统"""
+ try:
+ print("检测到 P 键按下")
+
+ if not self.patrol_system.is_patrolling:
+ # 如果巡检系统没有点,创建默认巡检路线
+ if not self.patrol_system.patrol_points:
+ self.createDefaultPatrolRoute()
+
+ # 开始巡检
+ if self.patrol_system.start_patrol():
+ print("✓ 巡检已开始")
+ else:
+ print("✗ 巡检启动失败")
+ else:
+ # 停止巡检
+ if self.patrol_system.stop_patrol():
+ print("✓ 巡检已停止")
+ else:
+ print("✗ 巡检停止失败")
+
+ except Exception as e:
+ print(f"处理 P 键事件失败: {e}")
+
+ def createDefaultPatrolRoute(self):
+ """创建默认巡检路线(使用自动朝向)"""
+ try:
+ # 清空现有巡检点
+ self.patrol_system.clear_patrol_points()
+
+ # 添加巡检点,使用None表示朝向下一个点
+ self.patrol_system.add_patrol_point((0, -10, 2), (0,0,0), 1.5)
+ self.patrol_system.add_patrol_point((10, -10, 2), (0,0,0), 1.5)
+ self.patrol_system.add_patrol_point((10, 5, 2), (0,0,0), 1.5)
+ self.patrol_system.add_patrol_point((10, 0, 5), (0,0,0), 1.5)
+
+ # 最后一个点可以指定特定的朝向,或者也设为None继续循环
+ self.patrol_system.add_patrol_point((0, -10, 2), None, 2.5)
+
+ print("✓ 默认自动朝向巡检路线已创建")
+ self.patrol_system.list_patrol_points()
+
+ except Exception as e:
+ print(f"创建默认自动朝向巡检路线失败: {e}")
+
+ def _serializeNode(self, node):
+ """序列化节点数据"""
+ try:
+ return self.world.scene_manager.serializeNode(node)
+ except Exception as e:
+ print(f"序列化节点失败: {e}")
+ return None
+
+ def _deserializeNode(self, node_data, parent_node):
+ """反序列化节点数据"""
+ try:
+ return self.world.scene_manager.deserializeNode(node_data, parent_node)
+ except Exception as e:
+ print(f"反序列化节点失败: {e}")
+ return None
+
+
+# ==================== 项目管理功能代理 ====================
+# 以下函数代理到project_manager模块的对应功能
+
+def updateWindowTitle(window, project_name=None):
+ """更新窗口标题 - 代理到project_manager"""
+ from project.project_manager import updateWindowTitle as pm_updateWindowTitle
+ return pm_updateWindowTitle(window, project_name)
+
+def createNewProject(parent_window):
+ """创建新项目 - 代理到project_manager"""
+ world = parent_window.centralWidget().world
+ return world.project_manager.createNewProject(parent_window)
+
+def saveProject(appw):
+ """保存项目 - 代理到project_manager"""
+
+ world = appw.centralWidget().world
+ return world.project_manager.saveProject(appw)
+
+def openProject(appw):
+ """打开项目 - 代理到project_manager"""
+ world = appw.centralWidget().world
+ return world.project_manager.openProject(appw)
+
+def openProjectForPath(project_path, appw):
+ """打开项目 - 代理到project_manager"""
+ world = appw.centralWidget().world
+ return world.project_manager.openProjectForPath(project_path, appw)
+
+def buildPackage(appw):
+ """打包项目 - 代理到project_manager"""
+ world = appw.centralWidget().world
+ return world.project_manager.buildPackage(appw)
+
+def run(args = None):
+ print(f'Path is {args}')
+ print("引擎已启动(使用Panda3D原生窗口)")
+
+ # 创建世界实例并运行
+ world = MyWorld()
+ world.run()
+
+if __name__ == "__main__":
run()
\ No newline at end of file
diff --git a/ui/main_window.py b/ui/main_window.py
index 286191f6..0c64dff7 100644
--- a/ui/main_window.py
+++ b/ui/main_window.py
@@ -199,11 +199,11 @@ class EmbeddedWebBrowser(QWidget):
def main():
- # 设置标准输出编码为 UTF-8,避免 Windows GBK 编码问题
- if sys.platform == 'win32':
- import io
- sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
- sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
+ # 注释掉标准输出重定向,让输出正常显示在终端
+ # if sys.platform == 'win32':
+ # import io
+ # sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
+ # sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
print("=" * 60)
print("[WebEngine] 启动独立 WebEngine 进程")
diff --git a/ui/widgets.py b/ui/widgets.py
index b9d434c9..d2b80430 100644
--- a/ui/widgets.py
+++ b/ui/widgets.py
@@ -1,4309 +1,4310 @@
-"""
-自定义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
-
- def write(self, text):
- """重定向写入"""
- if text.strip(): # 忽略空行
- self.console_widget.addMessage(text.strip(), self.stream_type)
-
- def flush(self):
- """刷新缓冲区"""
- pass
-
-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
-
-
-
+"""
+自定义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
+
+ def write(self, text):
+ """重定向写入"""
+ if text.strip(): # 忽略空行
+ self.console_widget.addMessage(text.strip(), self.stream_type)
+
+ def flush(self):
+ """刷新缓冲区"""
+ pass
+
+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
+
+
+