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