diff --git a/config/vr_settings.json b/config/vr_settings.json index 1688b9bf..902d31fb 100644 --- a/config/vr_settings.json +++ b/config/vr_settings.json @@ -1,15 +1,15 @@ -{ - "render_mode": "render_pipeline", - "resolution_scale": 0.75, - "pipeline_resolution_scale": 0.75, - "quality_preset": "quality", - "pipeline_vr_config": { - "enable_shadows": true, - "enable_ao": true, - "enable_bloom": false, - "enable_motion_blur": false, - "enable_ssr": false, - "shadow_quality": "medium", - "ao_quality": "low" - } +{ + "render_mode": "render_pipeline", + "resolution_scale": 0.75, + "pipeline_resolution_scale": 0.75, + "quality_preset": "quality", + "pipeline_vr_config": { + "enable_shadows": true, + "enable_ao": true, + "enable_bloom": false, + "enable_motion_blur": false, + "enable_ssr": false, + "shadow_quality": "medium", + "ao_quality": "low" + } } \ No newline at end of file diff --git a/core/vr/config/vr_settings.json b/core/vr/config/vr_settings.json index efc5f6db..902d31fb 100644 --- a/core/vr/config/vr_settings.json +++ b/core/vr/config/vr_settings.json @@ -1,18 +1,15 @@ -{ - "render_mode": "render_pipeline", - "resolution_scale": 0.75, - "pipeline_resolution_scale": 0.75, - "quality_preset": "quality", - "pipeline_vr_config": { - "enable_shadows": true, - "enable_ao": true, - "enable_bloom": false, - "enable_motion_blur": false, - "enable_ssr": false, - "shadow_quality": "medium", - "ao_quality": "low" - }, - "anti_aliasing": "4x", - "refresh_rate": "90Hz", - "async_reprojection": true +{ + "render_mode": "render_pipeline", + "resolution_scale": 0.75, + "pipeline_resolution_scale": 0.75, + "quality_preset": "quality", + "pipeline_vr_config": { + "enable_shadows": true, + "enable_ao": true, + "enable_bloom": false, + "enable_motion_blur": false, + "enable_ssr": false, + "shadow_quality": "medium", + "ao_quality": "low" + } } \ No newline at end of file diff --git a/imgui.ini b/imgui.ini index 1c34eff5..57093078 100644 --- a/imgui.ini +++ b/imgui.ini @@ -144,6 +144,16 @@ Pos=625,308 Size=600,400 Collapsed=0 +[Window][VR状态] +Pos=162,152 +Size=138,296 +Collapsed=0 + +[Window][VR设置] +Pos=474,130 +Size=120,384 +Collapsed=0 + [Docking][Data] DockSpace ID=0x08BD597D Window=0x1BBC0F80 Pos=0,20 Size=1850,996 Split=X DockNode ID=0x00000003 Parent=0x08BD597D SizeRef=1521,996 Split=Y diff --git a/main.py b/main.py index 0c9a7c26..19a09963 100644 --- a/main.py +++ b/main.py @@ -273,6 +273,11 @@ class MyWorld(CoreWorld): self.showToolbar = True self.showResourceManager = True + # VR窗口控制 + self.showVRSettingsWindow = False + self.showVRStatusWindow = False + self.showVRDebugWindow = False + # 脚本系统状态变量 self.hotReloadEnabled = True self._new_script_name = "new_script" @@ -727,6 +732,11 @@ class MyWorld(CoreWorld): if self.showDemoWindow: imgui.show_demo_window() + # 绘制VR窗口 + self._draw_vr_settings_window() + self._draw_vr_status_window() + self._draw_vr_debug_window() + # 绘制对话框 self._draw_new_project_dialog() self._draw_open_project_dialog() @@ -4239,6 +4249,431 @@ class MyWorld(CoreWorld): self._color_picker_current_color = initial_color self._color_picker_callback = callback + def _draw_vr_settings_window(self): + """绘制VR设置窗口""" + if not self.showVRSettingsWindow: + return + + flags = imgui.WindowFlags_.no_collapse + with imgui_ctx.begin("VR设置", self.showVRSettingsWindow, flags) as (expanded, opened): + self.showVRSettingsWindow = opened + + if expanded: + # 加载VR配置 + if not hasattr(self, '_vr_config'): + self._load_vr_config() + + config = self._vr_config + + # 渲染模式选择 + imgui.text("渲染模式:") + render_modes = ["normal", "render_pipeline"] + current_render_mode = 0 if config.get('render_mode') == 'normal' else 1 + + changed, new_render_mode = imgui.combo( + "##render_mode", current_render_mode, ["普通渲染", "RenderPipeline高级渲染"] + ) + if changed: + config['render_mode'] = render_modes[new_render_mode] + + imgui.separator() + + # 质量预设选择 + imgui.text("质量预设:") + quality_presets = ["performance", "balanced", "quality"] + quality_map = {"performance": 0, "balanced": 1, "quality": 2} + current_quality = quality_map.get(config.get('quality_preset', 'quality'), 2) + + changed, new_quality = imgui.combo( + "##quality_preset", current_quality, ["性能", "平衡", "质量"] + ) + if changed: + config['quality_preset'] = quality_presets[new_quality] + + imgui.separator() + + # 分辨率缩放 + imgui.text("分辨率缩放:") + resolution_scale = config.get('resolution_scale', 0.75) + changed, new_scale = imgui.slider_float( + "##resolution_scale", resolution_scale, 0.5, 1.0, "%.2f" + ) + if changed: + config['resolution_scale'] = new_scale + config['pipeline_resolution_scale'] = new_scale + + imgui.separator() + + # 渲染管线设置 + imgui.text("渲染管线设置:") + + if 'pipeline_vr_config' not in config: + config['pipeline_vr_config'] = {} + + pipeline_config = config['pipeline_vr_config'] + + # 阴影设置 + enable_shadows = pipeline_config.get('enable_shadows', True) + changed, new_shadows = imgui.checkbox("启用阴影", enable_shadows) + if changed: + pipeline_config['enable_shadows'] = new_shadows + + # AO设置 + enable_ao = pipeline_config.get('enable_ao', True) + changed, new_ao = imgui.checkbox("启用环境光遮蔽", enable_ao) + if changed: + pipeline_config['enable_ao'] = new_ao + + # Bloom设置 + enable_bloom = pipeline_config.get('enable_bloom', False) + changed, new_bloom = imgui.checkbox("启用Bloom效果", enable_bloom) + if changed: + pipeline_config['enable_bloom'] = new_bloom + + # Motion Blur设置 + enable_motion_blur = pipeline_config.get('enable_motion_blur', False) + changed, new_motion_blur = imgui.checkbox("启用动态模糊", enable_motion_blur) + if changed: + pipeline_config['enable_motion_blur'] = new_motion_blur + + # SSR设置 + enable_ssr = pipeline_config.get('enable_ssr', False) + changed, new_ssr = imgui.checkbox("启用屏幕空间反射", enable_ssr) + if changed: + pipeline_config['enable_ssr'] = new_ssr + + # 阴影质量 + shadow_quality_options = ["low", "medium", "high", "ultra"] + shadow_quality_map = {"low": 0, "medium": 1, "high": 2, "ultra": 3} + current_shadow_quality = shadow_quality_map.get(pipeline_config.get('shadow_quality', 'medium'), 1) + + changed, new_shadow_quality = imgui.combo( + "阴影质量", current_shadow_quality, ["低", "中", "高", "极高"] + ) + if changed: + pipeline_config['shadow_quality'] = shadow_quality_options[new_shadow_quality] + + # AO质量 + ao_quality_options = ["low", "medium", "high"] + ao_quality_map = {"low": 0, "medium": 1, "high": 2} + current_ao_quality = ao_quality_map.get(pipeline_config.get('ao_quality', 'low'), 0) + + changed, new_ao_quality = imgui.combo( + "AO质量", current_ao_quality, ["低", "中", "高"] + ) + if changed: + pipeline_config['ao_quality'] = ao_quality_options[new_ao_quality] + + imgui.separator() + + # 应用和重置按钮 + if imgui.button("应用设置"): + self._save_vr_config() + self._apply_vr_config() + self.add_info_message("VR设置已应用") + + imgui.same_line() + if imgui.button("重置默认"): + self._reset_vr_config() + self.add_info_message("VR设置已重置") + + def _load_vr_config(self): + """加载VR配置""" + try: + import json + config_path = os.path.join(os.getcwd(), 'config', 'vr_settings.json') + if os.path.exists(config_path): + with open(config_path, 'r', encoding='utf-8') as f: + self._vr_config = json.load(f) + else: + # 默认配置 + self._vr_config = { + "render_mode": "render_pipeline", + "resolution_scale": 0.75, + "pipeline_resolution_scale": 0.75, + "quality_preset": "quality", + "pipeline_vr_config": { + "enable_shadows": True, + "enable_ao": True, + "enable_bloom": False, + "enable_motion_blur": False, + "enable_ssr": False, + "shadow_quality": "medium", + "ao_quality": "low" + } + } + except Exception as e: + print(f"加载VR配置失败: {e}") + self._vr_config = {} + + def _save_vr_config(self): + """保存VR配置""" + try: + import json + config_path = os.path.join(os.getcwd(), 'config', 'vr_settings.json') + os.makedirs(os.path.dirname(config_path), exist_ok=True) + with open(config_path, 'w', encoding='utf-8') as f: + json.dump(self._vr_config, f, indent=4, ensure_ascii=False) + except Exception as e: + print(f"保存VR配置失败: {e}") + self.add_error_message("保存VR配置失败") + + def _apply_vr_config(self): + """应用VR配置""" + if not self.vr_manager: + return + + try: + # 应用VR配置到VR管理器 + config = self._vr_config + + # 如果VR已启用,可能需要重新配置 + if self.vr_manager.vr_enabled: + # TODO: 根据配置重新配置VR + pass + + except Exception as e: + print(f"应用VR配置失败: {e}") + self.add_error_message("应用VR配置失败") + + def _reset_vr_config(self): + """重置VR配置到默认值""" + self._vr_config = { + "render_mode": "render_pipeline", + "resolution_scale": 0.75, + "pipeline_resolution_scale": 0.75, + "quality_preset": "quality", + "pipeline_vr_config": { + "enable_shadows": True, + "enable_ao": True, + "enable_bloom": False, + "enable_motion_blur": False, + "enable_ssr": False, + "shadow_quality": "medium", + "ao_quality": "low" + } + } + + def _draw_vr_status_window(self): + """绘制VR状态窗口""" + if not self.showVRStatusWindow: + return + + flags = imgui.WindowFlags_.no_collapse | imgui.WindowFlags_.always_auto_resize + with imgui_ctx.begin("VR状态", self.showVRStatusWindow, flags) as (expanded, opened): + self.showVRStatusWindow = opened + + if expanded and self.vr_manager: + # 控制更新频率,避免每帧都获取VR状态 + current_time = imgui.get_io().delta_time + if not hasattr(self, '_vr_status_update_timer'): + self._vr_status_update_timer = 0.0 + self._cached_vr_status = {} + + self._vr_status_update_timer += current_time + + # 每2秒更新一次VR状态 + if self._vr_status_update_timer >= 2.0: + self._vr_status_update_timer = 0.0 + try: + self._cached_vr_status = self.vr_manager.get_vr_status() + except Exception as e: + print(f"获取VR状态失败: {e}") + self._cached_vr_status = {} + + vr_status = self._cached_vr_status + + # 基本状态 + imgui.text("基本状态:") + imgui.separator() + + available = vr_status.get('available', False) + imgui.text(f"VR系统可用: {'是' if available else '否'}") + + initialized = vr_status.get('initialized', False) + imgui.text(f"VR已初始化: {'是' if initialized else '否'}") + + enabled = vr_status.get('enabled', False) + imgui.text(f"VR已启用: {'是' if enabled else '否'}") + + imgui.separator() + + # 渲染信息 + imgui.text("渲染信息:") + imgui.separator() + + eye_resolution = vr_status.get('eye_resolution', (0, 0)) + imgui.text(f"眼睛分辨率: {eye_resolution[0]} x {eye_resolution[1]}") + + device_count = vr_status.get('device_count', 0) + imgui.text(f"连接的设备数量: {device_count}") + + vr_fps = vr_status.get('vr_fps', 0) + imgui.text(f"VR帧率: {vr_fps:.1f} FPS") + + frame_count = vr_status.get('frame_count', 0) + imgui.text(f"总帧数: {frame_count}") + + imgui.separator() + + # 错误统计 + imgui.text("错误统计:") + imgui.separator() + + submit_failures = vr_status.get('submit_failures', 0) + imgui.text(f"提交失败次数: {submit_failures}") + + pose_failures = vr_status.get('pose_failures', 0) + imgui.text(f"姿态失败次数: {pose_failures}") + + imgui.separator() + + # 刷新按钮和更新时间显示 + if imgui.button("立即刷新"): + self._vr_status_update_timer = 0.5 # 强制更新 + try: + self._cached_vr_status = self.vr_manager.get_vr_status() + except Exception as e: + print(f"获取VR状态失败: {e}") + + imgui.same_line() + imgui.text(f"(更新间隔: 2秒)") + + def _draw_vr_debug_window(self): + """绘制VR调试窗口""" + if not self.showVRDebugWindow: + return + + flags = imgui.WindowFlags_.no_collapse + with imgui_ctx.begin("VR调试", self.showVRDebugWindow, flags) as (expanded, opened): + self.showVRDebugWindow = opened + + if expanded: + # 调试选项 + imgui.text("调试选项:") + imgui.separator() + + changed, self.vr_debug_enabled = imgui.checkbox("启用调试输出", self.vr_debug_enabled) + if changed and self.vr_manager: + if self.vr_debug_enabled: + self.vr_manager.enable_debug_output() + else: + self.vr_manager.disable_debug_output() + + changed, self.vr_performance_monitor = imgui.checkbox("启用性能监控", self.vr_performance_monitor) + if changed and self.vr_manager: + if self.vr_performance_monitor: + self.vr_manager.enable_performance_monitoring() + else: + self.vr_manager.disable_performance_monitoring() + + imgui.separator() + + # 输出模式 + imgui.text("输出模式:") + if imgui.radio_button("简短模式", not self.vr_detailed_mode): + self.vr_detailed_mode = False + if imgui.radio_button("详细模式", self.vr_detailed_mode): + self.vr_detailed_mode = True + + imgui.separator() + + # 性能监控选项 + imgui.text("性能监控:") + gpu_timing = False + changed, gpu_timing = imgui.checkbox("GPU时间监控", gpu_timing) + if changed and self.vr_manager: + if gpu_timing: + self.vr_manager.enable_gpu_timing_monitoring() + else: + self.vr_manager.disable_gpu_timing_monitoring() + + pipeline_monitoring = False + changed, pipeline_monitoring = imgui.checkbox("管线监控", pipeline_monitoring) + if changed and self.vr_manager: + if pipeline_monitoring: + self.vr_manager.enable_pipeline_monitoring(True) + else: + self.vr_manager.enable_pipeline_monitoring(False) + + imgui.separator() + + # 姿态策略 + imgui.text("姿态策略:") + pose_strategies = ["默认", "Running Start", "Low Latency"] + current_pose_strategy = 0 + + changed, new_strategy = imgui.combo( + "##pose_strategy", current_pose_strategy, pose_strategies + ) + if changed and self.vr_manager: + # TODO: 应用姿态策略 + if new_strategy == 0: + pass # 默认策略 + elif new_strategy == 1: + self.vr_manager.set_prediction_time(11.0) # Running Start + elif new_strategy == 2: + self.vr_manager.set_prediction_time(5.0) # Low Latency + + imgui.separator() + + # 测试模式 + imgui.text("测试模式:") + test_mode = False + changed, test_mode = imgui.checkbox("启用测试模式", test_mode) + if changed and self.vr_manager: + if test_mode: + self.vr_manager.enable_vr_test_mode() + else: + self.vr_manager.disable_vr_test_mode() + + texture_test = False + changed, texture_test = imgui.checkbox("纹理提交测试", texture_test) + if changed and self.vr_manager: + # TODO: 应用纹理提交测试 + pass + + pose_wait_test = False + changed, pose_wait_test = imgui.checkbox("姿态等待测试", pose_wait_test) + if changed and self.vr_manager: + # TODO: 应用姿态等待测试 + pass + + step_by_step_test = False + changed, step_by_step_test = imgui.checkbox("分步骤测试", step_by_step_test) + if changed and self.vr_manager: + # TODO: 应用分步骤测试 + pass + + imgui.separator() + + # 实时性能数据 + if self.vr_manager and self.vr_manager.vr_enabled: + imgui.text("实时性能数据:") + imgui.separator() + + # 使用状态窗口的缓存数据,避免重复获取 + if hasattr(self, '_cached_vr_status') and self._cached_vr_status: + vr_status = self._cached_vr_status + else: + # 如果没有缓存数据,创建一个空字典 + vr_status = {} + + vr_fps = vr_status.get('vr_fps', 0) + imgui.text(f"VR帧率: {vr_fps:.1f} FPS") + + frame_count = vr_status.get('frame_count', 0) + imgui.text(f"总帧数: {frame_count}") + + submit_failures = vr_status.get('submit_failures', 0) + imgui.text(f"提交失败: {submit_failures}") + + pose_failures = vr_status.get('pose_failures', 0) + imgui.text(f"姿态失败: {pose_failures}") + + # 显示数据更新状态 + imgui.text(f"(数据更新率: 0.5秒)") + def _draw_color_picker(self): """绘制颜色选择器对话框""" if not self._color_picker_active: @@ -8071,10 +8506,10 @@ class MyWorld(CoreWorld): def _toggle_vr_mode(self): """切换VR模式""" if self.vr_manager: - if self.vr_manager.is_enabled(): + if self.vr_manager.vr_enabled: self._exit_vr_mode() else: - self.vr_manager.enable() + self.vr_manager.enable_vr() self.add_info_message("已进入VR模式") else: self.add_error_message("VR管理器未初始化") @@ -8082,39 +8517,29 @@ class MyWorld(CoreWorld): def _exit_vr_mode(self): """退出VR模式""" if self.vr_manager: - self.vr_manager.disable() + self.vr_manager.disable_vr() self.add_info_message("已退出VR模式") def _show_vr_status(self): - """显示VR状态""" + """显示VR状态窗口""" if self.vr_manager: - status = "已启用" if self.vr_manager.is_enabled() else "未启用" - self.add_info_message(f"VR状态: {status}") - - # 显示设备信息 - if self.vr_manager.is_enabled(): - devices = self.vr_manager.get_connected_devices() - if devices: - self.add_info_message(f"连接的设备: {', '.join(devices)}") - else: - self.add_info_message("未检测到VR设备") + self.showVRStatusWindow = not self.showVRStatusWindow else: self.add_error_message("VR管理器未初始化") def _show_vr_settings(self): - """显示VR设置""" + """显示VR设置窗口""" if self.vr_manager: - self.add_info_message("VR设置对话框待实现") + self.showVRSettingsWindow = not self.showVRSettingsWindow else: self.add_error_message("VR管理器未初始化") def _show_vr_performance_report(self): - """显示VR性能报告""" - if self.vr_manager and self.vr_manager.is_enabled(): - report = self.vr_manager.get_performance_report() - self.add_info_message(f"VR性能报告: {report}") + """显示VR调试窗口""" + if self.vr_manager: + self.showVRDebugWindow = not self.showVRDebugWindow else: - self.add_info_message("VR未启用或管理器未初始化") + self.add_error_message("VR管理器未初始化") def _get_chinese_font(self): """获取中文字体""" diff --git a/ui/main_window.py b/ui/main_window.py new file mode 100644 index 00000000..0c64dff7 --- /dev/null +++ b/ui/main_window.py @@ -0,0 +1,6724 @@ +""" +主窗口设置模块 + +负责主窗口的界面构建和事件绑定: +- 菜单栏、工具栏创建 +- 停靠窗口设置 +- 事件连接和信号处理 +""" +WEB_BROWSER_PROCESS_CODE = r""" +import sys +import os +from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout +from PyQt5.QtCore import Qt, QUrl, QTimer +from PyQt5.QtGui import QPalette, QColor + +# 禁用硬件加速 +os.environ["QTWEBENGINE_CHROMIUM_FLAGS"] = "--disable-gpu --disable-software-rasterizer --disable-gpu-compositing --in-process-gpu" +os.environ["QT_WEBENGINE_DISABLE_GPU"] = "1" +os.environ["QT_OPENGL"] = "software" + +try: + from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEngineSettings + WEB_ENGINE_AVAILABLE = True +except ImportError: + WEB_ENGINE_AVAILABLE = False + print("⚠️ QtWebEngine 不可用") + + +class EmbeddedWebBrowser(QWidget): + + def __init__(self, parent=None, url="https://www.baidu.com", parent_window_id=0): + super().__init__(parent) + self.setWindowTitle("Web 浏览器") + self.parent_window_id = parent_window_id + self.url = url + + # 设置窗口标志 - 如果要嵌入,使用子窗口样式 + if parent_window_id > 0: + # 使用 FramelessWindowHint 去除边框,便于嵌入 + self.setWindowFlags(Qt.FramelessWindowHint) + else: + self.setWindowFlags(Qt.Window) + + self.resize(800, 600) + + # 设置背景色 + palette = self.palette() + palette.setColor(QPalette.Window, QColor(25, 25, 27)) + self.setPalette(palette) + self.setAutoFillBackground(True) + + # 创建布局 + layout = QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + + if WEB_ENGINE_AVAILABLE: + # 创建 WebEngine 视图 + self.web_view = QWebEngineView() + + # 设置样式 + self.web_view.setStyleSheet("background-color: #19191b;") + + # 禁用硬件加速 + settings = self.web_view.settings() + settings.setAttribute(QWebEngineSettings.Accelerated2dCanvasEnabled, False) + settings.setAttribute(QWebEngineSettings.WebGLEnabled, False) + settings.setAttribute(QWebEngineSettings.PluginsEnabled, False) + settings.setAttribute(QWebEngineSettings.JavascriptEnabled, True) + settings.setAttribute(QWebEngineSettings.AutoLoadImages, True) + settings.setAttribute(QWebEngineSettings.LocalContentCanAccessRemoteUrls, True) + + # 连接加载信号 + self.web_view.loadStarted.connect(self.on_load_started) + self.web_view.loadProgress.connect(self.on_load_progress) + self.web_view.loadFinished.connect(self.on_load_finished) + + layout.addWidget(self.web_view) + print("[WebEngine] WebEngine 视图已创建") + + # 延迟加载网页,确保窗口完全初始化 + QTimer.singleShot(500, lambda: self.load_url(url)) + else: + from PyQt5.QtWidgets import QLabel + label = QLabel("QtWebEngine 不可用\n请安装: pip install PyQtWebEngine") + label.setAlignment(Qt.AlignCenter) + label.setStyleSheet("color: white; font-size: 14px;") + layout.addWidget(label) + + def on_load_started(self): + print("[WebEngine] 开始加载网页...") + + def on_load_progress(self, progress): + if progress % 20 == 0: # 每20%打印一次 + print(f"[WebEngine] 加载进度: {progress}%") + + def on_load_finished(self, success): + if success: + print("[WebEngine] 网页加载成功") + else: + print("[WebEngine] 网页加载失败") + + def load_url(self, url): + if WEB_ENGINE_AVAILABLE and hasattr(self, 'web_view'): + print(f"[WebEngine] 准备加载 URL: {url}") + qurl = QUrl(url) + print(f"[WebEngine] QUrl 有效性: {qurl.isValid()}") + print(f"[WebEngine] QUrl 内容: {qurl.toString()}") + self.web_view.load(qurl) + print(f"[WebEngine] load() 方法已调用") + else: + print(f"[WebEngine] 无法加载 URL - WEB_ENGINE_AVAILABLE={WEB_ENGINE_AVAILABLE}, has_web_view={hasattr(self, 'web_view')}") + + def get_native_window_id(self): + + return int(self.winId()) + + def embed_into_parent(self): + + if self.parent_window_id > 0: + try: + print(f"[WebEngine] 开始嵌入窗口到父窗口 ID: {self.parent_window_id}") + + # Windows 平台使用 win32 API 嵌入窗口 + if sys.platform == 'win32': + import ctypes + from ctypes import wintypes + + # 获取窗口句柄 + hwnd = int(self.winId()) + parent_hwnd = self.parent_window_id + + print(f"[WebEngine] 子窗口句柄: {hwnd}") + print(f"[WebEngine] 父窗口句柄: {parent_hwnd}") + + user32 = ctypes.windll.user32 + + # 先获取父窗口大小 + rect = wintypes.RECT() + user32.GetClientRect(parent_hwnd, ctypes.byref(rect)) + width = rect.right - rect.left + height = rect.bottom - rect.top + print(f"[WebEngine] 父窗口大小: {width}x{height}") + + # 设置父窗口 + result = user32.SetParent(hwnd, parent_hwnd) + print(f"[WebEngine] SetParent 结果: {result}") + + # 设置窗口样式为子窗口 + GWL_STYLE = -16 + WS_CHILD = 0x40000000 + WS_VISIBLE = 0x10000000 + WS_CLIPCHILDREN = 0x02000000 + WS_CLIPSIBLINGS = 0x04000000 + + old_style = user32.GetWindowLongW(hwnd, GWL_STYLE) + print(f"[WebEngine] 旧窗口样式: {hex(old_style)}") + + # 设置为子窗口样式,移除边框和标题栏 + new_style = (WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN | WS_CLIPSIBLINGS) + user32.SetWindowLongW(hwnd, GWL_STYLE, new_style) + + print(f"[WebEngine] 新窗口样式: {hex(new_style)}") + + # 调整窗口位置和大小,填充整个父窗口 + SWP_FRAMECHANGED = 0x0020 + SWP_SHOWWINDOW = 0x0040 + SWP_NOZORDER = 0x0004 + + result = user32.SetWindowPos( + hwnd, + 0, # HWND_TOP + 0, 0, # x, y + width, height, # width, height + SWP_FRAMECHANGED | SWP_SHOWWINDOW | SWP_NOZORDER + ) + print(f"[WebEngine] SetWindowPos 结果: {result}") + + # 强制刷新窗口 + user32.UpdateWindow(hwnd) + user32.InvalidateRect(hwnd, None, True) + + print(f"[WebEngine] 已成功嵌入到父窗口 (Windows)") + return True + else: + # 其他平台使用 Qt 的方式 + from PyQt5.QtGui import QWindow + parent_window = QWindow.fromWinId(self.parent_window_id) + if parent_window: + self.windowHandle().setParent(parent_window) + print(f"[WebEngine] 已嵌入到父窗口 (Qt): {self.parent_window_id}") + return True + except Exception as e: + print(f"[WebEngine] 嵌入窗口失败: {e}") + import traceback + traceback.print_exc() + return False + + +def main(): + + # 注释掉标准输出重定向,让输出正常显示在终端 + # 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 进程") + print("=" * 60) + + # 创建应用 + app = QApplication(sys.argv) + + # 从命令行参数获取 URL 和窗口 ID + url = sys.argv[1] if len(sys.argv) > 1 else "https://www.baidu.com" + parent_window_id = int(sys.argv[2]) if len(sys.argv) > 2 else 0 + + print(f"[WebEngine] 参数:") + print(f" - URL: {url}") + print(f" - 父窗口 ID: {parent_window_id}") + + # 创建浏览器窗口 + browser = EmbeddedWebBrowser(url=url, parent_window_id=parent_window_id) + + # 显示窗口 + browser.show() + + # 输出窗口 ID 供父进程使用 + window_id = browser.get_native_window_id() + print(f"WINDOW_ID:{window_id}") + sys.stdout.flush() + + # 等待网页开始加载后再嵌入窗口 + if parent_window_id > 0: + # 延迟2秒,确保网页已经开始加载和渲染 + QTimer.singleShot(10, browser.embed_into_parent) + + print("[WebEngine] 进程已就绪") + print("=" * 60) + + sys.exit(app.exec_()) + + +if __name__ == "__main__": + main() +""" + +import os +import sys + +# ==================== 禁用 QtWebEngine 硬件加速 ==================== +# 必须在导入 QtWebEngine 之前设置环境变量 +os.environ["QTWEBENGINE_CHROMIUM_FLAGS"] = "--disable-gpu --disable-software-rasterizer --disable-gpu-compositing --in-process-gpu" +os.environ["QT_WEBENGINE_DISABLE_GPU"] = "1" +os.environ["QT_OPENGL"] = "software" +print("✓ QtWebEngine 硬件加速已禁用,使用软件渲染模式") +# ==================== 结束配置 ==================== + +from PyQt5.QtGui import QKeySequence, QIcon, QPalette, QColor +# from PyQt5.QtWebEngineWidgets import QWebEngineView +from PyQt5.QtWidgets import (QApplication, QMainWindow, QMenuBar, QMenu, QAction, + QDockWidget, QTreeWidget, QListWidget, QWidget, QVBoxLayout, QTreeWidgetItem, + QLabel, QLineEdit, QFormLayout, QDoubleSpinBox, QScrollArea, + QFileSystemModel, QButtonGroup, QToolButton, QPushButton, QHBoxLayout, + QComboBox, QGroupBox, QInputDialog, QFileDialog, QMessageBox, QDesktopWidget, QDialog, + QSpinBox, QFrame, QRadioButton, QTextEdit, QCheckBox, QTabWidget, QSizePolicy, + QListWidgetItem) +from PyQt5.QtCore import Qt, QDir, QTimer, QSize, QPoint, QUrl, QRect, QCoreApplication +from direct.showbase.ShowBaseGlobal import aspect2d +from panda3d.core import OrthographicLens + +from ui.widgets import (CustomMeta3DWidget, CustomFileView, CustomTreeWidget, + CustomAssetsTreeWidget, CustomConsoleDockWidget, + UniversalMessageDialog) +from ui.icon_manager import get_icon_manager, get_icon + +# import yaml +from PyQt5.QtWidgets import QDialog, QVBoxLayout, QListWidget, QPushButton, QDialogButtonBox, QCheckBox, QLabel, QHBoxLayout +from PyQt5.QtCore import Qt + + +class StyledTerrainDialog(QDialog): + """与新建项目对话框风格一致的参数输入对话框""" + + def __init__(self, parent, title, fields, primary_text="确认", secondary_text="取消"): + super().__init__(parent) + self.setWindowTitle(title) + self.setObjectName("styledTerrainDialog") + self.setModal(True) + self.resize(508, 241) + 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.field_widgets = {} + + self.setStyleSheet(""" + QDialog#styledTerrainDialog { + background-color: transparent; + border: none; + } + QFrame#baseFrame { + background-color: #000000; + border: 1px solid #3E3E42; + border-radius: 5px; + } + QWidget#titleBar { + background-color: transparent; + border-radius: 5px 5px 0px 0px; + min-height: 32px; + max-height: 32px; + } + QLabel#titleLabel { + color: #FFFFFF; + font-family: 'Microsoft YaHei', 'Inter', 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: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 12px; + font-weight: 400; + letter-spacing: 0.6px; + } + QLabel[role="hint"] { + color: rgba(235, 235, 235, 0.6); + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 11px; + font-weight: 300; + letter-spacing: 0.55px; + padding: 0px; + } + QDoubleSpinBox, QSpinBox { + 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: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 11px; + font-weight: 300; + letter-spacing: 0.55px; + min-height: 30px; + max-height: 30px; + } + QDoubleSpinBox:focus, QSpinBox:focus { + border: 1px solid #3067C0; + background-color: rgba(48, 103, 192, 0.1); + } + QDoubleSpinBox:hover, QSpinBox:hover { + border: 1px solid #3067C0; + background-color: rgba(89, 100, 113, 0.3); + } + QDoubleSpinBox:disabled, QSpinBox: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 12px; + font-family: 'Microsoft YaHei', 'Inter', 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 { + min-width: 120px; + max-width: 120px; + } + QPushButton#secondaryButton { + min-width: 120px; + max-width: 120px; + } + """) + + 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(15, 10, 15, 10) + content_layout.setSpacing(0) + + content_container = QFrame() + content_container.setObjectName('contentContainer') + content_container.setFrameShape(QFrame.NoFrame) + content_container.setAttribute(Qt.WA_StyledBackground, True) + + container_layout = QVBoxLayout(content_container) + container_layout.setContentsMargins(15, 10, 15, 10) + container_layout.setSpacing(10) + + for field in fields: + row_widget = QWidget() + row_layout = QHBoxLayout(row_widget) + row_layout.setContentsMargins(0, 0, 0, 0) + row_layout.setSpacing(10) + + label = QLabel(field.get("label", "")) + label.setProperty('role', 'fieldLabel') + label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) + label.setMinimumWidth(field.get("label_width", 80)) + label.setMaximumWidth(field.get("label_width", 120)) + row_layout.addWidget(label) + + widget_type = field.get("type", "double") + if widget_type == "int": + widget = QSpinBox() + widget.setRange(field.get("min", 0), field.get("max", 1000)) + widget.setSingleStep(field.get("step", 1)) + widget.setValue(field.get("value", field.get("default", 0))) + elif widget_type == "text": + widget = QLineEdit() + widget.setText(field.get("value", field.get("default", ""))) + if field.get("placeholder"): + widget.setPlaceholderText(field.get("placeholder")) + widget.setClearButtonEnabled(True) + else: + widget = QDoubleSpinBox() + widget.setRange(field.get("min", 0.0), field.get("max", 1000.0)) + widget.setSingleStep(field.get("step", 0.1)) + widget.setDecimals(field.get("decimals", 2)) + widget.setValue(field.get("value", field.get("default", 0.0))) + + widget.setFixedHeight(30) + widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + row_layout.addWidget(widget, 1) + + if field.get("suffix"): + suffix_label = QLabel(field["suffix"]) + suffix_label.setProperty('role', 'fieldLabel') + row_layout.addWidget(suffix_label) + + self.field_widgets[field.get("name", field.get("label", ""))] = widget + container_layout.addWidget(row_widget) + + 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() + + self.confirmButton = QPushButton(primary_text) + self.confirmButton.setObjectName('primaryButton') + self.confirmButton.setFixedSize(120, 30) + self.confirmButton.clicked.connect(self.accept) + button_row_layout.addWidget(self.confirmButton) + + self.cancelButton = QPushButton(secondary_text) + self.cancelButton.setObjectName('secondaryButton') + self.cancelButton.setFixedSize(120, 30) + self.cancelButton.clicked.connect(self.reject) + button_row_layout.addWidget(self.cancelButton) + + container_layout.addWidget(button_row_widget) + + content_layout.addWidget(content_container, 0, Qt.AlignTop) + base_layout.addWidget(content_widget) + main_layout.addWidget(base_frame) + + 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) + + 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() + + 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 mousePressEvent(self, event): + if event.button() == Qt.LeftButton and hasattr(self, "title_bar"): + if 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) + + def get_value(self, name): + widget = self.field_widgets.get(name) + if isinstance(widget, (QDoubleSpinBox, QSpinBox)): + return widget.value() + if isinstance(widget, QLineEdit): + return widget.text() + return None +try: + from PyQt5.QtWebEngineWidgets import QWebEngineView + WEB_ENGINE_AVAILABLE = True +except ImportError: + QWebEngineView = None + WEB_ENGINE_AVAILABLE = False + +class MainWindow(QMainWindow): + """主窗口类""" + + def __init__(self, world): + super().__init__() + self.world = world + self.world.main_window = self # 关键:让world对象能访问主窗口 + + #剪切板相关属性 + self.clipboard = [] + self.clipboard_mode = None + + # 初始化图标管理器并打印调试信息 + self.icon_manager = get_icon_manager() + print("🔧 图标管理器初始化完成") + self.icon_manager.debug_info() + + self.setStyleSheet(""" + QMainWindow { + background-color: #000000; + } + QMenuBar { + background-color: #000000; + color: #ffffff; + border-bottom: 1px solid #4c5c6e; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 12px; + font-weight: 400; + } + QMenuBar::item { + background-color: transparent; + padding: 6px 12px; + color: #D4D4D4; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 12px; + } + QMenuBar::item:selected { + background-color: rgba(48, 103, 192, 0.4); + } + QMenuBar::item:pressed { + background-color: rgba(48, 103, 192, 0.6); + } + QMenu { + background-color: #2E3035; + color: #ebebeb; + border: 1px solid #4c5c6e; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 12px; + } + QMenu::item { + padding: 6px 20px; + } + QMenu::item:selected { + background-color: rgba(48, 103, 192, 0.4); + } + QPushButton { + background-color: rgba(89, 98, 118, 0.4); + color: #ebebeb; + border: none; + padding: 6px 12px; + border-radius: 2px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-weight: 300; + font-size: 10px; + } + QPushButton:hover { + background-color: #3067c0; + color: #ffffff; + } + QPushButton:pressed { + background-color: #2556a0; + } + QPushButton:disabled { + background-color: #394560; + color: rgba(235, 235, 235, 0.5); + } + QComboBox { + background-color: rgba(89, 100, 113, 0.2); + color: #ebebeb; + border: 1px solid rgba(76, 92, 110, 0.6); + border-radius: 2px; + padding: 4px 8px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 10px; + font-weight: 300; + } + QComboBox::drop-down { + border: none; + border-left: 1px solid rgba(76, 92, 110, 0.6); + background-color: rgba(89, 100, 113, 0.3); + width: 16px; + } + QComboBox::down-arrow { + image: url(icons/down_arrows.png); + width: 10px; + height: 10px; + } + QComboBox QAbstractItemView { + background-color: #596471; + color: #ebebeb; + selection-background-color: rgba(48, 103, 192, 0.4); + } + QLineEdit { + background-color: rgba(89, 100, 113, 0.2); + color: #ebebeb; + border: 1px solid rgba(76, 92, 110, 0.6); + border-radius: 2px; + padding: 4px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 10px; + font-weight: 300; + } + QSpinBox, QDoubleSpinBox { + background-color: rgba(89, 100, 113, 0.2); + color: #ebebeb; + border: 1px solid rgba(76, 92, 110, 0.6); + border-radius: 2px; + padding: 4px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 10px; + font-weight: 300; + } + QScrollBar:vertical { + background-color: #19191b; + width: 12px; + border: none; + } + QScrollBar::handle:vertical { + background-color: #596471; + border-radius: 2px; + min-height: 20px; + } + QScrollBar::handle:vertical:hover { + background-color: #3067c0; + } + QScrollBar:horizontal { + background-color: #19191b; + height: 12px; + border: none; + } + QScrollBar::handle:horizontal { + background-color: #596471; + border-radius: 2px; + min-width: 20px; + } + QScrollBar::handle:horizontal:hover { + background-color: #3067c0; + } + """) + # 设置 QMessageBox 样式表 + self.setupMessageBoxStyles() + + self.setupCenterWidget() # 创建中间部分Panda3D + self.setupMenus() # 创建菜单栏 + self.setupDockWindows() + # self.setupToolbar() + self.connectEvents() + + # 移动窗口到屏幕中央 + self.move_center() + + # 创建定时器来更新脚本管理面板状态 + self.updateTimer = QTimer() + self.updateTimer.timeout.connect(self.updateScriptPanel) + self.updateTimer.start(500) # 每500毫秒更新一次 + + self.toolbarDragging = False + self.dragStartPos = QPoint(0, 0) + self.toolbarStartPos = QPoint(0, 0) + + def setupMessageBoxStyles(self): + """设置 QMessageBox 的全局样式""" + # 设置 QMessageBox 的样式表 + msg_box_style = """ + QMessageBox { + background-color: #19191b; + color: #ebebeb; + border: 1px solid #4c5c6e; + } + QMessageBox QLabel { + color: #ebebeb; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 12px; + font-weight: 300; + } + QMessageBox QPushButton { + background-color: rgba(89, 98, 118, 0.4); + color: #ebebeb; + border: none; + padding: 6px 12px; + border-radius: 2px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-weight: 300; + font-size: 10px; + min-width: 80px; + } + QMessageBox QPushButton:hover { + background-color: #3067c0; + color: #ffffff; + } + QMessageBox QPushButton:pressed { + background-color: #2556a0; + } + QMessageBox QPushButton:disabled { + background-color: #394560; + color: rgba(235, 235, 235, 0.5); + } + """ + + # 应用全局样式 + self.setStyleSheet(self.styleSheet() + msg_box_style) + + def createStyledInputDialog(self, parent, title, label, mode=QLineEdit.Normal, text=""): + """创建带有统一主题样式的 QInputDialog""" + dialog = QInputDialog(parent) + dialog.setWindowTitle(title) + dialog.setLabelText(label) + dialog.setTextEchoMode(mode) + dialog.setTextValue(text) + + # 设置样式表 + dialog.setStyleSheet(""" + QInputDialog { + background-color: #19191b; + color: #ebebeb; + } + QLabel { + color: #ffffff; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-weight: 500; + font-size: 14px; + } + 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; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 10px; + } + QPushButton { + background-color: rgba(89, 98, 118, 0.4); + color: #ebebeb; + border: none; + padding: 6px 12px; + border-radius: 2px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-weight: 300; + font-size: 10px; + min-width: 80px; + } + QPushButton:hover { + background-color: #3067c0; + color: #ffffff; + } + QPushButton:pressed { + background-color: #2556a0; + } + QPushButton:disabled { + background-color: #394560; + color: rgba(235, 235, 235, 0.5); + } + """) + + return dialog + + def createStyledFileDialog(self, parent, caption, directory="", filter=""): + """创建带有统一主题样式的 QFileDialog""" + dialog = QFileDialog(parent) + dialog.setWindowTitle(caption) + + # 设置样式表 + dialog.setStyleSheet(""" + QFileDialog { + background-color: #19191b; + color: #ebebeb; + } + QLabel { + color: #ebebeb; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 10px; + font-weight: 300; + } + QListView { + background-color: #19191b; + color: #ebebeb; + border: 1px solid #4c5c6e; + alternate-background-color: #19191b; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 10px; + } + QListView::item:hover { + background-color: #394560; + } + QListView::item:selected { + background-color: rgba(48, 103, 192, 0.4); + color: #ffffff; + } + QTreeView { + background-color: #19191b; + color: #ebebeb; + border: 1px solid #4c5c6e; + alternate-background-color: #19191b; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 10px; + } + QTreeView::item:hover { + background-color: #394560; + } + QTreeView::item:selected { + background-color: rgba(48, 103, 192, 0.4); + color: #ffffff; + } + QComboBox { + background-color: rgba(89, 100, 113, 0.2); + color: #ebebeb; + border: 1px solid rgba(76, 92, 110, 0.6); + border-radius: 2px; + padding: 4px 8px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 10px; + } + QComboBox::drop-down { + border: none; + } + QComboBox QAbstractItemView { + background-color: #596471; + color: #ebebeb; + selection-background-color: rgba(48, 103, 192, 0.4); + } + QPushButton { + background-color: rgba(89, 98, 118, 0.4); + color: #ebebeb; + border: none; + padding: 6px 12px; + border-radius: 2px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-weight: 300; + font-size: 10px; + } + QPushButton:hover { + background-color: #3067c0; + color: #ffffff; + } + QPushButton:pressed { + background-color: #2556a0; + } + QPushButton:disabled { + background-color: #394560; + color: rgba(235, 235, 235, 0.5); + } + QLineEdit { + background-color: rgba(89, 100, 113, 0.2); + color: #ebebeb; + border: 1px solid rgba(76, 92, 110, 0.6); + border-radius: 2px; + padding: 4px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 10px; + } + """) + + return dialog + + def setupCenterWidget(self): + """设置窗口基本属性""" + self.setWindowTitle("引擎编辑器") + # 使用图标管理器设置窗口图标 + app_icon = get_icon('app_logo') + if not app_icon.isNull(): + self.setWindowIcon(app_icon) + print("✅ 应用图标设置成功") + else: + print("⚠️ 应用图标设置失败,使用默认图标") + # 使用自定义的 Panda3D 部件作为中央部件 + self.pandaWidget = CustomMeta3DWidget(self.world) + self.setCentralWidget(self.pandaWidget) + + # 创建内嵌工具栏 + self.setupEmbeddedToolbar() + + def move_center(self): + """设置窗口居中显示""" + self.setGeometry(50, 50, 1920, 1080) + + screen = QDesktopWidget().screenGeometry() + self.move( + int(screen.width() / 2 - self.width() / 2), + int(screen.height() / 2 - self.height() / 2), + ) + + @staticmethod + def get_icon_path(icon_name): + """获取图标文件的完整路径""" + # 假设 icons 文件夹在项目根目录下 + project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + icon_path = os.path.join(project_root, "icons", icon_name) + + # 检查文件是否存在,如果不存在则返回默认值或None + if not os.path.exists(icon_path): + print(f"警告: 图标文件不存在: {icon_path}") + return "" # 返回空字符串,QIcon会处理空路径 + + return icon_path + + def setupEmbeddedToolbar(self): + """创建Unity风格的内嵌工具栏""" + # 创建工具栏容器 + self.embeddedToolbar = QFrame(self.pandaWidget) + self.embeddedToolbar.setObjectName("UnityToolbar") + # self.embeddedToolbar.setStyleSheet(""" + # QFrame#UnityToolbar { + # background-color: rgba(240, 240, 240, 180); + # border: 1px solid rgba(200, 200, 200, 200); + # border-radius: 4px; + # } + # QToolButton { + # background-color: rgba(250, 250, 250, 150); + # border: none; + # color: #333333; + # padding: 5px; + # border-radius: 3px; + # } + # QToolButton:hover { + # background-color: rgba(220, 220, 220, 200); + # } + # QToolButton:checked { + # background-color: rgba(100, 150, 220, 180); + # color: white; + # } + # QToolButton:pressed { + # background-color: rgba(80, 130, 200, 200); + # } + # """) + self.embeddedToolbar.setStyleSheet(""" + QFrame#UnityToolbar { + background-color: transparent; + border: none; + padding: 0px; + } + QToolButton { + background-color: #1e1e1f; + border: 1px solid rgba(184, 211, 241, 0.2); + color: #ebebeb; + padding: 0px; + border-radius: 2px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 10px; + font-weight: 300; + min-width: 31px; + max-width: 31px; + min-height: 30px; + max-height: 30px; + } + QToolButton:hover { + background-color: rgba(89, 100, 113, 0.3); + border: 1px solid rgba(184, 211, 241, 0.3); + } + QToolButton:checked { + background-color: #3067c0; + color: #ffffff; + border: 1px solid rgba(48, 103, 192, 0.6); + } + QToolButton:pressed { + background-color: #2556a0; + } + """) + + # 水平布局 + self.toolbarLayout = QHBoxLayout(self.embeddedToolbar) + self.toolbarLayout.setContentsMargins(0, 0, 0, 0) + self.toolbarLayout.setSpacing(4) + + # 创建工具按钮组 + self.toolGroup = QButtonGroup() + + # 选择工具 + self.selectTool = QToolButton() + select_icon = get_icon('select_tool', QSize(20, 20)) + if not select_icon.isNull(): + self.selectTool.setIcon(select_icon) + self.selectTool.setText('选择') # 如果没有图标则显示文字 + self.selectTool.setIconSize(QSize(20, 20)) + self.selectTool.setCheckable(True) + self.selectTool.setToolTip("选择工具 (Q)") + # self.selectTool.setShortcut(QKeySequence("Q")) + self.toolGroup.addButton(self.selectTool) + self.toolbarLayout.addWidget(self.selectTool) + + # 移动工具 + self.moveTool = QToolButton() + icon_path = self.get_icon_path("move_tool.png") + if icon_path and os.path.exists(icon_path): + self.moveTool.setIcon(QIcon(icon_path)) + self.moveTool.setText("移动") + self.moveTool.setIconSize(QSize(20, 20)) + self.moveTool.setCheckable(True) + self.moveTool.setToolTip("移动工具 (W)") + # self.moveTool.setShortcut(QKeySequence("W")) + self.toolGroup.addButton(self.moveTool) + self.toolbarLayout.addWidget(self.moveTool) + + # 旋转工具 + self.rotateTool = QToolButton() + rotate_icon = get_icon('rotate_tool', QSize(20, 20)) + if not rotate_icon.isNull(): + self.rotateTool.setIcon(rotate_icon) + self.rotateTool.setText("旋转") + self.rotateTool.setIconSize(QSize(20, 20)) + self.rotateTool.setCheckable(True) + self.rotateTool.setToolTip("旋转工具 (E)") + # self.rotateTool.setShortcut(QKeySequence("E")) + self.toolGroup.addButton(self.rotateTool) + self.toolbarLayout.addWidget(self.rotateTool) + + # 缩放工具 + self.scaleTool = QToolButton() + scale_icon = get_icon('scale_tool', QSize(20, 20)) + if not scale_icon.isNull(): + self.scaleTool.setIcon(scale_icon) + self.scaleTool.setText("缩放") + self.scaleTool.setIconSize(QSize(20, 20)) + self.scaleTool.setCheckable(True) + self.scaleTool.setToolTip("缩放工具 (R)") + # self.scaleTool.setShortcut(QKeySequence("R")) + self.toolGroup.addButton(self.scaleTool) + self.toolbarLayout.addWidget(self.scaleTool) + + # 添加分隔符 + # separator = QFrame() + # separator.setFrameShape(QFrame.VLine) + # separator.setFrameShadow(QFrame.Sunken) + # separator.setStyleSheet("background-color: rgba(240, 240, 240, 255);") + # separator.setFixedWidth(1) + # self.toolbarLayout.addWidget(separator) + + # 设置位置到左上角 + self.embeddedToolbar.move(10, 10) + self.embeddedToolbar.adjustSize() + self.embeddedToolbar.show() + + # 连接事件 + self.toolGroup.buttonClicked.connect(self.onToolChanged) + # 设置工具栏拖拽事件 + self.embeddedToolbar.mousePressEvent = self.toolbarMousePressEvent + self.embeddedToolbar.mouseMoveEvent = self.toolbarMouseMoveEvent + self.embeddedToolbar.mouseReleaseEvent = self.toolbarMouseReleaseEvent + + # 默认选择"选择"工具 + self.selectTool.setChecked(True) + self.world.setCurrentTool("选择") + + def toolbarMousePressEvent(self, event): + """工具栏鼠标按下事件""" + if event.button() == Qt.LeftButton: + self.toolbarDragging = True + self.dragStartPos = event.globalPos() + self.toolbarStartPos = self.embeddedToolbar.pos() + event.accept() + + def toolbarMouseMoveEvent(self, event): + """工具栏鼠标移动事件""" + try: + if self.toolbarDragging and event.buttons() == Qt.LeftButton: + # 计算新位置 + delta = event.globalPos() - self.dragStartPos + new_pos = self.toolbarStartPos + delta + + # 边界检测 + panda_rect = self.pandaWidget.geometry() + toolbar_size = self.embeddedToolbar.size() + + # 限制在Panda3D区域内 + new_pos.setX(max(0, min(new_pos.x(), panda_rect.width() - toolbar_size.width()))) + new_pos.setY(max(0, min(new_pos.y(), panda_rect.height() - toolbar_size.height()))) + + self.embeddedToolbar.move(new_pos) + event.accept() + except Exception as e: + print(f"工具栏鼠标移动事件出错") + import traceback + traceback.print_exc() + + + def toolbarMouseReleaseEvent(self, event): + """工具栏鼠标释放事件""" + if event.button() == Qt.LeftButton and self.toolbarDragging: + self.toolbarDragging = False + + # 自动吸附到最近的预设位置 + self.snapToNearestPosition() + event.accept() + + def snapToNearestPosition(self): + """自动吸附到最近的预设位置""" + current_pos = self.embeddedToolbar.pos() + panda_rect = self.pandaWidget.geometry() + toolbar_size = self.embeddedToolbar.size() + + margin = 10 + + # 定义所有预设位置 + positions = { + "top_center": QPoint( + (panda_rect.width() - toolbar_size.width()) // 2, + margin + ), + "top_left": QPoint(margin, margin), + "top_right": QPoint( + panda_rect.width() - toolbar_size.width() - margin, + margin + ), + "bottom_center": QPoint( + (panda_rect.width() - toolbar_size.width()) // 2, + panda_rect.height() - toolbar_size.height() - margin + ), + "bottom_left": QPoint( + margin, + panda_rect.height() - toolbar_size.height() - margin + ), + "bottom_right": QPoint( + panda_rect.width() - toolbar_size.width() - margin, + panda_rect.height() - toolbar_size.height() - margin + ) + } + + # 找到最近的位置 + min_distance = float('inf') + nearest_position = "top_center" + + for pos_name, pos_point in positions.items(): + distance = ((current_pos.x() - pos_point.x()) ** 2 + + (current_pos.y() - pos_point.y()) ** 2) ** 0.5 + if distance < min_distance: + min_distance = distance + nearest_position = pos_name + + # 更新位置并平滑移动 + self.toolbarPosition = nearest_position + target_pos = positions[nearest_position] + + # 简单的平滑移动动画 + from PyQt5.QtCore import QPropertyAnimation, QEasingCurve + self.toolbarAnimation = QPropertyAnimation(self.embeddedToolbar, b"pos") + self.toolbarAnimation.setDuration(200) + self.toolbarAnimation.setStartValue(current_pos) + self.toolbarAnimation.setEndValue(target_pos) + self.toolbarAnimation.setEasingCurve(QEasingCurve.OutCubic) + self.toolbarAnimation.start() + + def setupMenus(self): + """创建菜单栏""" + menubar = self.menuBar() + + # 文件菜单 + self.fileMenu = menubar.addMenu('文件') + self.newAction = self.fileMenu.addAction('新建') + self.openAction = self.fileMenu.addAction('打开') + self.saveAction = self.fileMenu.addAction('保存') + self.buildAction = self.fileMenu.addAction('打包') + self.fileMenu.addSeparator() + self.exitAction = self.fileMenu.addAction('退出') + + # 编辑菜单 + self.editMenu = menubar.addMenu('编辑') + self.undoAction = self.editMenu.addAction('撤销') + self.redoAction = self.editMenu.addAction('重做') + self.editMenu.addSeparator() + self.cutAction = self.editMenu.addAction('剪切') + self.copyAction = self.editMenu.addAction('复制') + self.pasteAction = self.editMenu.addAction('粘贴') + + # # 视图菜单 + # self.viewMenu = menubar.addMenu('视图') + # self.viewPerspectiveAction = self.viewMenu.addAction('透视图') + # self.viewTopAction = self.viewMenu.addAction('俯视图') + # self.viewFrontAction = self.viewMenu.addAction('前视图') + # self.viewMenu.addSeparator() + # self.viewGridAction = self.viewMenu.addAction('显示网格') + + # 工具菜单 + self.toolsMenu = menubar.addMenu('工具') + self.selectAction = self.toolsMenu.addAction('选择工具') + self.moveAction = self.toolsMenu.addAction('移动工具') + self.rotateAction = self.toolsMenu.addAction('旋转工具') + self.scaleAction = self.toolsMenu.addAction('缩放工具') + self.toolsMenu.addSeparator() + self.sunsetAction = self.toolsMenu.addAction('光照编辑') + self.pluginAction = self.toolsMenu.addAction('图形编辑') + # self.toolsMenu.addSeparator() + # self.iconManagerAction = self.toolsMenu.addAction('图标管理器') + # self.iconManagerAction.triggered.connect(self.onOpenIconManager) + + # VR子菜单 + self.vrMenu = self.toolsMenu.addMenu('VR') + self.enterVRAction = self.vrMenu.addAction('进入VR模式') + self.exitVRAction = self.vrMenu.addAction('退出VR模式') + self.vrMenu.addSeparator() + self.vrStatusAction = self.vrMenu.addAction('VR状态') + self.vrSettingsAction = self.vrMenu.addAction('VR设置') + self.vrMenu.addSeparator() + + # VR调试子菜单 + self.vrDebugMenu = self.vrMenu.addMenu('VR调试') + self.vrDebugToggleAction = self.vrDebugMenu.addAction('启用调试输出') + self.vrDebugToggleAction.setCheckable(True) + self.vrDebugToggleAction.setChecked(False) # 默认关闭(节省资源) + + self.vrShowPerformanceAction = self.vrDebugMenu.addAction('立即显示性能报告') + + self.vrDebugMenu.addSeparator() + + # 调试模式切换 + self.vrDebugModeMenu = self.vrDebugMenu.addMenu('输出模式') + self.vrDebugBriefAction = self.vrDebugModeMenu.addAction('简短模式') + self.vrDebugDetailedAction = self.vrDebugModeMenu.addAction('详细模式') + self.vrDebugBriefAction.setCheckable(True) + self.vrDebugDetailedAction.setCheckable(True) + self.vrDebugDetailedAction.setChecked(True) # 默认详细模式 + + # 创建调试模式动作组(单选) + from PyQt5.QtWidgets import QActionGroup + self.vrDebugModeGroup = QActionGroup(self) + self.vrDebugModeGroup.addAction(self.vrDebugBriefAction) + self.vrDebugModeGroup.addAction(self.vrDebugDetailedAction) + + self.vrDebugMenu.addSeparator() + + # 性能监控选项 + self.vrPerformanceMonitorAction = self.vrDebugMenu.addAction('启用性能监控') + self.vrPerformanceMonitorAction.setCheckable(True) + self.vrPerformanceMonitorAction.setChecked(False) # 默认关闭(节省资源) + + self.vrGpuTimingAction = self.vrDebugMenu.addAction('启用GPU时间监控') + self.vrGpuTimingAction.setCheckable(True) + self.vrGpuTimingAction.setChecked(False) # 默认关闭(节省资源) + + # 管线监控选项 + self.vrPipelineMonitorAction = self.vrDebugMenu.addAction('启用管线监控') + self.vrPipelineMonitorAction.setCheckable(True) + self.vrPipelineMonitorAction.setChecked(False) # 默认关闭(节省资源) + + self.vrDebugMenu.addSeparator() + + # 姿态策略选项 + self.vrPoseStrategyMenu = self.vrDebugMenu.addMenu('姿态策略') + self.vrPoseRenderCallbackAction = self.vrPoseStrategyMenu.addAction('渲染回调策略') + self.vrPoseUpdateTaskAction = self.vrPoseStrategyMenu.addAction('更新任务策略') + self.vrPoseRenderCallbackAction.setCheckable(True) + self.vrPoseUpdateTaskAction.setCheckable(True) + self.vrPoseRenderCallbackAction.setChecked(True) # 默认策略 + + # 创建姿态策略动作组(单选) + self.vrPoseStrategyGroup = QActionGroup(self) + self.vrPoseStrategyGroup.addAction(self.vrPoseRenderCallbackAction) + self.vrPoseStrategyGroup.addAction(self.vrPoseUpdateTaskAction) + + self.vrDebugMenu.addSeparator() + + # 测试功能 + self.vrTestPipelineAction = self.vrDebugMenu.addAction('测试管线监控') + + # VR测试模式 + self.vrTestModeAction = self.vrDebugMenu.addAction('VR测试模式') + self.vrTestModeAction.setCheckable(True) + self.vrTestModeAction.setChecked(False) # 默认关闭 + + # VR测试模式调试子菜单 + self.vrTestDebugMenu = self.vrDebugMenu.addMenu('测试模式调试') + + # 渐进式功能开关 + self.vrTestSubmitTextureAction = self.vrTestDebugMenu.addAction('启用纹理提交') + self.vrTestSubmitTextureAction.setCheckable(True) + self.vrTestSubmitTextureAction.setChecked(False) # 默认关闭 + + self.vrTestWaitPosesAction = self.vrTestDebugMenu.addAction('启用姿态等待') + self.vrTestWaitPosesAction.setCheckable(True) + self.vrTestWaitPosesAction.setChecked(False) # 默认关闭 + + self.vrTestDebugMenu.addSeparator() + + # 快捷测试预设 + self.vrTestStep1Action = self.vrTestDebugMenu.addAction('步骤1: 只启用纹理提交') + self.vrTestStep2Action = self.vrTestDebugMenu.addAction('步骤2: 只启用姿态等待') + self.vrTestStep3Action = self.vrTestDebugMenu.addAction('步骤3: 同时启用两者') + self.vrTestResetAction = self.vrTestDebugMenu.addAction('重置: 禁用所有功能') + + self.vrDebugSettingsAction = self.vrDebugMenu.addAction('调试设置...') + + # 初始状态下禁用退出VR选项 + self.exitVRAction.setEnabled(False) + + # 统一创建菜单 - 关键修改 + self.createMenu = menubar.addMenu('创建') + self.setupCreateMenuActions() # 统一创建菜单动作 + + #添加地形菜单 + self.createTerrainMenu = self.createMenu.addMenu('地形') + self.createFlatTerrainAction = self.createTerrainMenu.addAction('创建平面地形') + self.createHeightmapTerrainAction = self.createTerrainMenu.addAction('从高度图创建地形') + self.createTerrainMenu.addSeparator() + # self.terrainEditModeAction = self.createTerrainMenu.addAction('地形编辑模式') + + + # 帮助菜单 + self.helpMenu = menubar.addMenu('帮助') + self.aboutAction = self.helpMenu.addAction('关于') + self.aboutAction.triggered.connect(self.showAboutDialog) + + tool_menu = self.menuBar().addMenu("配置") + plugin_config_action = tool_menu.addAction("渲染管线插件配置") + plugin_config_action.triggered.connect(self.open_plugin_config_dialog) + + def showAboutDialog(self): + msgBox = QMessageBox() + msgBox.setWindowTitle("关于") + msgBox.setText(f'元泰引擎系统\nMetaCore\n版本:v1.0') + msgBox.setIcon(QMessageBox.NoIcon) + msgBox.exec_() + + def open_plugin_config_dialog(self): + """打开插件配置对话框""" + try: + dialog = PluginConfigDialog(self) + dialog.exec_() + except Exception as e: + print(f"打开插件配置对话框失败: {e}") + + def refreshAssetsView(self): + """"刷新资源视图""" + if hasattr(self,'fileView') and self.fileView: + self.fileView.refreshView() + print("资源视图已刷新") + + def setupCreateMenuActions(self): + """统一设置创建菜单的所有动作 - 避免重复代码""" + # 基础对象 + self.createEnptyaddAction = self.createMenu.addAction('空对象') + + # 3D对象子菜单 + self.create3dObjectaddMenu = self.createMenu.addMenu('3D对象') + # 可以在这里添加更多3D对象类型 + + # 3D GUI子菜单 + self.create3dGUIaddMenu = self.createMenu.addMenu('3D GUI') + self.create3DTextAction = self.create3dGUIaddMenu.addAction('3D文本') + self.create3DImageAction = self.create3dGUIaddMenu.addAction('3D图片') + + # GUI子菜单 + self.createGUIaddMenu = self.createMenu.addMenu('GUI') + self.createButtonAction = self.createGUIaddMenu.addAction('创建按钮') + self.createLabelAction = self.createGUIaddMenu.addAction('创建标签') + self.createEntryAction = self.createGUIaddMenu.addAction('创建输入框') + self.createImageAction = self.createGUIaddMenu.addAction('创建图片') + self.createGUIaddMenu.addSeparator() + self.createVideoScreen = self.createGUIaddMenu.addAction('创建视频屏幕') + self.create2DVideoScreen = self.createGUIaddMenu.addAction('创建2D视频屏幕') + self.createSphericalVideo = self.createGUIaddMenu.addAction('创建球形视频') + self.createGUIaddMenu.addSeparator() + self.createVirtualScreenAction = self.createGUIaddMenu.addAction('创建虚拟屏幕') + + # 光源子菜单 + self.createLightaddMenu = self.createMenu.addMenu('光源') + self.createSpotLightAction = self.createLightaddMenu.addAction('聚光灯') + self.createPointLightAction = self.createLightaddMenu.addAction('点光源') + + self.createScriptMenu = self.createMenu.addMenu('脚本') + self.createScriptAction = self.createScriptMenu.addAction('创建脚本...') + self.loadScriptAction = self.createScriptMenu.addAction('加载脚本文件...') + self.loadAllScriptsAction = self.createScriptMenu.addAction('重载所有脚本') + self.createScriptMenu.addSeparator() + self.toggleHotReloadAction = self.createScriptMenu.addAction('启用热重载') + self.toggleHotReloadAction.setCheckable(True) + self.toggleHotReloadAction.setChecked(True) # 默认启用 + self.createScriptMenu.addSeparator() + self.openScriptsManagerAction = self.createScriptMenu.addAction('脚本管理器') + + self.createInfoPanelMenu = self.createMenu.addMenu('信息面板') + self.createSamplePanelAction = self.createInfoPanelMenu.addAction('创建2D示例面板') + self.create3DSamplePanelAction = self.createInfoPanelMenu.addAction('创建3D实例面板') + self.createInfoPanelMenu.addSeparator() + self.webBrowserAction = self.createInfoPanelMenu.addAction("Web面板") + + # 统一连接信号到处理方法 + self.connectCreateMenuActions() + + def setupViewMenuActions(self): + """设置视图菜单动作""" + # 连接视图菜单事件 + self.viewPerspectiveAction.triggered.connect(self.onViewPerspective) + #self.viewTopAction.triggered.connect(self.onViewTop) + #self.viewFrontAction.triggered.connect(self.onViewFront) + self.viewOrthographicAction = self.viewMenu.addAction('正交视图') # 添加正交视图动作 + #self.viewOrthographicAction.triggered.connect(self.onViewOrthographic) + #self.viewGridAction.triggered.connect(self.onViewGrid) # 添加网格显示的信号连接 + + # 保存原始相机设置 + self._original_camera_fov = 80 + self._original_camera_pos = (0, -50, 20) + self._original_camera_lookat = (0, 0, 0) + + self._grid_visible = False + def onViewPerspective(self): + """切换到透视视图""" + try: + lens = self.world.cam.node().getLens() + lens.setFov(self._original_camera_fov) + except Exception as e: + print(f"切换到透视视图失败{e}") + + def onViewOrthographic(self): + """切换到正交视图""" + try: + # 保存当前相机设置(如果是透视模式) + lens = self.world.cam.node().getLens() + if not hasattr(self, '_saved_perspective_settings'): + self._saved_perspective_settings = { + 'fov': lens.getFov()[0], + 'pos': self.world.cam.getPos(), + 'hpr': self.world.cam.getHpr() + } + + # 获取窗口尺寸 + win_width, win_height = self.world.getWindowSize() + aspect_ratio = win_width / win_height if win_height != 0 else 16 / 9 + + # 修改现有镜头为正交投影 + if not isinstance(lens, OrthographicLens): + # 保存当前镜头类型 + self._original_lens = lens + + # 创建正交镜头并替换现有镜头 + ortho_lens = OrthographicLens() + ortho_lens.setFilmSize(20 * aspect_ratio, 20) # 设置正交镜头大小 + ortho_lens.setNearFar(-1000, 1000) # 设置较大的近远裁剪面 + + # 应用正交镜头 + self.world.cam.node().setLens(ortho_lens) + else: + # 如果已经是正交镜头,则调整其参数 + film_height = 20 + film_width = film_height * aspect_ratio + lens.setFilmSize(film_width, film_height) + + print("切换到正交视图") + except Exception as e: + print(f"切换正交视图失败: {e}") + + def onViewTop(self): + """切换到俯视图(正交)""" + try: + # 保存当前设置 + self._saveCurrentCameraSettings() + + # 设置正交投影 + self._setupOrthographicLens() + + # 设置摄像机位置(从上方俯视) + self.world.cam.setPos(0, 0, 30) + self.world.cam.lookAt(0, 0, 0) + self.world.cam.setHpr(0, -90, 0) # 朝下看 + + # 更新菜单项文本 + self._updateViewMenuText() + + print("切换到俯视图") + except Exception as e: + print(f"切换俯视图失败: {e}") + + def onViewFront(self): + """切换到前视图(正交)""" + try: + # 保存当前设置 + self._saveCurrentCameraSettings() + + # 设置正交投影 + self._setupOrthographicLens() + + # 设置摄像机位置(从前方向看) + self.world.cam.setPos(0, -30, 0) + self.world.cam.lookAt(0, 0, 0) + self.world.cam.setHpr(0, 0, 0) # 正面朝向 + + # 更新菜单项文本 + self._updateViewMenuText() + + print("切换到前视图") + except Exception as e: + print(f"切换前视图失败: {e}") + + def onViewGrid(self): + """切换网格显示/隐藏""" + try: + # 切换网格显示状态 + self._grid_visible = not self._grid_visible + + # 查找网格节点 + grid_node = self.world.render.find("**/grid") + if grid_node.isEmpty(): + # 如果网格不存在则创建 + self._createGridView() + grid_node = self.world.render.find("**/grid") + + # 设置网格可见性 + if not grid_node.isEmpty(): + if self._grid_visible: + grid_node.show() + self.viewGridAction.setText("隐藏网格") + print("网格已显示") + else: + grid_node.hide() + self.viewGridAction.setText("显示网格") + print("网格已隐藏") + else: + print("网格节点未找到") + + except Exception as e: + print(f"切换网格显示失败: {e}") + def _createGridView(self): + """创建网格视图""" + try: + from panda3d.core import LineSegs,Vec3 + grid_node = self.world.render.attachNewNode("grid") + lines = LineSegs() + lines.setThickness(1.0) + lines.setColor(0.3,0.3,0.3,1.0) + grid_size = 20 + grid_step = 1 + + for i in range(-grid_size,grid_size+1,grid_step): + lines.moveTo(Vec3(-grid_size,i,0)) + lines.drawTo(Vec3(grid_size,i,0)) + + grid_node.attachNewNode(lines.create()) + # 添加中心轴线(红色X轴,绿色Y轴) + axis_lines = LineSegs() + axis_lines.setThickness(2.0) + # X轴(红色) + axis_lines.setColor(1.0,0.0,0.0,1.0) + axis_lines.moveTo(Vec3(0,0,0)) + axis_lines.drawTo(Vec3(grid_size,0,0)) + # Y轴(绿色) + axis_lines.setColor(0.0, 1.0, 0.0, 1.0) + axis_lines.moveTo(Vec3(0, 0, 0)) + axis_lines.drawTo(Vec3(0, grid_size, 0)) + + grid_node.attachNewNode(axis_lines.create()) + + print("网格已创建") + except Exception as e: + print(f"创建网格失败{e}") + + def _saveCurrentCameraSettings(self): + """保存当前相机设置""" + try: + lens = self.world.cam.node().getLens() + self._saved_camera_settings = { + 'lens_type': 'perspective' if not isinstance(lens, OrthographicLens) else 'orthographic', + 'fov': lens.getFov()[0] if hasattr(lens, 'getFov') else None, + 'film_size': (lens.getFilmSize()[0], lens.getFilmSize()[1]) if hasattr(lens, 'getFilmSize') else None, + 'pos': self.world.cam.getPos(), + 'hpr': self.world.cam.getHpr() + } + except Exception as e: + print(f"保存相机设置失败: {e}") + + def _setupOrthographicLens(self): + """设置正交镜头""" + try: + win_width, win_height = self.world.getWindowSize() + aspect_ratio = win_width / win_height if win_height != 0 else 16 / 9 + + from panda3d.core import OrthographicLens + ortho_lens = OrthographicLens() + ortho_lens.setFilmSize(20 * aspect_ratio, 20) # 设置正交镜头大小 + ortho_lens.setNearFar(-1000, 1000) # 设置较大的近远裁剪面 + + self.world.cam.node().setLens(ortho_lens) + except Exception as e: + print(f"设置正交镜头失败: {e}") + + def _updateViewMenuText(self): + """更新视图菜单文本""" + try: + lens = self.world.cam.node().getLens() + from panda3d.core import OrthographicLens + + # 更新正交/透视视图动作文本 + if isinstance(lens, OrthographicLens): + self.viewOrthographicAction.setText("切换到透视视图") + self.viewOrthographicAction.triggered.disconnect() + self.viewOrthographicAction.triggered.connect(self.onViewPerspective) + else: + self.viewOrthographicAction.setText("切换到正交视图") + self.viewOrthographicAction.triggered.disconnect() + self.viewOrthographicAction.triggered.connect(self.onViewOrthographic) + except Exception as e: + print(f"更新视图菜单文本失败: {e}") + + # 如果需要在窗口大小改变时调整正交镜头,可以添加以下方法 + def _onWindowResized(self): + """窗口大小改变时的处理""" + try: + lens = self.world.cam.node().getLens() + from panda3d.core import OrthographicLens + + # 如果当前是正交镜头,需要根据新窗口大小调整 + if isinstance(lens, OrthographicLens): + win_width, win_height = self.world.getWindowSize() + if win_height != 0: + aspect_ratio = win_width / win_height + film_height = 20 + film_width = film_height * aspect_ratio + lens.setFilmSize(film_width, film_height) + except Exception as e: + print(f"窗口大小调整失败: {e}") + + + def connectCreateMenuActions(self): + """统一连接创建菜单的信号到处理方法""" + # 连接到world对象的创建方法 + # self.createEnptyaddAction.triggered.connect(self.world.createEmptyObject) + self.create3DTextAction.triggered.connect(lambda: self.world.createGUI3DText()) + self.create3DImageAction.triggered.connect(lambda: self.world.createGUI3DImage()) + self.createButtonAction.triggered.connect(lambda: self.world.createGUIButton()) + self.createLabelAction.triggered.connect(lambda: self.world.createGUILabel()) + self.createEntryAction.triggered.connect(lambda: self.world.createGUIEntry()) + self.createImageAction.triggered.connect(lambda: self.world.createGUI2DImage()) + self.createVideoScreen.triggered.connect(self.world.createVideoScreen) + self.create2DVideoScreen.triggered.connect(self.world.create2DVideoScreen) + self.createSphericalVideo.triggered.connect(self.world.createSphericalVideo) + self.createVirtualScreenAction.triggered.connect(lambda: self.world.createGUIVirtualScreen()) + self.createSpotLightAction.triggered.connect(lambda :self.world.createSpotLight()) + self.createPointLightAction.triggered.connect(lambda :self.world.createPointLight()) + + self.createScriptAction.triggered.connect(self.onCreateScript) + self.loadScriptAction.triggered.connect(self.onLoadScript) + #self.loadAllScriptsAction.triggered.connect(self.onLoadAllScripts) + self.toggleHotReloadAction.triggered.connect(self.onToggleHotReload) + self.openScriptsManagerAction.triggered.connect(self.onOpenScriptsManager) + + self.createSamplePanelAction.triggered.connect(self.world.info_panel_manager.onCreateSampleInfoPanel) + self.create3DSamplePanelAction.triggered.connect(self.onCreate3DSampleInfoPanel) + #self.webBrowserAction.triggered.connect(self.openWebBrowser_win) + + if sys.platform == 'win32': + self.webBrowserAction.triggered.connect(self.openWebBrowser_win) + else: + self.webBrowserAction.triggered.connect(self.openWebBrowser) + + def getCreateMenuActions(self): + """获取所有创建菜单动作的字典 - 供右键菜单使用""" + return { + 'createEmpty': self.createEnptyaddAction, + 'create3DText': self.create3DTextAction, + 'create3DImage': self.create3DImageAction, + 'createButton': self.createButtonAction, + 'createLabel': self.createLabelAction, + 'createEntry': self.createEntryAction, + 'createImage': self.createImageAction, + 'createVideoScreen': self.createVideoScreen, + 'create2DVideoScreen':self.create2DVideoScreen, + 'createSphericalVideo': self.createSphericalVideo, + 'createVirtualScreen': self.createVirtualScreenAction, + 'createSpotLight': self.createSpotLightAction, + 'createPointLight': self.createPointLightAction, + } + + def setupDockWindows(self): + """创建停靠窗口""" + # 创建左侧停靠窗口(层级窗口) + self.leftDock = QDockWidget("层级", self) + self.leftDock.setStyleSheet(""" + QDockWidget { + background-color: #19191b; + color: #ffffff; + border: none; + } + QDockWidget::title { + background-color: #19191b; + padding: 8px 10px; + border-bottom: none; + text-align: left; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 12px; + font-weight: 500; + } + QDockWidget::close-button { + background-color: rgba(89, 100, 113, 0.3); + border: 1px solid rgba(184, 211, 241, 0.2); + icon-size: 10px; + border-radius: 2px; + width: 16px; + height: 16px; + right: 6px; + top: 6px; + } + QDockWidget::float-button { + background-color: rgba(89, 100, 113, 0.3); + border: 1px solid rgba(184, 211, 241, 0.2); + icon-size: 10px; + border-radius: 2px; + width: 16px; + height: 16px; + right: 26px; + top: 6px; + } + QDockWidget::close-button:hover, QDockWidget::float-button:hover { + background-color: rgba(89, 100, 113, 0.5); + border: 1px solid rgba(184, 211, 241, 0.3); + } + QDockWidget::close-button:pressed, QDockWidget::float-button:pressed { + background-color: #3067c0; + border: 1px solid rgba(48, 103, 192, 0.6); + } + QTreeView { + background-color: #19191b; + color: #ebebeb; + border: none; + padding: 0px 0px 0px 8px; + alternate-background-color: #19191b; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 10px; + font-weight: 400; + outline: none; + } + QTreeView::item { + padding: 3px 0px; + border: none; + } + QTreeView::item:hover { + background-color: rgba(89, 100, 113, 0.3); + } + QTreeView::item:selected { + background-color: rgba(48, 103, 192, 0.4); + color: #ffffff; + } + QTreeView::branch { + background-color: #19191b; + } + QTreeView::branch:has-children:!has-siblings:closed, + QTreeView::branch:closed:has-children:has-siblings { + image: url(icons/solid_right_arrows.png); + width: 12px; + height: 12px; + } + QTreeView::branch:open:has-children:!has-siblings, + QTreeView::branch:open:has-children:has-siblings { + image: url(icons/solid_down_arrows.png); + width: 12px; + height: 12px; + } + """) + self.treeWidget = CustomTreeWidget(self.world) + self.world.setTreeWidget(self.treeWidget) # 设置树形控件引用 + + # 创建包装容器,添加标题下方的分隔线 + leftDockContainer = QWidget() + leftDockContainer.setStyleSheet("QWidget { background-color: #19191b; }") + leftDockLayout = QVBoxLayout(leftDockContainer) + leftDockLayout.setContentsMargins(0, 0, 0, 0) + leftDockLayout.setSpacing(0) + + # 添加带左右间距的分隔线 + leftSeparator = QFrame() + leftSeparator.setFrameShape(QFrame.HLine) + leftSeparator.setFrameShadow(QFrame.Plain) + leftSeparator.setStyleSheet(""" + QFrame { + background-color: rgba(77, 116, 189, 0.4); + max-height: 1px; + margin-left: 8px; + margin-right: 8px; + border: none; + } + """) + leftDockLayout.addWidget(leftSeparator) + leftDockLayout.addWidget(self.treeWidget) + + self.leftDock.setWidget(leftDockContainer) + self.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, self.leftDock) + + # 创建右侧停靠窗口(属性窗口) + self.rightDock = QDockWidget("属性", self) + self.rightDock.setStyleSheet(""" + QDockWidget { + background-color: #19191b; + color: #ffffff; + border: none; + } + QDockWidget::title { + background-color: #19191b; + padding: 8px 10px; + border-bottom: none; + text-align: left; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 14px; + font-weight: 500; + } + QDockWidget::close-button { + background-color: rgba(89, 100, 113, 0.3); + border: 1px solid rgba(184, 211, 241, 0.2); + icon-size: 10px; + border-radius: 2px; + width: 16px; + height: 16px; + right: 6px; + top: 6px; + } + QDockWidget::float-button { + background-color: rgba(89, 100, 113, 0.3); + border: 1px solid rgba(184, 211, 241, 0.2); + icon-size: 10px; + border-radius: 2px; + width: 16px; + height: 16px; + right: 26px; + top: 6px; + } + QDockWidget::close-button:hover, QDockWidget::float-button:hover { + background-color: rgba(89, 100, 113, 0.5); + border: 1px solid rgba(184, 211, 241, 0.3); + } + QDockWidget::close-button:pressed, QDockWidget::float-button:pressed { + background-color: #3067c0; + border: 1px solid rgba(48, 103, 192, 0.6); + } + QScrollArea { + background-color: #19191b; + border: none; + } + QWidget#PropertyContainer { + background-color: #19191b; + } + QLabel { + color: #ebebeb; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 10px; + font-weight: 300; + } + + /* 现代化的输入控件样式 */ + QLineEdit { + background-color: rgba(89, 100, 113, 0.15); + color: #ebebeb; + border: 1px solid rgba(76, 92, 110, 0.4); + border-radius: 4px; + padding: 6px 8px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 10px; + font-weight: 300; + 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); + } + + QDoubleSpinBox, QSpinBox { + background-color: rgba(89, 100, 113, 0.15); + color: #ebebeb; + border: 1px solid rgba(76, 92, 110, 0.4); + border-radius: 4px; + padding: 6px 8px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 10px; + font-weight: 300; + min-height: 16px; + } + QDoubleSpinBox:focus, QSpinBox:focus { + border: 1px solid #4d74bd; + background-color: rgba(77, 116, 189, 0.1); + } + QDoubleSpinBox:hover, QSpinBox:hover { + border: 1px solid rgba(77, 116, 189, 0.6); + background-color: rgba(89, 100, 113, 0.2); + } + QDoubleSpinBox::up-button, QSpinBox::up-button { + background-color: rgba(77, 116, 189, 0.2); + border: none; + border-radius: 2px; + width: 16px; + subcontrol-origin: border; + subcontrol-position: top right; + } + QDoubleSpinBox::down-button, QSpinBox::down-button { + background-color: rgba(77, 116, 189, 0.2); + border: none; + border-radius: 2px; + width: 16px; + subcontrol-origin: border; + subcontrol-position: bottom right; + } + QDoubleSpinBox::up-button:hover, QSpinBox::up-button:hover, + QDoubleSpinBox::down-button:hover, QSpinBox::down-button:hover { + background-color: rgba(77, 116, 189, 0.4); + } + QDoubleSpinBox::up-arrow, QSpinBox::up-arrow { + image: url(icons/up_arrows.png); + width: 12px; + height: 12px; + } + QDoubleSpinBox::down-arrow, QSpinBox::down-arrow { + image: url(icons/down_arrows.png); + width: 12px; + height: 12px; + } + QDoubleSpinBox::up-arrow:hover, QSpinBox::up-arrow:hover { + image: url(icons/up_arrows.png); + } + QDoubleSpinBox::down-arrow:hover, QSpinBox::down-arrow:hover { + image: url(icons/down_arrows.png); + } + + QComboBox { + background-color: rgba(89, 100, 113, 0.15); + color: #ebebeb; + border: 1px solid rgba(76, 92, 110, 0.4); + border-radius: 4px; + padding: 6px 8px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 10px; + font-weight: 300; + min-height: 16px; + } + QComboBox:focus { + border: 1px solid #4d74bd; + background-color: rgba(77, 116, 189, 0.1); + } + QComboBox:hover { + border: 1px solid rgba(77, 116, 189, 0.6); + background-color: rgba(89, 100, 113, 0.2); + } + QComboBox::drop-down { + border: none; + border-left: 1px solid rgba(76, 92, 110, 0.4); + background-color: rgba(77, 116, 189, 0.2); + border-radius: 0 4px 4px 0; + width: 20px; + } + QComboBox::drop-down:hover { + background-color: rgba(77, 116, 189, 0.4); + } + QComboBox::down-arrow { + image: url(icons/down_arrows.png); + width: 12px; + height: 12px; + } + QComboBox QAbstractItemView { + background-color: #2a2a2e; + color: #ebebeb; + selection-background-color: rgba(77, 116, 189, 0.4); + border: 1px solid rgba(76, 92, 110, 0.6); + border-radius: 4px; + padding: 4px; + } + + 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: 4px; + padding: 8px 12px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 10px; + font-weight: 400; + min-height: 16px; + } + 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); + } + + QCheckBox { + color: #ebebeb; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 10px; + font-weight: 300; + spacing: 8px; + } + QCheckBox::indicator { + width: 16px; + height: 16px; + border: 1px solid rgba(76, 92, 110, 0.6); + border-radius: 3px; + background-color: rgba(89, 100, 113, 0.15); + } + QCheckBox::indicator:hover { + border: 1px solid rgba(77, 116, 189, 0.6); + background-color: rgba(89, 100, 113, 0.2); + } + QCheckBox::indicator:checked { + background-color: #4d74bd; + border: 1px solid #4d74bd; + image: none; + } + QCheckBox::indicator:checked:hover { + background-color: rgba(77, 116, 189, 1.0); + } + + QGroupBox { + /* 减少上边距,使相邻组更紧凑 */ + margin-top: 0px; + + /* 移除边框 */ + border: none; + + /* 设置内边距,为标题和内容留出空间,左右内边距与主标题对齐 */ + padding-top: 20px; /* 为标题留出足够空间 */ + padding-left: 0px; /* Align subtitle with dock title baseline */ + padding-right: 0px; /* 右内边距与主标题分隔线对齐 */ + + /* 减少下边距,使相邻组更紧凑 */ + background-color: transparent; + margin-bottom: 6px; + + /* 使用边框创建样式线,与主标题分隔线保持相同的8px左右间距 */ + border-top: 1px solid rgba(77, 116, 189, 0.4); + margin-left: 0px; + margin-right: 8px; + } + + QGroupBox::title { + /* 将标题定位到控件内部,在样条线下方 */ + subcontrol-origin: padding; + subcontrol-position: top left; + + /* 标题样式,左边距为0因为已经通过padding-left处理了对齐 */ + padding: 8px 3px 8px 0px; /* 左边距设为0,与内容对齐 */ + margin-top: 0px; /* 标题紧贴内边距顶部 */ + + /* 移除标题上的边框 */ + border: none; + + /* 字体和颜色样式 */ + color: rgba(255, 255, 255, 0.8); + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 12px; + font-weight: 500; + } + QGroupBox[groupRole="first"] { + padding-top: 16px; + margin-top: 0px; + border-top: none; /* 明确移除顶部边框 */ + } + QGroupBox[groupRole="first"]::title { + padding: 6px 3px 8px 0px; + } + """) + + # 创建属性面板的主容器和布局 + self.propertyContainer = QWidget() + self.propertyContainer.setObjectName("PropertyContainer") + + try: + self.propertyLayout = QVBoxLayout(self.propertyContainer) + # Keep subtitles aligned with the dock title left padding + self.propertyLayout.setContentsMargins(10, 0, 10, 12) + self.propertyLayout.setSpacing(12) + print(f"✓ 属性布局创建完成: {self.propertyLayout}") + + # 添加初始提示信息 + tipLabel = QLabel("选择对象以查看属性") + tipLabel.setStyleSheet("color: gray; padding: 10px 0px;") # 使用灰色字体 + self.propertyLayout.addWidget(tipLabel) + print("✓ 提示标签添加完成") + + # 创建滚动区域并设置属性 + self.scrollArea = QScrollArea() + self.scrollArea.setWidgetResizable(True) + self.scrollArea.setWidget(self.propertyContainer) + print("✓ 滚动区域设置完成") + + # 创建包装容器,添加标题下方的分隔线 + rightDockContainer = QWidget() + rightDockContainer.setStyleSheet("QWidget { background-color: #19191b; }") + rightDockLayout = QVBoxLayout(rightDockContainer) + rightDockLayout.setContentsMargins(0, 0, 0, 0) + rightDockLayout.setSpacing(0) + + # 添加带左右间距的分隔线 + rightSeparator = QFrame() + rightSeparator.setFrameShape(QFrame.HLine) + rightSeparator.setFrameShadow(QFrame.Plain) + rightSeparator.setStyleSheet(""" + QFrame { + background-color: rgba(77, 116, 189, 0.4); + max-height: 1px; + margin-left: 8px; + margin-right: 8px; + border: none; + } + """) + rightDockLayout.addWidget(rightSeparator) + rightDockLayout.addWidget(self.scrollArea) + + # 设置包装容器为停靠窗口的主部件 + self.rightDock.setWidget(rightDockContainer) + self.rightDock.setMinimumWidth(300) + self.addDockWidget(Qt.RightDockWidgetArea, self.rightDock) + print("✓ 右侧停靠窗口添加完成") + + # 设置属性面板到世界对象 + if hasattr(self.world, 'setPropertyLayout'): + print("开始设置属性布局到世界对象...") + self.world.setPropertyLayout(self.propertyLayout) + print("✓ 属性布局设置完成") + else: + print("⚠ 世界对象没有 setPropertyLayout 方法") + + except Exception as e: + print(f"✗ 设置属性面板时出错: {e}") + import traceback + traceback.print_exc() + + # 创建基本的属性面板作为后备方案 + fallback_widget = QLabel("属性面板初始化失败\n请查看控制台日志") + fallback_widget.setStyleSheet("color: red; background-color: white; padding: 10px;") + self.rightDock.setWidget(fallback_widget) + self.addDockWidget(Qt.RightDockWidgetArea, self.rightDock) + + # 创建脚本管理停靠窗口 + self.scriptDock = QDockWidget("脚本管理", self) + self.scriptDock.setStyleSheet(""" + QDockWidget { + background-color: #19191b; + color: #ffffff; + border: none; + } + QDockWidget::title { + background-color: #19191b; + padding: 8px 10px; + border-bottom: none; + text-align: left; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 14px; + font-weight: 500; + } + QDockWidget::close-button { + background-color: rgba(89, 100, 113, 0.3); + border: 1px solid rgba(184, 211, 241, 0.2); + icon-size: 10px; + border-radius: 2px; + width: 16px; + height: 16px; + right: 6px; + top: 6px; + } + QDockWidget::float-button { + background-color: rgba(89, 100, 113, 0.3); + border: 1px solid rgba(184, 211, 241, 0.2); + icon-size: 10px; + border-radius: 2px; + width: 16px; + height: 16px; + right: 26px; + top: 6px; + } + QDockWidget::close-button:hover, QDockWidget::float-button:hover { + background-color: rgba(89, 100, 113, 0.5); + border: 1px solid rgba(184, 211, 241, 0.3); + } + QDockWidget::close-button:pressed, QDockWidget::float-button:pressed { + background-color: #3067c0; + border: 1px solid rgba(48, 103, 192, 0.6); + } + QScrollArea { + background-color: #19191b; + border: none; + } + QWidget#ScriptContainer { + background-color: #19191b; + } + QLabel { + color: #ebebeb; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 10px; + font-weight: 300; + } + QGroupBox { + /* 减少上边距,使相邻组更紧凑 */ + margin-top: 0px; + + /* 移除边框 */ + border: none; + + /* 设置内边距,为标题和内容留出空间,同时与主标题左侧对齐 */ + padding-top: 20px; /* 为标题留出足够空间 */ + padding-left: 0px; + padding-right: 8px; /* 右内边距与主标题分隔线对齐 */ + + /* 减少下边距,使相邻组更紧凑 */ + background-color: transparent; + margin-bottom: 6px; + + /* 使用边框创建样式线,与主标题分隔线保持相同的8px右侧间距 */ + border-top: 1px solid rgba(77, 116, 189, 0.4); + margin-left: 0px; + margin-right: 8px; + } + + QGroupBox::title { + /* 将标题定位到控件内部,在样条线下方 */ + subcontrol-origin: padding; + subcontrol-position: top left; + + /* 标题样式,左边距为0因为已经通过padding-left处理了对齐 */ + padding: 8px 3px 8px 0px; /* 左边距设为0,与内容对齐 */ + margin-top: 0px; /* 标题紧贴内边距顶部 */ + + /* 移除标题上的边框 */ + border: none; + + /* 字体和颜色样式 */ + color: rgba(255, 255, 255, 0.8); + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 12px; + font-weight: 500; + } + QGroupBox[groupRole="first"] { + padding-top: 16px; + margin-top: 0px; + border-top: none; /* 明确移除顶部边框 */ + } + QGroupBox[groupRole="first"]::title { + padding: 6px 3px 8px 0px; + } + """) + + # 创建脚本面板的主容器和布局(与属性面板相同结构) + self.scriptContainer = QWidget() + # 设置脚本容器的背景色 + self.scriptContainer.setStyleSheet(""" + QWidget#ScriptContainer { + background-color: #19191b; + } + """) + self.scriptContainer.setObjectName("ScriptContainer") + self.scriptLayout = QVBoxLayout(self.scriptContainer) + # Match the dock title horizontal padding for consistent alignment + self.scriptLayout.setContentsMargins(10, 0, 10, 12) + self.scriptLayout.setSpacing(12) + + # 创建滚动区域并设置属性 + self.scriptScrollArea = QScrollArea() + self.scriptScrollArea.setWidgetResizable(True) + self.scriptScrollArea.setWidget(self.scriptContainer) + + # 创建包装容器,添加标题下方的分隔线 + scriptDockContainer = QWidget() + scriptDockContainer.setStyleSheet("QWidget { background-color: #19191b; }") + scriptDockLayout = QVBoxLayout(scriptDockContainer) + scriptDockLayout.setContentsMargins(0, 0, 0, 0) + scriptDockLayout.setSpacing(0) + + # 添加带左右间距的分隔线 + scriptSeparator = QFrame() + scriptSeparator.setFrameShape(QFrame.HLine) + scriptSeparator.setFrameShadow(QFrame.Plain) + scriptSeparator.setStyleSheet(""" + QFrame { + background-color: rgba(77, 116, 189, 0.4); + max-height: 1px; + margin-left: 8px; + margin-right: 8px; + border: none; + } + """) + scriptDockLayout.addWidget(scriptSeparator) + scriptDockLayout.addWidget(self.scriptScrollArea) + + # 设置包装容器为停靠窗口的主部件 + self.scriptDock.setWidget(scriptDockContainer) + self.scriptDock.setMinimumWidth(300) + + # 设置脚本面板内容 - 这里添加调用 + self.setupScriptPanel(self.scriptLayout) + self.addDockWidget(Qt.RightDockWidgetArea, self.scriptDock) + + + # 创建底部停靠窗口(资源窗口) + self.bottomDock = QDockWidget("资源", self) + self.bottomDock.setStyleSheet(""" + QDockWidget { + background-color: #19191b; + color: #ffffff; + border: none; + } + QDockWidget::title { + background-color: #19191b; + padding: 8px 10px; + border-bottom: none; + text-align: left; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 14px; + font-weight: 500; + } + QDockWidget::close-button { + background-color: rgba(89, 100, 113, 0.3); + border: 1px solid rgba(184, 211, 241, 0.2); + icon-size: 10px; + border-radius: 2px; + width: 16px; + height: 16px; + right: 6px; + top: 6px; + } + QDockWidget::float-button { + background-color: rgba(89, 100, 113, 0.3); + border: 1px solid rgba(184, 211, 241, 0.2); + icon-size: 10px; + border-radius: 2px; + width: 16px; + height: 16px; + right: 26px; + top: 6px; + } + QDockWidget::close-button:hover, QDockWidget::float-button:hover { + background-color: rgba(89, 100, 113, 0.5); + border: 1px solid rgba(184, 211, 241, 0.3); + } + QDockWidget::close-button:pressed, QDockWidget::float-button:pressed { + background-color: #3067c0; + border: 1px solid rgba(48, 103, 192, 0.6); + } + """) + + self.fileView = CustomAssetsTreeWidget(self.world) + # 为资源树添加样式 + self.fileView.setStyleSheet(""" + QTreeWidget { + background-color: #19191b; + color: #ebebeb; + border: none; + alternate-background-color: #19191b; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 10px; + font-weight: 400; + outline: none; + } + QTreeWidget::item { + padding: 3px 0px; + border: none; + } + QTreeWidget::item:hover { + background-color: rgba(89, 100, 113, 0.3); + } + QTreeWidget::item:selected { + background-color: rgba(48, 103, 192, 0.4); + color: #ffffff; + } + QTreeWidget::branch { + background-color: #19191b; + } + QTreeWidget::branch:has-children:!has-siblings:closed, + QTreeWidget::branch:closed:has-children:has-siblings { + image: url(icons/solid_right_arrows.png); + width: 12px; + height: 12px; + } + QTreeWidget::branch:open:has-children:!has-siblings, + QTreeWidget::branch:open:has-children:has-siblings { + image: url(icons/solid_down_arrows.png); + width: 12px; + height: 12px; + } + QHeaderView::section { + background-color: #19191b; + color: #ebebeb; + border: none; + border-bottom: 1px solid #4c5c6e; + padding: 6px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 12px; + font-weight: 500; + } + QGroupBox[groupRole="first"] { + border-top: none; + margin-top: 0px; + padding-top: 8px; + } + QGroupBox[groupRole="first"]::title { + padding: 8px 5px 8px 0px; + } + """) + + # 创建包装容器,添加标题下方的分隔线 + bottomDockContainer = QWidget() + bottomDockContainer.setStyleSheet("QWidget { background-color: #19191b; }") + bottomDockLayout = QVBoxLayout(bottomDockContainer) + bottomDockLayout.setContentsMargins(0, 0, 0, 0) + bottomDockLayout.setSpacing(0) + + # 添加带左右间距的分隔线 + bottomSeparator = QFrame() + bottomSeparator.setFrameShape(QFrame.HLine) + bottomSeparator.setFrameShadow(QFrame.Plain) + bottomSeparator.setStyleSheet(""" + QFrame { + background-color: rgba(77, 116, 189, 0.4); + max-height: 1px; + margin-left: 8px; + margin-right: 8px; + border: none; + } + """) + bottomDockLayout.addWidget(bottomSeparator) + + # Wrap the resource view to add consistent left padding + resourceContentWrapper = QWidget() + resourceContentLayout = QVBoxLayout(resourceContentWrapper) + resourceContentLayout.setContentsMargins(8, 0, 0, 0) + resourceContentLayout.setSpacing(0) + resourceContentLayout.addWidget(self.fileView) + bottomDockLayout.addWidget(resourceContentWrapper) + + self.bottomDock.setWidget(bottomDockContainer) + self.addDockWidget(Qt.BottomDockWidgetArea, self.bottomDock) + + # 创建底部停靠控制台 + self.consoleDock = QDockWidget("控制台", self) + self.consoleDock.setStyleSheet(""" + QDockWidget { + background-color: #19191b; + color: #ffffff; + border: none; + } + QDockWidget::title { + background-color: #19191b; + padding: 8px 10px; + border-bottom: none; + text-align: left; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 14px; + font-weight: 500; + } + QDockWidget::close-button { + background-color: rgba(89, 100, 113, 0.3); + border: 1px solid rgba(184, 211, 241, 0.2); + icon-size: 10px; + border-radius: 2px; + width: 16px; + height: 16px; + right: 6px; + top: 6px; + } + QDockWidget::float-button { + background-color: rgba(89, 100, 113, 0.3); + border: 1px solid rgba(184, 211, 241, 0.2); + icon-size: 10px; + border-radius: 2px; + width: 16px; + height: 16px; + right: 26px; + top: 6px; + } + QDockWidget::close-button:hover, QDockWidget::float-button:hover { + background-color: rgba(89, 100, 113, 0.5); + border: 1px solid rgba(184, 211, 241, 0.3); + } + QDockWidget::close-button:pressed, QDockWidget::float-button:pressed { + background-color: #3067c0; + border: 1px solid rgba(48, 103, 192, 0.6); + } + """) + self.consoleView = CustomConsoleDockWidget(self.world) + # 为控制台添加样式 + self.consoleView.setStyleSheet(""" + QTextEdit { + background-color: #19191b; + color: #ebebeb; + border: none; + font-family: 'Consolas', 'Monaco', 'Microsoft YaHei', monospace; + font-size: 10px; + font-weight: 300; + } + """) + + # 创建包装容器,添加标题下方的分隔线 + consoleDockContainer = QWidget() + consoleDockContainer.setStyleSheet("QWidget { background-color: #19191b; }") + consoleDockLayout = QVBoxLayout(consoleDockContainer) + consoleDockLayout.setContentsMargins(0, 0, 0, 0) + consoleDockLayout.setSpacing(0) + + # 添加带左右间距的分隔线 + consoleSeparator = QFrame() + consoleSeparator.setFrameShape(QFrame.HLine) + consoleSeparator.setFrameShadow(QFrame.Plain) + consoleSeparator.setStyleSheet(""" + QFrame { + background-color: rgba(77, 116, 189, 0.4); + max-height: 1px; + margin-left: 8px; + margin-right: 8px; + border: none; + } + """) + consoleDockLayout.addWidget(consoleSeparator) + consoleDockLayout.addWidget(self.consoleView) + + self.consoleDock.setWidget(consoleDockContainer) + self.addDockWidget(Qt.BottomDockWidgetArea, self.consoleDock) + + # 配置Dock区域的角落归属 + # 使右侧和下侧的Dock面板可以占据右下角区域 + # setCorner(角落位置, 归属的Dock区域) + self.setCorner(Qt.TopRightCorner, Qt.RightDockWidgetArea) + self.setCorner(Qt.BottomRightCorner, Qt.RightDockWidgetArea) + # self.setCorner(Qt.TopLeftCorner, Qt.LeftDockWidgetArea) + # self.setCorner(Qt.BottomLeftCorner, Qt.LeftDockWidgetArea) + + # 设置Dock标签栏位置为上方(North) + self.setTabPosition(Qt.RightDockWidgetArea, QTabWidget.North) + self.setTabPosition(Qt.LeftDockWidgetArea, QTabWidget.North) + self.setTabPosition(Qt.TopDockWidgetArea, QTabWidget.North) + self.setTabPosition(Qt.BottomDockWidgetArea, QTabWidget.North) + + # 将右侧的属性面板和脚本管理面板合并为标签页 + # self.tabifyDockWidget(self.rightDock, self.scriptDock) + + # 设定默认显示的标签(属性面板在前) + self.rightDock.raise_() + + # 确保其他面板也正常显示 + self.bottomDock.raise_() + self.consoleDock.raise_() + self.leftDock.raise_() + # ========================================================================= + # ↓↓↓ 为停靠窗口的标签栏(QTabBar)设置统一样式(根据Figma设计)↓↓↓ + # ========================================================================= + # 这段样式会应用到主窗口内的所有 QTabBar,特别是停靠区域的标签栏。 + tab_bar_style = """ + /* QTabBar 的整体样式 */ + QTabBar { + qproperty-drawBase: 0; + qproperty-expanding: 0; + background-color: #19191b; + border: none; + } + + /* 标签的基础样式 - 未选中状态 */ + QTabBar::tab { + background-color: #394560; + color: rgba(255, 255, 255, 0.7); + border: none; + border-radius: 2px; + padding: 7px 16px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-weight: 500; + font-size: 12px; + margin-right: 4px; + min-height: 20px; + max-height: 20px; + min-width: 80px; /* 增加一个最小宽度,根据您的标签文字长度调整 */ + } + + /* 鼠标悬停在标签上时的样式 */ + QTabBar::tab:hover { + background-color: #4a5568; + } + + /* 当前选中的标签页的样式 */ + QTabBar::tab:selected { + background-color: #3067c0; + color: #ffffff; + font-weight: 500; + } + + /* 未选中的标签 */ + QTabBar::tab:!selected { + margin-top: 0px; + } + + /* 标签栏的对齐方式 */ + QTabBar::tab-bar { + alignment: left; + } + + /* 标签栏的关闭按钮(如果有) */ + QTabBar::close-button { + image: none; + subcontrol-position: right; + } + + /* 移除标签栏的滚动按钮样式冲突 */ + QTabBar::scroller { + width: 0px; + } + """ + + # 获取主窗口现有的样式表,并附加我们新的样式规则 + # 这样可以避免覆盖掉其他可能存在的全局样式 + existing_style = self.styleSheet() + self.setStyleSheet(existing_style + tab_bar_style) + + def setupToolbar(self): + """创建工具栏""" + self.toolbar = self.addToolBar('工具栏') + + # 创建工具按钮组 + self.toolGroup = QButtonGroup() + + # 选择工具 + self.selectTool = QToolButton() + self.selectTool.setText("选择") + self.selectTool.setCheckable(True) + self.toolGroup.addButton(self.selectTool) + self.toolbar.addWidget(self.selectTool) + + # 旋转工具 + self.rotateTool = QToolButton() + self.rotateTool.setText("旋转") + self.rotateTool.setCheckable(True) + self.toolGroup.addButton(self.rotateTool) + self.toolbar.addWidget(self.rotateTool) + + # 缩放工具 + self.scaleTool = QToolButton() + self.scaleTool.setText("缩放") + self.scaleTool.setCheckable(True) + self.toolGroup.addButton(self.scaleTool) + self.toolbar.addWidget(self.scaleTool) + + # 添加分隔符 + self.toolbar.addSeparator() + + # GUI创建工具 + self.createButtonTool = QToolButton() + self.createButtonTool.setText("创建按钮") + self.toolbar.addWidget(self.createButtonTool) + + self.createLabelTool = QToolButton() + self.createLabelTool.setText("创建标签") + self.toolbar.addWidget(self.createLabelTool) + + self.create3DTextTool = QToolButton() + self.create3DTextTool.setText("3D文本") + self.toolbar.addWidget(self.create3DTextTool) + + self.create3DImageTool = QToolButton() + self.create3DImageTool.setText("3D图片") + self.toolbar.addWidget(self.create3DImageTool) + + self.create2DImageTool = QToolButton() + self.create2DImageTool.setText("2D图片") + self.toolbar.addWidget(self.create2DImageTool) + + self.createSpotLight = QToolButton() + self.createSpotLight.setText("聚光灯") + self.toolbar.addWidget(self.createSpotLight) + + self.createPointLight = QToolButton() + self.createPointLight.setText("点光灯") + self.toolbar.addWidget(self.createPointLight) + + # # Cesium 工具按钮 + # self.cesiumViewTool = QToolButton() + # self.cesiumViewTool.setText("地图视图") + # self.cesiumViewTool.clicked.connect(self.onCreateCesiumView) + # self.toolbar.addWidget(self.cesiumViewTool) + # + # self.refreshCesiumTool = QToolButton() + # self.refreshCesiumTool.setText("刷新地图") + # self.refreshCesiumTool.clicked.connect(self.onRefreshCesiumView) + # self.toolbar.addWidget(self.refreshCesiumTool) + # + # self.addModelTool = QToolButton() + # self.addModelTool.setText("添加模型") + # self.addModelTool.clicked.connect(self.onAddModelClicked) + # self.toolbar.addWidget(self.addModelTool) + + #地形 + # 地形工具 + self.createFlatTerrainTool = QToolButton() + self.createFlatTerrainTool.setText("平面地形") + self.toolbar.addWidget(self.createFlatTerrainTool) + + self.createHeightmapTerrainTool = QToolButton() + self.createHeightmapTerrainTool.setText("高度图地形") + self.toolbar.addWidget(self.createHeightmapTerrainTool) + + # 默认选择"选择"工具 + self.selectTool.setChecked(True) + self.world.setCurrentTool("选择") + + def setupScriptPanel(self, layout): + """创建脚本管理面板""" + # 脚本状态组 + statusGroup = QGroupBox("脚本系统状态") + + statusGroup.setProperty("groupRole", "first") + statusLayout = QVBoxLayout() + + # 脚本系统状态行 + scriptSystemLayout = QHBoxLayout() + scriptSystemLabel = QLabel("脚本系统:") + scriptSystemLabel.setStyleSheet(""" + QLabel { + color: #ebebeb; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 10px; + font-weight: 300; + letter-spacing: 0.5px; + } + """) + scriptSystemLayout.addWidget(scriptSystemLabel) + + self.scriptStatusLabel = QLabel("已启动") + self.scriptStatusLabel.setStyleSheet(""" + QLabel { + background-color: rgba(45, 255, 196, 0.17); + border: 1px solid #2dffc4; + color: #2dffc4; + border-radius: 2px; + padding: 2px 8px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 10px; + font-weight: 300; + letter-spacing: 0.5px; + } + """) + scriptSystemLayout.addWidget(self.scriptStatusLabel) + scriptSystemLayout.addStretch() + statusLayout.addLayout(scriptSystemLayout) + + # 热重载状态行 + hotReloadLayout = QHBoxLayout() + hotReloadLabel = QLabel("热重载:") + hotReloadLabel.setStyleSheet(""" + QLabel { + color: #ebebeb; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 10px; + font-weight: 300; + letter-spacing: 0.5px; + } + """) + hotReloadLayout.addWidget(hotReloadLabel) + + self.hotReloadLabel = QLabel("已启用") + self.hotReloadLabel.setStyleSheet(""" + QLabel { + background-color: rgba(45, 136, 255, 0.17); + border: 1px solid #289eff; + color: #289eff; + border-radius: 2px; + padding: 2px 8px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 10px; + font-weight: 300; + letter-spacing: 0.5px; + } + """) + hotReloadLayout.addWidget(self.hotReloadLabel) + hotReloadLayout.addStretch() + statusLayout.addLayout(hotReloadLayout) + + statusGroup.setLayout(statusLayout) + layout.addWidget(statusGroup) + + # 脚本创建组 + createGroup = QGroupBox("创建脚本") + createLayout = QVBoxLayout() + + # 脚本名称输入 + nameLayout = QHBoxLayout() + nameLabel = QLabel("脚本名称:") + nameLabel.setStyleSheet(""" + QLabel { + color: #ebebeb; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 10px; + font-weight: 300; + letter-spacing: 0.5px; + min-width: 60px; + } + """) + nameLayout.addWidget(nameLabel) + + self.scriptNameEdit = QLineEdit() + self.scriptNameEdit.setPlaceholderText("输入脚本名称...") + self.scriptNameEdit.setStyleSheet(""" + QLineEdit { + background-color: rgba(89, 100, 113, 0.2); + color: #ebebeb; + border: 1px solid rgba(76, 92, 110, 0.6); + border-radius: 2px; + padding: 4px 8px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 10px; + font-weight: 300; + letter-spacing: 0.5px; + min-height: 18px; + } + QLineEdit::placeholder { + color: rgba(235, 235, 235, 0.7); + } + QLineEdit:focus { + border: 1px solid #289eff; + } + """) + nameLayout.addWidget(self.scriptNameEdit) + createLayout.addLayout(nameLayout) + + # 模板选择 + templateLayout = QHBoxLayout() + templateLabel = QLabel("模板:") + templateLabel.setStyleSheet(""" + QLabel { + color: #ebebeb; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 10px; + font-weight: 300; + letter-spacing: 0.5px; + min-width: 60px; + } + """) + templateLayout.addWidget(templateLabel) + + self.templateCombo = QComboBox() + self.templateCombo.addItems(["basic", "movement", "animation"]) + self.templateCombo.setStyleSheet(""" + QComboBox { + background-color: rgba(89, 100, 113, 0.2); + color: #ebebeb; + border: 1px solid rgba(76, 92, 110, 0.6); + border-radius: 2px; + padding: 4px 8px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 10px; + font-weight: 300; + letter-spacing: 0.5px; + min-height: 18px; + } + QComboBox::drop-down { + border: none; + border-left: 1px solid rgba(76, 92, 110, 0.6); + background-color: rgba(89, 100, 113, 0.3); + width: 16px; + } + QComboBox::down-arrow { + image: url(icons/down_arrows.png); + width: 10px; + height: 10px; + } + QComboBox QAbstractItemView { + background-color: #596471; + color: #ebebeb; + selection-background-color: rgba(48, 103, 192, 0.4); + border: 1px solid rgba(76, 92, 110, 0.6); + } + """) + templateLayout.addWidget(self.templateCombo) + createLayout.addLayout(templateLayout) + + # 创建按钮 + self.createScriptBtn = QPushButton("创建脚本") + self.createScriptBtn.clicked.connect(self.onCreateScript) + self.createScriptBtn.setStyleSheet(""" + QPushButton { + background-color: rgba(89, 98, 118, 0.4); + color: #ebebeb; + border: none; + padding: 6px 12px; + border-radius: 2px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-weight: 300; + font-size: 10px; + letter-spacing: 0.5px; + min-height: 18px; + } + QPushButton:hover { + background-color: #3067c0; + color: #ffffff; + } + QPushButton:pressed { + background-color: #2556a0; + } + QPushButton:disabled { + background-color: #394560; + color: rgba(235, 235, 235, 0.5); + } + """) + createLayout.addWidget(self.createScriptBtn) + + createGroup.setLayout(createLayout) + layout.addWidget(createGroup) + + # 可用脚本组 + scriptsGroup = QGroupBox("可用脚本") + scriptsLayout = QVBoxLayout() + + # 脚本列表 + self.scriptsList = QListWidget() + self.scriptsList.setStyleSheet(""" + QListWidget { + background-color: rgba(89, 100, 113, 0.15); + color: #ebebeb; + border: 1px solid rgba(76, 92, 110, 0.6); + border-radius: 2px; + alternate-background-color: rgba(89, 100, 113, 0.15); + selection-background-color: rgba(48, 103, 192, 0.4); + selection-color: #ffffff; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 10px; + font-weight: 300; + letter-spacing: 0.5px; + outline: none; + min-height: 120px; + max-height: 120px; + } + QListWidget::item { + padding: 6px 8px; + border: none; + border-bottom: 1px solid rgba(76, 92, 110, 0.3); + } + QListWidget::item:last { + border-bottom: none; + } + QListWidget::item:hover { + background-color: rgba(89, 100, 113, 0.3); + } + QListWidget::item:selected { + background-color: rgba(48, 103, 192, 0.4); + color: #ffffff; + } + """) + self.scriptsList.itemDoubleClicked.connect(self.onScriptDoubleClick) + scriptsLayout.addWidget(self.scriptsList) + + # 脚本操作按钮 + scriptButtonsLayout = QHBoxLayout() + + self.loadScriptBtn = QPushButton("加载脚本") + self.loadScriptBtn.clicked.connect(self.onLoadScript) + self.loadScriptBtn.setStyleSheet(""" + QPushButton { + background-color: rgba(89, 98, 118, 0.4); + color: #ebebeb; + border: none; + padding: 6px 12px; + border-radius: 2px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-weight: 300; + font-size: 10px; + letter-spacing: 0.5px; + min-height: 18px; + } + QPushButton:hover { + background-color: #3067c0; + color: #ffffff; + } + QPushButton:pressed { + background-color: #2556a0; + } + """) + scriptButtonsLayout.addWidget(self.loadScriptBtn) + + self.reloadAllBtn = QPushButton("重载全部") + self.reloadAllBtn.clicked.connect(self.onReloadAllScripts) + self.reloadAllBtn.setStyleSheet(""" + QPushButton { + background-color: rgba(89, 98, 118, 0.4); + color: #ebebeb; + border: none; + padding: 6px 12px; + border-radius: 2px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-weight: 300; + font-size: 10px; + letter-spacing: 0.5px; + min-height: 18px; + } + QPushButton:hover { + background-color: #3067c0; + color: #ffffff; + } + QPushButton:pressed { + background-color: #2556a0; + } + """) + scriptButtonsLayout.addWidget(self.reloadAllBtn) + + scriptsLayout.addLayout(scriptButtonsLayout) + scriptsGroup.setLayout(scriptsLayout) + layout.addWidget(scriptsGroup) + + # 脚本挂载组 + mountGroup = QGroupBox("脚本挂载") + mountLayout = QVBoxLayout() + + # 当前选中对象显示 + self.selectedObjectLabel = QLabel("未选择对象") + self.selectedObjectLabel.setStyleSheet(""" + QLabel { + color: #2dffc4; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 10px; + font-weight: 300; + letter-spacing: 0.5px; + padding: 2px 0px; + } + """) + mountLayout.addWidget(self.selectedObjectLabel) + + # 脚本选择和挂载 + mountControlLayout = QHBoxLayout() + + self.mountScriptCombo = QComboBox() + self.mountScriptCombo.setEnabled(False) + self.mountScriptCombo.setStyleSheet(""" + QComboBox { + background-color: rgba(89, 100, 113, 0.2); + color: #ebebeb; + border: 1px solid rgba(76, 92, 110, 0.6); + border-radius: 2px; + padding: 4px 8px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 10px; + font-weight: 300; + letter-spacing: 0.5px; + min-height: 18px; + } + QComboBox:disabled { + background-color: rgba(89, 100, 113, 0.1); + color: rgba(235, 235, 235, 0.5); + } + QComboBox::drop-down { + border: none; + border-left: 1px solid rgba(76, 92, 110, 0.6); + background-color: rgba(89, 100, 113, 0.3); + width: 16px; + } + QComboBox::down-arrow { + image: url(icons/down_arrows.png); + width: 10px; + height: 10px; + } + QComboBox QAbstractItemView { + background-color: #596471; + color: #ebebeb; + selection-background-color: rgba(48, 103, 192, 0.4); + border: 1px solid rgba(76, 92, 110, 0.6); + } + """) + mountControlLayout.addWidget(self.mountScriptCombo) + + self.mountBtn = QPushButton("挂载") + self.mountBtn.setEnabled(False) + self.mountBtn.clicked.connect(self.onMountScript) + self.mountBtn.setStyleSheet(""" + QPushButton { + background-color: rgba(89, 98, 118, 0.4); + color: #ebebeb; + border: none; + padding: 6px 12px; + border-radius: 2px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-weight: 300; + font-size: 10px; + letter-spacing: 0.5px; + min-height: 18px; + } + QPushButton:hover { + background-color: #3067c0; + color: #ffffff; + } + QPushButton:pressed { + background-color: #2556a0; + } + QPushButton:disabled { + background-color: #394560; + color: rgba(235, 235, 235, 0.5); + } + """) + mountControlLayout.addWidget(self.mountBtn) + + mountLayout.addLayout(mountControlLayout) + + # 已挂载脚本列表 + self.mountedScriptsList = QListWidget() + self.mountedScriptsList.setStyleSheet(""" + QListWidget { + background-color: rgba(89, 100, 113, 0.15); + color: #ebebeb; + border: 1px solid rgba(76, 92, 110, 0.6); + border-radius: 2px; + alternate-background-color: rgba(89, 100, 113, 0.15); + selection-background-color: rgba(48, 103, 192, 0.4); + selection-color: #ffffff; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 10px; + font-weight: 300; + letter-spacing: 0.5px; + outline: none; + min-height: 80px; + max-height: 80px; + } + QListWidget::item { + padding: 4px 8px; + border: none; + border-bottom: 1px solid rgba(76, 92, 110, 0.3); + } + QListWidget::item:last { + border-bottom: none; + } + QListWidget::item:hover { + background-color: rgba(89, 100, 113, 0.3); + } + QListWidget::item:selected { + background-color: rgba(48, 103, 192, 0.4); + color: #ffffff; + } + """) + self.mountedScriptsList.setMaximumHeight(100) + + mountedLabel = QLabel("已挂载脚本:") + mountedLabel.setStyleSheet(""" + QLabel { + color: #ebebeb; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 10px; + font-weight: 300; + letter-spacing: 0.5px; + padding: 2px 0px; + } + """) + mountLayout.addWidget(mountedLabel) + mountLayout.addWidget(self.mountedScriptsList) + + # 卸载按钮 + self.unmountBtn = QPushButton("卸载选中脚本") + self.unmountBtn.clicked.connect(self.onUnmountScript) + self.unmountBtn.setStyleSheet(""" + QPushButton { + background-color: rgba(89, 98, 118, 0.4); + color: #ebebeb; + border: none; + padding: 6px 12px; + border-radius: 2px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-weight: 300; + font-size: 10px; + letter-spacing: 0.5px; + min-height: 18px; + } + QPushButton:hover { + background-color: #3067c0; + color: #ffffff; + } + QPushButton:pressed { + background-color: #2556a0; + } + """) + mountLayout.addWidget(self.unmountBtn) + + mountGroup.setLayout(mountLayout) + layout.addWidget(mountGroup) + + # 添加拉伸以填充剩余空间 + layout.addStretch() + + # 初始化脚本列表 + self.refreshScriptsList() + + def connectEvents(self): + """连接事件信号""" + # 导入项目管理功能函数 + from main import createNewProject, saveProject, openProject, buildPackage + + # 连接文件菜单事件 + self.newAction.triggered.connect(lambda: createNewProject(self)) + self.openAction.triggered.connect(lambda: openProject(self)) + self.saveAction.triggered.connect(lambda: saveProject(self)) + self.buildAction.triggered.connect(lambda: buildPackage(self)) + self.exitAction.triggered.connect(QApplication.instance().quit) + + #添加保存项目快捷键盘 + self.saveAction.setShortcut(QKeySequence.Save) + + # 连接工具事件 + self.sunsetAction.triggered.connect(lambda: self.world.setCurrentTool("光照编辑")) + self.pluginAction.triggered.connect(lambda: self.world.setCurrentTool("图形编辑")) + + # 连接GUI编辑模式事件 + #self.guiEditModeAction.triggered.connect(lambda: self.world.toggleGUIEditMode()) + + # 连接创建事件 - 使用菜单动作而不是不存在的工具栏按钮 + # self.createSpotLightAction.triggered.connect(lambda: self.world.createSpotLight()) + # self.createPointLightAction.triggered.connect(lambda: self.world.createPointLight()) + # self.createButtonAction.triggered.connect(lambda: self.world.createGUIButton()) + # self.createLabelAction.triggered.connect(lambda: self.world.createGUILabel()) + # self.createEntryAction.triggered.connect(lambda: self.world.createGUIEntry()) + # self.create3DTextAction.triggered.connect(lambda: self.world.createGUI3DText()) + # self.createVirtualScreenAction.triggered.connect(lambda: self.world.createGUIVirtualScreen()) + # self.createCesiumViewAction.triggered.connect(self.onCreateCesiumView) + # self.toggleCesiumViewAction.triggered.connect(self.onToggleCesiumView) + # self.refreshCesiumViewAction.triggered.connect(self.onRefreshCesiumView) + + # 连接地形创建事件 + self.createFlatTerrainAction.triggered.connect(self.onCreateFlatTerrain) + self.createHeightmapTerrainAction.triggered.connect(self.onCreateHeightmapTerrain) + # self.terrainEditModeAction.triggered.connect(self.onTerrainEditMode) + + # 连接树节点点击信号 + self.treeWidget.itemSelectionChanged.connect( + lambda: self.world.onTreeItemClicked(self.treeWidget.currentItem(), 0)) + print("已连接点击信号") + + self.undoAction.triggered.connect(self.onUndo) + self.redoAction.triggered.connect(self.onRedo) + self.cutAction.triggered.connect(self.onCut) + self.copyAction.triggered.connect(self.onCopy) + self.pasteAction.triggered.connect(self.onPaste) + + self.undoAction.setShortcut(QKeySequence.Undo) + self.redoAction.setShortcut(QKeySequence.Redo) + self.cutAction.setShortcut(QKeySequence.Cut) + self.copyAction.setShortcut(QKeySequence.Copy) + self.pasteAction.setShortcut(QKeySequence.Paste) + + #连接视图菜单事件 + #self.setupViewMenuActions() + + # 连接工具切换信号 + #self.toolGroup.buttonClicked.connect(self.onToolChanged) + + # 连接脚本菜单事件 + # self.createScriptAction.triggered.connect(self.onCreateScriptDialog) + # self.loadScriptAction.triggered.connect(self.onLoadScriptFile) + # self.loadAllScriptsAction.triggered.connect(self.onReloadAllScripts) + # self.toggleHotReloadAction.triggered.connect(self.onToggleHotReload) + # self.openScriptsManagerAction.triggered.connect(self.onOpenScriptsManager) + + # 连接VR菜单事件 + self.enterVRAction.triggered.connect(self.onEnterVR) + self.exitVRAction.triggered.connect(self.onExitVR) + self.vrStatusAction.triggered.connect(self.onShowVRStatus) + self.vrSettingsAction.triggered.connect(self.onShowVRSettings) + + # 连接VR调试菜单事件 + self.vrDebugToggleAction.triggered.connect(self.onToggleVRDebug) + self.vrShowPerformanceAction.triggered.connect(self.onShowVRPerformance) + self.vrDebugBriefAction.triggered.connect(lambda: self.onSetVRDebugMode('brief')) + self.vrDebugDetailedAction.triggered.connect(lambda: self.onSetVRDebugMode('detailed')) + self.vrPerformanceMonitorAction.triggered.connect(self.onToggleVRPerformanceMonitor) + self.vrGpuTimingAction.triggered.connect(self.onToggleVRGpuTiming) + self.vrPipelineMonitorAction.triggered.connect(self.onToggleVRPipelineMonitor) + self.vrPoseRenderCallbackAction.triggered.connect(lambda: self.onSetVRPoseStrategy('render_callback')) + self.vrPoseUpdateTaskAction.triggered.connect(lambda: self.onSetVRPoseStrategy('update_task')) + self.vrTestPipelineAction.triggered.connect(self.onTestVRPipeline) + self.vrTestModeAction.triggered.connect(self.onToggleVRTestMode) + + # 连接VR测试模式调试菜单事件 + self.vrTestSubmitTextureAction.triggered.connect(self.onToggleVRTestSubmitTexture) + self.vrTestWaitPosesAction.triggered.connect(self.onToggleVRTestWaitPoses) + self.vrTestStep1Action.triggered.connect(lambda: self.onSetVRTestStep(1)) + self.vrTestStep2Action.triggered.connect(lambda: self.onSetVRTestStep(2)) + self.vrTestStep3Action.triggered.connect(lambda: self.onSetVRTestStep(3)) + self.vrTestResetAction.triggered.connect(lambda: self.onSetVRTestStep(0)) + + self.vrDebugSettingsAction.triggered.connect(self.onShowVRDebugSettings) + + def onCopy(self): + """复制操作""" + try: + selected_item = self.treeWidget.currentItem() + if not selected_item: + UniversalMessageDialog.show_warning( + self, + "提示", + "请先选择要复制的节点", + show_cancel=False, + confirm_text="确认" + ) + return + + # 获取选中的节点 + selected_node = getattr(selected_item, 'node_path', None) + if not selected_node: + selected_node = getattr(selected_item, 'node', None) + + if not selected_node and hasattr(self.world, 'selection'): + selected_node = getattr(self.world.selection, 'selectedNode', None) + + if not selected_node or selected_node.isEmpty(): + UniversalMessageDialog.show_warning( + self, + "错误", + "无法获取选中节点", + show_cancel=False, + confirm_text="确认" + ) + return + + # 检查是否是根节点 + if selected_node.getName() == "render": + UniversalMessageDialog.show_warning( + self, + "错误", + "不能复制根节点", + show_cancel=False, + confirm_text="确认" + ) + return + + # 序列化节点数据 + node_data = self.world.scene_manager.serializeNodeForCopy(selected_node) + if not node_data: + UniversalMessageDialog.show_warning( + self, + "错误", + "无法序列化选中节点", + show_cancel=False, + confirm_text="确认" + ) + return + + # 存储到剪切板 + self.clipboard = [node_data] + self.clipboard_mode = "copy" + + UniversalMessageDialog.show_success( + self, + "成功", + "节点已复制到剪切板", + show_cancel=False, + confirm_text="确认" + ) + + except Exception as e: + UniversalMessageDialog.show_error( + self, + "错误", + f"复制操作失败: {str(e)}", + show_cancel=False, + confirm_text="确认" + ) + + def onCut(self): + """剪切操作""" + try: + selected_item = self.treeWidget.currentItem() + if not selected_item: + UniversalMessageDialog.show_warning( + self, + "提示", + "请先选择要剪切的节点", + show_cancel=False, + confirm_text="确认" + ) + return + + # 获取选中的节点 + selected_node = getattr(selected_item, 'node_path', None) + if not selected_node: + selected_node = getattr(selected_item, 'node', None) + + if not selected_node and hasattr(self.world, 'selection'): + selected_node = getattr(self.world.selection, 'selectedNode', None) + + if not selected_node or selected_node.isEmpty(): + UniversalMessageDialog.show_warning( + self, + "错误", + "无法获取选中节点", + show_cancel=False, + confirm_text="确认" + ) + return + + # 检查是否是根节点或特殊节点 + if selected_node.getName() in ["render", "camera", "ambientLight", "directionalLight"]: + UniversalMessageDialog.show_warning( + self, + "错误", + "不能剪切根节点或系统节点", + show_cancel=False, + confirm_text="确认" + ) + return + + # 序列化节点数据 + node_data = self.world.scene_manager.serializeNodeForCopy(selected_node) + if not node_data: + UniversalMessageDialog.show_warning( + self, + "错误", + "无法序列化选中节点", + show_cancel=False, + confirm_text="确认" + ) + return + + # 存储到剪切板 + self.clipboard = [node_data] + self.clipboard_mode = "cut" + + # 删除原节点 + self.treeWidget.delete_items([selected_item]) + + UniversalMessageDialog.show_success( + self, + "成功", + "节点已剪切到剪切板", + show_cancel=False, + confirm_text="确认" + ) + + except Exception as e: + UniversalMessageDialog.show_error( + self, + "错误", + f"剪切操作失败: {str(e)}", + show_cancel=False, + confirm_text="确认" + ) + + def onPaste(self): + """粘贴操作""" + try: + if not self.clipboard: + UniversalMessageDialog.show_warning( + self, + "提示", + "剪切板为空", + show_cancel=False, + confirm_text="确认" + ) + return + + # 获取粘贴目标节点 + parent_item = self.treeWidget.currentItem() + parent_node = None + + # 如果选中了节点,将其作为父节点 + if parent_item: + parent_node = getattr(parent_item, 'node_path', None) + if not parent_node: + parent_node = getattr(parent_item, 'node', None) + + # 确保获取到有效的父节点 + if parent_node and not parent_node.isEmpty(): + print(f"将粘贴到选中的节点: {parent_node.getName()}") + else: + parent_node = None + + # 如果没有选中有效节点,默认粘贴到render节点下 + if not parent_node: + print("未选中有效节点,将粘贴到根节点下") + # 查找render节点 + for i in range(self.treeWidget.topLevelItemCount()): + item = self.treeWidget.topLevelItem(i) + if item.text(0) == "render": + parent_item = item + break + + # 如果找到了render节点项,获取对应的节点 + if parent_item: + parent_node = getattr(parent_item, 'node_path', None) + if not parent_node: + parent_node = getattr(parent_item, 'node', None) + + # 如果仍然没有找到父节点项,直接使用world.render + if not parent_node: + parent_node = self.world.render + + # 检查父节点有效性 + if not parent_node or parent_node.isEmpty(): + UniversalMessageDialog.show_warning( + self, + "错误", + "无法获取有效的父节点", + show_cancel=False, + confirm_text="确认" + ) + return + + # 检查目标节点是否为允许的父节点类型 + parent_name = parent_node.getName() + if parent_name in ["camera", "ambientLight", "directionalLight"]: + UniversalMessageDialog.show_warning( + self, + "错误", + "不能粘贴到该类型节点下", + show_cancel=False, + confirm_text="确认" + ) + return + + # 粘贴节点 + pasted_nodes = [] + for node_data in self.clipboard: + print(f"正在粘贴节点数据:{node_data.get('name','Unknown')}") + new_node = self.world.scene_manager.recreateNodeFromData(node_data, parent_node) + if new_node: + pasted_nodes.append(new_node) + print(f"成功粘贴节点: {new_node.getName()}") + else: + print(f"粘贴节点失败: {node_data.get('name', 'Unknown')}") + + # 如果是剪切操作,清空剪切板 + if self.clipboard_mode == "cut": + self.clipboard.clear() + self.clipboard_mode = None + + UniversalMessageDialog.show_success( + self, + "成功", + f"已粘贴 {len(pasted_nodes)} 个节点", + show_cancel=False, + confirm_text="确认" + ) + + except Exception as e: + UniversalMessageDialog.show_error( + self, + "错误", + f"粘贴操作失败: {str(e)}", + show_cancel=False, + confirm_text="确认" + ) + + def _serializeNode(self, node): + """序列化节点数据""" + try: + if not node or node.isEmpty(): + return None + + node_data = { + 'name': node.getName(), + 'type': type(node.node()).__name__, + 'pos': (node.getX(), node.getY(), node.getZ()), + 'hpr': (node.getH(), node.getP(), node.getR()), + 'scale': (node.getSx(), node.getSy(), node.getSz()), + 'tags': {}, + 'children': [] + } + + # 保存所有标签 + try: + # 使用更安全的方式获取标签 + if hasattr(node, 'getTagKeys'): + for tag_key in node.getTagKeys(): + node_data['tags'][tag_key] = node.getTag(tag_key) + except Exception as e: + print(f"获取标签时出错: {e}") + + # 递归序列化子节点(跳过辅助节点) + try: + if hasattr(node, 'getChildren'): + for child in node.getChildren(): + # 跳过辅助节点 + child_name = child.getName() if hasattr(child, 'getName') else "" + if not child_name.startswith(('gizmo', 'selectionBox', 'grid')): + child_data = self._serializeNode(child) + if child_data: + node_data['children'].append(child_data) + except Exception as e: + print(f"序列化子节点时出错: {e}") + + return node_data + + except Exception as e: + print(f"序列化节点失败: {e}") + return None + + def _deserializeNode(self, node_data, parent_node): + """反序列化节点数据""" + try: + if not node_data or not parent_node or parent_node.isEmpty(): + return None + + # 创建新节点 + node_name = node_data.get('name', 'node') + new_node = parent_node.attachNewNode(node_name) + + # 设置变换 + try: + pos = node_data.get('pos', (0, 0, 0)) + hpr = node_data.get('hpr', (0, 0, 0)) + scale = node_data.get('scale', (1, 1, 1)) + + new_node.setPos(*pos) + new_node.setHpr(*hpr) + new_node.setScale(*scale) + except Exception as e: + print(f"设置变换时出错: {e}") + + # 恢复标签 + try: + for tag_key, tag_value in node_data.get('tags', {}).items(): + new_node.setTag(tag_key, str(tag_value)) # 确保标签值是字符串 + except Exception as e: + print(f"恢复标签时出错: {e}") + + # 递归创建子节点 + try: + for child_data in node_data.get('children', []): + self._deserializeNode(child_data, new_node) + except Exception as e: + print(f"创建子节点时出错: {e}") + + return new_node + + except Exception as e: + print(f"反序列化节点失败: {e}") + return None + + def _deleteNode(self, node, tree_item): + """删除节点""" + try: + if not node or node.isEmpty(): + return + + # 特殊处理选中节点 + if hasattr(self.world, 'selection') and self.world.selection.selectedNode == node: + self.world.selection.clearSelection() + + # 从场景中删除节点 + node.removeNode() + + # 从树形控件中删除项目 + if tree_item: + try: + parent = tree_item.parent() + if parent: + parent.removeChild(tree_item) + else: + index = self.treeWidget.indexOfTopLevelItem(tree_item) + if index >= 0: + self.treeWidget.takeTopLevelItem(index) + except Exception as e: + print(f"从树形控件删除项目时出错: {e}") + + except Exception as e: + print(f"删除节点失败: {e}") + + # 添加撤销/重做功能的基础实现 + def onUndo(self): + """撤销操作""" + if hasattr(self.world,'command_manager'): + if self.world.command_manager.can_undo(): + success = self.world.command_manager.undo() + if success: + print("成功操作") + else: + print("撤销失败") + UniversalMessageDialog.show_warning( + self, + "提示", + "撤销操作失败", + show_cancel=False, + confirm_text="确认" + ) + else: + print("没有可撤销的操作") + UniversalMessageDialog.show_warning( + self, + "提示", + "没有可撤销的操作", + show_cancel=False, + confirm_text="确认" + ) + else: + print("命令管理器未初始化") + UniversalMessageDialog.show_warning( + self, + "提示", + "命令系统未初始化", + show_cancel=False, + confirm_text="确认" + ) + + def onRedo(self): + """重做操作""" + if hasattr(self.world,'command_manager'): + if self.world.command_manager.can_redo(): + success = self.world.command_manager.redo() + if success: + print("成功重做") + else: + print("重做失败") + UniversalMessageDialog.show_warning( + self, + "提示", + "重做操作失败", + show_cancel=False, + confirm_text="确认" + ) + else: + print("没有可重做的操作") + UniversalMessageDialog.show_warning( + self, + "提示", + "没有可重做的操作", + show_cancel=False, + confirm_text="确认" + ) + else: + print("命令管理器未初始化") + UniversalMessageDialog.show_warning( + self, + "提示", + "命令系统未初始化", + show_cancel=False, + confirm_text="确认" + ) + + def onCreateCesiumView(self): + if hasattr(self.world,'gui_manager') and self.world.gui_manager: + self.world.gui_manager.createCesiumView() + else: + UniversalMessageDialog.show_warning( + self, + "错误", + "GUI管理器不可用", + show_cancel=False, + confirm_text="确认" + ) + def onToggleCesiumView(self): + """切换 Cesium 视图显示状态""" + if hasattr(self.world, 'gui_manager') and self.world.gui_manager: + self.world.gui_manager.toggleCesiumView() + else: + UniversalMessageDialog.show_warning( + self, + "错误", + "GUI 管理器不可用", + show_cancel=False, + confirm_text="确认" + ) + def onRefreshCesiumView(self): + """刷新 Cesium 视图""" + if hasattr(self.world, 'gui_manager') and self.world.gui_manager: + self.world.gui_manager.refreshCesiumView() + else: + UniversalMessageDialog.show_warning( + self, + "错误", + "GUI 管理器不可用", + show_cancel=False, + confirm_text="确认" + ) + def onUpdateCesiumURL(self): + """更新 Cesium URL""" + dialog = self.createStyledInputDialog( + self, + "更新 Cesium URL", + "输入新的 URL:", + QLineEdit.Normal, + "http://localhost:8080/Apps/HelloWorld.html" + ) + if dialog.exec_() == QDialog.Accepted: + url = dialog.textValue() + if url: + if hasattr(self.world, 'gui_manager') and self.world.gui_manager: + self.world.gui_manager.updateCesiumURL(url) + else: + UniversalMessageDialog.show_warning( + self, + "错误", + "GUI 管理器不可用", + show_cancel=False, + confirm_text="确认" + ) + + + def onAddModelClicked(self): + """处理加入模型按钮点击事件""" + # 检查 Cesium 视图是否存在 + cesium_view_exists = False + if hasattr(self.world, 'gui_manager') and self.world.gui_manager: + for element in self.world.gui_manager.gui_elements: + if hasattr(element, 'objectName') and element.objectName() == "CesiumView": + cesium_view_exists = True + break + + if not cesium_view_exists: + result = UniversalMessageDialog.show_info( + self, + "提示", + "Cesium 地图视图尚未打开,是否先打开地图视图?", + show_cancel=True, + confirm_text="打开视图", + cancel_text="取消" + ) + + if result == QDialog.Accepted: + self.onCreateCesiumView() + # 给一点时间让 Cesium 视图加载 + QTimer.singleShot(1000, self.showAddModelDialog) + return + else: + return + + self.showAddModelDialog() + + def showAddModelDialog(self): + """显示添加模型对话框""" + # 打开文件选择对话框 + dialog = self.createStyledFileDialog( + self, + "选择 3D 模型文件", + "", + "3D 模型文件 (*.glb *.gltf *.obj);;所有文件 (*)" + ) + + if dialog.exec_() == QDialog.Accepted: + file_path = dialog.selectedFiles()[0] + if file_path: + # 获取模型位置信息 + coords, ok = self.getModelCoordinates() + if ok: + longitude, latitude, height, scale = coords + + # 生成唯一的模型 ID + import uuid + model_id = f"model_{uuid.uuid4().hex[:8]}" + + try: + # 添加模型到 Cesium + if hasattr(self.world, 'gui_manager') and self.world.gui_manager: + success = self.world.gui_manager.addModelToCesium( + model_id, + file_path, + longitude, + latitude, + height, + scale + ) + + if success: + UniversalMessageDialog.show_success( + self, + "成功", + f"模型已成功添加到地图!\n模型ID: {model_id}", + show_cancel=False, + confirm_text="确认" + ) + else: + UniversalMessageDialog.show_warning( + self, + "失败", + "添加模型失败,请检查控制台输出", + show_cancel=False, + confirm_text="确认" + ) + except Exception as e: + UniversalMessageDialog.show_error( + self, + "错误", + f"添加模型时发生错误:\n{str(e)}", + show_cancel=False, + confirm_text="确认" + ) + + + def getModelCoordinates(self): + """获取模型坐标信息的对话框""" + # 创建对话框 + dialog = QDialog(self) + dialog.setWindowTitle("设置模型位置") + dialog.setModal(True) + dialog.resize(300, 200) + + layout = QVBoxLayout(dialog) + + # 经度 + lon_layout = QHBoxLayout() + lon_layout.addWidget(QLabel("经度:")) + lon_spin = QDoubleSpinBox() + lon_spin.setRange(-180, 180) + lon_spin.setValue(116.3975) # 默认北京位置 + lon_layout.addWidget(lon_spin) + layout.addLayout(lon_layout) + + # 纬度 + lat_layout = QHBoxLayout() + lat_layout.addWidget(QLabel("纬度:")) + lat_spin = QDoubleSpinBox() + lat_spin.setRange(-90, 90) + lat_spin.setValue(39.9085) # 默认北京位置 + lat_layout.addWidget(lat_spin) + layout.addLayout(lat_layout) + + # 高度 + height_layout = QHBoxLayout() + height_layout.addWidget(QLabel("高度(米):")) + height_spin = QDoubleSpinBox() + height_spin.setRange(-10000, 100000) + height_spin.setValue(0) + height_layout.addWidget(height_spin) + layout.addLayout(height_layout) + + # 缩放 + scale_layout = QHBoxLayout() + scale_layout.addWidget(QLabel("缩放:")) + scale_spin = QDoubleSpinBox() + scale_spin.setRange(0.001, 100000) + scale_spin.setValue(1.0) + scale_spin.setSingleStep(0.1) + scale_layout.addWidget(scale_spin) + layout.addLayout(scale_layout) + + # 按钮 + button_layout = QHBoxLayout() + ok_button = QPushButton("确定") + cancel_button = QPushButton("取消") + button_layout.addWidget(ok_button) + button_layout.addWidget(cancel_button) + layout.addLayout(button_layout) + + # 连接信号 + ok_button.clicked.connect(dialog.accept) + cancel_button.clicked.connect(dialog.reject) + + # 显示对话框 + if dialog.exec_() == QDialog.Accepted: + return ( + lon_spin.value(), + lat_spin.value(), + height_spin.value(), + scale_spin.value() + ), True + else: + return None, False + + def onLoadCesiumTileset(self): + """加载 Cesium 3D Tiles""" + fields = [ + { + "name": "url", + "label": "Tileset URL", + "type": "text", + "value": "https://assets.ion.cesium.com/96128/tileset.json", + "placeholder": "输入 tileset.json 地址", + "label_width": 110 + }, + { + "name": "longitude", + "label": "经度(°)", + "type": "double", + "min": -180.0, + "max": 180.0, + "step": 0.1, + "decimals": 6, + "value": 0.0, + "label_width": 110 + }, + { + "name": "latitude", + "label": "纬度(°)", + "type": "double", + "min": -90.0, + "max": 90.0, + "step": 0.1, + "decimals": 6, + "value": 0.0, + "label_width": 110 + }, + { + "name": "height", + "label": "高度(米)", + "type": "double", + "min": -10000.0, + "max": 10000.0, + "step": 1.0, + "decimals": 2, + "value": 0.0, + "label_width": 110 + } + ] + + dialog = StyledTerrainDialog(self, "加载 Cesium 3D Tiles", fields, primary_text="加载", secondary_text="取消") + + if dialog.exec_() == QDialog.Accepted: + url = dialog.get_value("url") + if not url or not url.strip(): + UniversalMessageDialog.show_warning( + self, + "提示", + "请输入有效的 tileset URL", + show_cancel=False, + confirm_text="确认" + ) + return + + longitude = dialog.get_value("longitude") + latitude = dialog.get_value("latitude") + height = dialog.get_value("height") + + try: + import uuid + tileset_name = f"tileset_{uuid.uuid4().hex[:8]}" + + if hasattr(self.world, 'addCesiumTileset'): + success = self.world.addCesiumTileset( + tileset_name, + url.strip(), + (longitude, latitude, height) + ) + if success: + UniversalMessageDialog.show_success( + self, + "成功", + f"Cesium 3D Tiles 已加载到场景中!\n名称: {tileset_name}", + show_cancel=False, + confirm_text="确认" + ) + else: + UniversalMessageDialog.show_warning( + self, + "失败", + "加载 Cesium 3D Tiles 失败", + show_cancel=False, + confirm_text="确认" + ) + except Exception as e: + UniversalMessageDialog.show_error( + self, + "错误", + f"加载 Cesium 3D Tiles 时发生错误:\n{str(e)}", + show_cancel=False, + confirm_text="确认" + ) + + def onToolChanged(self, button): + """工具切换事件处理""" + if button.isChecked(): + tool_name = button.text().strip() # 添加strip()去除空格 + if tool_name: # 确保工具名称不为空 + self.world.setCurrentTool(tool_name) + print(f"工具栏: 选择了 {tool_name} 工具") + else: + print("工具栏: 选择了空工具名称") + else: + self.world.setCurrentTool(None) + print("工具栏: 取消选择工具") + + def openWebBrowser_win(self): + """打开Web浏览器面板(使用独立进程避免与Panda3D冲突)""" + try: + from PyQt5.QtWidgets import QDockWidget, QWidget, QVBoxLayout + from PyQt5.QtCore import QProcess, Qt + import subprocess + import sys + + main_window = self.world.main_window + + # 尝试获取主窗口引用 + if main_window is None: + print("🔍 尝试获取主窗口引用...") + if hasattr(self.world, 'interface_manager'): + print(f" - interface_manager 存在: {self.world.interface_manager}") + if hasattr(self.world.interface_manager, 'main_window'): + main_window = self.world.interface_manager.main_window + print(f" - interface_manager.main_window: {main_window}") + + if main_window is None and hasattr(self.world, 'main_window'): + main_window = self.world.main_window + print(f" - world.main_window: {main_window}") + + if main_window is None and self.world.treeWidget: + try: + main_window = self.world.treeWidget.window() + print(f" - 从 treeWidget 获取窗口: {main_window}") + except: + pass + + if main_window is None: + print("✗ 无法获取主窗口引用") + UniversalMessageDialog.show_warning( + self, "错误", "无法获取主窗口引用", False, "确定" + ) + return None + else: + print(f"✅ 使用传入的主窗口引用: {main_window}") + + # 检查主窗口是否有效 + if not hasattr(main_window, 'addDockWidget'): + print(f"✗ 主窗口引用无效,缺少 addDockWidget 方法") + return None + + # 检查是否已经存在浏览器视图 + for element in self.world.gui_elements: + if hasattr(element, 'objectName') and element.objectName() == "WebBrowserView": + print("⚠ 浏览器视图已经存在") + element.show() + element.raise_() + return element + + print(f"🔧 创建浏览器停靠窗口(独立进程模式),父窗口: {main_window}") + + # 创建停靠窗口 + browser_dock = QDockWidget("信息面板", main_window) + browser_dock.setObjectName("WebBrowserView") + + # 创建容器 Widget + container_widget = QWidget() + container_layout = QVBoxLayout(container_widget) + container_layout.setContentsMargins(0, 0, 0, 0) + + # 创建一个占位 Widget,用于嵌入外部进程窗口 + embed_widget = QWidget() + embed_widget.setMinimumSize(400, 300) + container_layout.addWidget(embed_widget) + + browser_dock.setWidget(container_widget) + + # 添加到主窗口 + print("📍 将浏览器视图添加到主窗口") + main_window.addDockWidget(Qt.RightDockWidgetArea, browser_dock) + + # 获取嵌入窗口的 ID + embed_window_id = int(embed_widget.winId()) + print(f"🔑 嵌入窗口 ID: {embed_window_id}") + + # 启动独立进程 + print("🚀 启动独立 WebEngine 进程...") + #web_process_script = os.path.join(os.path.dirname(__file__), "web_browser_process.py") + import tempfile + + temp_dir = tempfile.gettempdir() + web_process_script = os.path.join(temp_dir, "web_browser_process_temp.py") + + with open(web_process_script, "w", encoding="utf-8") as f: + f.write(WEB_BROWSER_PROCESS_CODE) + + # ----------------------------------------------------- + # 创建 QProcess(注意:必须在 start 之前) + # ----------------------------------------------------- + # ----------------------------------------------------- + # 创建 QProcess(注意:必须在 start 之前) + # ----------------------------------------------------- + process = QProcess(browser_dock) + browser_dock.web_process = process # 保存进程引用 + + # ----------------------------------------------------- + # 输出信号 + # ----------------------------------------------------- + def on_process_output(): + output = bytes(process.readAllStandardOutput()).decode('utf-8', errors='ignore') + if output.strip(): + print(f"[WebProcess] {output.strip()}") + + def on_process_error(): + error = bytes(process.readAllStandardError()).decode('utf-8', errors='ignore') + if error.strip(): + print(f"[WebProcess Error] {error.strip()}") + + def on_process_finished(exit_code, exit_status): + print(f"⚠️ WebEngine 进程退出: {exit_code} {exit_status}") + + process.readyReadStandardOutput.connect(on_process_output) + process.readyReadStandardError.connect(on_process_error) + process.finished.connect(on_process_finished) + + # ----------------------------------------------------- + # 写入临时脚本 + # ----------------------------------------------------- + import tempfile + temp_dir = tempfile.gettempdir() + web_process_script = os.path.join(temp_dir, "web_browser_process_temp.py") + + with open(web_process_script, "w", encoding="utf-8") as f: + f.write(WEB_BROWSER_PROCESS_CODE) + + # ----------------------------------------------------- + # 启动子进程(这里顺序不能错) + # ----------------------------------------------------- + python_exe = sys.executable + url = "https://baidu.com" # 更可控的测试站点 + + print(f"🚀 启动: {python_exe} {web_process_script} {url} {embed_window_id}") + process.start(python_exe, [web_process_script, url, str(embed_window_id)]) + + if not process.waitForStarted(3000): + print("✗ 启动 WebEngine 失败") + return None + + print("✓ WebEngine 进程已启动") + + # 添加到GUI元素列表以便管理 + self.world.gui_elements.append(browser_dock) + + # 添加清理函数 + def cleanup_process(): + if hasattr(browser_dock, 'web_process') and browser_dock.web_process: + print("🧹 清理 WebEngine 进程...") + browser_dock.web_process.terminate() + if not browser_dock.web_process.waitForFinished(2000): + browser_dock.web_process.kill() + + browser_dock.destroyed.connect(cleanup_process) + + print("✓ 网页浏览器视图已创建并集成到项目中(独立进程模式)") + return browser_dock + + except Exception as e: + print(f"✗ 创建浏览器视图失败: {str(e)}") + import traceback + traceback.print_exc() + UniversalMessageDialog.show_error( + self, "错误", + f"创建浏览器视图失败:\n{str(e)}", + False, "确定" + ) + return None + + def openWebBrowser(self): + if not WEB_ENGINE_AVAILABLE: + return None + try: + # from PyQt5.QtWebEngineWidgets import QWebEngineView + from PyQt5.QtWidgets import QDockWidget + from PyQt5.QtCore import QUrl + import os + + main_window = self.world.main_window + + # 尝试获取主窗口引用 + if main_window is None: + print("🔍 尝试获取主窗口引用...") + + # 检查各种可能的主窗口引用 + if hasattr(self.world, 'interface_manager'): + print(f" - interface_manager 存在: {self.world.interface_manager}") + if hasattr(self.world.interface_manager, 'main_window'): + main_window = self.world.interface_manager.main_window + print(f" - interface_manager.main_window: {main_window}") + + if main_window is None and hasattr(self.world, 'main_window'): + main_window = self.world.main_window + print(f" - world.main_window: {main_window}") + + # 如果仍然没有主窗口,尝试从树形控件获取 + if main_window is None and self.world.treeWidget: + try: + main_window = self.world.treeWidget.window() + print(f" - 从 treeWidget 获取窗口: {main_window}") + except: + pass + + if main_window is None: + print("✗ 无法获取主窗口引用") + return None + else: + print(f"✅ 使用传入的主窗口引用: {main_window}") + + # 检查主窗口是否有效 + if not hasattr(main_window, 'addDockWidget'): + print(f"✗ 主窗口引用无效,缺少 addDockWidget 方法") + return None + + # 检查是否已经存在浏览器视图 + for element in self.world.gui_elements: + if hasattr(element, 'objectName') and element.objectName() == "WebBrowserView": + print("⚠ 浏览器视图已经存在") + # 将其前置显示 + element.show() + element.raise_() + return element + + # 创建停靠窗口 + print(f"🔧 创建浏览器停靠窗口,父窗口: {main_window}") + browser_dock = QDockWidget("信息面板", main_window) + browser_dock.setObjectName("WebBrowserView") + + # 创建 Web 视图 + self.web_view = QWebEngineView() + + # 加载百度网页 + # print("🌐 加载百度网页: https://www.baidu.com") + # self.web_view.load(QUrl("https://www.bootstrapmb.com/item/15762/preview")) + self.web_view.load(QUrl("https://www.baidu.com")) + + # 设置内容 + browser_dock.setWidget(self.web_view) + + # 添加到主窗口 + print("📍 将浏览器视图添加到主窗口") + main_window.addDockWidget(Qt.RightDockWidgetArea, browser_dock) + + # 添加到GUI元素列表以便管理 + self.world.gui_elements.append(browser_dock) + + print("✓ 网页浏览器视图已创建并集成到项目中") + return browser_dock + + except Exception as e: + print(f"✗ 创建浏览器视图失败: {str(e)}") + import traceback + traceback.print_exc() + return None + + + def getSampleWeatherData(self): + """获取示例天气数据""" + try: + from datetime import datetime + import random + + # 模拟天气数据 + cities = ["北京", "上海", "广州", "深圳", "杭州", "成都", "武汉", "西安"] + conditions = ["晴天", "多云", "阴天", "小雨", "雷阵雨", "雪", "雾"] + + city = random.choice(cities) + condition = random.choice(conditions) + temp = random.randint(-5, 35) + humidity = random.randint(30, 90) + wind_speed = round(random.uniform(0, 20), 1) + pressure = random.randint(980, 1030) + + current_time = datetime.now().strftime("%Y-%m-%d %H:%M") + + return f"城市: {city}\n天气状况: {condition}\n温度: {temp}°C\n湿度: {humidity}%\n风速: {wind_speed} m/s\n气压: {pressure} hPa\n更新时间: {current_time}" + except Exception as e: + return f"获取示例数据失败: {str(e)}" + + def onCreate3DSampleInfoPanel(self): + """创建3D示例天气信息面板(修复透明度问题)""" + try: + # 获取中文字体 + from panda3d.core import TextNode + font = self.world.getChineseFont() if self.world.getChineseFont() else None + + # 创建面板 + info_manager = self.world.info_panel_manager + info_manager.setParent(self.world.render) + + # 使用唯一的面板ID + import time + unique_id = f"weather_info_3d_{int(time.time())}" + + # 创建3D示例面板 - 修复透明度问题 + weather_panel = info_manager.create3DInfoPanel( + panel_id=unique_id, + position=(2, 0, 2), # 调整Z坐标避免与其他对象重叠 + size=(5, 5), + bg_color=(0.15, 0.25, 0.35, 0.85), # 设置合适的透明度值 + border_color=(0.3, 0.5, 0.7, 1.0), + title_color=(0.7, 0.9, 1.0, 1.0), + content_color=(0.95, 0.95, 0.95, 1.0), + font=font + ) + weather_panel.setTag("name",unique_id) + + # 更新面板标题 + info_manager.update3DPanelContent(unique_id, title="3D北京天气") + + # 添加到场景树 + self.addInfoPanelToTree(weather_panel, "3D天气信息面板") + + # 显示加载中信息 + info_manager.update3DPanelContent(unique_id, content="正在获取天气数据...") + + info_manager.registerDataSource(unique_id, info_manager.getRealWeatherData, update_interval=5.0) + + print("✓ 3D示例天气信息面板已创建") + + except Exception as e: + print(f"✗ 创建3D示例天气信息面板失败: {e}") + import traceback + traceback.print_exc() + UniversalMessageDialog.show_error(self, "错误", f"创建3D示例天气信息面板时出错: {str(e)}") + + # 更新 addInfoPanelToTree 方法以支持3D面板 + def addInfoPanelToTree(self, panel, panel_name): + """ + 将信息面板添加到场景树控件中 + """ + if panel and self.treeWidget: + # 找到场景根节点 + scene_root = None + for i in range(self.treeWidget.topLevelItemCount()): + item = self.treeWidget.topLevelItem(i) + if item.text(0) == "render": + scene_root = item + break + + # 如果找不到场景根节点,使用第一个顶级节点 + if not scene_root and self.treeWidget.topLevelItemCount() > 0: + scene_root = self.treeWidget.topLevelItem(0) + + if scene_root: + # 根据面板类型确定节点类型 + node_type = "INFO_PANEL_3D" if "3d" in panel_name.lower() else "INFO_PANEL" + + tree_item = self.treeWidget.add_node_to_tree_widget( + node=panel, + parent_item=scene_root, + node_type=node_type + ) + if tree_item: + self.treeWidget.setCurrentItem(tree_item) + self.treeWidget.update_selection_and_properties(panel, tree_item) + print(f"✓ {panel_name}节点已添加到场景树") + return True + else: + print(f"⚠️ {panel_name}节点添加到场景树失败") + else: + print("❌ 未找到场景根节点") + return False + + def onCreateSystemStatusPanel(self): + """创建系统状态信息面板""" + try: + # 获取中文字体 + from panda3d.core import TextNode + font = self.world.getChineseFont() if self.world.getChineseFont() else None + + # 创建面板 + info_manager = self.world.info_panel_manager + info_manager.setParent(aspect2d) + + + panel = info_manager.createInfoPanel( + panel_id="system_status", + position=(1.4, 0.2), + size=(0.8, 1.2), + bg_color=(0.25, 0.15, 0.15, 0.95), # 红色背景 + border_color=(0.7, 0.3, 0.3, 1.0), # 红色边框 + title_color=(1.0, 0.5, 0.5, 1.0), # 浅红色标题 + content_color=(0.95, 0.95, 0.95, 1.0), + font=font + ) + + # 添加到场景树 + self.addInfoPanelToTree(panel, "系统状态信息面板") + + # 立即显示初始数据 + initial_data = self.getSystemStatusData() + info_manager.updatePanelContent("system_status", content=initial_data) + + # 注册数据源,每5秒更新一次 + info_manager.registerDataSource("system_status", self.getSystemStatusData, update_interval=5.0) + + except Exception as e: + print(f"✗ 创建系统状态信息面板失败: {e}") + import traceback + traceback.print_exc() + UniversalMessageDialog.show_error(self, "错误", f"创建系统状态信息面板时出错: {str(e)}") + + def getSystemStatusData(self): + """ + 获取系统状态数据的回调函数 + """ + try: + import psutil + import time + from datetime import datetime + + # 获取系统信息 + cpu_percent = psutil.cpu_percent(interval=0.1) + memory = psutil.virtual_memory() + memory_mb = round(memory.used / (1024 * 1024), 1) + memory_total_mb = round(memory.total / (1024 * 1024), 1) + memory_percent = memory.percent + + # 网络状态 + net_io = psutil.net_io_counters() + bytes_sent = round(net_io.bytes_sent / (1024 * 1024), 2) # MB + bytes_recv = round(net_io.bytes_recv / (1024 * 1024), 2) # MB + + # 磁盘使用情况 + disk = psutil.disk_usage('/') + disk_used_gb = round(disk.used / (1024 ** 3), 2) + disk_total_gb = round(disk.total / (1024 ** 3), 2) + disk_percent = round((disk.used / disk.total) * 100, 1) + + # 时间戳 + timestamp = datetime.now().strftime("%H:%M:%S") + + return f"CPU使用率: {cpu_percent}%\n内存使用: {memory_mb}MB / {memory_total_mb}MB ({memory_percent}%)\n磁盘使用: {disk_used_gb}GB / {disk_total_gb}GB ({disk_percent}%)\n网络发送: {bytes_sent}MB\n网络接收: {bytes_recv}MB\n更新时间: {timestamp}" + except Exception as e: + return f"获取系统状态失败: {str(e)}" + + def onCreateSensorDataPanel(self): + """创建传感器数据信息面板""" + try: + # 获取中文字体 + from panda3d.core import TextNode + font = self.world.getChineseFont() if self.world.getChineseFont() else None + + # 创建面板 + info_manager = self.world.info_panel_manager + info_manager.setParent(aspect2d) + + panel = info_manager.createInfoPanel( + panel_id="sensor_data", + position=(0.8, -0.2), + size=(0.8, 0.6), + bg_color=(0.15, 0.25, 0.15, 0.95), # 绿色背景 + border_color=(0.3, 0.7, 0.3, 1.0), # 绿色边框 + title_color=(0.5, 1.0, 0.5, 1.0), # 浅绿色标题 + content_color=(0.95, 0.95, 0.95, 1.0), + font=font + ) + + # 添加到场景树 + self.addInfoPanelToTree(panel, "传感器数据信息面板") + + # 立即显示初始数据 + initial_data = self.getSensorData() + info_manager.updatePanelContent("sensor_data", content=initial_data) + + # 注册数据源,每2秒更新一次 + info_manager.registerDataSource("sensor_data", self.getSensorData, update_interval=2.0) + + # 绑定键盘事件 + info_manager.accept("F3", info_manager.togglePanel, ["sensor_data"]) + + print("✓ 传感器数据信息面板已创建(按 F3 切换显示)") + + except Exception as e: + print(f"✗ 创建传感器数据信息面板失败: {e}") + import traceback + traceback.print_exc() + UniversalMessageDialog.show_error(self, "错误", f"创建传感器数据信息面板时出错: {str(e)}") + + def getSensorData(self): + """ + 获取传感器数据的回调函数(模拟数据) + """ + try: + import random + from datetime import datetime + + # 模拟传感器数据 + temperature = round(random.uniform(20, 35), 1) + humidity = round(random.uniform(30, 70), 1) + pressure = round(random.uniform(990, 1030), 1) + light_level = round(random.uniform(0, 1000), 1) + + # 时间戳 + timestamp = datetime.now().strftime("%H:%M:%S") + + return f"温度: {temperature}°C\n湿度: {humidity}%\n气压: {pressure} hPa\n光照: {light_level} lux\n更新时间: {timestamp}" + except Exception as e: + return f"获取传感器数据失败: {str(e)}" + + def onCreateSceneInfoPanel(self): + """创建场景信息面板""" + try: + # 获取中文字体 + from panda3d.core import TextNode + font = self.world.getChineseFont() if self.world.getChineseFont() else None + + # 创建面板 + info_manager = self.world.info_panel_manager + info_manager.setParent(aspect2d) + + panel = info_manager.createInfoPanel( + panel_id="scene_info", + position=(-0.8, 0.5), + size=(0.8, 0.6), + bg_color=(0.12, 0.12, 0.12, 0.95), # 深灰色背景 + border_color=(0.4, 0.4, 0.4, 1.0), # 灰色边框 + title_color=(0.2, 0.8, 1.0, 1.0), # 蓝色标题 + content_color=(0.9, 0.9, 0.9, 1.0), + font=font + ) + + # 添加到场景树 + self.addInfoPanelToTree(panel, "场景信息面板") + + # 立即显示初始数据 + initial_data = self.getSceneInfoData() + info_manager.updatePanelContent("scene_info", content=initial_data) + + # 注册数据源,每3秒更新一次 + info_manager.registerDataSource("scene_info", self.getSceneInfoData, update_interval=3.0) + + # 绑定键盘事件 + info_manager.accept("F4", info_manager.togglePanel, ["scene_info"]) + + print("✓ 场景信息面板已创建(按 F4 切换显示)") + + except Exception as e: + print(f"✗ 创建场景信息面板失败: {e}") + import traceback + traceback.print_exc() + UniversalMessageDialog.show_error(self, "错误", f"创建场景信息面板时出错: {str(e)}") + + def getSceneInfoData(self): + """ + 获取场景信息数据的回调函数 + """ + try: + # 获取场景信息 + node_count = 0 + texture_count = 0 + light_count = 0 + + # 如果有场景管理器,获取实际数据 + if hasattr(self.world, 'scene_graph'): + # 这里可以根据实际的场景结构来统计节点数 + node_count = len([node for node in self.world.scene_graph.nodes]) if hasattr(self.world.scene_graph, + 'nodes') else 0 + + # 统计光源数量 + if hasattr(self.world, 'lights'): + light_count = len(self.world.lights) + + # 统计纹理数量 + if hasattr(self.world, 'textures'): + texture_count = len(self.world.textures) + + # 当前时间 + from datetime import datetime + current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + return f"场景节点数: {node_count}\n纹理数量: {texture_count}\n光源数量: {light_count}\nFPS: {self.world.clock.getAverageFrameRate():.1f}\n更新时间: {current_time}" + except Exception as e: + return f"获取场景信息失败: {str(e)}" + + + # ==================== 脚本管理事件处理 ==================== + + def refreshScriptsList(self): + """刷新脚本列表""" + self.scriptsList.clear() + self.mountScriptCombo.clear() + + available_scripts = self.world.getAvailableScripts() + for script_name in available_scripts: + self.scriptsList.addItem(script_name) + self.mountScriptCombo.addItem(script_name) + + def updateScriptPanel(self): + """更新脚本面板状态""" + # 更新热重载状态 + hot_reload_enabled = self.world.script_manager.hot_reload_enabled + + # 更新热重载标签文本和样式 + if hot_reload_enabled: + self.hotReloadLabel.setText("已启用") + self.hotReloadLabel.setStyleSheet(""" + QLabel { + background-color: rgba(243, 157, 120, 0.15); + border: 1px solid #f39d78 ; + color: #f39d78 ; + border-radius: 2px; + padding: 2px 8px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 10px; + font-weight: 300; + letter-spacing: 0.5px; + } + """) + else: + self.hotReloadLabel.setText("已禁用") + self.hotReloadLabel.setStyleSheet(""" + QLabel { + background-color: rgba(45, 136, 255, 0.17) ; + border: 1px solid #289eff ; + color: #289eff ; + border-radius: 2px; + padding: 2px 8px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 10px; + font-weight: 300; + letter-spacing: 0.5px; + } + """) + + # 更新热重载菜单状态 + self.toggleHotReloadAction.setChecked(hot_reload_enabled) + + # 更新脚本系统状态(确保使用正确的样式) + self.scriptStatusLabel.setText("已启动") + self.scriptStatusLabel.setStyleSheet(""" + QLabel { + background-color: rgba(45, 255, 196, 0.17); + border: 1px solid #2dffc4; + color: #2dffc4; + border-radius: 2px; + padding: 2px 8px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 10px; + font-weight: 300; + letter-spacing: 0.5px; + } + """) + + # 更新选中对象信息 + selected_object = getattr(self.world.selection, 'selectedObject', None) + if selected_object: + self.selectedObjectLabel.setText(f"选中对象:{selected_object.getName()}") + self.selectedObjectLabel.setStyleSheet(""" + QLabel { + color: #2dffc4; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 10px; + font-weight: 300; + letter-spacing: 0.5px; + padding: 2px 0px; + } + """) + self.mountScriptCombo.setEnabled(True) + self.mountBtn.setEnabled(True) + + # 更新已挂载脚本列表 + self.updateMountedScriptsList(selected_object) + else: + self.selectedObjectLabel.setText("未选择对象") + self.selectedObjectLabel.setStyleSheet(""" + QLabel { + color: rgba(235, 235, 235, 0.5); + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 10px; + font-weight: 300; + letter-spacing: 0.5px; + padding: 2px 0px; + font-style: italic; + } + """) + self.mountScriptCombo.setEnabled(False) + self.mountBtn.setEnabled(False) + self.mountedScriptsList.clear() + + def updateMountedScriptsList(self, game_object): + """更新已挂载脚本列表""" + # 保存当前选中项的脚本名(去除状态前缀) + current_item = self.mountedScriptsList.currentItem() + selected_script_name = None + if current_item: + # 提取脚本名(移除 "✓ " 或 "✗ " 前缀) + selected_script_name = current_item.text()[2:] + + # 清空并重新填充列表 + self.mountedScriptsList.clear() + scripts = self.world.getScripts(game_object) + for script_component in scripts: + script_name = script_component.script_name + enabled = "✓" if script_component.enabled else "✗" + item_text = f"{enabled} {script_name}" + self.mountedScriptsList.addItem(item_text) + + # 恢复选中状态(根据脚本名匹配) + if selected_script_name: + for i in range(self.mountedScriptsList.count()): + item = self.mountedScriptsList.item(i) + # 提取当前项的脚本名进行比较 + current_script_name = item.text()[2:] + if current_script_name == selected_script_name: + self.mountedScriptsList.setCurrentItem(item) + break + + def onCreateScript(self): + """创建脚本按钮事件""" + script_name = self.scriptNameEdit.text().strip() + if not script_name: + UniversalMessageDialog.show_warning( + self, "警告", "请输入脚本名称!", show_cancel=False, confirm_text="确认" + ) + return + + template = self.templateCombo.currentText() + + try: + success = self.world.createScript(script_name, template) + if success: + UniversalMessageDialog.show_success( + self, + "成功", + f"脚本 '{script_name}' 创建成功!", + show_cancel=False, + confirm_text="确认" + ) + self.scriptNameEdit.clear() + self.refreshScriptsList() + else: + UniversalMessageDialog.show_warning( + self, + "警告", + f"脚本 '{script_name}' 创建失败!", + show_cancel=False, + confirm_text="确认" + ) + except Exception as e: + UniversalMessageDialog.show_error( + self, + "错误", + f"创建脚本时发生错误: {str(e)}", + show_cancel=False, + confirm_text="确认" + ) + + def onCreateScriptDialog(self): + """菜单创建脚本事件""" + dialog = self.createStyledInputDialog(self, "创建脚本", "请输入脚本名称:") + + if dialog.exec_() == QDialog.Accepted: + script_name = dialog.textValue() + if script_name.strip(): + try: + success = self.world.createScript(script_name.strip(), "basic") + if success: + UniversalMessageDialog.show_success( + self, + "成功", + f"脚本 '{script_name}' 创建成功!", + show_cancel=False, + confirm_text="确认" + ) + self.refreshScriptsList() + else: + UniversalMessageDialog.show_warning( + self, + "警告", + f"脚本 '{script_name}' 创建失败!", + show_cancel=False, + confirm_text="确认" + ) + except Exception as e: + UniversalMessageDialog.show_error( + self, + "错误", + f"创建脚本时发生错误: {str(e)}", + show_cancel=False, + confirm_text="确认" + ) + + def onLoadScript(self): + """加载脚本按钮事件""" + current_item = self.scriptsList.currentItem() + if not current_item: + UniversalMessageDialog.show_warning( + self, "警告", "请选择要加载的脚本!", show_cancel=False, confirm_text="确认" + ) + return + + script_name = current_item.text() + try: + success = self.world.reloadScript(script_name) + if success: + UniversalMessageDialog.show_success( + self, "成功", f"脚本 '{script_name}' 加载成功!", show_cancel=False, confirm_text="确认" + ) + else: + UniversalMessageDialog.show_warning( + self, "警告", f"脚本 '{script_name}' 加载失败!", show_cancel=False, confirm_text="确认" + ) + except Exception as e: + UniversalMessageDialog.show_error( + self, "错误", f"加载脚本时发生错误: {str(e)}", show_cancel=False, confirm_text="确认" + ) + + def onLoadScriptFile(self): + """加载脚本文件菜单事件""" + dialog = self.createStyledFileDialog( + self, + "选择脚本文件", + "", + "Python文件 (*.py)" + ) + + if dialog.exec_() == QDialog.Accepted: + file_path = dialog.selectedFiles()[0] + if file_path: + try: + success = self.world.loadScript(file_path) + if success: + UniversalMessageDialog.show_success( + self, "成功", "脚本文件加载成功!", show_cancel=False, confirm_text="确认" + ) + self.refreshScriptsList() + else: + UniversalMessageDialog.show_warning( + self, "警告", "脚本文件加载失败!", show_cancel=False, confirm_text="确认" + ) + except Exception as e: + UniversalMessageDialog.show_error( + self, "错误", f"加载脚本文件时发生错误: {str(e)}", show_cancel=False, confirm_text="确认" + ) + + def onReloadAllScripts(self): + """重新加载所有脚本事件""" + try: + scripts_loaded = self.world.loadAllScripts() + UniversalMessageDialog.show_success( + self, + "成功", + f"更新完成,共加载 {len(scripts_loaded)} 个脚本!", + show_cancel=False, + confirm_text="确认" + ) + self.refreshScriptsList() + except Exception as e: + UniversalMessageDialog.show_error( + self, + "错误", + f"加载脚本时发生错误: {str(e)}", + show_cancel=False, + confirm_text="确认" + ) + + def onToggleHotReload(self): + """切换热更新状态""" + enabled = self.toggleHotReloadAction.isChecked() + self.world.enableHotReload(enabled) + status = "已开启" if enabled else "已关闭" + UniversalMessageDialog.show_info( + self, + "脚本热更", + f"脚本热更{status}", + show_cancel=False, + confirm_text="确认" + ) + + def onOpenScriptsManager(self): + """打开脚本管理器""" + # 显示脚本管理停靠窗口 + self.scriptDock.show() + self.scriptDock.raise_() + + def onScriptDoubleClick(self, item): + """脚本列表双击事件""" + script_name = item.text() + UniversalMessageDialog.show_info( + self, + "提示", + f"双击脚本: {script_name}\n\n请使用外部编辑器编辑脚本文件。", + show_cancel=False, + confirm_text="确认" + ) + + def onMountScript(self): + """挂载脚本事件""" + selected_object = getattr(self.world.selection, 'selectedObject', None) + if not selected_object: + UniversalMessageDialog.show_warning( + self, "警告", "请选择一个对象!", show_cancel=False, confirm_text="确认" + ) + return + + script_name = self.mountScriptCombo.currentText() + if not script_name: + UniversalMessageDialog.show_warning( + self, "警告", "请选择要挂载的脚本!", show_cancel=False, confirm_text="确认" + ) + return + + try: + success = self.world.addScript(selected_object, script_name) + if success: + UniversalMessageDialog.show_success( + self, "成功", f"脚本 '{script_name}' 已挂载到对象!", show_cancel=False, confirm_text="确认" + ) + self.updateMountedScriptsList(selected_object) + + if self.treeWidget and self.treeWidget.currentItem(): + self.world.updatePropertyPanel(self.treeWidget.currentItem()) + else: + UniversalMessageDialog.show_warning( + self, "警告", "挂载脚本失败!", show_cancel=False, confirm_text="确认" + ) + except Exception as e: + UniversalMessageDialog.show_error( + self, "错误", f"挂载脚本时发生错误: {str(e)}", show_cancel=False, confirm_text="确认" + ) + + def onUnmountScript(self): + """卸载脚本事件""" + selected_object = getattr(self.world.selection, 'selectedObject', None) + if not selected_object: + UniversalMessageDialog.show_warning( + self, + "警告", + "请选择一个对象!", + show_cancel=False, + confirm_text="确认" + ) + return + + current_item = self.mountedScriptsList.currentItem() + if not current_item: + UniversalMessageDialog.show_warning( + self, + "警告", + "请选择要卸载的脚本!", + show_cancel=False, + confirm_text="确认" + ) + return + + item_text = current_item.text() + script_name = item_text[2:] + + try: + success = self.world.removeScript(selected_object, script_name) + if success: + UniversalMessageDialog.show_success( + self, + "成功", + f"脚本 '{script_name}' 已从对象卸载!", + show_cancel=False, + confirm_text="确认" + ) + self.updateMountedScriptsList(selected_object) + + if self.treeWidget and self.treeWidget.currentItem(): + self.world.updatePropertyPanel(self.treeWidget.currentItem()) + else: + UniversalMessageDialog.show_warning( + self, + "警告", + "卸载脚本失败!", + show_cancel=False, + confirm_text="确认" + ) + except Exception as e: + UniversalMessageDialog.show_error( + self, + "错误", + f"卸载脚本时发生错误: {str(e)}", + show_cancel=False, + confirm_text="确认" + ) + + def onOpenIconManager(self): + """打开图标管理器""" + try: + from ui.icon_manager_gui import show_icon_manager + self.icon_manager_dialog = show_icon_manager(self) + print("🎨 图标管理器已打开") + except Exception as e: + print(f"❌ 打开图标管理器失败: {e}") + UniversalMessageDialog.show_warning(self, "警告", f"打开图标管理器失败: {str(e)}", show_cancel=False, confirm_text="确认") + + def closeEvent(self, event): + """处理窗口关闭事件""" + try: + print("🔄 正在关闭应用程序...") + + # 关闭拆装交互相关的弹窗 + if hasattr(self.world, 'assembly_interaction') and self.world.assembly_interaction: + print("🧹 关闭拆装交互弹窗...") + if hasattr(self.world.assembly_interaction, 'step_dialog') and self.world.assembly_interaction.step_dialog: + self.world.assembly_interaction.step_dialog.close() + self.world.assembly_interaction.step_dialog = None + # 停止交互模式 + if self.world.assembly_interaction.is_active: + self.world.assembly_interaction.stop_interaction_mode() + + # 清理工具管理器中的进程 + if hasattr(self.world, 'tool_manager') and self.world.tool_manager: + print("🧹 清理工具管理器进程...") + if hasattr(self.world.tool_manager, 'cleanup_processes'): + self.world.tool_manager.cleanup_processes() + else: + print("✓ 工具管理器无需清理进程") + + # 停止更新定时器 + if hasattr(self, 'updateTimer') and self.updateTimer: + self.updateTimer.stop() + print("⏹️ 更新定时器已停止") + + # 清理VR资源 + if hasattr(self.world, 'vr_manager') and self.world.vr_manager: + print("🧹 清理VR资源...") + self.world.vr_manager.cleanup() + + # 清理Panda3D资源 + if hasattr(self, 'pandaWidget') and self.pandaWidget: + print("🧹 清理Panda3D资源...") + self.pandaWidget.cleanup() + + print("✅ 应用程序清理完成") + event.accept() + + except Exception as e: + print(f"⚠️ 关闭应用程序时出错: {e}") + event.accept() # 即使出错也要关闭 + + def onCreateFlatTerrain(self): + """创建平面地形""" + fields = [ + { + "name": "width", + "label": "宽度:", + "type": "double", + "min": 0.0, + "max": 10000.0, + "step": 0.1, + "decimals": 2, + "value": 0.30, + "label_width": 60 + }, + { + "name": "height", + "label": "高度:", + "type": "double", + "min": 0.0, + "max": 10000.0, + "step": 0.1, + "decimals": 2, + "value": 0.30, + "label_width": 60 + }, + { + "name": "resolution", + "label": "分辨率:", + "type": "int", + "min": 16, + "max": 2048, + "step": 16, + "value": 256, + "label_width": 60 + }, + ] + dialog = StyledTerrainDialog(self, "创建平面地形", fields, primary_text="创建", secondary_text="取消") + + if dialog.exec_() == QDialog.Accepted: + width = dialog.get_value("width") + height = dialog.get_value("height") + resolution = int(dialog.get_value("resolution")) + + terrain_info = self.world.createFlatTerrain((width, height), resolution) + if terrain_info: + UniversalMessageDialog.show_success( + self, + "成功", + "平面地形创建成功!", + show_cancel=False, + confirm_text="确认" + ) + else: + UniversalMessageDialog.show_warning( + self, + "警告", + "平面地形创建失败!", + show_cancel=False, + confirm_text="确认" + ) + + def onCreateHeightmapTerrain(self): + """从高度图创建地形""" + file_dialog = self.createStyledFileDialog( + self, + "选择高度图文件", + "", + "图像文件 (*.png *.jpg *.jpeg *.bmp *.tga);;所有文件 (*)" + ) + + if file_dialog.exec_() == QDialog.Accepted: + file_path = file_dialog.selectedFiles()[0] + if file_path: + fields = [ + { + "name": "x_scale", + "label": "X缩放:", + "type": "double", + "min": 0.1, + "max": 1000.0, + "step": 0.1, + "decimals": 2, + "value": 0.30, + "label_width": 70 + }, + { + "name": "y_scale", + "label": "Y缩放:", + "type": "double", + "min": 0.1, + "max": 1000.0, + "step": 0.1, + "decimals": 2, + "value": 0.30, + "label_width": 70 + }, + { + "name": "z_scale", + "label": "Z缩放:", + "type": "double", + "min": 0.1, + "max": 1000.0, + "step": 1.0, + "decimals": 2, + "value": 50.0, + "label_width": 70 + }, + ] + + params_dialog = StyledTerrainDialog(self, "设置地形参数", fields, primary_text="创建", secondary_text="取消") + + if params_dialog.exec_() == QDialog.Accepted: + x_scale = params_dialog.get_value("x_scale") + y_scale = params_dialog.get_value("y_scale") + z_scale = params_dialog.get_value("z_scale") + + terrain_info = self.world.createTerrainFromHeightMap( + file_path, + (x_scale, y_scale, z_scale) + ) + if terrain_info: + UniversalMessageDialog.show_success( + self, + "成功", + "高度图地形创建成功!", + show_cancel=False, + confirm_text="确认" + ) + else: + UniversalMessageDialog.show_warning( + self, + "警告", + "高度图地形创建失败!", + show_cancel=False, + confirm_text="确认" + ) + + def onOpenAssemblyDisassemblyConfig(self): + """打开拆装配置界面""" + try: + from ui.assembly_disassembly_config_simple import AssemblyDisassemblyConfigDialog + config_dialog = AssemblyDisassemblyConfigDialog(self, self.world) + config_dialog.show() + except Exception as e: + UniversalMessageDialog.show_error(self, "错误", f"打开拆装配置界面失败: {str(e)}") + import traceback + traceback.print_exc() + + def onStartAssemblyInteraction(self): + """开始拆装交互""" + try: + # 显示模式选择对话框 + mode_dialog = AssemblyModeSelectionDialog(self) + if mode_dialog.exec_() != QDialog.Accepted: + return + + selected_mode = mode_dialog.get_selected_mode() + print(f"🎯 用户选择的拆装模式: {selected_mode}") + + from core.assembly_interaction import AssemblyInteractionManager + + # 检查是否已有交互管理器实例 + if not hasattr(self.world, 'assembly_interaction'): + self.world.assembly_interaction = AssemblyInteractionManager(self.world) + + # 启动交互模式,传递模式参数 + self.world.assembly_interaction.start_interaction_mode(mode=selected_mode) + + except Exception as e: + UniversalMessageDialog.show_error(self, "错误", f"启动拆装交互失败: {str(e)}") + import traceback + traceback.print_exc() + + def onOpenMaintenanceSystem(self): + """打开维修系统""" + try: + # 导入简化的登录界面 + from ui.simple_maintenance_login import SimpleMaintenanceLoginDialog + from ui.maintenance_system import MaintenanceSubjectDialog, MaintenanceSystemManager + + print("🔧 启动维修系统...") + + # 显示登录界面 + login_dialog = SimpleMaintenanceLoginDialog(self) + + if login_dialog.exec_() == QDialog.Accepted: + print("✅ 登录成功,显示科目选择界面") + + # 获取当前项目路径 + project_path = None + if hasattr(self.world, 'project_manager') and self.world.project_manager: + project_path = self.world.project_manager.getCurrentProjectPath() + + # 显示科目选择界面 + subject_dialog = MaintenanceSubjectDialog(project_path, self) + + def on_subject_selected(subject_path, mode): + """处理科目选择""" + try: + print(f"🎯 启动维修科目: {subject_path}") + print(f"📝 模式: {mode}") + + # 加载科目配置 + import json + with open(subject_path, 'r', encoding='utf-8') as f: + subject_config = json.load(f) + + # 初始化拆装交互系统(如果还没有) + from core.assembly_interaction import AssemblyInteractionManager + + if not hasattr(self.world, 'assembly_interaction') or not self.world.assembly_interaction: + print("🔧 初始化拆装交互系统...") + self.world.assembly_interaction = AssemblyInteractionManager(self.world) + + # 设置配置并启动交互模式 + self.world.assembly_interaction.config_data = subject_config + + # 启动交互模式 + success = self.world.assembly_interaction.start_interaction_mode(mode=mode) + + if success: + print(f"✅ 维修科目启动成功") + else: + print("❌ 维修科目启动失败") + UniversalMessageDialog.show_warning(self, "错误", "维修科目启动失败",False,"确认") + + except Exception as e: + print(f"❌ 启动维修科目失败: {e}") + UniversalMessageDialog.show_error(self, "错误", f"启动维修科目失败: {str(e)}") + import traceback + traceback.print_exc() + + subject_dialog.subject_selected.connect(on_subject_selected) + subject_dialog.exec_() + + else: + print("ℹ️ 用户取消了登录") + + except Exception as e: + print(f"❌ 打开维修系统失败: {e}") + UniversalMessageDialog.show_error(self, "错误", f"打开维修系统失败: {str(e)}") + import traceback + traceback.print_exc() + + + + # ==================== VR事件处理 ==================== + + def onEnterVR(self): + """进入VR模式""" + try: + if hasattr(self.world, 'vr_manager') and self.world.vr_manager: + success = self.world.vr_manager.enable_vr() + if success: + # 更新菜单状态 + self.enterVRAction.setEnabled(False) + self.exitVRAction.setEnabled(True) + UniversalMessageDialog.show_info( + self, "成功", "VR模式已启用!\n请确保您的VR头显已正确连接。" + , False, "确认") + else: + UniversalMessageDialog.show_warning( + self, "错误", "无法启用VR模式!\n请检查:\n1. SteamVR是否正在运行\n2. VR头显是否已连接\n3. OpenVR库是否已正确安装" + , False, "确认") + else: + UniversalMessageDialog.show_warning( + self, "错误", "VR管理器不可用!" + , False, "确认") + except Exception as e: + UniversalMessageDialog.show_error( + self, "错误", f"启用VR模式时发生错误:\n{str(e)}" + , False, "确认") + def onExitVR(self): + """退出VR模式""" + try: + if hasattr(self.world, 'vr_manager') and self.world.vr_manager: + self.world.vr_manager.disable_vr() + # 更新菜单状态 + self.enterVRAction.setEnabled(True) + self.exitVRAction.setEnabled(False) + UniversalMessageDialog.show_info(self, "成功", "已退出VR模式" + , False, "确认") + else: + UniversalMessageDialog.show_warning(self, "错误", "VR管理器不可用!" + , False, "确认") + except Exception as e: + UniversalMessageDialog.show_error(self, "错误", f"退出VR模式时发生错误:\n{str(e)}" + , False, "确认") + + def onShowVRStatus(self): + """显示VR状态""" + try: + if hasattr(self.world, 'vr_manager') and self.world.vr_manager: + status = self.world.vr_manager.get_vr_status() + + status_text = f"""VR系统状态: + +可用性: {'✅ 可用' if status['available'] else '❌ 不可用'} +初始化: {'✅ 已初始化' if status['initialized'] else '❌ 未初始化'} +启用状态: {'✅ 已启用' if status['enabled'] else '❌ 未启用'} +渲染分辨率: {status['eye_resolution'][0]}x{status['eye_resolution'][1]} +追踪设备数: {status['device_count']} + +提示: +- 如果VR不可用,请确保已安装SteamVR并连接VR头显 +- 如果OpenVR库未安装,请运行:pip install openvr +""" + UniversalMessageDialog.show_info(self, "VR状态", status_text + , False, "确认") + else: + UniversalMessageDialog.show_warning(self, "错误", "VR管理器不可用!" + , False, "确认") + except Exception as e: + UniversalMessageDialog.show_error(self, "错误", f"获取VR状态时发生错误:\n{str(e)}" + , False, "确认") + + def onShowVRSettings(self): + """显示VR设置对话框""" + try: + if hasattr(self.world, 'vr_manager') and self.world.vr_manager: + dialog = self.createVRSettingsDialog() + dialog.exec_() + else: + UniversalMessageDialog.show_warning(self, "错误", "VR管理器不可用!" + , False, "确认") + except Exception as e: + UniversalMessageDialog.show_error(self, "错误", f"打开VR设置时发生错误:\n{str(e)}" + , False, "确认") + + def createVRSettingsDialog(self): + """创建VR设置对话框(统一为 NewProjectDialog 风格)""" + dialog = QDialog(self) + dialog.setWindowTitle("VR设置") + dialog.setModal(True) + dialog.resize(508, 420) + dialog.setObjectName("newProjectDialog") + dialog.setWindowFlags(Qt.Dialog | Qt.FramelessWindowHint) + dialog.setAttribute(Qt.WA_TranslucentBackground, True) + + dialog.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 0 0; 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: 0 0 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; } + QLabel[role="fieldLabel"] { color: #EBEBEB; font-family: 'Inter', 'Microsoft YaHei', sans-serif; font-size: 12px; font-weight: 400; letter-spacing: 0.6px; } + QLabel { color: #EBEBEB; font-family: 'Inter', 'Microsoft YaHei', sans-serif; font-size: 11px; font-weight: 300; letter-spacing: 0.55px; } + QComboBox, QSpinBox, QDoubleSpinBox { color: #EBEBEB; font-family: 'Inter', 'Microsoft YaHei', sans-serif; font-size: 11px; font-weight: 300; } + QCheckBox { color: #EBEBEB; font-family: 'Inter', 'Microsoft YaHei', sans-serif; font-size: 11px; font-weight: 300; } + 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); } + QTextEdit { background-color: rgba(89,100,113,0.1); color: #EBEBEB; border: 1px solid rgba(76,92,110,0.4); border-radius: 4px; font-size: 11px; } + 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; } + """) + + main_layout = QVBoxLayout(dialog) + 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) + + # Title bar + title_bar = QWidget() + title_bar.setObjectName('titleBar') + tb_layout = QHBoxLayout(title_bar) + tb_layout.setContentsMargins(8, 0, 8, 0) + tb_layout.setSpacing(6) + control_buttons = QWidget() + control_buttons.setObjectName('controlButtons') + cb_layout = QHBoxLayout(control_buttons) + cb_layout.setContentsMargins(0, 0, 0, 0) + cb_layout.setSpacing(0) + close_btn = QPushButton() + close_btn.setObjectName('closeButton') + try: + close_btn.setIcon(get_icon('close_icon', QSize(18, 18))) + close_btn.setIconSize(QSize(18, 18)) + except Exception: + pass + close_btn.clicked.connect(dialog.reject) + cb_layout.addWidget(close_btn) + left_placeholder = QWidget() + left_placeholder.setFixedWidth(control_buttons.sizeHint().width()) + tb_layout.addWidget(left_placeholder) + title_label = QLabel("VR设置") + title_label.setObjectName('titleLabel') + title_label.setAlignment(Qt.AlignCenter) + tb_layout.addWidget(title_label, 1) + tb_layout.addWidget(control_buttons) + base_layout.addWidget(title_bar) + + # drag handlers + dragging_state = {'dragging': False, 'pos': QPoint()} + def _tb_press(event): + if event.button() == Qt.LeftButton: + dragging_state['dragging'] = True + dragging_state['pos'] = event.globalPos() - dialog.frameGeometry().topLeft() + event.accept() + else: + event.ignore() + def _tb_move(event): + if event.buttons() & Qt.LeftButton and dragging_state['dragging']: + dialog.move(event.globalPos() - dragging_state['pos']) + event.accept() + else: + event.ignore() + def _tb_release(event): + if event.button() == Qt.LeftButton: + dragging_state['dragging'] = False + event.accept() + else: + event.ignore() + title_bar.mousePressEvent = _tb_press + title_bar.mouseMoveEvent = _tb_move + title_bar.mouseReleaseEvent = _tb_release + + # Content + 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) + container_layout = QVBoxLayout(content_container) + container_layout.setContentsMargins(15, 10, 15, 10) + container_layout.setSpacing(10) + + # 左侧对齐辅助:统一字段列宽,并让小标题与控件左侧对齐 + label_column_width = 110 + def add_section_title(text): + title = QLabel(text) + title.setProperty('role', 'sectionTitle') + container_layout.addWidget(title) + + # VR 状态 + add_section_title("VR状态") + status_widget = QWidget() + status_vlayout = QVBoxLayout(status_widget) + status_vlayout.setContentsMargins(0, 0, 0, 0) + status_vlayout.setSpacing(6) + if hasattr(self.world, 'vr_manager') and self.world.vr_manager: + status = self.world.vr_manager.get_vr_status() + available_label = QLabel(f"VR可用性: {'是' if status['available'] else '否'}") + available_label.setStyleSheet(f"color: {'#2dffc4' if status['available'] else 'red'};") + status_vlayout.addWidget(available_label) + enabled_label = QLabel(f"VR状态: {'已启用' if status['enabled'] else '未启用'}") + enabled_label.setStyleSheet(f"color: {'#2dffc4' if status['enabled'] else 'gray'};") + status_vlayout.addWidget(enabled_label) + resolution_label = QLabel(f"渲染分辨率: {status['eye_resolution'][0]}x{status['eye_resolution'][1]}") + status_vlayout.addWidget(resolution_label) + container_layout.addWidget(status_widget) + + # 渲染模式 + add_section_title("渲染模式") + render_mode_button_group = QButtonGroup(dialog) + row_mode1 = QWidget(); row_mode1_layout = QHBoxLayout(row_mode1); row_mode1_layout.setContentsMargins(0,0,0,0); row_mode1_layout.setSpacing(10) + normal_render_radio = QRadioButton("普通渲染模式") + normal_render_radio.setStyleSheet(""" + QRadioButton { + color: #EBEBEB; + font-family: 'Inter', 'Microsoft YaHei', sans-serif; + font-size: 12px; + font-weight: 400; + letter-spacing: 0.6px; + } + """) + row_mode1_layout.addWidget(normal_render_radio) + row_mode1_layout.addStretch(); container_layout.addWidget(row_mode1) + row_mode2 = QWidget(); row_mode2_layout = QHBoxLayout(row_mode2); row_mode2_layout.setContentsMargins(0,0,0,0); row_mode2_layout.setSpacing(10) + pipeline_render_radio = QRadioButton("RenderPipeline高级渲染(推荐)") + pipeline_render_radio.setStyleSheet(""" + QRadioButton { + color: #EBEBEB; + font-family: 'Inter', 'Microsoft YaHei', sans-serif; + font-size: 12px; + font-weight: 400; + letter-spacing: 0.6px; + } + """) + row_mode2_layout.addWidget(pipeline_render_radio) + row_mode2_layout.addStretch(); container_layout.addWidget(row_mode2) + render_mode_button_group.addButton(normal_render_radio, 0) + render_mode_button_group.addButton(pipeline_render_radio, 1) + if hasattr(self.world, 'vr_manager') and self.world.vr_manager: + try: + from core.vr import VRRenderMode + current_mode = self.world.vr_manager.get_vr_render_mode() + if current_mode == VRRenderMode.RENDER_PIPELINE: + pipeline_render_radio.setChecked(True) + else: + normal_render_radio.setChecked(True) + except Exception: + normal_render_radio.setChecked(True) + else: + normal_render_radio.setChecked(True) + info_text = QTextEdit() + info_text.setReadOnly(True) + info_text.setMaximumHeight(60) + info_text.setPlainText("• 普通渲染:性能最优,适合低配置\n• RenderPipeline:高级图形效果(阴影、AO等),需要较高性能") + container_layout.addWidget(info_text) + dialog.render_mode_button_group = render_mode_button_group + + # 载入配置 + vr_config = {} + if hasattr(self.world, 'vr_manager') and self.world.vr_manager and self.world.vr_manager.config_manager: + vr_config = self.world.vr_manager.config_manager.load_config() + + # 渲染设置 + add_section_title("渲染设置") + row_quality = QWidget() + row_quality_layout = QHBoxLayout(row_quality) + row_quality_layout.setContentsMargins(0,0,0,0) + row_quality_layout.setSpacing(10) + quality_label = QLabel("渲染质量:") + quality_label.setProperty('role','fieldLabel') + quality_label.setAlignment(Qt.AlignRight|Qt.AlignVCenter) + row_quality_layout.addWidget(quality_label) + quality_combo = QComboBox() + quality_combo.addItems(["低","中","高"]) + quality_preset = vr_config.get("quality_preset", "balanced") + quality_map={"performance":"低","balanced":"中","quality":"高"} + quality_combo.setCurrentText(quality_map.get(quality_preset,"中")) + row_quality_layout.addWidget(quality_combo,1) + container_layout.addWidget(row_quality) + row_aa = QWidget(); row_aa_layout = QHBoxLayout(row_aa) + row_aa_layout.setContentsMargins(0,0,0,0) + row_aa_layout.setSpacing(10) + aa_label = QLabel("抗锯齿:") + aa_label.setProperty('role','fieldLabel') + aa_label.setAlignment(Qt.AlignRight|Qt.AlignVCenter) + row_aa_layout.addWidget(aa_label) + aa_combo = QComboBox() + aa_combo.addItems(["无","2x","4x","8x"]) + aa_combo.setCurrentText(vr_config.get("anti_aliasing","4x")) + row_aa_layout.addWidget(aa_combo,1) + container_layout.addWidget(row_aa) + + # 性能设置 + add_section_title("性能设置") + row_refresh = QWidget() + row_refresh_layout = QHBoxLayout(row_refresh) + row_refresh_layout.setContentsMargins(0,0,0,0) + row_refresh_layout.setSpacing(10) + refresh_label = QLabel("刷新率:") + refresh_label.setProperty('role','fieldLabel') + refresh_label.setAlignment(Qt.AlignRight|Qt.AlignVCenter) + row_refresh_layout.addWidget(refresh_label) + refresh_combo = QComboBox() + refresh_combo.addItems(["72Hz","90Hz","120Hz","144Hz"]) + refresh_combo.setCurrentText(vr_config.get("refresh_rate","90Hz")) + row_refresh_layout.addWidget(refresh_combo,1) + container_layout.addWidget(row_refresh) + row_async = QWidget() + row_async_layout = QHBoxLayout(row_async) + row_async_layout.setContentsMargins(0,0,0,0) + row_async_layout.setSpacing(10) + async_check = QCheckBox("启用异步重投影") + async_check.setChecked(vr_config.get("async_reprojection",True)) + row_async_layout.addWidget(async_check) + row_async_layout.addStretch() + container_layout.addWidget(row_async) + + # Save refs + dialog.quality_combo = quality_combo + dialog.aa_combo = aa_combo + dialog.refresh_combo = refresh_combo + dialog.async_check = async_check + + # Separator before buttons + separator_buttons = QFrame() + separator_buttons.setFrameShape(QFrame.HLine) + separator_buttons.setFrameShadow(QFrame.Plain) + separator_buttons.setFixedHeight(1) + separator_buttons.setStyleSheet("background-color: #2C2F36; border: none;") + container_layout.addWidget(separator_buttons) + + # Buttons + button_layout = QHBoxLayout() + apply_button = QPushButton("应用") + ok_button = QPushButton("确定") + # ok_button.setObjectName("primaryButton") + cancel_button = QPushButton("取消") + button_layout.addWidget(apply_button) + button_layout.addStretch() + button_layout.addWidget(ok_button) + button_layout.addWidget(cancel_button) + container_layout.addLayout(button_layout) + + content_layout.addWidget(content_container, 0, Qt.AlignTop) + base_layout.addWidget(content_widget) + main_layout.addWidget(base_frame) + + # signals + apply_button.clicked.connect(lambda: self.applyVRSettings(dialog)) + ok_button.clicked.connect(lambda: self.onVRSettingsOK(dialog)) + cancel_button.clicked.connect(dialog.reject) + + return dialog + + def onVRSettingsOK(self, dialog): + """确定按钮 - 应用设置并关闭对话框""" + # 先应用设置 + self.applyVRSettings(dialog) + # 关闭对话框 + dialog.accept() + + def applyVRSettings(self, dialog): + """应用VR设置""" + try: + if not hasattr(self.world, 'vr_manager') or not self.world.vr_manager: + UniversalMessageDialog.show_warning(dialog, "错误", "VR管理器不可用!",False,"确定") + return + + if not self.world.vr_manager.config_manager: + UniversalMessageDialog.show_warning(dialog, "错误", "VR配置管理器不可用!",False,"确定") + return + + # 1️⃣ 读取所有UI控件的值 + # 渲染模式 + selected_mode_id = dialog.render_mode_button_group.checkedId() + new_mode = "render_pipeline" if selected_mode_id == 1 else "normal" + mode_name = "RenderPipeline高级渲染" if selected_mode_id == 1 else "普通渲染" + + # 渲染质量 + quality_text = dialog.quality_combo.currentText() + quality_map_reverse = {"低": "performance", "中": "balanced", "高": "quality"} + quality_preset = quality_map_reverse.get(quality_text, "balanced") + + # 其他设置 + anti_aliasing = dialog.aa_combo.currentText() + refresh_rate = dialog.refresh_combo.currentText() + async_reprojection = dialog.async_check.isChecked() + + # 2️⃣ 加载当前配置 + config = self.world.vr_manager.config_manager.load_config() + + # 3️⃣ 更新配置 + config["quality_preset"] = quality_preset + config["anti_aliasing"] = anti_aliasing + config["refresh_rate"] = refresh_rate + config["async_reprojection"] = async_reprojection + + # 4️⃣ 检查渲染模式是否改变 + from core.vr import VRRenderMode + current_mode = self.world.vr_manager.get_vr_render_mode() + mode_changed = (current_mode.value != new_mode) + + # 5️⃣ 如果渲染模式改变,询问用户确认 + if mode_changed: + reply = UniversalMessageDialog.show_info(dialog, "确认切换", f"确定要切换到{mode_name}模式吗?\n\n注意:切换渲染模式将重新创建VR缓冲区,可能需要几秒钟。",True,"取消","确定") + + if reply == QDialog.Rejected: + # 用户取消渲染模式切换,但仍然保存其他设置 + self.world.vr_manager.config_manager.save_config(config) + UniversalMessageDialog.show_info(dialog, "提示", "已保存其他设置(未切换渲染模式)", + False, "确定") + return + + # 应用渲染模式切换 + success = self.world.vr_manager.set_vr_render_mode(new_mode) + + if not success: + UniversalMessageDialog.show_warning(dialog, "失败", f"切换到{mode_name}模式失败!\n请查看控制台输出了解详情。", + False, "确定") + return + + # 6️⃣ 保存配置(如果模式改变,set_vr_render_mode已经保存了,但我们需要确保其他设置也被保存) + self.world.vr_manager.config_manager.save_config(config) + + # 7️⃣ 应用质量预设到VR管理器 + if hasattr(self.world.vr_manager, 'current_quality_preset'): + self.world.vr_manager.current_quality_preset = quality_preset + + # 8️⃣ 显示成功消息 + if mode_changed: + UniversalMessageDialog.show_info(dialog, "成功", f"VR设置已应用!\n• 渲染模式: {mode_name}\n• 渲染质量: {quality_text}\n配置已自动保存。", + False, "确定") + else: + UniversalMessageDialog.show_info(dialog, "成功", f"VR设置已保存!\n• 渲染质量: {quality_text}", + False, "确定") + + except Exception as e: + UniversalMessageDialog.show_critical(dialog, "错误", f"应用VR设置时发生错误:\n{str(e)}", + False, "确定") + import traceback + traceback.print_exc() + + # ==================== VR调试事件处理 ==================== + + def onToggleVRDebug(self): + """切换VR调试输出""" + try: + if hasattr(self.world, 'vr_manager') and self.world.vr_manager: + enabled = self.world.vr_manager.toggle_debug_output() + self.vrDebugToggleAction.setChecked(enabled) + + status = "启用" if enabled else "禁用" + UniversalMessageDialog.show_info(self, "提示", f"已{status}VR调试输出", + False, "确定") + else: + UniversalMessageDialog.show_warning(self, "错误", "VR管理器不可用!", + False, "确定") + except Exception as e: + UniversalMessageDialog.show_error(self, "错误", f"切换VR调试时发生错误:\n{str(e)}", + False, "确定") + + def onShowVRPerformance(self): + """立即显示VR性能报告""" + try: + if hasattr(self.world, 'vr_manager') and self.world.vr_manager: + if self.world.vr_manager.vr_enabled: + self.world.vr_manager.force_performance_report() + UniversalMessageDialog.show_info(self, "提示", "已立即显示VR性能报告", + False, "确定") + else: + UniversalMessageDialog.show_warning(self, "提示", "请先启用VR模式", + False, "确定") + else: + UniversalMessageDialog.show_warning(self, "错误", "VR管理器不可用!", + False, "确定") + except Exception as e: + UniversalMessageDialog.show_error(self, "错误", f"显示VR性能报告时发生错误:\n{str(e)}", + False, "确定") + + def onSetVRDebugMode(self, mode): + """设置VR调试模式""" + try: + if hasattr(self.world, 'vr_manager') and self.world.vr_manager: + self.world.vr_manager.set_debug_mode(mode) + + # 更新菜单状态 + if mode == 'brief': + self.vrDebugBriefAction.setChecked(True) + self.vrDebugDetailedAction.setChecked(False) + else: + self.vrDebugBriefAction.setChecked(False) + self.vrDebugDetailedAction.setChecked(True) + + mode_name = "简短" if mode == 'brief' else "详细" + UniversalMessageDialog.show_info(self, "提示", f"已设置VR调试模式为:{mode_name}", + False, "确定") + else: + UniversalMessageDialog.show_warning(self, "错误", "VR管理器不可用!", + False, "确定") + except Exception as e: + UniversalMessageDialog.show_error(self, "错误", f"设置VR调试模式时发生错误:\n{str(e)}", + False, "确定") + + def onToggleVRPerformanceMonitor(self): + """切换VR性能监控""" + try: + if hasattr(self.world, 'vr_manager') and self.world.vr_manager: + enabled = self.vrPerformanceMonitorAction.isChecked() + if enabled: + self.world.vr_manager.enable_performance_monitoring() + else: + self.world.vr_manager.disable_performance_monitoring() + + status = "启用" if enabled else "禁用" + UniversalMessageDialog.show_info(self, "提示", f"已{status}VR性能监控", + False, "确定") + else: + UniversalMessageDialog.show_warning(self, "错误", "VR管理器不可用!", + False, "确定") + self.vrPerformanceMonitorAction.setChecked(False) + except Exception as e: + UniversalMessageDialog.show_error(self, "错误", f"切换VR性能监控时发生错误:\n{str(e)}", + False, "确定") + + def onToggleVRGpuTiming(self): + """切换VR GPU时间监控""" + try: + if hasattr(self.world, 'vr_manager') and self.world.vr_manager: + enabled = self.vrGpuTimingAction.isChecked() + if enabled: + self.world.vr_manager.enable_gpu_timing_monitoring() + else: + self.world.vr_manager.disable_gpu_timing_monitoring() + + status = "启用" if enabled else "禁用" + UniversalMessageDialog.show_info(self, "提示", f"已{status}VR GPU时间监控", + False, "确定") + else: + UniversalMessageDialog.show_warning(self, "错误", "VR管理器不可用!", + False, "确定") + self.vrGpuTimingAction.setChecked(False) + except Exception as e: + UniversalMessageDialog.show_error(self, "错误", f"切换VR GPU时间监控时发生错误:\n{str(e)}", + False, "确定") + + def onToggleVRPipelineMonitor(self): + """切换VR管线监控""" + try: + if hasattr(self.world, 'vr_manager') and self.world.vr_manager: + enabled = self.vrPipelineMonitorAction.isChecked() + self.world.vr_manager.enable_pipeline_monitoring = enabled + status = "启用" if enabled else "禁用" + print(f"✓ VR管线监控已{status}") + else: + UniversalMessageDialog.show_warning(self, "错误", "VR管理器不可用!", + False, "确定") + self.vrPipelineMonitorAction.setChecked(False) + except Exception as e: + UniversalMessageDialog.show_error(self, "错误", f"切换VR管线监控时发生错误:\n{str(e)}", + False, "确定") + + def onSetVRPoseStrategy(self, strategy): + """设置VR姿态策略""" + try: + if hasattr(self.world, 'vr_manager') and self.world.vr_manager: + success = self.world.vr_manager.set_pose_strategy(strategy) + if success: + strategy_names = { + 'render_callback': '渲染回调策略', + 'update_task': '更新任务策略' + } + UniversalMessageDialog.show_info(self, "提示", f"已设置VR姿态策略为:{strategy_names.get(strategy, strategy)}", + False, "确定") + else: + UniversalMessageDialog.show_warning(self, "错误", "设置VR姿态策略失败!", + False, "确定") + else: + UniversalMessageDialog.show_warning(self, "错误", "VR管理器不可用!", + False, "确定") + except Exception as e: + UniversalMessageDialog.show_error(self, "错误", f"设置VR姿态策略时发生错误:\n{str(e)}", + False, "确定") + + def onTestVRPipeline(self): + """测试VR管线监控功能""" + try: + if hasattr(self.world, 'vr_manager') and self.world.vr_manager: + self.world.vr_manager.test_pipeline_monitoring() + UniversalMessageDialog.show_info(self, "提示", "VR管线监控测试已开始,请稍等...", + False, "确定") + else: + UniversalMessageDialog.show_warning(self, "错误", "VR管理器不可用!", + False, "确定") + except Exception as e: + UniversalMessageDialog.show_error(self, "错误", f"测试VR管线监控时发生错误:\n{str(e)}", + False, "确定") + + def onShowVRDebugSettings(self): + """显示VR调试设置对话框""" + try: + if hasattr(self.world, 'vr_manager') and self.world.vr_manager: + dialog = self.createVRDebugSettingsDialog() + dialog.exec_() + else: + UniversalMessageDialog.show_warning(self, "错误", "VR管理器不可用!", + False, "确定") + except Exception as e: + UniversalMessageDialog.show_error(self, "错误", f"打开VR调试设置时发生错误:\n{str(e)}", + False, "确定") + + def createVRDebugSettingsDialog(self): + """创建VR调试设置对话框(统一为 NewProjectDialog 风格)""" + from PyQt5.QtWidgets import QCheckBox, QSlider + from PyQt5.QtCore import Qt + + dialog = QDialog(self) + dialog.setWindowTitle("VR调试设置") + dialog.setModal(True) + dialog.resize(508, 460) + dialog.setObjectName("newProjectDialog") + dialog.setWindowFlags(Qt.Dialog | Qt.FramelessWindowHint) + dialog.setAttribute(Qt.WA_TranslucentBackground, True) + + dialog.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 0 0; 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: 0 0 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; } + QLabel[role="fieldLabel"] { color: #EBEBEB; font-family: 'Inter', 'Microsoft YaHei', sans-serif; font-size: 12px; font-weight: 400; letter-spacing: 0.6px; } + QLabel { color: #EBEBEB; font-family: 'Inter', 'Microsoft YaHei', sans-serif; font-size: 11px; font-weight: 300; letter-spacing: 0.55px; } + QComboBox, QSpinBox, QDoubleSpinBox { color: #EBEBEB; font-family: 'Inter', 'Microsoft YaHei', sans-serif; font-size: 11px; font-weight: 300; } + QCheckBox { color: #EBEBEB; font-family: 'Inter', 'Microsoft YaHei', sans-serif; font-size: 11px; font-weight: 300; } + 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; } + """) + + main_layout = QVBoxLayout(dialog) + 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) + + title_bar = QWidget() + title_bar.setObjectName('titleBar') + tb_layout = QHBoxLayout(title_bar) + tb_layout.setContentsMargins(8,0,8,0) + tb_layout.setSpacing(6) + control_buttons = QWidget() + control_buttons.setObjectName('controlButtons') + cb_layout = QHBoxLayout(control_buttons) + cb_layout.setContentsMargins(0,0,0,0) + cb_layout.setSpacing(0) + close_btn = QPushButton() + close_btn.setObjectName('closeButton') + try: + close_btn.setIcon(get_icon('close_icon', QSize(18, 18))) + close_btn.setIconSize(QSize(18, 18)) + except Exception: pass + close_btn.clicked.connect(dialog.reject) + cb_layout.addWidget(close_btn) + left_placeholder = QWidget() + left_placeholder.setFixedWidth(control_buttons.sizeHint().width()) + tb_layout.addWidget(left_placeholder) + title_label = QLabel("VR调试设置") + title_label.setObjectName('titleLabel') + title_label.setAlignment(Qt.AlignCenter) + tb_layout.addWidget(title_label,1) + tb_layout.addWidget(control_buttons) + base_layout.addWidget(title_bar) + + dragging_state={'dragging':False,'pos':QPoint()} + def _tb_press(e): + if e.button()==Qt.LeftButton: dragging_state['dragging']=True; dragging_state['pos']=e.globalPos()-dialog.frameGeometry().topLeft(); e.accept() + else: e.ignore() + def _tb_move(e): + if e.buttons() & Qt.LeftButton and dragging_state['dragging']: dialog.move(e.globalPos()-dragging_state['pos']); e.accept() + else: e.ignore() + def _tb_release(e): + if e.button()==Qt.LeftButton: dragging_state['dragging']=False; e.accept() + else: e.ignore() + title_bar.mousePressEvent=_tb_press; title_bar.mouseMoveEvent=_tb_move; title_bar.mouseReleaseEvent=_tb_release + + 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) + container_layout = QVBoxLayout(content_container) + container_layout.setContentsMargins(15,10,15,10) + container_layout.setSpacing(10) + + # 获取当前设置 + vr_manager = self.world.vr_manager + debug_status = vr_manager.get_debug_status() + perf_config = vr_manager.get_performance_monitoring_config() + + + # 左侧对齐辅助:统一字段列宽,并让小标题与控件左侧对齐 + label_column_width = 110 + def add_section_title(text): + title = QLabel(text) + title.setProperty('role','sectionTitle') + container_layout.addWidget(title) + + # 调试状态 + add_section_title("调试状态") + status_widget = QWidget() + status_layout = QVBoxLayout(status_widget) + status_layout.setContentsMargins(0,0,0,0) + status_layout.setSpacing(6) + debug_enabled_label = QLabel(f"调试输出: {'启用' if debug_status['debug_enabled'] else '禁用'}") + debug_enabled_label.setStyleSheet(f"color: {'#2dffc4' if debug_status['debug_enabled'] else 'red'};") + status_layout.addWidget(debug_enabled_label) + debug_mode_label = QLabel(f"调试模式: {debug_status['debug_mode']}") + status_layout.addWidget(debug_mode_label) + performance_label = QLabel(f"性能监控: {'启用' if debug_status['performance_monitoring'] else '禁用'}") + performance_label.setStyleSheet(f"color: {'#2dffc4' if debug_status['performance_monitoring'] else 'red'};") + status_layout.addWidget(performance_label) + container_layout.addWidget(status_widget) + + # 报告设置 + add_section_title("报告设置") + row_interval = QWidget() + row_interval_layout = QHBoxLayout(row_interval) + row_interval_layout.setContentsMargins(0,0,0,0) + row_interval_layout.setSpacing(10) + interval_text_label = QLabel("报告间隔:") + interval_text_label.setProperty('role','fieldLabel') + interval_text_label.setAlignment(Qt.AlignRight|Qt.AlignVCenter) + row_interval_layout.addWidget(interval_text_label) + interval_slider = QSlider(Qt.Horizontal) + interval_slider.setMinimum(5) + interval_slider.setMaximum(120) + interval_slider.setValue(int(debug_status['report_interval_seconds'])) + interval_slider.setTickPosition(QSlider.TicksBelow) + interval_slider.setTickInterval(15) + interval_label = QLabel(f"{int(debug_status['report_interval_seconds'])}秒") + interval_slider.valueChanged.connect(lambda v: interval_label.setText(f"{v}秒")) + inner = QHBoxLayout() + inner.setContentsMargins(0,0,0,0) + inner.setSpacing(6) + inner.addWidget(interval_slider) + inner.addWidget(interval_label) + row_interval_layout.addLayout(inner,1) + container_layout.addWidget(row_interval) + + row_check = QWidget() + row_check_layout = QHBoxLayout(row_check) + row_check_layout.setContentsMargins(0,0,0,0) + row_check_layout.setSpacing(10) + check_label = QLabel("性能检查间隔:") + check_label.setProperty('role','fieldLabel') + check_label.setAlignment(Qt.AlignRight|Qt.AlignVCenter) + row_check_layout.addWidget(check_label) + check_interval_combo = QComboBox() + check_interval_combo.addItems(["0.1秒","0.5秒","1.0秒","2.0秒"]) + current_check_interval = perf_config['check_interval'] + if current_check_interval == 0.1: + check_interval_combo.setCurrentIndex(0) + elif current_check_interval == 0.5: + check_interval_combo.setCurrentIndex(1) + elif current_check_interval == 1.0: + check_interval_combo.setCurrentIndex(2) + else: + check_interval_combo.setCurrentIndex(3) + row_check_layout.addWidget(check_interval_combo,1) + container_layout.addWidget(row_check) + + row_hist = QWidget(); row_hist_layout = QHBoxLayout(row_hist) + row_hist_layout.setContentsMargins(0,0,0,0) + row_hist_layout.setSpacing(10) + hist_label = QLabel("帧时间历史:") + hist_label.setProperty('role','fieldLabel') + hist_label.setAlignment(Qt.AlignRight|Qt.AlignVCenter) + row_hist_layout.addWidget(hist_label) + frame_history_spin = QSpinBox() + frame_history_spin.setMinimum(10) + frame_history_spin.setMaximum(1000) + frame_history_spin.setValue(perf_config['frame_history_size']) + frame_history_spin.setSuffix(" 帧") + row_hist_layout.addWidget(frame_history_spin,1) + container_layout.addWidget(row_hist) + + # 监控项目 + add_section_title("监控项目") + monitor_widget = QWidget() + monitor_layout = QVBoxLayout(monitor_widget) + monitor_layout.setContentsMargins(0,0,0,0) + monitor_layout.setSpacing(6) + cpu_check = QCheckBox("CPU使用率") + cpu_check.setChecked(perf_config['psutil_available']) + cpu_check.setEnabled(perf_config['psutil_available']) + monitor_layout.addWidget(cpu_check) + memory_check = QCheckBox("内存使用率") + memory_check.setChecked(perf_config['psutil_available']) + memory_check.setEnabled(perf_config['psutil_available']) + monitor_layout.addWidget(memory_check) + gpu_check = QCheckBox("GPU使用率") + gpu_check.setChecked(perf_config['gputil_available'] or perf_config['nvidia_ml_available']) + gpu_check.setEnabled(perf_config['gputil_available'] or perf_config['nvidia_ml_available']) + monitor_layout.addWidget(gpu_check) + frame_time_check = QCheckBox("帧时间统计") + frame_time_check.setChecked(True) + monitor_layout.addWidget(frame_time_check) + container_layout.addWidget(monitor_widget) + + separator_buttons = QFrame() + separator_buttons.setFrameShape(QFrame.HLine) + separator_buttons.setFrameShadow(QFrame.Plain) + separator_buttons.setFixedHeight(1) + separator_buttons.setStyleSheet("background-color: #2C2F36; border: none;") + container_layout.addWidget(separator_buttons) + button_layout = QHBoxLayout() + apply_button = QPushButton("应用") + reset_button = QPushButton("重置计数器") + ok_button = QPushButton("确定") + # ok_button.setObjectName("primaryButton") + cancel_button = QPushButton("取消") + button_layout.addWidget(apply_button) + button_layout.addWidget(reset_button) + button_layout.addStretch() + button_layout.addWidget(ok_button) + button_layout.addWidget(cancel_button) + container_layout.addLayout(button_layout) + + content_layout.addWidget(content_container, 0, Qt.AlignTop) + base_layout.addWidget(content_widget); main_layout.addWidget(base_frame) + + def apply_settings(): + try: + new_interval_seconds = interval_slider.value() + new_interval_frames = int(new_interval_seconds * 60) + vr_manager.set_performance_report_interval(new_interval_frames) + check_intervals = [0.1, 0.5, 1.0, 2.0] + new_check_interval = check_intervals[check_interval_combo.currentIndex()] + vr_manager.set_performance_check_interval(new_check_interval) + vr_manager.set_frame_time_history_size(frame_history_spin.value()) + UniversalMessageDialog.show_success(dialog, "成功", "VR调试设置已应用!", False, "确定") + except Exception as e: + UniversalMessageDialog.show_error(dialog, "错误", f"应用设置时发生错误:\n{str(e)}", False, "确定") + def reset_counters(): + try: + vr_manager.reset_performance_counters() + UniversalMessageDialog.show_success(dialog, "成功", "性能计数器已重置!", False, "确定") + except Exception as e: + UniversalMessageDialog.show_error(dialog, "错误", f"重置计数器时发生错误:\n{str(e)}", False, "确定") + + apply_button.clicked.connect(apply_settings) + reset_button.clicked.connect(reset_counters) + ok_button.clicked.connect(lambda: (apply_settings(), dialog.accept())) + cancel_button.clicked.connect(dialog.reject) + return dialog + + # ==================== VR测试模式事件处理 ==================== + + def onToggleVRTestMode(self): + """切换VR测试模式""" + try: + if hasattr(self.world, 'vr_manager') and self.world.vr_manager: + if self.vrTestModeAction.isChecked(): + # 启用VR测试模式 + success = self.world.vr_manager.enable_vr_test_mode(display_mode='stereo') + if success: + UniversalMessageDialog.show_info(self, "VR测试模式", + "VR测试模式已启用!\n\n现在VR渲染内容将直接显示在PC屏幕上,无需VR头显。\n\n特点:\n- 显示VR左右眼视图\n- 实时性能监控HUD\n- 复用完整VR渲染管线\n- 可测量纯渲染性能", + False, "确定") + print("✅ VR测试模式已启用") + + # 可选:自动开启性能测试 + self.world.vr_manager.run_vr_performance_test(duration_seconds=10) + else: + self.vrTestModeAction.setChecked(False) + UniversalMessageDialog.show_warning(self, "错误", "启用VR测试模式失败!", + False, "确定") + else: + # 禁用VR测试模式 + self.world.vr_manager.disable_vr_test_mode() + UniversalMessageDialog.show_info(self, "VR测试模式", "VR测试模式已禁用!", + False, "确定") + print("✅ VR测试模式已禁用") + else: + self.vrTestModeAction.setChecked(False) + UniversalMessageDialog.show_warning(self, "错误", "VR管理器不可用!", + False, "确定") + except Exception as e: + self.vrTestModeAction.setChecked(False) + UniversalMessageDialog.show_error(self, "错误", f"切换VR测试模式时发生错误:\n{str(e)}", + False, "确定") + + def onToggleVRTestSubmitTexture(self): + """切换VR测试模式纹理提交功能""" + try: + if hasattr(self.world, 'vr_manager') and self.world.vr_manager: + enabled = self.vrTestSubmitTextureAction.isChecked() + self.world.vr_manager.set_test_mode_features(submit_texture=enabled) + except Exception as e: + UniversalMessageDialog.show_error(self, "错误", f"设置纹理提交功能时发生错误:\n{str(e)}", + False, "确定") + + def onToggleVRTestWaitPoses(self): + """切换VR测试模式姿态等待功能""" + try: + if hasattr(self.world, 'vr_manager') and self.world.vr_manager: + enabled = self.vrTestWaitPosesAction.isChecked() + self.world.vr_manager.set_test_mode_features(wait_poses=enabled) + except Exception as e: + UniversalMessageDialog.show_error(self, "错误", f"设置姿态等待功能时发生错误:\n{str(e)}", + False, "确定") + + def onSetVRTestStep(self, step): + """设置VR测试步骤""" + try: + if hasattr(self.world, 'vr_manager') and self.world.vr_manager: + if step == 0: # 重置 + self.world.vr_manager.set_test_mode_features(submit_texture=False, wait_poses=False) + self.vrTestSubmitTextureAction.setChecked(False) + self.vrTestWaitPosesAction.setChecked(False) + UniversalMessageDialog.show_info(self, "VR测试", "已重置为基线状态:两个功能都禁用", + False, "确定") + elif step == 1: # 只启用纹理提交 + self.world.vr_manager.set_test_mode_features(submit_texture=True, wait_poses=False) + self.vrTestSubmitTextureAction.setChecked(True) + self.vrTestWaitPosesAction.setChecked(False) + UniversalMessageDialog.show_info(self, "VR测试", "步骤1:只启用纹理提交\n观察FPS变化来判断submit_texture是否影响性能", + False, "确定") + elif step == 2: # 只启用姿态等待 + self.world.vr_manager.set_test_mode_features(submit_texture=False, wait_poses=True) + self.vrTestSubmitTextureAction.setChecked(False) + self.vrTestWaitPosesAction.setChecked(True) + UniversalMessageDialog.show_info(self, "VR测试", "步骤2:只启用姿态等待\n观察FPS变化来判断waitGetPoses是否影响性能", + False, "确定") + elif step == 3: # 同时启用两者 + self.world.vr_manager.set_test_mode_features(submit_texture=True, wait_poses=True) + self.vrTestSubmitTextureAction.setChecked(True) + self.vrTestWaitPosesAction.setChecked(True) + UniversalMessageDialog.show_info(self, "VR测试", "步骤3:同时启用两者\n这应该完全复现普通VR模式的36FPS问题", + False, "确定") + except Exception as e: + UniversalMessageDialog.show_error(self, "错误", f"设置VR测试步骤时发生错误:\n{str(e)}", + False, "确定") + + +class PluginConfigDialog(QDialog): + def __init__(self,parent=None): + super().__init__(parent) + self.setWindowTitle("渲染管线插件配置") + self.setGeometry(200,200,400,500) + self.plugins_config_path = os.path.join("RenderPipelineFile", "config", "plugins.yaml") + self.init_ui() + self.load_plugins_config() + + def init_ui(self): + layout = QVBoxLayout() + + # 标题 + title_label = QLabel("启用的插件:") + layout.addWidget(title_label) + + # 插件列表 + self.plugins_list = QListWidget() + layout.addWidget(self.plugins_list) + + # 按钮布局 + button_layout = QHBoxLayout() + + # 全选/全不选按钮 + self.select_all_btn = QPushButton("全选") + self.select_all_btn.clicked.connect(self.select_all_plugins) + button_layout.addWidget(self.select_all_btn) + + self.deselect_all_btn = QPushButton("全不选") + self.deselect_all_btn.clicked.connect(self.deselect_all_plugins) + button_layout.addWidget(self.deselect_all_btn) + + button_layout.addStretch() + + # 对话框按钮 + self.button_box = QDialogButtonBox( + QDialogButtonBox.Ok | QDialogButtonBox.Cancel | QDialogButtonBox.Apply + ) + self.button_box.accepted.connect(self.accept) + self.button_box.rejected.connect(self.reject) + self.button_box.button(QDialogButtonBox.Apply).clicked.connect(self.apply_changes) + + button_layout.addWidget(self.button_box) + layout.addLayout(button_layout) + + self.setLayout(layout) + + def load_plugins_config(self): + """加载插件配置""" + try: + # 检查文件是否存在 + if not os.path.exists(self.plugins_config_path): + print(f"插件配置文件不存在: {self.plugins_config_path}") + return + + with open(self.plugins_config_path, 'r', encoding='utf-8') as f: + config = yaml.safe_load(f) + + enabled_plugins = config.get('enabled', []) + + # 清空列表 + self.plugins_list.clear() + + # 添加启用的插件 + for plugin in enabled_plugins: + item = QListWidgetItem(plugin) + item.setFlags(item.flags() | Qt.ItemIsUserCheckable) + item.setCheckState(Qt.Checked) + self.plugins_list.addItem(item) + + except Exception as e: + print(f"加载插件配置失败: {e}") + + def select_all_plugins(self): + """全选所有插件""" + for i in range(self.plugins_list.count()): + item = self.plugins_list.item(i) + item.setCheckState(Qt.Checked) + + def deselect_all_plugins(self): + """取消选择所有插件""" + for i in range(self.plugins_list.count()): + item = self.plugins_list.item(i) + item.setCheckState(Qt.Unchecked) + + def apply_changes(self): + """应用更改到配置文件,保留原有配置结构""" + try: + # 检查文件是否存在 + if not os.path.exists(self.plugins_config_path): + print(f"插件配置文件不存在: {self.plugins_config_path}") + return + + # 读取完整配置 + with open(self.plugins_config_path, 'r', encoding='utf-8') as f: + config = yaml.safe_load(f) or {} + + # 获取选中的插件 + enabled_plugins = [] + for i in range(self.plugins_list.count()): + item = self.plugins_list.item(i) + if item.checkState() == Qt.Checked: + enabled_plugins.append(item.text()) + + # 只更新enabled部分,保留其他配置 + config['enabled'] = enabled_plugins + + # 保存配置时保留原有格式(尽可能) + with open(self.plugins_config_path, 'w', encoding='utf-8') as f: + yaml.dump(config, f, default_flow_style=False, allow_unicode=True, + sort_keys=False, indent=4) + + print("插件配置已保存") + + except Exception as e: + print(f"保存插件配置失败: {e}") + + def accept(self): + """点击确定按钮时保存并关闭""" + self.apply_changes() + super().accept() + +def setup_main_window(world,path = None): + """设置主窗口的便利函数""" + app = QApplication.instance() + if app is None: + app = QApplication(sys.argv) + + main_window = MainWindow(world) + main_window.show() + from main import openProjectForPath + if path: + openProjectForPath(path,main_window) + + return app, main_window + +class AssemblyModeSelectionDialog(QDialog): + """拆装模式选择对话框""" + + def __init__(self, parent=None): + super().__init__(parent) + self.selected_mode = "training" # 默认选择训练模式 + self.setupUI() + + def setupUI(self): + self.setWindowTitle("选择拆装模式") + self.setFixedSize(400, 300) + self.setModal(True) + + layout = QVBoxLayout(self) + + # 标题 + title_label = QLabel("请选择拆装交互模式") + title_label.setStyleSheet("font-size: 16px; font-weight: bold; color: #2E86C1; margin: 10px;") + title_label.setAlignment(Qt.AlignCenter) + layout.addWidget(title_label) + + # 模式选择组 + mode_group = QGroupBox("模式选择") + mode_layout = QVBoxLayout(mode_group) + + # 训练模式 + self.training_radio = QRadioButton("训练模式") + self.training_radio.setChecked(True) # 默认选中 + self.training_radio.setStyleSheet("font-size: 14px; margin: 5px;") + mode_layout.addWidget(self.training_radio) + + training_desc = QTextEdit() + training_desc.setMaximumHeight(60) + training_desc.setReadOnly(True) + training_desc.setPlainText("• 显示详细的步骤描述和操作提示\n• 提供工具选择的正确性提示\n• 播放语音指导") + training_desc.setStyleSheet("background-color: #f0f8ff; border: 1px solid #ccc; margin-left: 20px;") + mode_layout.addWidget(training_desc) + + # 考核模式 + self.exam_radio = QRadioButton("考核模式") + self.exam_radio.setStyleSheet("font-size: 14px; margin: 5px;") + mode_layout.addWidget(self.exam_radio) + + exam_desc = QTextEdit() + exam_desc.setMaximumHeight(60) + exam_desc.setReadOnly(True) + exam_desc.setPlainText("• 不显示步骤描述\n• 工具选择错误时不提示,直接扣分\n• 不播放语音指导") + exam_desc.setStyleSheet("background-color: #fff5f5; border: 1px solid #ccc; margin-left: 20px;") + mode_layout.addWidget(exam_desc) + + layout.addWidget(mode_group) + + # 按钮 + button_layout = QHBoxLayout() + + self.ok_button = QPushButton("开始") + self.ok_button.setStyleSheet(""" + QPushButton { + background-color: #27AE60; + color: white; + font-size: 14px; + font-weight: bold; + padding: 8px 20px; + border: none; + border-radius: 4px; + } + QPushButton:hover { + background-color: #2ECC71; + } + """) + self.ok_button.clicked.connect(self.accept) + + self.cancel_button = QPushButton("取消") + self.cancel_button.setStyleSheet(""" + QPushButton { + background-color: #95A5A6; + color: white; + font-size: 14px; + padding: 8px 20px; + border: none; + border-radius: 4px; + } + QPushButton:hover { + background-color: #BDC3C7; + } + """) + self.cancel_button.clicked.connect(self.reject) + + button_layout.addStretch() + button_layout.addWidget(self.ok_button) + button_layout.addWidget(self.cancel_button) + + layout.addLayout(button_layout) + + # 连接单选按钮信号 + self.training_radio.toggled.connect(self.on_mode_changed) + self.exam_radio.toggled.connect(self.on_mode_changed) + + def on_mode_changed(self): + """模式改变时的处理""" + if self.training_radio.isChecked(): + self.selected_mode = "training" + elif self.exam_radio.isChecked(): + self.selected_mode = "exam" + print(f"🔄 模式选择改变: {self.selected_mode}") + + def get_selected_mode(self): + """获取选中的模式""" + return self.selected_mode + + +def setup_main_window(world,path = None): + """设置主窗口的便利函数""" + app = QApplication.instance() + if app is None: + # 修复 Windows 下 WebEngine 与 OpenGL 上下文共享问题 + QCoreApplication.setAttribute(Qt.AA_ShareOpenGLContexts) + if sys.platform == 'win32': + QCoreApplication.setAttribute(Qt.AA_UseDesktopOpenGL) + + # 添加 Chromium 参数禁用硬件加速,避免与 Panda3D 的 OpenGL 冲突 + sys.argv.extend([ + '--disable-gpu', + '--disable-software-rasterizer', + '--disable-gpu-compositing', + '--disable-accelerated-2d-canvas', + '--num-raster-threads=1' + ]) + app = QApplication(sys.argv) + print("✓ QApplication 已创建,QtWebEngine 硬件加速已禁用") + + main_window = MainWindow(world) + main_window.show() + from main import openProjectForPath + if path: + openProjectForPath(path,main_window) + + return app, main_window diff --git a/ui/widgets.py b/ui/widgets.py new file mode 100644 index 00000000..c667635b --- /dev/null +++ b/ui/widgets.py @@ -0,0 +1,4319 @@ +""" +自定义Qt部件模块 + +包含所有自定义的Qt界面组件: +- NewProjectDialog: 新建项目对话框 +- CustomPanda3DWidget: 自定义Panda3D显示部件 +- CustomFileView: 自定义文件浏览器 +- CustomTreeWidget: 自定义场景树部件 +""" + +import os +import re +from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, + QLineEdit, QPushButton, QLabel, + QTreeView, QTreeWidget, QTreeWidgetItem, QWidget, + QFileDialog, QMessageBox, QAbstractItemView, QMenu, QDockWidget, QButtonGroup, QToolButton, QFrame, QSizePolicy) +from PyQt5.QtCore import Qt, QUrl, QMimeData, QPoint, QSize +from PyQt5.QtGui import QDrag, QPainter, QPixmap, QPen, QBrush, QFont +from PyQt5.sip import wrapinstance +from direct.showbase.ShowBaseGlobal import aspect2d +from panda3d.core import ModelRoot, NodePath, CollisionNode + +from QMeta3D.QMeta3DWidget import QMeta3DWidget +from scene import util +from ui.icon_manager import get_icon_manager + +class NewProjectDialog(QDialog): + """新建项目对话框""" + + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle("新建项目") + self.setObjectName("newProjectDialog") + self.resize(508, 274) + self.setMinimumSize(508, 274) + self.setModal(True) + + # 移除默认窗口装饰,使用自定义顶部栏 + self.setWindowFlags(Qt.Dialog | Qt.FramelessWindowHint) + self.setAttribute(Qt.WA_TranslucentBackground, True) + + # 拖拽和窗口状态 + self.dragging = False + self.drag_position = QPoint() + self.icon_manager = get_icon_manager() + self._title_icon_size = QSize(18, 18) + self._icon_close = self.icon_manager.get_icon('close_icon', self._title_icon_size) + + # 设置严格按照Figma设计的样式 + self.setStyleSheet(""" + QDialog#newProjectDialog { + background-color: transparent; + color: #EBEBEB; + border: none; + } + QFrame#baseFrame { + background-color: #000000; + border: 1px solid #3E3E42; + border-radius: 5px; + } + QWidget#titleBar { + background-color: transparent; + border: none; + border-radius: 5px 5px 0px 0px; + min-height: 32px; + max-height: 32px; + } + QWidget#titleBar QWidget { + background-color: transparent; + border: none; + } + QLabel#titleLabel { + color: #FFFFFF; + font-family: 'Inter', 'Microsoft YaHei', sans-serif; + font-size: 14px; + font-weight: 500; + letter-spacing: 0.7px; + } + QWidget#controlButtons QPushButton { + background-color: transparent; + border: none; + color: #EBEBEB; + font-size: 14px; + min-width: 18px; + max-width: 18px; + min-height: 18px; + max-height: 18px; + padding: 0px; + border-radius: 3px; + } + QWidget#controlButtons QPushButton:hover { + background-color: #2A2D2E; + color: #FFFFFF; + } + QPushButton#closeButton { + border-radius: 0px 5px 0px 0px; + } + QPushButton#closeButton:hover { + background-color: #2A2D2E; + color: #FFFFFF; + } + QWidget#contentWidget { + background-color: transparent; + border-radius: 0px 0px 5px 5px; + } + QFrame#contentContainer { + background-color: #19191B; + border: 1px solid #2C2F36; + border-radius: 5px; + } + QLabel[role="sectionTitle"] { + color: #EBEBEB; + font-family: 'Inter', 'Microsoft YaHei', sans-serif; + font-size: 12px; + font-weight: 500; + letter-spacing: 0.6px; + padding: 0px; + margin-bottom: 0px; + } + QLabel[role="hint"] { + color: rgba(235, 235, 235, 0.6); + font-family: 'Inter', 'Microsoft YaHei', sans-serif; + font-size: 11px; + font-weight: 300; + letter-spacing: 0.55px; + margin-top: 0px; + padding: 0px; + } + QLineEdit { + background-color: rgba(89, 100, 113, 0.2); + color: #EBEBEB; + border: 1px solid rgba(76, 92, 110, 0.6); + border-radius: 2px; + padding: 6px 10px; + font-family: 'Inter', 'Microsoft YaHei', sans-serif; + font-size: 11px; + font-weight: 300; + letter-spacing: 0.55px; + min-height: 14px; + max-height: 30px; + } + QLineEdit:focus { + border: 1px solid #3067C0; + background-color: rgba(48, 103, 192, 0.1); + } + QLineEdit:hover { + border: 1px solid #3067C0; + background-color: rgba(89, 100, 113, 0.3); + } + QLineEdit:disabled { + background-color: rgba(89, 100, 113, 0.1); + color: rgba(235, 235, 235, 0.4); + border: 1px solid rgba(76, 92, 110, 0.2); + } + QPushButton { + background-color: rgba(89, 98, 118, 0.5); + color: #EBEBEB; + border: none; + border-radius: 2px; + padding: 0px 0px; + font-family: 'Inter', 'Microsoft YaHei', sans-serif; + font-weight: 300; + font-size: 10px; + letter-spacing: 0.5px; + min-width: 90px; + min-height: 30px; + max-height: 30px; + } + QPushButton:hover { + background-color: #3067C0; + color: #FFFFFF; + } + QPushButton:pressed { + background-color: #2556A0; + color: #FFFFFF; + } + QPushButton:disabled { + background-color: rgba(89, 98, 118, 0.3); + color: rgba(235, 235, 235, 0.4); + } + QPushButton#primaryButton { + background-color: rgba(89, 98, 118, 0.5); + border: none; + color: #EBEBEB; + font-weight: 300; + min-width: 120px; + max-width: 120px; + } + QPushButton#primaryButton:hover { + background-color: #2556A0; + } + QPushButton#primaryButton:pressed { + background-color: #1E4A8C; + } + QPushButton#secondaryButton { + background-color: rgba(89, 98, 118, 0.5); + border: none; + color: #EBEBEB; + min-width: 120px; + max-width: 120px; + } + QPushButton#secondaryButton:hover { + background-color: #3067C0; + color: #FFFFFF; + } + QPushButton#secondaryButton:pressed { + background-color: #2556A0; + color: #FFFFFF; + } + QPushButton#browseButton { + min-width: 90px; + max-width: 90px; + min-height: 28px; + max-height: 28px; + } + """) + + # 创建主布局 + main_layout = QVBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.setSpacing(0) + + # 创建基础容器,负责绘制圆角和边框 + base_frame = QFrame() + base_frame.setObjectName('baseFrame') + base_frame.setFrameShape(QFrame.NoFrame) + base_frame.setAttribute(Qt.WA_StyledBackground, True) + + base_layout = QVBoxLayout(base_frame) + base_layout.setContentsMargins(0, 0, 0, 0) + base_layout.setSpacing(0) + + # 创建自定义顶部栏 + self.createTitleBar() + base_layout.addWidget(self.title_bar) + + # 创建内容区域 + content_widget = QWidget() + content_widget.setObjectName('contentWidget') + content_layout = QVBoxLayout(content_widget) + content_layout.setContentsMargins(10, 10, 10, 10) + content_layout.setSpacing(0) + + content_container = QFrame() + content_container.setObjectName('contentContainer') + content_container.setFrameShape(QFrame.NoFrame) + content_container.setAttribute(Qt.WA_StyledBackground, True) + content_container.setFixedWidth(488) + content_container.setMinimumHeight(213) + + container_layout = QVBoxLayout(content_container) + container_layout.setContentsMargins(15, 10, 15, 10) + container_layout.setSpacing(10) + + pathLabel = QLabel('项目路径') + pathLabel.setProperty('role', 'sectionTitle') + container_layout.addWidget(pathLabel) + + path_row_widget = QWidget() + path_row_layout = QHBoxLayout(path_row_widget) + path_row_layout.setContentsMargins(0, 0, 0, 0) + path_row_layout.setSpacing(10) + path_row_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + self.pathEdit = QLineEdit() + self.pathEdit.setReadOnly(True) + self.pathEdit.setPlaceholderText('请选择项目路径') + self.pathEdit.setMinimumWidth(358) + self.pathEdit.setFixedHeight(30) + path_row_layout.addWidget(self.pathEdit) + + browseButton = QPushButton('浏览...') + browseButton.setObjectName('browseButton') + browseButton.clicked.connect(self.browsePath) + path_row_layout.addWidget(browseButton) + container_layout.addWidget(path_row_widget) + + nameLabel = QLabel('项目名称') + nameLabel.setProperty('role', 'sectionTitle') + container_layout.addWidget(nameLabel) + + self.nameEdit = QLineEdit() + self.nameEdit.setText('新项目') + self.nameEdit.setPlaceholderText('请输入项目名称') + self.nameEdit.selectAll() + self.nameEdit.setFixedHeight(30) + container_layout.addWidget(self.nameEdit) + + self.tipLabel = QLabel('项目名称只能包含字母、数字、下划线、中划线和中文') + self.tipLabel.setProperty('role', 'hint') + container_layout.addWidget(self.tipLabel) + + separator = QFrame() + separator.setFrameShape(QFrame.HLine) + separator.setFrameShadow(QFrame.Plain) + separator.setFixedHeight(1) + separator.setStyleSheet('background-color: #2C2F36; border: none;') + container_layout.addWidget(separator) + + button_row_widget = QWidget() + button_row_layout = QHBoxLayout(button_row_widget) + button_row_layout.setContentsMargins(0, 0, 0, 0) + button_row_layout.setSpacing(10) + button_row_layout.addStretch() + button_row_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + + self.cancelButton = QPushButton('取消') + self.cancelButton.setObjectName('secondaryButton') + self.cancelButton.clicked.connect(self.reject) + self.cancelButton.setFixedSize(120, 30) + button_row_layout.addWidget(self.cancelButton) + + self.confirmButton = QPushButton('确认') + self.confirmButton.setObjectName('primaryButton') + self.confirmButton.clicked.connect(self.validate) + self.confirmButton.setFixedSize(120, 30) + button_row_layout.addWidget(self.confirmButton) + + container_layout.addWidget(button_row_widget) + + self.confirmButton.setDefault(True) + self.confirmButton.setAutoDefault(True) + self.cancelButton.setAutoDefault(False) + + content_layout.addWidget(content_container, 0, Qt.AlignTop) + base_layout.addWidget(content_widget) + main_layout.addWidget(base_frame) + + self.projectPath = "" + self.projectName = "" + + def createTitleBar(self): + """创建自定义顶部栏""" + self.title_bar = QFrame() + self.title_bar.setObjectName("titleBar") + + title_layout = QHBoxLayout(self.title_bar) + title_layout.setContentsMargins(12, 0, 12, 0) + title_layout.setSpacing(0) + + # Control buttons area + controls = QWidget() + controls.setObjectName("controlButtons") + controls_layout = QHBoxLayout(controls) + controls_layout.setContentsMargins(0, 0, 0, 0) + controls_layout.setSpacing(0) + + self.close_button = QPushButton() + self.close_button.setObjectName("closeButton") + self.close_button.clicked.connect(self.reject) + self.close_button.setFocusPolicy(Qt.NoFocus) + controls_layout.addWidget(self.close_button) + + self._applyTitleBarIcons() + + # Reserve left space equal to control buttons to keep title centered + left_placeholder = QWidget() + left_placeholder.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + title_layout.addWidget(left_placeholder) + + self.title_label = QLabel(self.windowTitle()) + self.title_label.setObjectName("titleLabel") + self.title_label.setAlignment(Qt.AlignCenter) + title_layout.addWidget(self.title_label, 1) + + title_layout.addWidget(controls) + + left_placeholder.setFixedWidth(controls.sizeHint().width()) + + + def _applyTitleBarIcons(self): + """为标题栏按钮应用图标""" + if self._icon_close: + self.close_button.setIcon(self._icon_close) + self.close_button.setIconSize(self._title_icon_size) + self.close_button.setText("") + self.close_button.setToolTip("关闭") + + + def toggleMaximize(self): + """切换窗口最大化/还原""" + if self.isMaximized(): + self.showNormal() + else: + self.showMaximized() + + + def setWindowTitle(self, title): + super().setWindowTitle(title) + if hasattr(self, "title_label"): + self.title_label.setText(title) + + + def mousePressEvent(self, event): + """鼠标按下事件 - 用于拖拽窗口""" + if event.button() == Qt.LeftButton and self.title_bar.geometry().contains(event.pos()): + if self.isMaximized(): + self.showNormal() + self.dragging = True + self.drag_position = event.globalPos() - self.frameGeometry().topLeft() + event.accept() + super().mousePressEvent(event) + + def mouseDoubleClickEvent(self, event): + """双击标题栏切换最大化""" + if event.button() == Qt.LeftButton and self.title_bar.geometry().contains(event.pos()): + self.toggleMaximize() + event.accept() + return + super().mouseDoubleClickEvent(event) + + + def mouseMoveEvent(self, event): + """鼠标移动事件 - 用于拖拽窗口""" + if event.buttons() == Qt.LeftButton and self.dragging: + self.move(event.globalPos() - self.drag_position) + event.accept() + super().mouseMoveEvent(event) + + def mouseReleaseEvent(self, event): + """鼠标释放事件 - 停止拖拽""" + if event.button() == Qt.LeftButton: + self.dragging = False + super().mouseReleaseEvent(event) + + def browsePath(self): + """浏览选择项目路径""" + path = QFileDialog.getExistingDirectory(self, "选择项目路径") + if path: + self.pathEdit.setText(path) + + def validate(self): + """验证输入并关闭对话框""" + # 获取并验证路径 + self.projectPath = self.pathEdit.text() + if not self.projectPath: + UniversalMessageDialog.show_warning(self, "错误", "请选择项目路径!", show_cancel=False, confirm_text="确认") + return + + # 获取并验证项目名称 + self.projectName = self.nameEdit.text().strip() + if not self.projectName: + UniversalMessageDialog.show_warning(self, "错误", "请输入项目名称!", show_cancel=False, confirm_text="确认") + return + + # 验证项目名称格式 + if not re.match(r'^[a-zA-Z0-9_\-\u4e00-\u9fa5]+$', self.projectName): + UniversalMessageDialog.show_warning(self, "错误", + "项目名称只能包含字母、数字、下划线、中划线和中文!", show_cancel=False, confirm_text="确认") + return + + # 检查项目是否已存在 + full_path = os.path.join(self.projectPath, self.projectName) + if os.path.exists(full_path): + UniversalMessageDialog.show_warning(self, "错误", "项目已存在!", show_cancel=False, confirm_text="确认") + return + + self.accept() + +class CustomMeta3DWidget(QMeta3DWidget): + """自定义Panda3D显示部件""" + + def __init__(self, world, parent=None): + if parent is None: + parent = wrapinstance(0, QWidget) + super().__init__(world, parent) + self.world = world + self.setFocusPolicy(Qt.StrongFocus) # 允许接收键盘焦点 + self.setAcceptDrops(True) # 允许接收拖放 + self.setMouseTracking(True) # 启用鼠标追踪 + + # 让world引用这个widget,以便获取准确的尺寸 + if hasattr(world, 'setQtWidget'): + world.setQtWidget(self) + + def getActualSize(self): + """获取Qt部件的实际渲染尺寸""" + return (self.width(), self.height()) + + def dragEnterEvent(self, event): + """处理拖拽进入事件""" + # 检查是否是文件拖拽 + if event.mimeData().hasUrls(): + # 检查是否包含支持的模型文件 + for url in event.mimeData().urls(): + filepath = url.toLocalFile() + if filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')): + event.acceptProposedAction() + return + event.ignore() + + def dragMoveEvent(self, event): + """处理拖拽移动事件""" + if event.mimeData().hasUrls(): + event.acceptProposedAction() + else: + event.ignore() + + def dropEvent(self, event): + """处理拖放事件""" + if event.mimeData().hasUrls(): + for url in event.mimeData().urls(): + filepath = url.toLocalFile() + if filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')): + # 使用关键字参数确保兼容性 + self.world.importModel(filepath) + event.acceptProposedAction() + else: + event.ignore() + + def wheelEvent(self, event): + """处理滚轮事件""" + if event.angleDelta().y() > 0: + # 滚轮向前滚动 + self.world.wheelForward() + else: + # 滚轮向后滚动 + self.world.wheelBackward() + event.accept() + + def mousePressEvent(self, event): + """处理 Qt 鼠标按下事件""" + if event.button() == Qt.LeftButton: + self.world.mousePressEventLeft({ + 'x': event.x(), + 'y': event.y() + }) + elif event.button() == Qt.RightButton: + self.world.mousePressEventRight({ + 'x': event.x(), + 'y': event.y() + }) + elif event.button() == Qt.MiddleButton: # 添加滑轮按下事件处理 + self.world.mousePressEventMiddle({ + 'x': event.x(), + 'y': event.y() + }) + event.accept() + + def mouseReleaseEvent(self, event): + """处理 Qt 鼠标释放事件""" + if event.button() == Qt.LeftButton: + self.world.mouseReleaseEventLeft({ + 'x': event.x(), + 'y': event.y() + }) + elif event.button() == Qt.RightButton: + self.world.mouseReleaseEventRight({ + 'x': event.x(), + 'y': event.y() + }) + elif event.button() == Qt.MiddleButton: # 添加滑轮释放事件处理 + self.world.mouseReleaseEventMiddle({ + 'x': event.x(), + 'y': event.y() + }) + event.accept() + + def mouseMoveEvent(self, event): + """处理 Qt 鼠标移动事件""" + self.world.mouseMoveEvent({ + 'x': event.x(), + 'y': event.y() + }) + event.accept() + + def cleanup(self): + """清理Panda3D资源""" + try: + print("🧹 清理CustomPanda3DWidget资源...") + + # 清理world资源 + if hasattr(self, 'world') and self.world: + # 如果world有cleanup方法,调用它 + if hasattr(self.world, 'cleanup'): + self.world.cleanup() + # 清理world引用 + self.world = None + + # 调用父类的清理方法(如果存在) + if hasattr(super(), 'cleanup'): + super().cleanup() + + print("✅ CustomPanda3DWidget资源清理完成") + + except Exception as e: + print(f"⚠️ 清理CustomPanda3DWidget资源时出错: {e}") + +class CustomFileView(QTreeView): + """自定义文件浏览器""" + + def __init__(self, world, parent=None): + if parent is None: + parent = wrapinstance(0, QWidget) + super().__init__(parent) + base_font = self.font() + base_font.setFamily("Inter") + base_font.setPointSize(10) + base_font.setWeight(QFont.Normal) + self.setFont(base_font) + if self.model(): + self.model().rowsInserted.connect(self._handle_rows_inserted) + self.world = world + self.setupUI() + self.setupDragDrop() + + def setupUI(self): + """初始化UI设置""" + self.setHeaderHidden(True) + # 启用多选和拖拽 + self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) + self.setDropIndicatorShown(True) # 启用拖放指示线 + + def setupDragDrop(self): + """设置拖拽功能""" + # 使用自定义拖拽模式 + self.setDragDropMode(QTreeView.DragOnly) # 只允许拖出,不允许拖入 + self.setDefaultDropAction(Qt.DropAction.MoveAction) + self.setDragEnabled(True) + self.setAcceptDrops(True) + + def startDrag(self, supportedActions): + """开始拖拽操作""" + # 获取选中的文件 + indexes = self.selectedIndexes() + if not indexes: + return + + # 只处理文件名列 + indexes = [idx for idx in indexes if idx.column() == 0] + + # 创建 MIME 数据 + mimeData = self.model().mimeData(indexes) + + # 检查是否包含支持的模型文件 + urls = [] + for index in indexes: + filepath = self.model().filePath(index) + if filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')): + urls.append(QUrl.fromLocalFile(filepath)) + + if not urls: + return + + # 设置 URL 列表 + mimeData.setUrls(urls) + + # 创建拖拽对象 + drag = QDrag(self) + drag.setMimeData(mimeData) + + # 设置拖拽图标(可选) + pixmap = QPixmap(32, 32) + pixmap.fill(Qt.transparent) + painter = QPainter(pixmap) + painter.drawText(pixmap.rect(), Qt.AlignCenter, str(len(urls))) + painter.end() + drag.setPixmap(pixmap) + + # 执行拖拽 + drag.exec_(supportedActions) + + def mouseDoubleClickEvent(self, event): + """双击标题栏切换最大化""" + index = self.indexAt(event.pos()) + if index.isValid(): + model = self.model() + filepath = model.filePath(index) + + # 检查是否是模型文件 + if filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')): + self.world.importModel(filepath) + else: + print("不支持的文件类型") + super().mouseDoubleClickEvent(event) + +from PyQt5.QtCore import QFileSystemWatcher,QTimer +import os + +class CustomAssetsTreeWidget(QTreeWidget): + def __init__(self, world, parent=None): + if parent is None: + parent = wrapinstance(0, QWidget) + super().__init__(parent) + self.world = world + self.root_path = None + self.setupUI() + self.setupDragDrop() + + #添加文件系统监控器 + self.file_watcher = QFileSystemWatcher() + self.file_watcher.directoryChanged.connect(self.onDirectoryChanged) + self.file_watcher.fileChanged.connect(self.onFileChanged) + + self.refresh_timer = QTimer() + self.refresh_timer.setSingleShot(True) + self.refresh_timer.timeout.connect(self.refreshView) + + #存储监控的目录 + self.watched_directories = set() + + # 默认加载项目根路径 + self.load_file_tree() + # 设置右键菜单 + self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) + self.customContextMenuRequested.connect(self.showContextMenu) + + def showContextMenu(self, position): + """显示右键菜单""" + item = self.itemAt(position) + if not item: + return + + filepath = item.data(0, Qt.UserRole) + is_folder = item.data(0, Qt.UserRole + 1) + + menu = QMenu(self) + + if is_folder: + # 文件夹右键菜单 + menu.addAction("📁 新建文件夹", lambda: self.createNewFolder(item)) + menu.addAction("📄 新建文件", lambda: self.createNewFile(item)) + menu.addSeparator() + menu.addAction("✏️ 重命名", lambda: self.renameItem(item)) + menu.addAction("🗑️ 删除", lambda: self.deleteItem(item)) + menu.addSeparator() + menu.addAction("📋 复制路径", lambda: self.copyPath(filepath)) + menu.addAction("🔍 查看属性", lambda: self.showProperties(item)) + else: + # 文件右键菜单 + menu.addAction("📂 打开文件", lambda: self.openFile(filepath)) + menu.addSeparator() + menu.addAction("✏️ 重命名", lambda: self.renameItem(item)) + menu.addAction("🗑️ 删除", lambda: self.deleteItem(item)) + menu.addSeparator() + menu.addAction("📋 复制路径", lambda: self.copyPath(filepath)) + menu.addAction("🔍 查看属性", lambda: self.showProperties(item)) + + # 显示菜单 + menu.exec_(self.mapToGlobal(position)) + + def _saveExpandedState(self): + """保存展开状态""" + expanded_paths = set() + + def collectExpanded(item): + if item.isExpanded(): + path = item.data(0, Qt.UserRole) + if path: + expanded_paths.add(path) + + for i in range(item.childCount()): + collectExpanded(item.child(i)) + + # 遍历所有顶级项目 + for i in range(self.topLevelItemCount()): + collectExpanded(self.topLevelItem(i)) + + return expanded_paths + + def _restoreExpandedState(self, expanded_paths): + """恢复展开状态""" + def restoreExpanded(item): + path = item.data(0, Qt.UserRole) + if path in expanded_paths: + item.setExpanded(True) + + for i in range(item.childCount()): + restoreExpanded(item.child(i)) + + # 遍历所有顶级项目 + for i in range(self.topLevelItemCount()): + restoreExpanded(self.topLevelItem(i)) + + def _refreshWithStatePreservation(self): + """刷新树形视图并保持状态""" + # 保存当前状态 + expanded_paths = self._saveExpandedState() + + # 刷新树形结构 + self.load_file_tree() + + # 恢复展开状态 + self._restoreExpandedState(expanded_paths) + + def createNewFolder(self, parent_item): + """新建文件夹""" + import os + + parent_path = parent_item.data(0, Qt.UserRole) + dialog = StyledTextInputDialog(self, "新建文件夹", "文件夹名称", "请输入文件夹名称") + if dialog.exec_() != QDialog.Accepted: + return + + folder_name = dialog.text() + if not folder_name: + return + + new_folder_path = os.path.join(parent_path, folder_name) + try: + os.makedirs(new_folder_path, exist_ok=True) + self._refreshWithStatePreservation() + print(f"新建文件夹: {new_folder_path}") + except OSError as e: + print(f"新建文件夹失败: {e}") + + + def createNewFile(self, parent_item): + """新建文件""" + import os + + parent_path = parent_item.data(0, Qt.UserRole) + dialog = StyledTextInputDialog(self, "新建文件", "文件名称", "请输入文件名称") + if dialog.exec_() != QDialog.Accepted: + return + + file_name = dialog.text() + if not file_name: + return + + new_file_path = os.path.join(parent_path, file_name) + try: + with open(new_file_path, "w", encoding="utf-8") as f: + f.write("") + self._refreshWithStatePreservation() + print(f"新建文件: {new_file_path}") + except OSError as e: + print(f"新建文件失败: {e}") + + + def renameItem(self, item): + """重命名文件或文件夹""" + import os + + old_path = item.data(0, Qt.UserRole) + old_name = os.path.basename(old_path) + + dialog = StyledTextInputDialog(self, "重命名", "新名称", "请输入新名称", old_name) + if dialog.exec_() != QDialog.Accepted: + return + + new_name = dialog.text() + if not new_name or new_name == old_name: + return + + parent_dir = os.path.dirname(old_path) + new_path = os.path.join(parent_dir, new_name) + + try: + os.rename(old_path, new_path) + item.setText(0, new_name) + item.setData(0, Qt.UserRole, new_path) + self._refreshWithStatePreservation() + except OSError as e: + print(f"重命名失败: {e}") + + + def deleteItem(self, item): + """删除文件或文件夹""" + import os + import shutil + + filepath = item.data(0, Qt.UserRole) + is_folder = item.data(0, Qt.UserRole + 1) + + item_type = "文件夹" if is_folder else "文件" + result = UniversalMessageDialog.show_warning( + self, + "确认删除", + f"确定要删除这个{item_type}吗?\n{filepath}", + show_cancel=True, + confirm_text="删除", + cancel_text="取消" + ) + + if result == QDialog.Accepted: + try: + if is_folder: + shutil.rmtree(filepath) + else: + os.remove(filepath) + self._refreshWithStatePreservation() + print(f"删除{item_type}: {filepath}") + except OSError as e: + print(f"删除{item_type}失败: {e}") + UniversalMessageDialog.show_error( + self, + "错误", + f"删除{item_type}失败: {e}", + show_cancel=False, + confirm_text="确认" + ) + + def copyPath(self, filepath): + """复制路径到剪贴板""" + from PyQt5.QtWidgets import QApplication + + clipboard = QApplication.clipboard() + clipboard.setText(filepath) + print(f"已复制路径: {filepath}") + + def openFile(self, filepath): + """打开文件""" + import os + import subprocess + import platform + + try: + system = platform.system() + if system == "Windows": + os.startfile(filepath) + elif system == "Darwin": # macOS + subprocess.run(["open", filepath]) + else: # Linux + subprocess.run(["xdg-open", filepath]) + print(f"打开文件: {filepath}") + except Exception as e: + print(f"打开文件失败: {e}") + + def showProperties(self, item): + """显示属性面板""" + import os + + filepath = item.data(0, Qt.UserRole) + is_folder = item.data(0, Qt.UserRole + 1) + + try: + stat = os.stat(filepath) + size = stat.st_size + modified = os.path.getmtime(filepath) + + import datetime + modified_str = datetime.datetime.fromtimestamp(modified).strftime('%Y-%m-%d %H:%M:%S') + + item_type = "文件夹" if is_folder else "文件" + size_str = f"{size} 字节" if not is_folder else "文件夹" + + properties = f""" + 路径: {filepath} + 类型: {item_type} + 大小: {size_str} + 修改时间: {modified_str} + """ + + UniversalMessageDialog.show_info( + self, + "属性", + properties.strip(), + show_cancel=False, + confirm_text="确认" + ) + + except OSError as e: + UniversalMessageDialog.show_warning( + self, + "错误", + f"无法获取属性: {e}", + show_cancel=False, + confirm_text="确认" + ) + + + # def mouseDoubleClickEvent(self, event): + # """处理双击事件""" + # item = self.itemAt(event.pos()) + # if item: + # filepath = item.data(0, Qt.UserRole) + # is_folder = item.data(0, Qt.UserRole + 1) + # + # if is_folder: + # # 文件夹:展开/折叠 + # item.setExpanded(not item.isExpanded()) + # else: + # # 文件:检查是否是模型文件 + # if filepath and filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')): + # self.world.importModel(filepath) + # else: + # # 其他文件:用系统默认程序打开 + # self.openFile(filepath) + # + # super().mouseDoubleClickEvent(event) + + def setupUI(self): + """初始化UI设置""" + self.setHeaderHidden(True) + # 启用多选和拖拽 + self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) + self.setDropIndicatorShown(True) # 启用拖放指示线 + + def setupDragDrop(self): + """设置拖拽功能""" + # 使用InternalMove模式以正确显示插入指示线 + self.setDragDropMode(QAbstractItemView.InternalMove) + self.setDefaultDropAction(Qt.DropAction.MoveAction) + self.setDragEnabled(True) + self.setAcceptDrops(True) + self.setDropIndicatorShown(True) + + def getProjectRootPath(self): + """获取项目根路径下的Resources文件夹,考虑跨平台""" + import os + + # 获取当前文件所在目录,然后向上查找项目根目录 + current_dir = os.path.dirname(os.path.abspath(__file__)) + + # 向上查找直到找到项目根目录(包含特定标识文件或文件夹) + project_root = current_dir + max_depth = 10 # 限制向上查找的深度 + depth = 0 + + while depth < max_depth: + # 检查是否是项目根目录(可以根据实际情况调整判断条件) + if (os.path.exists(os.path.join(project_root, "main.py")) or + os.path.exists(os.path.join(project_root, "setup.py")) or + os.path.exists(os.path.join(project_root, ".git"))): + break + parent_dir = os.path.dirname(project_root) + if parent_dir == project_root: # 已经到达文件系统根目录 + # 回退到使用当前工作目录 + project_root = os.getcwd() + break + project_root = parent_dir + depth += 1 + + # 构建Resources文件夹路径(跨平台) + resources_path = os.path.join(project_root, "Resources") + + # 如果Resources文件夹不存在,创建它 + if not os.path.exists(resources_path): + try: + os.makedirs(resources_path, exist_ok=True) + print(f"创建Resources文件夹: {resources_path}") + except OSError as e: + print(f"无法创建Resources文件夹: {e}") + # 如果无法创建,回退到项目根路径 + return project_root + + return resources_path + + # def getProjectRootPath(self): + # """获取项目根路径下的Resources文件夹,考虑跨平台""" + # import os + # + # # 获取项目根路径 + # project_root = os.getcwd() + # + # # 构建Resources文件夹路径(跨平台) + # resources_path = os.path.join(project_root, "Resources") + # + # # 如果Resources文件夹不存在,创建它 + # if not os.path.exists(resources_path): + # try: + # os.makedirs(resources_path, exist_ok=True) + # print(f"创建Resources文件夹: {resources_path}") + # except OSError as e: + # print(f"无法创建Resources文件夹: {e}") + # # 如果无法创建,回退到项目根路径 + # return project_root + # + # return resources_path + + def load_file_tree(self): + """加载树形视图""" + self.clear() + self.current_path = self.getProjectRootPath() + try: + # 创建当前目录的根节点 + root_name = os.path.basename(self.current_path) or self.current_path + root_item = QTreeWidgetItem([f"📁 {root_name}"]) + root_item.setData(0, Qt.UserRole, self.current_path) + root_item.setData(0, Qt.UserRole + 1, True) + font = QFont("Microsoft YaHei", 10, QFont.Light) # 假设您希望字体大小为10 + root_item.setFont(0, font) + self.addTopLevelItem(root_item) + + # 加载当前目录内容 + self.load_directory_tree(self.current_path, root_item) + + #添加目录到监控器 + self.addWatchedDirectory(self.current_path) + + # 展开根节点 + root_item.setExpanded(True) + + except PermissionError: + error_item = QTreeWidgetItem(["❌ 无权限访问此目录"]) + self.addTopLevelItem(error_item) + + def addWatchedDirectory(self,directory): + """添加监控目录""" + if os.path.exists(directory) and directory not in self.watched_directories: + if self.file_watcher.addPath(directory): + self.watched_directories.add(directory) + #print(f"开始监控目录:{directory}") + try: + for item in os.listdir(directory): + item_path = os.path.join(directory,item) + if os.path.isdir(item_path): + self.addWatchedDirectory(item_path) + except Exception as e: + pass + else: + print(f"无法监控目录:{directory}") + + def removeWatchedDirectory(self,directory): + """移除监控目录""" + if directory in self.watched_directories: + if self.file_watcher.removePath(directory): + self.watched_directories.discard(directory) + print(f"停止监控目录:{directory}") + else: + print(f"无法停止监控目录:{directory}") + + def onDirectoryChanged(self,path): + """目录发生变化时处理""" + print(f"目录变化{path}") + if not self.refresh_timer.isActive(): + self.refresh_timer.start(1000) + + def onFileChanged(self,path): + """目录发生变化时的处理""" + print(f"目录变化{path}") + if not self.refresh_timer.isActive(): + self.refresh_timer.start(1000) + + def refreshView(self): + """刷新视图""" + print("刷新资源视图...") + try: + expanded_paths = self._saveExpandedState() + self.load_file_tree() + self._restoreExpandedState(expanded_paths) + print("资源视图刷新完成") + except Exception as e: + print(f"刷新资源视图失败{e}") + + def load_directory_tree(self, path, parent_item, max_depth=3, current_depth=0): + """递归加载目录树(类似左侧导航面板)""" + if current_depth >= max_depth: + return + + try: + items = os.listdir(path) + items.sort() + + # 分别处理文件夹和文件 + folders = [] + files = [] + + for item in items: + # 跳过隐藏文件和系统文件 + if item.startswith('.') or item.startswith('__'): + continue + + item_path = os.path.join(path, item) + if os.path.isdir(item_path): + folders.append(item) + elif os.path.isfile(item_path): + files.append(item) + + # 先添加文件夹 + for folder in folders: + folder_path = os.path.join(path, folder) + folder_item = self.create_simple_tree_item(folder, folder_path, True) + parent_item.addChild(folder_item) + + # 递归加载子目录(限制深度) + if current_depth < max_depth - 1: + self.load_directory_tree(folder_path, folder_item, max_depth, current_depth + 1) + + # 再添加文件(显示重要文件类型,包括图片和模型) + important_extensions = { + # 编程文件 + '.py', '.js', '.html', '.css', '.java', '.cpp', '.c', '.php', '.rb', '.go', '.rs', + # 文档文件 + '.md', '.txt', '.pdf', '.doc', '.docx', '.rtf', + # 配置和数据文件 + '.json', '.xml', '.yaml', '.yml', '.ini', '.cfg', '.toml', + # 图片文件 + '.jpg', '.jpeg', '.png', '.gif', '.bmp', '.svg', '.ico', '.webp', '.tiff', '.tif', + # 3D模型文件 + '.fbx', '.obj', '.3ds', '.max', '.blend', '.dae', '.gltf', '.glb', '.stl', '.ply', + # 音视频文件 + '.mp3', '.wav', '.mp4', '.avi', '.mov', '.wmv', '.flac', '.aac', + # 压缩文件 + '.zip', '.rar', '.7z', '.tar', '.gz', + # 材质和纹理 + '.mtl', '.mat', '.exr', '.hdr', '.hdri', '.dds', + # CAD文件 + '.dwg', '.dxf', '.step', '.stp', '.iges', '.sldprt', '.sldasm', + # 其他重要文件 + '.exe', '.dll', '.bat', '.sh', '.log' + } + + # 重要文件名(不依赖扩展名) + important_files = {'requirements.txt', 'README.md', 'main.py', 'LICENSE', 'CHANGELOG.md', 'INSTALL.md'} + + for file in files: + file_ext = os.path.splitext(file)[1].lower() + if file_ext in important_extensions or file in important_files: + file_path = os.path.join(path, file) + file_item = self.create_simple_tree_item(file, file_path, False) + parent_item.addChild(file_item) + + except (OSError, PermissionError): + pass + + def create_simple_tree_item(self, name, path, is_folder): + """创建简单的树形项目""" + try: + if is_folder: + # 文件夹项目,展开/折叠图标由CSS样式控制 + item = QTreeWidgetItem([f"📁 {name}"]) + item.setData(0, Qt.UserRole + 1, True) # 标记为文件夹 + else: + icon = self.get_file_icon(name) + item = QTreeWidgetItem([f"{icon} {name}"]) + item.setData(0, Qt.UserRole + 1, False) # 标记为文件 + + item.setData(0, Qt.UserRole, path) + + return item + + except (OSError, PermissionError): + item = QTreeWidgetItem([name]) + item.setData(0, Qt.UserRole, path) + return item + + def get_file_icon(self, filename): + """ + 根据文件扩展名获取图标 + + 这个函数根据文件的扩展名返回对应的Unicode图标字符。 + 如果文件类型不在映射表中,返回默认的文档图标。 + + 参数: + filename (str): 文件名(包含扩展名) + + 返回值: + str: 对应的Unicode图标字符 + """ + # 获取文件扩展名并转换为小写 + ext = os.path.splitext(filename)[1].lower() + + # 文件扩展名到图标的映射表 + icon_map = { + # 编程语言文件 + '.py': '🐍', # Python文件 + '.js': '⚡', # JavaScript文件 + '.html': '🌐', # HTML文件 + '.css': '🎨', # CSS样式文件 + '.java': '☕', # Java文件 + '.cpp': '⚙️', # C++文件 + '.c': '⚙️', # C文件 + '.h': '📋', # 头文件 + '.php': '🐘', # PHP文件 + '.rb': '💎', # Ruby文件 + '.go': '🐹', # Go文件 + '.rs': '🦀', # Rust文件 + '.swift': '🐦', # Swift文件 + '.kt': '🎯', # Kotlin文件 + + # 文档文件 + '.txt': '📄', # 纯文本文件 + '.md': '📝', # Markdown文档 + '.rst': '📝', # reStructuredText文档 + '.pdf': '📕', # PDF文档 + '.doc': '📘', # Word文档 + '.docx': '📘', # Word文档 + '.rtf': '📄', # RTF文档 + '.odt': '📄', # OpenDocument文本 + + # 数据文件 + '.json': '📋', # JSON数据文件 + '.xml': '📋', # XML文件 + '.yaml': '📋', # YAML文件 + '.yml': '📋', # YAML文件 + '.csv': '📊', # CSV表格文件 + '.xls': '📗', # Excel表格 + '.xlsx': '📗', # Excel表格 + '.ods': '📊', # OpenDocument表格 + + # 图像文件 + '.jpg': '🖼️', # JPEG图像 + '.jpeg': '🖼️', # JPEG图像 + '.png': '🖼️', # PNG图像 + '.gif': '🖼️', # GIF图像 + '.bmp': '🖼️', # BMP图像 + '.svg': '🎨', # SVG矢量图 + '.ico': '🎯', # 图标文件 + '.webp': '🖼️', # WebP图像 + '.tiff': '🖼️', # TIFF图像 + '.tif': '🖼️', # TIFF图像 + + # 音视频文件 + '.mp4': '🎬', # MP4视频 + '.avi': '🎬', # AVI视频 + '.mov': '🎬', # MOV视频 + '.wmv': '🎬', # WMV视频 + '.flv': '🎬', # FLV视频 + '.webm': '🎬', # WebM视频 + '.mp3': '🎵', # MP3音频 + '.wav': '🎵', # WAV音频 + '.flac': '🎵', # FLAC音频 + '.aac': '🎵', # AAC音频 + '.ogg': '🎵', # OGG音频 + + # 压缩文件 + '.zip': '📦', # ZIP压缩包 + '.rar': '📦', # RAR压缩包 + '.7z': '📦', # 7Z压缩包 + '.tar': '📦', # TAR归档 + '.gz': '📦', # GZIP压缩 + '.bz2': '📦', # BZIP2压缩 + '.xz': '📦', # XZ压缩 + + # 可执行文件 + '.exe': '⚙️', # Windows可执行文件 + '.msi': '📦', # Windows安装包 + '.deb': '📦', # Debian包 + '.rpm': '📦', # RPM包 + '.dmg': '💿', # macOS磁盘镜像 + '.app': '📱', # macOS应用程序 + + # 系统文件 + '.dll': '🔧', # 动态链接库 + '.so': '🔧', # 共享库 + '.dylib': '🔧', # macOS动态库 + '.lib': '📚', # 静态库 + + # 脚本文件 + '.bat': '📜', # Windows批处理 + '.cmd': '📜', # Windows命令脚本 + '.sh': '📜', # Shell脚本 + '.ps1': '💙', # PowerShell脚本 + '.vbs': '📜', # VBScript脚本 + + # 配置文件 + '.ini': '⚙️', # INI配置文件 + '.cfg': '⚙️', # 配置文件 + '.conf': '⚙️', # 配置文件 + '.config': '⚙️', # 配置文件 + '.toml': '⚙️', # TOML配置文件 + + # 3D模型文件 + '.fbx': '🎭', # FBX模型文件 + '.obj': '🎭', # OBJ模型文件 + '.3ds': '🎭', # 3DS Max模型 + '.max': '🎭', # 3DS Max场景 + '.blend': '🎭', # Blender模型 + '.dae': '🎭', # COLLADA模型 + '.gltf': '🎭', # glTF模型 + '.glb': '🎭', # glTF二进制模型 + '.x3d': '🎭', # X3D模型 + '.ply': '🎭', # PLY模型 + '.stl': '🎭', # STL模型(3D打印) + '.off': '🎭', # OFF模型 + '.3mf': '🎭', # 3MF模型 + '.amf': '🎭', # AMF模型 + '.x': '🎭', # DirectX模型 + '.md2': '🎭', # Quake II模型 + '.md3': '🎭', # Quake III模型 + '.mdl': '🎭', # Source引擎模型 + '.mesh': '🎭', # OGRE模型 + '.scene': '🎭', # OGRE场景 + '.ac': '🎭', # AC3D模型 + '.ase': '🎭', # ASCII Scene Export + '.assbin': '🎭', # Assimp二进制 + '.b3d': '🎭', # Blitz3D模型 + '.bvh': '🎭', # BioVision层次 + '.csm': '🎭', # CharacterStudio Motion + '.hmp': '🎭', # 3D GameStudio模型 + '.irr': '🎭', # Irrlicht场景 + '.irrmesh': '🎭', # Irrlicht网格 + '.lwo': '🎭', # LightWave对象 + '.lws': '🎭', # LightWave场景 + '.ms3d': '🎭', # MilkShape 3D + '.nff': '🎭', # Neutral文件格式 + '.q3o': '🎭', # Quick3D对象 + '.q3s': '🎭', # Quick3D场景 + '.raw': '🎭', # RAW三角形 + '.smd': '🎭', # Valve SMD + '.ter': '🎭', # Terragen地形 + '.uc': '🎭', # Unreal模型 + '.vta': '🎭', # Valve VTA + '.xgl': '🎭', # XGL模型 + '.zgl': '🎭', # ZGL模型 + + # 纹理和材质文件 + '.mtl': '🎨', # OBJ材质文件 + '.mat': '🎨', # 材质文件 + '.sbsar': '🎨', # Substance Archive + '.sbs': '🎨', # Substance Designer + '.sbsm': '🎨', # Substance材质 + '.exr': '🖼️', # OpenEXR高动态范围图像 + '.hdr': '🖼️', # HDR图像 + '.hdri': '🖼️', # HDRI环境贴图 + '.dds': '🖼️', # DirectDraw Surface + '.ktx': '🖼️', # Khronos纹理 + '.astc': '🖼️', # ASTC纹理 + '.pvr': '🖼️', # PowerVR纹理 + '.etc1': '🖼️', # ETC1纹理 + '.etc2': '🖼️', # ETC2纹理 + + # 动画文件 + '.anim': '🎬', # 动画文件 + '.fbx': '🎬', # FBX动画(也可以是模型) + '.bip': '🎬', # Character Studio Biped + '.cal3d': '🎬', # Cal3D动画 + '.motion': '🎬', # 动作文件 + '.mocap': '🎬', # 动作捕捉数据 + + # 其他常见文件 + '.log': '📋', # 日志文件 + '.tmp': '🗂️', # 临时文件 + '.bak': '💾', # 备份文件 + '.old': '📦', # 旧文件 + } + + # 返回对应的图标,如果找不到则返回默认文档图标 + return icon_map.get(ext, '📄') + + def startDrag(self, supportedActions): + """开始拖拽操作""" + selected_items = self.selectedItems() + if not selected_items: + return + + # 创建 MIME 数据 + mimeData = QMimeData() + + # 收集文件路径用于向外拖拽 + urls = [] + internal_paths = [] + + for item in selected_items: + filepath = item.data(0, Qt.UserRole) + if filepath: + internal_paths.append(filepath) + # 检查是否是模型文件(用于向外拖拽) + if filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')): + urls.append(QUrl.fromLocalFile(filepath)) + + # 设置内部拖拽数据 + mimeData.setText('\n'.join(internal_paths)) + + # 设置向外拖拽数据 + if urls: + mimeData.setUrls(urls) + + # 创建拖拽对象 + drag = QDrag(self) + drag.setMimeData(mimeData) + + # 设置拖拽图标 + pixmap = QPixmap(32, 32) + pixmap.fill(Qt.transparent) + painter = QPainter(pixmap) + painter.drawText(pixmap.rect(), Qt.AlignCenter, str(len(selected_items))) + painter.end() + drag.setPixmap(pixmap) + + # 执行拖拽 + drag.exec_(supportedActions) + + def dragEnterEvent(self, event): + """处理拖拽进入事件""" + # 检查是否是内部拖拽 + if event.mimeData().hasText(): + event.acceptProposedAction() + else: + event.ignore() + + def dragMoveEvent(self, event): + """处理拖拽移动事件""" + if not event.mimeData().hasText(): + event.ignore() + return + + # 获取目标项 + target_item = self.itemAt(event.pos()) + selected_items = self.selectedItems() + + # 检查是否拖拽到多选区域内的项目 + if target_item and target_item in selected_items: + event.ignore() + return + + # 检查是否拖拽到自己的子级 + if target_item: + for selected_item in selected_items: + if self._isChildOf(target_item, selected_item): + event.ignore() + return + + # 如果拖到文件夹,允许拖到文件夹内部 + if target_item: + is_folder = target_item.data(0, Qt.UserRole + 1) + if is_folder: + # 接受拖放,显示指示框 + event.acceptProposedAction() + return + + + # 调用父类方法处理插入指示线 + event.accept() + super().dragMoveEvent(event) + + def _isChildOf(self, potential_child, potential_parent): + """检查 potential_child 是否是 potential_parent 的子级""" + current = potential_child.parent() + while current: + if current == potential_parent: + return True + current = current.parent() + return False + + def dropEvent(self, event): + """处理拖放事件""" + if not event.mimeData().hasText(): + event.ignore() + return + + drag_paths = event.mimeData().text().split('\n') + if not drag_paths: + event.ignore() + return + + # 获取目标项 + target_item = self.itemAt(event.pos()) + if not target_item: + # 如果拖到空白处,默认放到根目录 + target_path = self.current_path + else: + # 如果是文件夹,就放到里面 + is_folder = target_item.data(0, Qt.UserRole + 1) + if is_folder: + target_path = target_item.data(0, Qt.UserRole) + else: + # 如果是文件,则放到其父目录 + parent_item = target_item.parent() + if parent_item: + target_path = parent_item.data(0, Qt.UserRole) + else: + target_path = self.current_path + + # 执行移动 + self._moveFiles(drag_paths, target_path) + event.acceptProposedAction() + # 让 Qt 更新界面 + super().dropEvent(event) + + def _moveFiles(self, source_paths, target_dir): + """移动文件到目标目录""" + import os + import shutil + from PyQt5.QtWidgets import QMessageBox + + moved_files = [] + failed_files = [] + + for source_path in source_paths: + if not source_path or not os.path.exists(source_path): + continue + + if os.path.isdir(source_path) and target_dir.startswith(source_path): + failed_files.append(f"{source_path} (不能移动到子目录)") + continue + + if os.path.dirname(source_path) == target_dir: + continue + + filename = os.path.basename(source_path) + target_path = os.path.join(target_dir, filename) + + if os.path.exists(target_path): + failed_files.append(f"{filename} (目标已存在)") + continue + + try: + shutil.move(source_path, target_path) + moved_files.append(filename) + print(f"移动文件: {source_path} -> {target_path}") + except Exception as e: + failed_files.append(f"{filename} ({str(e)})") + + # 使用状态保持刷新 + if moved_files: + self._refreshWithStatePreservation() + + if failed_files: + StyledMessageBox.warning( + self, + "移动失败", + f"以下文件移动失败:\n" + "\n".join(failed_files) + ) + + if moved_files: + print(f"成功移动 {len(moved_files)} 个文件") + + # def mouseDoubleClickEvent(self, event): + # """处理双击事件""" + # item = self.itemAt(event.pos()) + # if item: + # filepath = item.data(0, Qt.UserRole) + # is_folder = item.data(0, Qt.UserRole + 1) + # + # if is_folder: + # # 文件夹:展开/折叠 + # item.setExpanded(not item.isExpanded()) + # else: + # # 文件:检查是否是模型文件 + # if filepath and filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')): + # self.world.importModel(filepath) + # else: + # # 其他文件:用系统默认程序打开 + # self.openFile(filepath) + # + # super().mouseDoubleClickEvent(event) + + +class CustomConsoleDockWidget(QWidget): + """自定义控制台停靠部件""" + + def __init__(self, world, parent=None): + if parent is None: + parent = wrapinstance(0, QWidget) + super().__init__(parent) + self.world = world + self.setupUI() + # 注释掉控制台重定向,让输出正常显示在终端 + # self.setupConsoleRedirect() + + def setupUI(self): + """初始化控制台UI""" + layout = QVBoxLayout(self) + + # 控制台工具栏 + toolbar = QHBoxLayout() + + # 清空按钮 + self.clearBtn = QPushButton("清空") + self.clearBtn.setStyleSheet(""" + QPushButton { + background-color: rgba(84, 89, 98, 0.5); + color: #ffffff; + border: none; + padding: 4px 12px; + border-radius: 2px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 12px; + font-weight: 300; + letter-spacing: 0.7px; + } + QPushButton:hover { + background-color: rgba(84, 89, 98, 0.7); + } + QPushButton:pressed { + background-color: rgba(84, 89, 98, 0.9); + } + """) + self.clearBtn.clicked.connect(self.clearConsole) + toolbar.addWidget(self.clearBtn) + + # 自动滚动开关 + self.autoScrollBtn = QPushButton("自动滚动") + self.autoScrollBtn.setCheckable(True) + self.autoScrollBtn.setChecked(True) + self.autoScrollBtn.setStyleSheet(""" + QPushButton { + background-color: rgba(84, 89, 98, 0.5); + color: #ffffff; + border: none; + padding: 4px 12px; + border-radius: 2px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 12px; + font-weight: 300; + letter-spacing: 0.7px; + } + QPushButton:checked { + background-color: #3067c0; + color: #ffffff; + } + QPushButton:hover { + background-color: rgba(84, 89, 98, 0.7); + } + QPushButton:pressed { + background-color: rgba(84, 89, 98, 0.9); + } + """) + toolbar.addWidget(self.autoScrollBtn) + + # 时间戳开关 + self.timestampBtn = QPushButton("显示时间") + self.timestampBtn.setCheckable(True) + self.timestampBtn.setChecked(True) + self.timestampBtn.setStyleSheet(""" + QPushButton { + background-color: rgba(84, 89, 98, 0.5); + color: #ffffff; + border: none; + padding: 4px 12px; + border-radius: 2px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 12px; + font-weight: 300; + letter-spacing: 0.7px; + } + QPushButton:checked { + background-color: #3067c0; + color: #ffffff; + } + QPushButton:hover { + background-color: rgba(84, 89, 98, 0.7); + } + QPushButton:pressed { + background-color: rgba(84, 89, 98, 0.9); + } + """) + toolbar.addWidget(self.timestampBtn) + + self.fpsLabel = QLabel("FPS:0.0") + self.fpsLabel.setStyleSheet(""" + QLabel { + background-color: #3067c0; + color: #ffffff; + border: none; + padding: 4px 12px; + border-radius: 2px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 12px; + font-weight: 300; + letter-spacing: 0.7px; + } + """) + self.fpsLabel.setMinimumWidth(100) + self.fpsLabel.setAlignment(Qt.AlignCenter) + toolbar.addWidget(self.fpsLabel) + + # 帧率更新定时器 + self.fpsTimer = QTimer() + self.fpsTimer.timeout.connect(self.updateFPS) + self.fpsTimer.start(105) # 每秒更新一次 + + toolbar.addStretch() + layout.addLayout(toolbar) + + # 控制台文本区域 + from PyQt5.QtWidgets import QTextEdit + self.consoleText = QTextEdit() + self.consoleText.setReadOnly(True) + self.consoleText.setStyleSheet(""" + QTextEdit { + background-color: #19191b; + color: #ebebeb; + font-family: 'Consolas', 'Monaco', 'Microsoft YaHei', monospace; + font-size: 10px; + font-weight: 300; + border: none; + letter-spacing: 0.5px; + line-height: 12px; + } + """) + layout.addWidget(self.consoleText) + + # # 命令输入区域 + # inputLayout = QHBoxLayout() + # inputLayout.addWidget(QLabel(">>> ")) + # + # self.commandInput = QLineEdit() + # self.commandInput.setStyleSheet(""" + # QLineEdit { + # background-color: #2d2d2d; + # color: #ffffff; + # font-family: 'Consolas', 'Monaco', monospace; + # font-size: 10pt; + # border: 1px solid #3e3e3e; + # padding: 5px; + # } + # """) + # self.commandInput.returnPressed.connect(self.executeCommand) + # inputLayout.addWidget(self.commandInput) + # + # layout.addLayout(inputLayout) + + # 添加欢迎信息 + self.addMessage("🎮 编辑器控制台已启动", "INFO") + + def updateFPS(self): + try: + if hasattr(self.world,'clock'): + fps = self.world.clock.getAverageFrameRate() + self.fpsLabel.setText(f"FPS:{fps:.1f}") + + # 根据帧率设置颜色 + if fps >= 50: + color = "#80ff80" # 绿色 - 优秀 + elif fps >= 30: + color = "#ffff80" # 黄色 - 一般 + else: + color = "#ff8080" # 红色 - 较差 + + self.fpsLabel.setStyleSheet(f""" + QLabel {{ + background-color: #3067c0; + color: {color}; + border: 1px solid #3a3a4a; + padding: 6px 12px; + border-radius: 4px; + font-weight: 500; + font-family: 'Consolas', 'Monaco', monospace; + }} + """) + except Exception as e: + pass # 静默处理错误,避免影响控制台功能 + + def setupConsoleRedirect(self): + """设置控制台重定向""" + import sys + + # 保存原始的stdout和stderr + self.original_stdout = sys.stdout + self.original_stderr = sys.stderr + + # 创建自定义输出流 + sys.stdout = ConsoleRedirect(self, "STDOUT") + sys.stderr = ConsoleRedirect(self, "STDERR") + + def addMessage(self, message, msg_type="INFO"): + """添加消息到控制台""" + import datetime + + # 获取当前时间 + timestamp = datetime.datetime.now().strftime("%H:%M:%S") + + # 根据消息类型设置颜色 + color_map = { + "INFO": "#ffffff", # 白色 + "WARNING": "#ffaa00", # 橙色 + "ERROR": "#ff4444", # 红色 + "SUCCESS": "#44ff44", # 绿色 + "STDOUT": "#cccccc", # 浅灰色 + "STDERR": "#ff6666", # 浅红色 + } + + color = color_map.get(msg_type, "#ffffff") + + # 构建HTML格式的消息 + if self.timestampBtn.isChecked(): + html_message = f'[{timestamp}] {message}' + else: + html_message = f'{message}' + + # 添加到控制台 + self.consoleText.append(html_message) + + # 自动滚动到底部 + if self.autoScrollBtn.isChecked(): + scrollbar = self.consoleText.verticalScrollBar() + scrollbar.setValue(scrollbar.maximum()) + + def clearConsole(self): + """清空控制台""" + self.consoleText.clear() + self.addMessage("控制台已清空", "INFO") + + def executeCommand(self): + """执行命令""" + command = self.commandInput.text().strip() + if not command: + return + + # 显示输入的命令 + self.addMessage(f">>> {command}", "INFO") + self.commandInput.clear() + + try: + # 执行Python命令 + if hasattr(self.world, 'executeCommand'): + result = self.world.executeCommand(command) + if result: + self.addMessage(str(result), "SUCCESS") + else: + # 简单的eval执行 + result = eval(command) + if result is not None: + self.addMessage(str(result), "SUCCESS") + + except Exception as e: + self.addMessage(f"错误: {str(e)}", "ERROR") + + def cleanup(self): + """清理资源""" + import sys + + # 恢复原始的stdout和stderr + if hasattr(self, 'original_stdout'): + sys.stdout = self.original_stdout + if hasattr(self, 'original_stderr'): + sys.stderr = self.original_stderr + + +class ConsoleRedirect: + """控制台重定向类""" + + def __init__(self, console_widget, stream_type): + self.console_widget = console_widget + self.stream_type = stream_type + # 保存原始输出流的引用 + if stream_type == "STDOUT": + self.original_stream = sys.stdout + else: + self.original_stream = sys.stderr + + def write(self, text): + """重定向写入 - 同时输出到GUI控制台和原始终端""" + # 先输出到原始终端 + self.original_stream.write(text) + + # 然后输出到GUI控制台(仅非空行) + if text.strip(): # 忽略空行 + self.console_widget.addMessage(text.strip(), self.stream_type) + + def flush(self): + """刷新缓冲区 - 同时刷新原始流""" + self.original_stream.flush() + +class StyledMessageBox: + """统一样式的消息框辅助类""" + + MESSAGEBOX_STYLE = """ + QMessageBox { + background-color: #1e1e1f; + color: #ebebeb; + border: 1px solid rgba(77, 116, 189, 0.3); + border-radius: 8px; + padding: 20px; + min-width: 300px; + } + QMessageBox QLabel { + color: #ffffff; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 13px; + font-weight: 400; + line-height: 1.4; + padding: 10px 0px; + } + QMessageBox QPushButton { + background-color: rgba(89, 100, 113, 0.4); + color: rgba(255, 255, 255, 0.8); + border: 1px solid rgba(76, 92, 110, 0.4); + border-radius: 6px; + padding: 8px 16px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-weight: 400; + font-size: 11px; + min-width: 80px; + min-height: 28px; + } + QMessageBox QPushButton:hover { + background-color: rgba(89, 100, 113, 0.6); + border: 1px solid rgba(77, 116, 189, 0.6); + color: #ffffff; + } + QMessageBox QPushButton:pressed, QMessageBox QPushButton:checked { + background-color: rgba(77, 116, 189, 0.8); + border: 1px solid #4d74bd; + color: #ffffff; + } + QMessageBox QPushButton:disabled { + background-color: rgba(89, 100, 113, 0.2); + color: rgba(235, 235, 235, 0.4); + border: 1px solid rgba(76, 92, 110, 0.2); + } + QMessageBox QPushButton:default { + background-color: rgba(77, 116, 189, 0.8); + border: 1px solid #4d74bd; + color: #ffffff; + font-weight: 500; + } + QMessageBox QPushButton:default:hover { + background-color: rgba(77, 116, 189, 1.0); + border: 1px solid rgba(77, 116, 189, 1.0); + } + QMessageBox QIcon { + padding: 0px 10px 0px 0px; + } + """ + + INPUTDIALOG_STYLE = """ + QInputDialog { + background-color: #1e1e1f; + color: #ebebeb; + border: 1px solid rgba(77, 116, 189, 0.3); + border-radius: 8px; + padding: 20px; + min-width: 350px; + } + QLabel { + color: #ffffff; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-weight: 500; + font-size: 13px; + padding: 0px 0px 10px 0px; + } + QLineEdit { + background-color: rgba(89, 100, 113, 0.15); + color: #ebebeb; + border: 1px solid rgba(76, 92, 110, 0.4); + border-radius: 6px; + padding: 10px 12px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 11px; + font-weight: 400; + min-height: 16px; + } + QLineEdit:focus { + border: 1px solid #4d74bd; + background-color: rgba(77, 116, 189, 0.1); + } + QLineEdit:hover { + border: 1px solid rgba(77, 116, 189, 0.6); + background-color: rgba(89, 100, 113, 0.2); + } + QPushButton { + background-color: rgba(89, 100, 113, 0.4); + color: rgba(255, 255, 255, 0.8); + border: 1px solid rgba(76, 92, 110, 0.4); + border-radius: 6px; + padding: 8px 16px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-weight: 400; + font-size: 11px; + min-width: 80px; + min-height: 28px; + } + QPushButton:hover { + background-color: rgba(89, 100, 113, 0.6); + border: 1px solid rgba(77, 116, 189, 0.6); + color: #ffffff; + } + QPushButton:pressed, QPushButton:checked { + background-color: rgba(77, 116, 189, 0.8); + border: 1px solid #4d74bd; + color: #ffffff; + } + QPushButton:disabled { + background-color: rgba(89, 100, 113, 0.2); + color: rgba(235, 235, 235, 0.4); + border: 1px solid rgba(76, 92, 110, 0.2); + } + QPushButton:default { + background-color: rgba(77, 116, 189, 0.8); + border: 1px solid #4d74bd; + color: #ffffff; + font-weight: 500; + } + """ + + BUTTON_STYLE = """ + QPushButton { + background-color: rgba(89, 100, 113, 0.4); + color: rgba(255, 255, 255, 0.8); + border: 1px solid rgba(76, 92, 110, 0.4); + border-radius: 6px; + padding: 8px 16px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-weight: 400; + font-size: 11px; + min-width: 80px; + min-height: 28px; + } + QPushButton:hover { + background-color: rgba(89, 100, 113, 0.6); + border: 1px solid rgba(77, 116, 189, 0.6); + color: #ffffff; + } + QPushButton:pressed, QPushButton:checked { + background-color: rgba(77, 116, 189, 0.8); + border: 1px solid #4d74bd; + color: #ffffff; + } + QPushButton:disabled { + background-color: rgba(89, 100, 113, 0.2); + color: rgba(235, 235, 235, 0.4); + border: 1px solid rgba(76, 92, 110, 0.2); + } + """ + + @staticmethod + def information(parent, title, message): + """显示信息提示框""" + msg = QMessageBox(QMessageBox.Information, title, message, QMessageBox.Ok, parent) + msg.setStyleSheet(StyledMessageBox.MESSAGEBOX_STYLE) + # 强制设置所有按钮的样式 + for button in msg.buttons(): + button.setStyleSheet(StyledMessageBox.BUTTON_STYLE) + return msg.exec_() + + @staticmethod + def question(parent, title, message, buttons=QMessageBox.Yes | QMessageBox.No, defaultButton=QMessageBox.No): + """显示确认对话框""" + msg = QMessageBox(QMessageBox.Question, title, message, buttons, parent) + msg.setDefaultButton(defaultButton) + msg.setStyleSheet(StyledMessageBox.MESSAGEBOX_STYLE) + # 强制设置所有按钮的样式 + for button in msg.buttons(): + button.setStyleSheet(StyledMessageBox.BUTTON_STYLE) + return msg.exec_() + + @staticmethod + def warning(parent, title, message): + """显示警告对话框""" + msg = QMessageBox(QMessageBox.Warning, title, message, QMessageBox.Ok, parent) + msg.setStyleSheet(StyledMessageBox.MESSAGEBOX_STYLE) + # 强制设置所有按钮的样式 + for button in msg.buttons(): + button.setStyleSheet(StyledMessageBox.BUTTON_STYLE) + return msg.exec_() + + @staticmethod + def critical(parent, title, message): + """显示错误对话框""" + msg = QMessageBox(QMessageBox.Critical, title, message, QMessageBox.Ok, parent) + msg.setStyleSheet(StyledMessageBox.MESSAGEBOX_STYLE) + # 强制设置所有按钮的样式 + for button in msg.buttons(): + button.setStyleSheet(StyledMessageBox.BUTTON_STYLE) + return msg.exec_() + + @staticmethod + def getText(parent, title, label, text=""): + """显示样式统一的文本输入对话框""" + from PyQt5.QtWidgets import QInputDialog + dialog = QInputDialog(parent) + dialog.setWindowTitle(title) + dialog.setLabelText(label) + dialog.setTextValue(text) + dialog.setStyleSheet(StyledMessageBox.INPUTDIALOG_STYLE) + + # 强制设置所有按钮的样式 + for button in dialog.findChildren(QPushButton): + button.setStyleSheet(StyledMessageBox.BUTTON_STYLE) + + ok = dialog.exec_() + return dialog.textValue(), ok + +class UniversalMessageDialog(QDialog): + """通用消息对话框类 - 支持不同图标和按钮配置""" + + # 消息类型枚举 + SUCCESS = "success_icon" + WARNING = "warning_icon" + ERROR = "fail_icon" + INFO = "info" + + def __init__(self, parent, title, message, message_type=INFO, show_cancel=True, + confirm_text="确认", cancel_text="取消", icon_size=QSize(20, 20)): + """ + 初始化通用消息对话框 + + Args: + parent: 父窗口 + title: 对话框标题 + message: 消息内容 + message_type: 消息类型 (SUCCESS, WARNING, ERROR, INFO) + show_cancel: 是否显示取消按钮 + confirm_text: 确认按钮文字 + cancel_text: 取消按钮文字 + icon_size: 图标尺寸 + """ + super().__init__(parent) + self.setWindowTitle(title) + self.setObjectName("universalMessageDialog") + self.setModal(True) + self.resize(508, 134) + self.setMinimumSize(508, 134) + self.setWindowFlags(Qt.Dialog | Qt.FramelessWindowHint) + self.setAttribute(Qt.WA_TranslucentBackground, True) + + # 对话框配置 + self.message_type = message_type + self.show_cancel = show_cancel + self.confirm_text = confirm_text + self.cancel_text = cancel_text + self.icon_size = icon_size + + # 拖拽相关 + self.dragging = False + self.drag_position = QPoint() + + # 图标管理 + self.icon_manager = get_icon_manager() + self._title_icon_size = QSize(18, 18) + self._icon_close = self.icon_manager.get_icon('close_icon', self._title_icon_size) + + # 根据消息类型获取对应图标 + self._message_icon = self._get_message_icon() + + # 设置样式 + self._setup_styles() + + # 创建UI + self._create_ui(message) + + def _get_message_icon(self): + """根据消息类型获取对应图标""" + icon_map = { + self.SUCCESS: 'success_icon', + self.WARNING: 'warning_icon', + self.ERROR: 'fail_icon', + self.INFO: 'success_icon' # 默认使用成功图标 + } + + icon_name = icon_map.get(self.message_type, 'success_icon') + return self.icon_manager.get_icon(icon_name, self.icon_size) + + def _setup_styles(self): + """设置对话框样式""" + self.setStyleSheet(""" + QDialog#universalMessageDialog { + background-color: transparent; + border: none; + } + QFrame#baseFrame { + background-color: #19191B; + border: 1px solid #3E3E42; + border-radius: 5px; + } + QWidget#titleBar { + background-color: transparent; + border: none; + border-radius: 5px 5px 0px 0px; + min-height: 32px; + max-height: 32px; + } + QWidget#titleBar QWidget { + background-color: transparent; + border: none; + } + QLabel#titleLabel { + color: #FFFFFF; + font-family: 'Inter', 'Microsoft YaHei', sans-serif; + font-size: 14px; + font-weight: 500; + letter-spacing: 0.7px; + } + QWidget#controlButtons QPushButton { + background-color: transparent; + border: none; + color: #EBEBEB; + font-size: 14px; + min-width: 18px; + max-width: 18px; + min-height: 18px; + max-height: 18px; + padding: 0px; + border-radius: 3px; + } + QWidget#controlButtons QPushButton:hover { + background-color: #2A2D2E; + color: #FFFFFF; + } + QPushButton#closeButton { + border-radius: 0px 5px 0px 0px; + } + QPushButton#closeButton:hover { + background-color: #2A2D2E; + color: #FFFFFF; + } + QFrame#titleSeparator { + background-color: #3E3E42; + border: none; + min-height: 1px; + max-height: 1px; + } + QWidget#contentWidget { + background-color: transparent; + border: none; + } + QLabel#messageLabel { + color: #EBEBEB; + font-family: 'Inter', 'Microsoft YaHei', sans-serif; + font-size: 13px; + font-weight: 400; + letter-spacing: 0.6px; + line-height: 1.4; + padding: 0px; + margin: 0px; + } + QPushButton { + background-color: rgba(89, 98, 118, 0.5); + color: #EBEBEB; + border: none; + border-radius: 2px; + padding: 0px 12px; + font-family: 'Inter', 'Microsoft YaHei', sans-serif; + font-weight: 300; + font-size: 10px; + letter-spacing: 0.5px; + min-width: 90px; + max-width: 90px; + min-height: 28px; + max-height: 28px; + } + QPushButton:hover { + background-color: #3067C0; + color: #FFFFFF; + } + QPushButton:pressed { + background-color: #2556A0; + color: #FFFFFF; + } + QPushButton#primaryButton { + background-color: rgba(89, 98, 118, 0.5); + border: none; + color: #EBEBEB; + font-weight: 300; + } + QPushButton#primaryButton:hover { + background-color: #2556A0; + } + QPushButton#primaryButton:pressed { + background-color: #1E4A8C; + } + QPushButton#secondaryButton { + background-color: rgba(89, 98, 118, 0.5); + border: none; + color: #EBEBEB; + } + QPushButton#secondaryButton:hover { + background-color: #3067C0; + color: #FFFFFF; + } + QPushButton#secondaryButton:pressed { + background-color: #2556A0; + color: #FFFFFF; + } + """) + + def _create_ui(self, message): + """构建用户界面""" + main_layout = QVBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.setSpacing(0) + + base_frame = QFrame() + base_frame.setObjectName('baseFrame') + base_frame.setFrameShape(QFrame.NoFrame) + base_frame.setAttribute(Qt.WA_StyledBackground, True) + + base_layout = QVBoxLayout(base_frame) + base_layout.setContentsMargins(25, 14, 25, 14) + base_layout.setSpacing(0) + + self._create_title_bar() + base_layout.addWidget(self.title_bar) + base_layout.addSpacing(4) + + title_separator = QFrame() + title_separator.setObjectName('titleSeparator') + title_separator.setFrameShape(QFrame.HLine) + title_separator.setFrameShadow(QFrame.Plain) + base_layout.addWidget(title_separator) + base_layout.addSpacing(10) + + content_widget = QWidget() + content_widget.setObjectName('contentWidget') + content_layout = QVBoxLayout(content_widget) + content_layout.setContentsMargins(0, 0, 0, 0) + content_layout.setSpacing(10) + + message_area = QHBoxLayout() + message_area.setContentsMargins(0, 0, 0, 0) + message_area.setSpacing(10) + + # 用一个垂直布局包裹icon_label,确保顶部对齐 + icon_vbox = QVBoxLayout() + icon_vbox.setContentsMargins(0, 0, 0, 0) + icon_vbox.setSpacing(0) + icon_label = QLabel() + if self._message_icon and not self._message_icon.isNull(): + icon_label.setPixmap(self._message_icon.pixmap(self.icon_size)) + icon_label.setAlignment(Qt.AlignTop | Qt.AlignLeft) + icon_label.setFixedSize(self.icon_size) + icon_vbox.addWidget(icon_label, alignment=Qt.AlignTop) + icon_vbox.addStretch() + message_area.addLayout(icon_vbox) + + self.message_label = QLabel(message) + self.message_label.setObjectName('messageLabel') + self.message_label.setWordWrap(True) + self.message_label.setAlignment(Qt.AlignTop | Qt.AlignLeft) + self.message_label.setMinimumHeight(self.icon_size.height()) + self.message_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum) + message_area.addWidget(self.message_label, 1) + message_area.addStretch() + + content_layout.addLayout(message_area) + base_layout.addWidget(content_widget) + base_layout.addSpacing(10) + + button_widget = QWidget() + button_layout = QHBoxLayout(button_widget) + button_layout.setContentsMargins(0, 0, 0, 0) + button_layout.setSpacing(10) + button_layout.addStretch() + + if self.show_cancel: + self.cancel_button = QPushButton(self.cancel_text) + self.cancel_button.setObjectName("secondaryButton") + self.cancel_button.clicked.connect(self.reject) + self.cancel_button.setFixedSize(90, 28) + button_layout.addWidget(self.cancel_button) + + self.confirm_button = QPushButton(self.confirm_text) + self.confirm_button.setObjectName("primaryButton") + self.confirm_button.clicked.connect(self.accept) + self.confirm_button.setFixedSize(90, 28) + button_layout.addWidget(self.confirm_button) + + self.confirm_button.setDefault(True) + self.confirm_button.setAutoDefault(True) + if self.show_cancel: + self.cancel_button.setAutoDefault(False) + + base_layout.addWidget(button_widget) + + main_layout.addWidget(base_frame) + + def _create_title_bar(self): + """创建自定义标题栏""" + self.title_bar = QFrame() + self.title_bar.setObjectName("titleBar") + + title_layout = QHBoxLayout(self.title_bar) + title_layout.setContentsMargins(0, 0, 0, 0) + title_layout.setSpacing(0) + + self.title_label = QLabel(self.windowTitle()) + self.title_label.setObjectName("titleLabel") + self.title_label.setAlignment(Qt.AlignVCenter | Qt.AlignLeft) + title_layout.addWidget(self.title_label, 1) + + title_layout.addStretch() + + controls = QWidget() + controls.setObjectName("controlButtons") + controls_layout = QHBoxLayout(controls) + controls_layout.setContentsMargins(0, 0, 0, 0) + controls_layout.setSpacing(0) + + self.close_button = QPushButton() + self.close_button.setObjectName("closeButton") + self.close_button.clicked.connect(self.reject) + self.close_button.setFocusPolicy(Qt.NoFocus) + controls_layout.addWidget(self.close_button) + + self._apply_title_bar_icons() + + title_layout.addWidget(controls) + + + def _apply_title_bar_icons(self): + """应用标题栏图标""" + if self._icon_close: + self.close_button.setIcon(self._icon_close) + self.close_button.setIconSize(self._title_icon_size) + self.close_button.setText("") + self.close_button.setToolTip("关闭") + + def setWindowTitle(self, title): + """设置窗口标题""" + super().setWindowTitle(title) + if hasattr(self, "title_label"): + self.title_label.setText(title) + + def mousePressEvent(self, event): + """鼠标按下事件 - 用于拖拽窗口""" + if event.button() == Qt.LeftButton and self.title_bar.geometry().contains(event.pos()): + self.dragging = True + self.drag_position = event.globalPos() - self.frameGeometry().topLeft() + event.accept() + super().mousePressEvent(event) + + def mouseMoveEvent(self, event): + """鼠标移动事件 - 用于拖拽窗口""" + if event.buttons() == Qt.LeftButton and self.dragging: + self.move(event.globalPos() - self.drag_position) + event.accept() + super().mouseMoveEvent(event) + + def mouseReleaseEvent(self, event): + """鼠标释放事件 - 停止拖拽""" + if event.button() == Qt.LeftButton: + self.dragging = False + super().mouseReleaseEvent(event) + + @staticmethod + def show_success(parent, title, message, show_cancel=False, confirm_text="确认"): + """显示成功消息对话框""" + dialog = UniversalMessageDialog( + parent, title, message, + UniversalMessageDialog.SUCCESS, + show_cancel, confirm_text + ) + return dialog.exec_() + + @staticmethod + def show_warning(parent, title, message, show_cancel=True, confirm_text="确认", cancel_text="取消"): + """显示警告消息对话框""" + dialog = UniversalMessageDialog( + parent, title, message, + UniversalMessageDialog.WARNING, + show_cancel, confirm_text, cancel_text + ) + return dialog.exec_() + + @staticmethod + def show_error(parent, title, message, show_cancel=False, confirm_text="确认"): + """显示错误消息对话框""" + dialog = UniversalMessageDialog( + parent, title, message, + UniversalMessageDialog.ERROR, + show_cancel, confirm_text + ) + return dialog.exec_() + + @staticmethod + def show_info(parent, title, message, show_cancel=True, confirm_text="确认", cancel_text="取消"): + """显示信息消息对话框""" + dialog = UniversalMessageDialog( + parent, title, message, + UniversalMessageDialog.INFO, + show_cancel, confirm_text, cancel_text + ) + return dialog.exec_() + + +class StyledTextInputDialog(QDialog): + """与新建项目样式一致的文本输入对话框""" + + def __init__(self, parent, title, label_text="", placeholder="", default_text=""): + super().__init__(parent) + self.setWindowTitle(title) + self.setObjectName("styledTextInputDialog") + self.setModal(True) + self.resize(420, 186) + self.setWindowFlags(Qt.Dialog | Qt.FramelessWindowHint) + self.setAttribute(Qt.WA_TranslucentBackground, True) + + self.dragging = False + self.drag_position = QPoint() + self.icon_manager = get_icon_manager() + self._title_icon_size = QSize(18, 18) + self._icon_close = self.icon_manager.get_icon('close_icon', self._title_icon_size) + + self.setStyleSheet(""" + QDialog#styledTextInputDialog { + background-color: transparent; + border: none; + } + QFrame#baseFrame { + background-color: #000000; + border: 1px solid #3E3E42; + border-radius: 5px; + } + QWidget#titleBar { + background-color: transparent; + border: none; + border-radius: 5px 5px 0px 0px; + min-height: 32px; + max-height: 32px; + } + QWidget#titleBar QWidget { + background-color: transparent; + border: none; + } + QLabel#titleLabel { + color: #FFFFFF; + font-family: 'Inter', 'Microsoft YaHei', sans-serif; + font-size: 14px; + font-weight: 500; + letter-spacing: 0.7px; + } + QWidget#controlButtons QPushButton { + background-color: transparent; + border: none; + color: #EBEBEB; + font-size: 14px; + min-width: 18px; + max-width: 18px; + min-height: 18px; + max-height: 18px; + padding: 0px; + border-radius: 3px; + } + QWidget#controlButtons QPushButton:hover { + background-color: #2A2D2E; + color: #FFFFFF; + } + QPushButton#closeButton { + border-radius: 0px 5px 0px 0px; + } + QPushButton#closeButton:hover { + background-color: #2A2D2E; + color: #FFFFFF; + } + QWidget#contentWidget { + background-color: transparent; + border-radius: 0px 0px 5px 5px; + } + QFrame#contentContainer { + background-color: #19191B; + border: 1px solid #2C2F36; + border-radius: 5px; + } + QLabel[role="fieldLabel"] { + color: #EBEBEB; + font-family: 'Inter', 'Microsoft YaHei', sans-serif; + font-size: 12px; + font-weight: 400; + letter-spacing: 0.6px; + } + QLineEdit { + background-color: rgba(89, 100, 113, 0.2); + color: #EBEBEB; + border: 1px solid rgba(76, 92, 110, 0.6); + border-radius: 2px; + padding: 0px 10px; + font-family: 'Inter', 'Microsoft YaHei', sans-serif; + font-size: 11px; + font-weight: 300; + letter-spacing: 0.55px; + min-height: 30px; + max-height: 30px; + } + QLineEdit:focus { + border: 1px solid #3067C0; + background-color: rgba(48, 103, 192, 0.1); + } + QLineEdit:hover { + border: 1px solid #3067C0; + background-color: rgba(89, 100, 113, 0.3); + } + QPushButton { + background-color: rgba(89, 98, 118, 0.5); + color: #EBEBEB; + border: none; + border-radius: 2px; + padding: 0px 12px; + font-family: 'Inter', 'Microsoft YaHei', sans-serif; + font-weight: 300; + font-size: 10px; + letter-spacing: 0.5px; + min-width: 120px; + min-height: 30px; + max-height: 30px; + } + QPushButton:hover { + background-color: #3067C0; + color: #FFFFFF; + } + QPushButton:pressed { + background-color: #2556A0; + color: #FFFFFF; + } + """) + + main_layout = QVBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.setSpacing(0) + + base_frame = QFrame() + base_frame.setObjectName('baseFrame') + base_frame.setFrameShape(QFrame.NoFrame) + base_frame.setAttribute(Qt.WA_StyledBackground, True) + + base_layout = QVBoxLayout(base_frame) + base_layout.setContentsMargins(0, 0, 0, 0) + base_layout.setSpacing(0) + + self._createTitleBar() + base_layout.addWidget(self.title_bar) + base_layout.addSpacing(10) + + content_widget = QWidget() + content_widget.setObjectName('contentWidget') + content_layout = QVBoxLayout(content_widget) + content_layout.setContentsMargins(10, 0, 10, 10) + content_layout.setSpacing(0) + + content_container = QFrame() + content_container.setObjectName('contentContainer') + content_container.setFrameShape(QFrame.NoFrame) + content_container.setAttribute(Qt.WA_StyledBackground, True) + content_container.setFixedWidth(400) + + container_layout = QVBoxLayout(content_container) + container_layout.setContentsMargins(15, 10, 15, 10) + container_layout.setSpacing(10) + + if label_text: + label = QLabel(label_text) + label.setProperty('role', 'fieldLabel') + container_layout.addWidget(label) + + self.line_edit = QLineEdit() + self.line_edit.setPlaceholderText(placeholder) + self.line_edit.setText(default_text) + container_layout.addWidget(self.line_edit) + + separator = QFrame() + separator.setFrameShape(QFrame.HLine) + separator.setFrameShadow(QFrame.Plain) + separator.setFixedHeight(1) + separator.setStyleSheet("background-color: #2C2F36; border: none;") + container_layout.addWidget(separator) + + button_row = QHBoxLayout() + button_row.setContentsMargins(0, 0, 0, 0) + button_row.setSpacing(10) + button_row.addStretch() + + self.confirmButton = QPushButton("确认") + self.confirmButton.setObjectName("primaryButton") + self.confirmButton.clicked.connect(self._onAccept) + button_row.addWidget(self.confirmButton) + + self.cancelButton = QPushButton("取消") + self.cancelButton.setObjectName("secondaryButton") + self.cancelButton.clicked.connect(self.reject) + button_row.addWidget(self.cancelButton) + + container_layout.addLayout(button_row) + + content_layout.addWidget(content_container, 0, Qt.AlignTop) + base_layout.addWidget(content_widget) + main_layout.addWidget(base_frame) + + self.confirmButton.setDefault(True) + self.confirmButton.setAutoDefault(True) + self.line_edit.selectAll() + self.line_edit.setFocus() + + def _createTitleBar(self): + self.title_bar = QFrame() + self.title_bar.setObjectName("titleBar") + + title_layout = QHBoxLayout(self.title_bar) + title_layout.setContentsMargins(12, 6, 12, 6) + title_layout.setSpacing(0) + + controls = QWidget() + controls.setObjectName("controlButtons") + controls_layout = QHBoxLayout(controls) + controls_layout.setContentsMargins(0, 0, 0, 0) + controls_layout.setSpacing(0) + + self.close_button = QPushButton() + self.close_button.setObjectName("closeButton") + self.close_button.clicked.connect(self.reject) + self.close_button.setFocusPolicy(Qt.NoFocus) + self.close_button.setFixedSize(18, 18) + controls_layout.addWidget(self.close_button) + + self._applyTitleBarIcons() + + left_placeholder = QWidget() + left_placeholder.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + title_layout.addWidget(left_placeholder) + + self.title_label = QLabel(self.windowTitle()) + self.title_label.setObjectName("titleLabel") + self.title_label.setAlignment(Qt.AlignCenter) + title_layout.addWidget(self.title_label, 1) + + title_layout.addWidget(controls) + left_placeholder.setFixedWidth(controls.sizeHint().width()) + + def _applyTitleBarIcons(self): + if self._icon_close: + self.close_button.setIcon(self._icon_close) + self.close_button.setIconSize(self._title_icon_size) + self.close_button.setText("") + self.close_button.setToolTip("关闭") + + def setWindowTitle(self, title): + super().setWindowTitle(title) + if hasattr(self, "title_label"): + self.title_label.setText(title) + + def _onAccept(self): + if self.line_edit.text().strip(): + self.accept() + + def text(self): + return self.line_edit.text().strip() + + def mousePressEvent(self, event): + if event.button() == Qt.LeftButton and self.title_bar.geometry().contains(event.pos()): + self.dragging = True + self.drag_position = event.globalPos() - self.frameGeometry().topLeft() + event.accept() + super().mousePressEvent(event) + + def mouseMoveEvent(self, event): + if event.buttons() == Qt.LeftButton and self.dragging: + self.move(event.globalPos() - self.drag_position) + event.accept() + super().mouseMoveEvent(event) + + def mouseReleaseEvent(self, event): + if event.button() == Qt.LeftButton: + self.dragging = False + super().mouseReleaseEvent(event) + + +class CustomTreeWidget(QTreeWidget): + """自定义场景树部件""" + + def __init__(self, world, parent=None): + if parent is None: + parent = wrapinstance(0, QWidget) + super().__init__(parent) + self.world = world + # self.selectedItems = None + self.initData() + self.setupUI() # 初始化界面 + self.setupContextMenu() # 初始化右键菜单 + self.setupDragDrop() # 设置拖拽功能 + self.original_scales={} + + def initData(self): + """初始化变量""" + # 定义2D GUI元素类型 + self.gui_2d_types = { + "GUI_BUTTON", # GUI 按钮 + "GUI_LABEL", # GUI 标签 + "GUI_ENTRY", # GUI 输入框 + "GUI_IMAGE", # GUI 图片 + "GUI_2D_VIDEO_SCREEN", # GUI 2D视频 + "GUI_SPHERICAL_VIDEO", # GUI 3D球形视频 + "GUI_NODE" # 其他2D GUI容器 + } + + # 定义3D GUI元素类型 + self.gui_3d_types = { + "GUI_3DTEXT", # 3D 文本节点 + "GUI_3DIMAGE", # 3D 图片节点 + "GUI_VIRTUAL_SCREEN", # 3D视频 + "GUI_VirtualScreen" # 3D虚拟视频 + } + + # 定义3D场景节点类型(可以接受3D GUI元素和其他3D场景元素) + self.scene_3d_types = { + "SCENE_ROOT", + "SCENE_NODE", + "LIGHT_NODE", # 灯节点 + "CAMERA_NODE", + "IMPORTED_MODEL_NODE", # 导入模型节点 + "MODEL_NODE", + "TERRAIN_NODE", # 地形节点 + "CESIUM_TILESET_NODE" # 3D Tileset + } + + # 这是一个最佳实践,它让代码的意图变得非常清晰。 + self.valid_3d_parent_types = self.scene_3d_types.union(self.gui_3d_types) + + def setupUI(self): + """初始化UI设置""" + self.setHeaderHidden(True) + # 启用多选和拖拽 + self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) + self.setDropIndicatorShown(True) # 启用拖放指示线 + + def setupDragDrop(self): + """设置拖拽功能""" + # 使用自定义拖拽模式 + self.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove) # 或者使用 DragDrop + self.setDefaultDropAction(Qt.DropAction.MoveAction) + self.setDragEnabled(True) + self.setAcceptDrops(True) + + def setupContextMenu(self): + """设置右键菜单""" + self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) + self.customContextMenuRequested.connect(self.showContextMenu) + + def showContextMenu(self, position): + """显示右键菜单 - 复用主菜单的创建动作""" + if self.selectedItems(): + item = self.selectedItems()[0] + print(f"为项目 '{item.text(0)}' 显示右键菜单") + + # 创建右键菜单 + menu = QMenu(self) + + # 获取主窗口的创建菜单动作 - 关键修改 + if hasattr(self.world, 'main_window'): + main_window = self.world.main_window + create_actions = main_window.getCreateMenuActions() + + # 创建子菜单 - 复用主菜单结构 + createMenu = menu.addMenu('创建') + + # 基础对象 + createMenu.addAction(create_actions['createEmpty']) + + # 3D对象菜单 + create3dObjectMenu = createMenu.addMenu('3D对象') + + # 3D GUI菜单 + create3dGUIMenu = createMenu.addMenu('3D GUI') + create3dGUIMenu.addAction(create_actions['create3DText']) + create3dGUIMenu.addAction(create_actions['create3DImage']) + + # GUI菜单 + createGUIMenu = createMenu.addMenu('GUI') + createGUIMenu.addAction(create_actions['createButton']) + createGUIMenu.addAction(create_actions['createLabel']) + createGUIMenu.addAction(create_actions['createEntry']) + createGUIMenu.addAction(create_actions['createImage']) + createGUIMenu.addSeparator() + createGUIMenu.addAction(create_actions['createVideoScreen']) + createGUIMenu.addAction(create_actions['createSphericalVideo']) + createGUIMenu.addSeparator() + createGUIMenu.addAction(create_actions['createVirtualScreen']) + + # 光源菜单 + createLightMenu = createMenu.addMenu('光源') + createLightMenu.addAction(create_actions['createSpotLight']) + createLightMenu.addAction(create_actions['createPointLight']) + + else: + # 备用方案:如果无法获取主窗口动作,显示提示 + createMenu = menu.addMenu('创建') + noActionItem = createMenu.addAction('功能不可用') + noActionItem.setEnabled(False) + + # 添加删除选项 + menu.addSeparator() + deleteAction = menu.addAction('删除') + if hasattr(self, 'delete_items'): + deleteAction.triggered.connect(lambda: self.delete_items(self.selectedItems())) + + # 显示菜单 + global_pos = self.mapToGlobal(position) + action = menu.exec_(global_pos) + + print(f"右键菜单已显示在位置: {global_pos}") + if action: + print(f"用户选择了动作: {action.text()}") + else: + print("用户取消了菜单选择") + + # 在 CustomTreeWidget 类的 dropEvent 方法中替换缩放处理部分 + def dropEvent(self, event): + # 1. 获取所有被拖拽的项 + dragged_items = self.selectedItems() + if not dragged_items: + event.ignore() + return + + # 2. 在执行Qt的默认拖拽前,记录所有拖拽项的原始状态 + drag_info = [] + for item in dragged_items: + panda_node = item.data(0, Qt.UserRole) + if not panda_node or panda_node.is_empty(): + continue # 跳过无效节点 + + drag_info.append({ + "item": item, + "node": panda_node, + "old_parent_node": item.parent().data(0, Qt.UserRole) if item.parent() else None + }) + + # 3. 执行Qt的默认拖拽,让UI树先行更新 + # 这一步会自动处理移动或复制,并将项目从旧父节点移除,添加到新父节点 + super().dropEvent(event) + + # 4. 遍历记录下的信息,同步每一个Panda3D节点的状态 + try: + for info in drag_info: + dragged_item = info["item"] + dragged_node = info["node"] + old_parent_node = info["old_parent_node"] + + # 获取拖拽后的新父节点 + new_parent_item = dragged_item.parent() + new_parent_node = new_parent_item.data(0, Qt.UserRole) if new_parent_item else None + + # 仅当父节点实际发生变化时才执行重新父化 + if old_parent_node != new_parent_node: + print(f"跨层级拖拽:从 {old_parent_node} 移动到 {new_parent_node}") + + # # 保存世界坐标位置 + # world_pos = dragged_node.getPos(self.world.render) + # world_hpr = dragged_node.getHpr(self.world.render) + # world_scale = dragged_node.getScale(self.world.render) + + # 检查是否是2D GUI元素 + dragged_type = dragged_item.data(0, Qt.UserRole + 1) + is_2d_gui = dragged_type in self.gui_2d_types + + # 重新父化到新的父节点 + if new_parent_node: + if is_2d_gui: + # 2D GUI元素需要特殊处理 + if hasattr(new_parent_node, 'getTag') and new_parent_node.getTag("is_gui_element") == "1": + # 目标是GUI元素,直接重新父化 + dragged_node.wrtReparentTo(new_parent_node) + else: + # 目标是3D节点,保持GUI特性,重新父化到aspect2d + # from direct.showbase.ShowBase import aspect2d + dragged_node.wrtReparentTo(self.world.aspect2d) + print(f"2D GUI元素 {dragged_item.text(0)} 保持在aspect2d下") + else: + # 非GUI元素正常重新父化 + dragged_node.wrtReparentTo(new_parent_node) + else: + # 如果新父节点为None,根据元素类型决定父节点 + if is_2d_gui: + # from direct.showbase.ShowBase import aspect2d + dragged_node.wrtReparentTo(self.world.aspect2d) + print(f"2D GUI元素 {dragged_item.text(0)} 重新父化到aspect2d") + else: + dragged_node.wrtReparentTo(self.world.render) + + # # 恢复世界坐标位置(对于2D GUI可能需要调整) + # if is_2d_gui: + # # 2D GUI元素使用屏幕坐标系,可能需要特殊处理 + # dragged_node.setPos(world_pos) + # dragged_node.setHpr(world_hpr) + # dragged_node.setScale(world_scale) + # else: + # dragged_node.setPos(self.world.render, world_pos) + # dragged_node.setHpr(self.world.render, world_hpr) + # dragged_node.setScale(self.world.render, world_scale) + + print(f"✅ Panda3D父子关系已更新") + else: + print(f"同层级移动:父节点未变化,跳过Panda3D重新父化") + + except Exception as e: + print(f"⚠️ 同步Panda3D场景图失败: {e}") + # 不影响Qt树的更新,继续执行 + + # 事后验证:确保节点仍在"场景"根节点下 + self._ensureUnderSceneRoot(dragged_item) + self.world.property_panel._syncEffectiveVisibility(dragged_node) + event.accept() + + def _ensureUnderSceneRoot(self, item): + """确保节点在场景根节点下,如果不是则自动修正""" + if not item: + return + + # 检查是否成为了顶级节点 + if not item.parent(): + # 通过数据标识判断是否是场景根节点 + scene_root_marker = item.data(0, Qt.UserRole + 1) + if scene_root_marker != "SCENE_ROOT": + print(f"⚠️ 检测到节点 {item.text(0)} 意外成为顶级节点,正在修正...") + + # 找到场景根节点 + scene_root = None + for i in range(self.topLevelItemCount()): + top_item = self.topLevelItem(i) + if top_item.data(0, Qt.UserRole + 1) == "SCENE_ROOT": + scene_root = top_item + break + + if scene_root: + # 将节点移回场景根节点下 + self.takeTopLevelItem(self.indexOfTopLevelItem(item)) + scene_root.addChild(item) + print(f"✅ 已将节点 {item.text(0)} 移回场景根节点下") + + def isValidParentChild(self, dragged_item, target_item): + """检查是否是有效的父子关系(防止循环)+ GUI类型验证""" + + # 1. 禁止拖放到自身 + if dragged_item == target_item: + return False + + # 2. 禁止拖到根节点之外(根节点本身除外) + target_root = self._getRootNode(target_item) + if not target_root or target_root.data(0, Qt.UserRole + 1) != "SCENE_ROOT": + print(f"❌ 目标节点 {target_item.text(0)} 不在场景下") + return False + + # 3. 禁止拖拽场景根节点 + dragged_root = self._getRootNode(dragged_item) + if (dragged_item.data(0, Qt.UserRole + 1) == "SCENE_ROOT" or + not dragged_root or dragged_root.data(0, Qt.UserRole + 1) != "SCENE_ROOT"): + print(f"❌ 禁止拖拽场景根节点或根节点外的节点") + return False + + # 4. Qt 树循环检查 + current = target_item + while current: + if current == dragged_item: + print(f"❌ Qt 树检测:{target_item.text(0)} 是 {dragged_item.text(0)} 的后代") + return False + current = current.parent() + + # 5. GUI元素类型验证 - 新增功能 + if not self._validateGUITypeCompatibility(dragged_item, target_item): + return False + + return True + + def _validateGUITypeCompatibility(self, dragged_item, target_item): + """验证GUI元素类型兼容性""" + try: + # 获取节点类型标识 + dragged_type = dragged_item.data(0, Qt.UserRole + 1) + target_type = target_item.data(0, Qt.UserRole + 1) + + # 定义2D GUI元素类型 + gui_2d_types = self.gui_2d_types + + # 定义3D GUI元素类型 + gui_3d_types = self.gui_3d_types + + # 定义3D场景节点类型(可以接受3D GUI元素和其他3D场景元素) + scene_3d_types = self.scene_3d_types + + # 检查拖拽元素的类型 + is_dragged_2d_gui = dragged_type in gui_2d_types + is_dragged_3d_gui = dragged_type in gui_3d_types + is_dragged_3d_scene = dragged_type in scene_3d_types + + # 检查目标的类型 + is_target_2d_gui = target_type in gui_2d_types + is_target_3d_gui = target_type in gui_3d_types + is_target_3d_scene = target_type in scene_3d_types + + # === 严格的类型隔离验证逻辑 === + + # 1. 2D GUI元素的拖拽限制 - 只能拖拽到其他2D GUI元素下 + if is_dragged_2d_gui: + if target_type == "SCENE_ROOT": + return True + elif is_target_2d_gui: + print(f"✅ 2D GUI元素 {dragged_item.text(0)} 可以拖拽到2D GUI父节点 {target_item.text(0)}") + return True + elif is_target_3d_gui: + print(f"❌ 2D GUI元素 {dragged_item.text(0)} 不能拖拽到3D GUI元素 {target_item.text(0)} 下") + print(" 💡 提示: 2D GUI元素只能作为其他2D GUI元素的子节点") + return False + elif is_target_3d_scene: + print(f"❌ 2D GUI元素 {dragged_item.text(0)} 不能拖拽到3D场景节点 {target_item.text(0)} 下") + print(" 💡 提示: 2D GUI元素应该保持在2D GUI层级结构中") + return False + else: + print(f"❌ 2D GUI元素 {dragged_item.text(0)} 不能拖拽到未知类型节点 {target_item.text(0)} 下") + return False + + # 2. 3D GUI元素的拖拽限制 - 只能拖拽到3D场景节点下 + elif is_dragged_3d_gui: + if is_target_3d_scene: + print(f"✅ 3D GUI元素 {dragged_item.text(0)} 可以拖拽到3D场景节点 {target_item.text(0)}") + return False + elif is_target_2d_gui: + print(f"❌ 3D GUI元素 {dragged_item.text(0)} 不能拖拽到2D GUI元素 {target_item.text(0)} 下") + print(" 💡 提示: 3D GUI元素不能与2D GUI元素建立父子关系") + return False + elif is_target_3d_gui: + print(f"✅ 3D GUI元素 {dragged_item.text(0)} 可以拖拽到其他3D GUI元素 {target_item.text(0)} 下") + print(" 💡 提示: 允许3D GUI元素之间建立父子关系") + return True + else: + print(f"❌ 3D GUI元素 {dragged_item.text(0)} 不能拖拽到未知类型节点 {target_item.text(0)} 下") + return False + + # 3. 3D场景元素的拖拽限制 - 只能拖拽到3D场景节点或3D GUI元素下 + elif is_dragged_3d_scene: + if is_target_3d_scene: + print(f"✅ 3D场景元素 {dragged_item.text(0)} 可以拖拽到3D场景节点 {target_item.text(0)}") + return True + elif is_target_2d_gui: + print(f"❌ 3D场景元素 {dragged_item.text(0)} 不能拖拽到2D GUI元素 {target_item.text(0)} 下") + print(" 💡 提示: 3D场景元素不能与2D GUI元素建立父子关系") + print(" 💡 建议: 将3D场景元素拖拽到其他3D场景节点或场景根节点下") + return False + elif is_target_3d_gui: + print(f"✅ 3D场景元素 {dragged_item.text(0)} 可以拖拽到3D GUI元素 {target_item.text(0)} 下") + print(" 💡 提示: 允许3D场景元素挂载在3D GUI元素下") + return False + else: + print(f"❌ 3D场景元素 {dragged_item.text(0)} 不能拖拽到未知类型节点 {target_item.text(0)} 下") + return False + + # 4. 其他未分类元素 - 严格禁止拖拽到2D GUI下 + else: + if is_target_2d_gui: + print(f"❌ 元素 {dragged_item.text(0)} 不能拖拽到2D GUI元素 {target_item.text(0)} 下") + print(" 💡 提示: 只有2D GUI元素才能作为其他2D GUI元素的子节点") + return False + elif is_target_3d_gui or is_target_3d_scene: + print(f"✅ 允许元素 {dragged_item.text(0)} 拖拽到3D节点 {target_item.text(0)}") + return True + else: + print(f"❌ 元素 {dragged_item.text(0)} 不能拖拽到未知类型节点 {target_item.text(0)} 下") + return False + + # 默认情况(理论上不应该到达这里) + print(f"⚠️ GUI类型验证遇到未处理的情况: {dragged_type} -> {target_type}") + return False + + except Exception as e: + print(f"❌ GUI类型兼容性验证失败: {str(e)}") + # 出错时采用保守策略,禁止拖拽 + return False + + def _getRootNode(self, item): + """获取树中节点的根节点项""" + if not item: + return None + + current = item + while current.parent(): + current = current.parent() + return current + + def dragEnterEvent(self, event): + """处理拖入事件""" + if event.source() == self: + event.accept() + else: + event.ignore() + + def dragMoveEvent(self, event): + """处理拖动事件""" + indicator_pos = self.dropIndicatorPosition() + indicator_str = "Unknown" + + if indicator_pos == QAbstractItemView.DropIndicatorPosition.OnItem: + indicator_str = "OnItem" + elif indicator_pos == QAbstractItemView.DropIndicatorPosition.AboveItem: + indicator_str = "AboveItem" + elif indicator_pos == QAbstractItemView.DropIndicatorPosition.BelowItem: + indicator_str = "BelowItem" + elif indicator_pos == QAbstractItemView.DropIndicatorPosition.OnViewport: + indicator_str = "OnViewport" + + #print(f'indicator pos: {indicator_str} (value: {int(indicator_pos)})') + + if event.source() != self: + event.ignore() + return + + # 获取当前拖拽的项目和目标位置 + target_item = self.itemAt(event.pos()) + selected_items = self.selectedItems() + + # 检查是否拖拽到多选区域内的项目 + if target_item and target_item in selected_items: + event.ignore() + return + + # 检查其他禁止条件 + if target_item and selected_items: + for dragged_item in selected_items: + if not self.isValidParentChild(dragged_item, target_item): + event.ignore() + return + + super().dragMoveEvent(event) + event.accept() + + def keyPressEvent(self, event): + """处理键盘按键事件""" + if event.key() == Qt.Key_Delete: + # currentItem = self.currentItem() + # if currentItem and currentItem.parent(): + # # 检查是否是模型节点或其子节点 + # if self.world.interface_manager.isModelOrChild(currentItem): + # nodePath = currentItem.data(0, Qt.UserRole) + # if nodePath: + # print("正在删除节点...") + # self.world.interface_manager.deleteNode(nodePath, currentItem) + # print("删除完成") + selected_items = self.selectedItems() + if selected_items: + # 执行删除操作 + # if selected_items.data(0, Qt.UserRole + 1) == "LIGHT_NODE": + # self._preprocess_light_items_for_deletion(selected_items) + self.delete_items(selected_items) + else: + # 没有选中任何项目,执行默认操作 + super().keyPressEvent(event) + else: + super().keyPressEvent(event) + + def _preprocess_light_items_for_deletion(self, selected_items): + """预处理灯光节点删除,特别处理最后一个灯光节点的问题""" + if not selected_items: + return selected_items + + # 检查选中的项目中是否包含灯光节点 + light_items = [] + for item in selected_items: + node_type = item.data(0, Qt.UserRole + 1) + if node_type == "LIGHT_NODE": + light_items.append(item) + + # 如果没有灯光节点,直接返回 + if not light_items: + return selected_items + + # 检查是否只有最后一个灯光节点被选中 + processed_items = list(selected_items) # 创建副本 + + for item in light_items: + panda_node = item.data(0, Qt.UserRole) + if not panda_node: + continue + + # 获取灯光类型 + if hasattr(panda_node, 'getTag'): + light_type = panda_node.getTag("light_type") + + # 检查是否是最后一个Spotlight + if (light_type == "spot_light" and hasattr(self.world, 'Spotlight') and + self.world.Spotlight and self.world.Spotlight[-1] == panda_node and + len(self.world.Spotlight) > 1): + + print(f"⚠️ 检测到选中最后一个Spotlight节点: {item.text(0)}") + # 这里可以添加特殊处理逻辑,比如提示用户或阻止删除 + + # 检查是否是最后一个Pointlight + elif (light_type == "point_light" and hasattr(self.world, 'Pointlight') and + self.world.Pointlight and self.world.Pointlight[-1] == panda_node and + len(self.world.Pointlight) > 1): + + print(f"⚠️ 检测到选中最后一个Pointlight节点: {item.text(0)}") + # 这里可以添加特殊处理逻辑,比如提示用户或阻止删除 + + return processed_items + + def delete_items(self, selected_items): + """删除选中的item - 简化版本""" + if not selected_items: + return + + # 1. 过滤掉不能删除的节点 + deletable_items = [] + for item in selected_items: + node_type = item.data(0, Qt.UserRole + 1) + panda_node = item.data(0, Qt.UserRole) + + # 跳过场景根节点和主相机 + if (node_type == "SCENE_ROOT" or + (panda_node and hasattr(self.world, 'cam') and panda_node == self.world.cam)): + continue + deletable_items.append(item) + + if not deletable_items: + StyledMessageBox.information(self, "提示", "没有可删除的节点") + return + + # 2. 确认删除 + item_count = len(deletable_items) + if item_count == 1: + message = f"确定要删除节点 \"{deletable_items[0].text(0)}\" 吗?" + else: + message = f"确定要删除 {item_count} 个节点吗?" + + dialog = UniversalMessageDialog( + self, + "确认删除", + message, + message_type=UniversalMessageDialog.WARNING, + show_cancel=True, + confirm_text="确认", + cancel_text="取消" + ) + + if dialog.exec_() != QDialog.Accepted: + return + + # 默认选中场景根节点,通常是第一个顶级节点 + #next_item_to_select = self.topLevelItem(0) + + # 3. 执行删除循环 + deleted_count = 0 + for item in deletable_items: + try: + # 在删除前,记录其父节点,作为删除后的新选择 + # 选择最后一个被删除项的父节点作为新的焦点 + if item.parent(): + next_item_to_select = item.parent() + panda_node = item.data(0, Qt.UserRole) + + if panda_node: + # 清理选择状态 + if (hasattr(self.world, 'selection') and + hasattr(self.world.selection, 'selectedNode') and + self.world.selection.selectedNode == panda_node): + self.world.selection.updateSelection(None) + + # 清理特殊资源(参考interface_manager.py的逻辑) + if hasattr(self.world, 'property_panel'): + self.world.property_panel.removeActorForModel(panda_node) + + # 清理灯光 + if hasattr(panda_node, 'getPythonTag'): + light_object = panda_node.getPythonTag('rp_light_object') + if light_object and hasattr(self.world, 'render_pipeline'): + self.world.render_pipeline.remove_light(light_object) + + # 从world列表中移除 + if hasattr(self.world, 'gui_elements') and panda_node in self.world.gui_elements: + self.world.gui_elements.remove(panda_node) + if hasattr(self.world, 'models') and panda_node in self.world.models: + self.world.models.remove(panda_node) + if hasattr(self.world, 'Spotlight') and panda_node in self.world.Spotlight: + self.world.Spotlight.remove(panda_node) + if hasattr(self.world, 'Pointlight') and panda_node in self.world.Pointlight: + self.world.Pointlight.remove(panda_node) + if hasattr(self.world, 'terrains') and panda_node in self.world.terrains: + self.world.terrains.remove(panda_node) + if hasattr(self.world, 'tilesets') and panda_node in self.world.tilesets: + # self.world.tilesets.remove(panda_node) + # 从 tilesets 列表中移除 + if hasattr(self.world, 'scene_manager'): + tilesets_to_remove = [] + for i, tileset_info in enumerate(self.world.scene_manager.tilesets): + if tileset_info['node'] == panda_node: + tilesets_to_remove.append(i) + + # 从后往前删除,避免索引问题 + for i in reversed(tilesets_to_remove): + del self.world.scene_manager.tilesets[i] + + # 从Panda3D场景中移除 + try: + if not panda_node.isEmpty(): + panda_node.removeNode() + except Exception as e: + print(f"❌ 删除节点 {item.text(0)} 失败: {str(e)}") + + # 从Qt树中移除 + parent_item = item.parent() + if parent_item: + parent_item.removeChild(item) + else: + index = self.indexOfTopLevelItem(item) + if index >= 0: + self.takeTopLevelItem(index) + + deleted_count += 1 + print(f"✅ 删除节点: {item.text(0)}") + + except Exception as e: + print(f"❌ 删除节点 {item.text(0)} 失败: {str(e)}") + import traceback + traceback.print_exc() + + # 最终清理 + # if hasattr(self.world, 'property_panel'): + # self.world.property_panel.clearPropertyPanel() + + # 4. 删除操作完成后,更新UI --- + if deleted_count > 0: + print(f"🎉 成功删除 {deleted_count} 个节点。正在更新UI...") + self.update_selection_and_properties(None, None) + + def delete_item(self, panda_node): + """删除指定节点 panda3D(node)- 优化和修复版本""" + if not panda_node or panda_node.is_empty(): + print("ℹ️ 尝试删除一个空的或无效的节点,操作取消。") + return + + # #如果有命令管理系统,则使用命令系统 + # if hasattr(self.world,'command_manager') and self.world.command_manager: + # from core.Command_System import DeleteNodeCommand + # parent_node = panda_node.getParent() + # command = DeleteNodeCommand(panda_node,parent_node) + # self.world.command_manager.execute_command(command) + # return + + # --- 关键修复:在操作前,安全地获取节点名字 --- + node_name_for_logging = panda_node.getName() + + # 1. 寻找对应的Qt Item + item = self.world.interface_manager.findTreeItem(panda_node, self._findSceneRoot()) + + # 场景清理(无论是否找到item,都应该执行) + self._cleanup_panda_node_resources(panda_node) + panda_node.removeNode() + + # 如果没有找到item,说明UI已经移除或不同步,清理完Panda3D资源后即可退出 + if not item: + print(f"✅ Panda3D节点 '{node_name_for_logging}' 已清理并移除。UI树中未找到对应项。") + return + try: + # 2. 过滤受保护节点 + node_type = item.data(0, Qt.UserRole + 1) + if node_type == "SCENE_ROOT": # 相机检查已包含在panda_node判空中 + print(f"ℹ️ 节点 {item.text(0)} 是受保护节点,无法删除。") + return + + # 3. 从UI树中移除 + parent_for_next_selection = item.parent() + if item.parent(): + item.parent().removeChild(item) + else: + index = self.indexOfTopLevelItem(item) + if index >= 0: + self.takeTopLevelItem(index) + + print(f"✅ 成功删除节点: {node_name_for_logging}") + + # 4. 更新UI + print(f"🔄 正在更新UI...") + if parent_for_next_selection and self.indexFromItem(parent_for_next_selection).isValid(): + new_selection_item = parent_for_next_selection + else: + new_selection_item = self.topLevelItem(0) + + if new_selection_item: + self.setCurrentItem(new_selection_item) + new_panda_node_to_select = new_selection_item.data(0, Qt.UserRole) + self.update_selection_and_properties(new_panda_node_to_select, new_selection_item) + else: + self.update_selection_and_properties(None, None) + + except Exception as e: + print(f"❌ 删除节点 {node_name_for_logging} 时发生意外错误: {str(e)}") + import traceback + traceback.print_exc() + + def clear_tree(self): + """清空UI树""" + print("Clear") + self.clear() + # 创建场景根节点 + sceneRoot = QTreeWidgetItem(self, ['场景']) + sceneRoot.setData(0, Qt.UserRole, self.world.render) + sceneRoot.setData(0, Qt.UserRole + 1, "SCENE_ROOT") + self._apply_font_to_item(sceneRoot) + # 添加相机节点 + cameraItem = QTreeWidgetItem(sceneRoot, ['相机']) + cameraItem.setData(0, Qt.UserRole, self.world.cam) + cameraItem.setData(0, Qt.UserRole + 1, "CAMERA_NODE") + self._apply_font_to_item(cameraItem) + # 添加地板节点 + if hasattr(self.world, 'ground') and self.world.ground: + groundItem = QTreeWidgetItem(sceneRoot, ['地板']) + groundItem.setData(0, Qt.UserRole, self.world.ground) + groundItem.setData(0,Qt.UserRole + 1, "SCENE_NODE") + self._apply_font_to_item(groundItem) + + def _apply_font_to_item(self, item): + """根据节点层级设置字体大小""" + if not item: + return + font = QFont(self.font()) + marker = item.data(0, Qt.UserRole + 1) + if item.parent() is None and (marker == "SCENE_ROOT" or item.text(0) == '场景'): + font = QFont("Microsoft YaHei", 12) + font.setWeight(QFont.Light) + else: + font = QFont("Microsoft YaHei", 10) + font.setWeight(QFont.Light) + item.setFont(0, font) + + def _apply_font_recursively(self, item): + """递归应用字体样式""" + if not item: + return + self._apply_font_to_item(item) + for index in range(item.childCount()): + self._apply_font_recursively(item.child(index)) + + def _handle_rows_inserted(self, parent_index, first, last): + """在插入新节点时自动调整字体""" + model = self.model() + if not model: + return + for row in range(first, last + 1): + index = model.index(row, 0, parent_index) + item = self.itemFromIndex(index) + if item: + self._apply_font_recursively(item) + + def _cleanup_panda_node_resources(self, panda_node): + """一个集中的辅助函数,用于清理与Panda3D节点相关的所有资源。""" + if not panda_node or panda_node.is_empty(): + return + try: + # 清理选择状态 + if hasattr(self.world, 'selection') and self.world.selection.selectedNode == panda_node: + self.world.selection.updateSelection(None) + # 清理属性面板 + if hasattr(self.world, 'property_panel'): + self.world.property_panel.removeActorForModel(panda_node) + # 清理灯光 + if hasattr(panda_node, 'getPythonTag'): + light_object = panda_node.getPythonTag('rp_light_object') + if light_object and hasattr(self.world, 'render_pipeline'): + self.world.render_pipeline.remove_light(light_object) + # 从各种world管理列表中移除 + lists_to_check = ['gui_elements', 'models', 'Spotlight', 'Pointlight', 'terrains'] + for list_name in lists_to_check: + if hasattr(self.world, list_name): + world_list = getattr(self.world, list_name) + if panda_node in world_list: + world_list.remove(panda_node) + # 特殊处理tilesets + if hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, 'tilesets'): + tilesets_to_remove = [i for i, info in enumerate(self.world.scene_manager.tilesets) if + info.get('node') == panda_node] + for i in reversed(tilesets_to_remove): + del self.world.scene_manager.tilesets[i] + print(f"🧹 已清理节点 {panda_node.getName()} 的所有关联资源。") + except Exception as e: + # 即便这里出错,也要打印信息,但不要让整个删除流程中断 + print(f"⚠️ 清理节点 {panda_node.getName()} 资源时出错: {e}") + + # def mousePressEvent(self, event): + # """鼠标按下事件""" + # if event.button() == Qt.LeftButton: + # if self.currentItem(): + # print(f"self.currentItem() = {self.currentItem()}") + # else: + # print(f"self.currentItem() = None") + # + # # 调用父类处理其他事件 + # super().mousePressEvent(event) + + def update_item_name(self, text, item): + """ 树节点名字 """ + if not item: + return + try: + # 正确的代码 + node = item.data(0, Qt.UserRole) + + item.setText(0, text) + node.setName(text) + except Exception as e: + print(e) + + def _findSceneRoot(self): + """查找场景根节点""" + for i in range(self.topLevelItemCount()): + top_item = self.topLevelItem(i) + if top_item.data(0, Qt.UserRole + 1) == "SCENE_ROOT": + return top_item + return None + + def create_model_items(self, model: NodePath): + """ + 【此函数保持不变】 + 创建模型项。 + 只寻找模型下一层带有 'is_scene_element' 标签的子节点作为分支的根, + 然后完整地展示这些分支。 + """ + if not model: + print("传入的参数model为空") + return + + # 找到场景树的根节点,我们将把模型节点添加到这里 + root_item = self._findSceneRoot() + if not root_item: + print("错误:未能找到场景根节点项") + return + + # 1. 在模型的第一层子节点中进行筛选 + for child_node in model.getChildren(): + if child_node.hasTag("is_scene_element"): + print(f"找到带标签的根节点:{child_node.getName()}") + if (child_node.hasTag("gui_type")and + child_node.getTag("gui_type") in ["3d_text","3d_image","video_screen"]): + print(f"跳过3dGUI节点{child_node.getName()}") + continue + + # 为这个带标签的节点创建一个树项 + child_item = QTreeWidgetItem(root_item) + child_item.setText(0, child_node.getName() or "Unnamed Tagged Node") + child_item.setData(0, Qt.UserRole, child_node) + child_item.setData(0, Qt.UserRole + 1, child_node.getTag("tree_item_type")) + # self._add_node_info(child_item, child_node) # 可选信息 + + # 2. 对这个节点的所有后代进行“无条件”递归添加 (但会跳过碰撞体) + self._add_all_children_unconditionally(child_item, child_node) + + def _add_all_children_unconditionally(self, parent_item: QTreeWidgetItem, node_path: NodePath): + """ + 【此函数已更新】 + 无条件地、递归地添加一个节点下的所有子节点,但会跳过碰撞节点。 + """ + for child_node in node_path.getChildren(): + + # 新增:检查节点是否为碰撞节点 + if isinstance(child_node.node(), CollisionNode): + # print(f"跳过碰撞节点: {child_node.getName()}") # 用于调试 + continue # 如果是,则跳过此节点及其所有子节点 + + # 创建子项 + child_item = QTreeWidgetItem(parent_item) + child_item.setText(0, child_node.getName() or "Unnamed Child") + child_item.setData(0, Qt.UserRole, child_node) + child_item.setData(0, Qt.UserRole + 1, child_node.getTag("tree_item_type")) + # self._add_node_info(child_item, child_node) # 可选信息 + + # 继续无条件地递归 + if not child_node.is_empty(): + self._add_all_children_unconditionally(child_item, child_node) + + # ==================== 辅助方法 ==================== + def _findSceneRoot(self): + """查找场景根节点""" + for i in range(self.topLevelItemCount()): + top_item = self.topLevelItem(i) + if top_item.data(0, Qt.UserRole + 1) == "SCENE_ROOT": + return top_item + return + + def _generateUniqueNodeName(self, base_name, parent_node): + """生成唯一的节点名称""" + # 获取父节点下所有子节点的名称 + existing_names = set() + for child in parent_node.getChildren(): + existing_names.add(child.getName()) + + # 如果基础名称不存在,直接使用 + if base_name not in existing_names: + return base_name + + # 否则添加数字后缀 + counter = 1 + while f"{base_name}_{counter}" in existing_names: + counter += 1 + + return f"{base_name}_{counter}" + + def add_node_to_tree_widget(self, node, parent_item, node_type): + """将node元素添加到树形控件""" + if hasattr(node, 'getTag'): + if node.hasTag('tree_item_type'): + print(f"node0: {node.getName()},{node.getTag('tree_item_type')}") + tree_type = node.getTag('tree_item_type') + else: + node.setTag('tree_item_type', node_type) + else: + print(f"node2: {node.getName()},{node_type}") + tree_type = node_type + + + # BLACK_LIST 和依赖项导入保持不变 + BLACK_LIST = {'', '**', 'temp', 'collision'} + from panda3d.core import CollisionNode, ModelRoot + from PyQt5.QtWidgets import QTreeWidgetItem + from PyQt5.QtCore import Qt + + # 1. 修改内部函数,让它返回创建的节点 + def addNodeToTree(node, parentItem, force=False): + """内部递归函数,现在会返回创建的顶级节点项""" + if not force and should_skip(node): + return None # 如果跳过,返回None + + nodeItem = QTreeWidgetItem(parentItem, [node.getName()]) + nodeItem.setData(0, Qt.UserRole, node) + nodeItem.setData(0, Qt.UserRole + 1, tree_type) + + for child in node.getChildren(): + # 递归调用,但我们只关心顶级的nodeItem + addNodeToTree(child, nodeItem, force=False) + + return nodeItem # <-- 新增:返回创建的QTreeWidgetItem + + def should_skip(node): + name = node.getName() + return name in BLACK_LIST or name.startswith('__') or isinstance(node.node(), CollisionNode) or isinstance( + node.node(), ModelRoot) or name == "" + + # 使用一个变量来确保无论哪个分支都有返回值 + new_qt_item = None + node_name = "" + + try: + if tree_type == "IMPORTED_MODEL_NODE": + # getTag('file') 可能是你自己设置的tag,这里假设它存在 + node_name = node.getTag("file") if hasattr(node, 'getTag') and node.hasTag("file") else node.getName() + + # 2. 接收 addNodeToTree 的返回值 + new_qt_item = addNodeToTree(node, parent_item, force=True) + + else: + node_name = node.getName() if hasattr(node, 'getName') else "node" + new_qt_item = QTreeWidgetItem(parent_item, [node_name]) + new_qt_item.setData(0, Qt.UserRole, node) + new_qt_item.setData(0, Qt.UserRole + 1, tree_type) + + # 确保 new_qt_item 成功创建后再继续操作 + if new_qt_item: + # 展开父节点 + if hasattr(parent_item, 'setExpanded'): + parent_item.setExpanded(True) + + print(f"✅ Qt树节点添加成功: {node_name}") + return new_qt_item + else: + # 如果 addNodeToTree 因为 should_skip 返回了 None + print(f"ℹ️ 节点 {node_name} 被跳过,未添加到树中。") + return None + + except Exception as e: + import traceback + print(f"❌ 添加node到树形控件失败: {str(e)}") + traceback.print_exc() # 打印更详细的错误堆栈,方便调试 + return None + + def update_selection_and_properties(self, node, qt_item): + """更新选择状态和属性面板""" + try: + # 更新选择状态 + if hasattr(self.world, 'selection'): + self.world.selection.updateSelection(node) + + # 更新属性面板 + if hasattr(self.world, 'property_panel'): + self.world.property_panel.updatePropertyPanel(qt_item) + elif hasattr(self.world, 'updatePropertyPanel'): + self.world.updatePropertyPanel(qt_item) + + print(f"✅ 更新选择和属性面板: {qt_item.text(0)}") + + except Exception as e: + print(f"❌ 更新选择和属性面板失败: {str(e)}") + + # ==================== 3D辅助方法 ==================== + def get_target_parents_for_creation(self): + """获取创建目标的父节点列表""" + from PyQt5.QtCore import Qt + + target_parents = [] + + try: + selected_items = self.selectedItems() + + # 检查选中的项目,找出所有有效的父节点 + for item in selected_items: + if self._isValidParentForNewNode(item): + parent_node = item.data(0, Qt.UserRole) + if parent_node: + target_parents.append((item, parent_node)) + print(f"📍 找到有效父节点: {item.text(0)}") + + # 如果没有有效的选中节点,使用场景根节点 + if not target_parents: + print("⚠️ 没有有效选中节点,使用场景根节点") + scene_root = self._findSceneRoot() + if scene_root: + parent_node = scene_root.data(0, Qt.UserRole) + if parent_node: + target_parents.append((scene_root, parent_node)) + else: + # 如果没有_findSceneRoot方法,使用world.render作为默认父节点 + if hasattr(self.world, 'render'): + # 创建一个虚拟的树项目来表示render节点 + class MockTreeItem: + def text(self, column): + return "render" + + mock_item = MockTreeItem() + target_parents.append((mock_item, self.world.render)) + + print(f"📊 总共找到 {len(target_parents)} 个目标父节点") + return target_parents + + except Exception as e: + print(f"❌ 获取目标父节点失败: {str(e)}") + return [] + + def _isValidParentForNewNode(self, item): + """检查节点是否适合作为新节点的父节点-3d""" + if not item: + return False + + # 获取节点类型标识 + node_type = item.data(0, Qt.UserRole + 1) + + # 场景根节点和普通场景节点可以作为父节点 + if node_type in self.valid_3d_parent_types: + return True + + # # 模型节点也可以作为父节点 + # panda_node = item.data(0, Qt.UserRole) + # if panda_node and panda_node.hasTag("is_model_root"): + # return True + # + # # 其他类型的节点也可以,但排除一些特殊情况 + # if panda_node: + # # # 排除相机节点 + # # if panda_node == self.world.cam: + # # return False + # # 排除碰撞节点 + # from panda3d.core import CollisionNode + # if isinstance(panda_node.node(), CollisionNode): + # return False + # return True + + return False + + + def get_target_parents_for_gui_creation(self): + """获取GUI创建目标的父节点列表 - 支持GUI元素作为父节点""" + from PyQt5.QtCore import Qt + + target_parents = [] + + try: + selected_items = self.selectedItems() + + # 检查选中的项目,找出所有有效的父节点 + for item in selected_items: + if self.isValidParentForGUI(item): + parent_node = item.data(0, Qt.UserRole) + if parent_node: + target_parents.append((item, parent_node)) # 修复:确保添加到列表 + print(f"📍 找到有效GUI父节点: {item.text(0)}") + else: + print(f"⚠️ GUI父节点 {item.text(0)} 的Panda3D数据为空") + + # 如果没有有效的选中节点,使用场景根节点 + if not target_parents: + print("⚠️ 没有有效选中节点,使用场景根节点") + scene_root = self._findSceneRoot() + if scene_root: + parent_node = scene_root.data(0, Qt.UserRole) + if parent_node: + target_parents.append((scene_root, parent_node)) + else: + if hasattr(self.world, 'render'): + class MockTreeItem: + def text(self, column): + return "render" + + mock_item = MockTreeItem() + target_parents.append((mock_item, self.world.render)) + + print(f"📊 总共找到 {len(target_parents)} 个目标父节点") + return target_parents + + except Exception as e: + print(f"❌ 获取目标父节点失败: {str(e)}") + return [] + + def isValidParentForGUI(self, item): + """检查节点是否适合作为GUI元素的父节点""" + if not item: + return False + + # 获取节点类型标识 + node_type = item.data(0, Qt.UserRole + 1) + + # GUI元素可以作为其他GUI元素的父节点 + if node_type in self.gui_2d_types: + return True + + # 场景根节点和普通场景节点也可以作为父节点 + if node_type in ["SCENE_ROOT"]: + return True + + return False + + def is_gui_element(self, node): + """判断节点是否是GUI元素""" + if not node: + return False + + # 检查是否有GUI标签 + if hasattr(node, 'getTag'): + return node.getTag("is_gui_element") == "1" or node.getTag("gui_type") in ["info_panel", "button", "label", "entry", "3d_text", "virtual_screen"] + + # 检查是否是DirectGUI对象 + try: + from direct.gui.DirectGuiBase import DirectGuiBase + return isinstance(node, DirectGuiBase) + except ImportError: + return False + + def calculate_relative_gui_position(self, pos, parent_gui): + """计算相对于GUI父节点的位置""" + try: + # 对于GUI子元素,使用相对坐标 + # 这里使用较小的缩放因子,因为是相对于父GUI的位置 + relative_pos = (pos[0] * 0.05, 0, pos[2] * 0.05) + print(f"📐 计算GUI相对位置: {pos} -> {relative_pos}") + return relative_pos + except Exception as e: + print(f"❌ 计算GUI相对位置失败: {str(e)}") + # 如果计算失败,返回默认的屏幕坐标 + return (pos[0] * 0.1, 0, pos[2] * 0.1) + + #---------------------------暂时无用------------------------------- + + def add_existing_node(self, panda_node, node_type="SCENE_NODE", parent_item=None): + """将已存在的Panda3D节点添加到Qt树形控件中 + + Args: + panda_node: 已创建的Panda3D节点 + node_type: 节点类型标识 + parent_item: 父Qt项目,如果为None则使用当前选中项或根节点 + + Returns: + QTreeWidgetItem: 创建的Qt树项目 + """ + try: + if not panda_node: + print("❌ 传入的Panda3D节点为空") + return None + + # 确定父项目 - 保持简单,只处理单个父节点 + if parent_item is None: + # 优先使用当前选中的第一个有效节点作为父节点 + selected_items = self.selectedItems() + if selected_items: + # 找到第一个有效的父节点 + for potential_parent in selected_items: + if self._isValidParentForNewNode(potential_parent): + parent_item = potential_parent + print(f"📍 使用选中节点作为父节点: {parent_item.text(0)}") + break + + # 如果没有找到有效的选中节点 + if not parent_item: + print("⚠️ 所有选中节点都不适合作为父节点,查找场景根节点") + parent_item = self._findSceneRoot() + else: + # 没有选中任何节点,使用场景根节点 + print("📍 没有选中节点,使用场景根节点作为父节点") + parent_item = self._findSceneRoot() + + # 如果场景根节点也找不到,最后尝试render节点 + if not parent_item: + print("📍 场景根节点未找到,尝试使用render节点") + parent_item = self._findRenderItem() + + if not parent_item: + print("❌ 无法找到合适的父节点") + return None + + # 创建Qt树项目 + node_name = panda_node.getName() + new_qt_item = QTreeWidgetItem(parent_item, [node_name]) + new_qt_item.setData(0, Qt.UserRole, panda_node) + new_qt_item.setData(0, Qt.UserRole + 1, node_type) + + # 展开父节点 + parent_item.setExpanded(True) + + print(f"✅ 成功将现有节点添加到树形控件: {node_name} -> 父节点: {parent_item.text(0)}") + return new_qt_item + + except Exception as e: + print(f"❌ 添加现有节点到树形控件失败: {str(e)}") + import traceback + traceback.print_exc() + return None + + def _findRenderItem(self): + """查找render根节点项目""" + try: + root = self.invisibleRootItem() + for i in range(root.childCount()): + item = root.child(i) + if item.text(0) == "render": + return item + + # 如果没找到render节点,返回第一个子项目 + if root.childCount() > 0: + return root.child(0) + + return None + except Exception as e: + print(f"查找render节点失败: {e}") + return None + + def create_item(self, node_type="empty", selected_items=None): + """创建不同类型的场景节点 + + Args: + node_type: 节点类型 ("empty", "spot_light", "point_light") + selected_items: 选中的父节点项目列表,如果为None则使用当前选中项或根节点 + """ + try: + # 确定父节点 + parent_items = self._determineParentItems(selected_items) + if not parent_items: + print("⚠️ 无法确定父节点") + return [] + + created_nodes = [] + + for parent_item in parent_items: + # 验证父节点的有效性 + if not self._isValidParentForNewNode(parent_item): + print(f"⚠️ 节点 {parent_item.text(0)} 不适合作为父节点") + continue + + # 获取父节点的Panda3D对象 + parent_node = parent_item.data(0, Qt.UserRole) + if not parent_node: + print(f"⚠️ 父节点 {parent_item.text(0)} 没有对应的Panda3D对象") + continue + + # 根据节点类型创建不同的节点 + if node_type == "empty": + new_node = self._createEmptyNode(parent_node, parent_item) + elif node_type == "spot_light": + new_node = self._createSpotLightNode(parent_node, parent_item) + elif node_type == "point_light": + new_node = self._createPointLightNode(parent_node, parent_item) + else: + print(f"❌ 不支持的节点类型: {node_type}") + continue + + if new_node: + created_nodes.append(new_node) + + # 如果只创建了一个节点,自动选中它 + if len(created_nodes) == 1: + _, qt_item = created_nodes[0] + self.setCurrentItem(qt_item) + # 更新选择和属性面板 + if hasattr(self.world, 'selection'): + self.world.selection.updateSelection(qt_item.data(0, Qt.UserRole)) + if hasattr(self.world, 'property_panel'): + self.world.property_panel.updatePropertyPanel(qt_item) + + print(f"✅ 总共创建了 {len(created_nodes)} 个 {node_type} 节点") + return created_nodes + + except Exception as e: + print(f"❌ 创建 {node_type} 节点失败: {str(e)}") + import traceback + traceback.print_exc() + return [] + + def _determineParentItems(self, selected_items): + """确定父节点项目列表""" + if selected_items is not None: + return selected_items + + # 使用当前选中的项目 + current_selected = self.selectedItems() + if current_selected: + return current_selected + + # 如果没有选中任何项目,使用场景根节点 + scene_root = self._findSceneRoot() + if scene_root: + return [scene_root] + + return [] + + def _setupNewNodeDefaults(self, node): + """设置新节点的默认属性""" + # 设置默认位置(相对于父节点) + node.setPos(0, 0, 0) + node.setHpr(0, 0, 0) + node.setScale(1, 1, 1) + + # 设置默认可见性 + node.show() + + # 可以根据需要添加更多默认设置 + # 例如:默认材质、碰撞检测等 + + # ==================== 创建节点 ==================== + def _createEmptyNode(self, parent_node, parent_item): + """创建空节点""" + # 生成唯一的节点名称 + node_name = self._generateUniqueNodeName("空节点", parent_node) + + # 在Panda3D场景中创建新节点 + new_panda_node = parent_node.attachNewNode(node_name) + + # 设置新节点的默认属性 + self._setupNewNodeDefaults(new_panda_node) + + # 设置节点标签 + new_panda_node.setTag("is_scene_element", "1") + new_panda_node.setTag("node_type", "empty_node") + new_panda_node.setTag("created_by_user", "1") + + # 在Qt树中创建对应的项目 + new_qt_item = QTreeWidgetItem(parent_item, [node_name]) + new_qt_item.setData(0, Qt.UserRole, new_panda_node) + new_qt_item.setData(0, Qt.UserRole + 1, "SCENE_NODE") + + # 展开父节点 + parent_item.setExpanded(True) + + print(f"✅ 成功创建空节点: {node_name}") + return (new_panda_node, new_qt_item) + + def _createSpotLightNode(self, parent_node, parent_item): + """创建聚光灯节点""" + from RenderPipelineFile.rpcore import SpotLight + from QMeta3D.Meta3DWorld import get_render_pipeline + from panda3d.core import Vec3, NodePath + + try: + render_pipeline = get_render_pipeline() + + # 生成唯一的节点名称 + light_name = self._generateUniqueNodeName(f"Spotlight_{len(self.world.Spotlight)}", parent_node) + + # 创建挂载节点 + light_np = NodePath(light_name) + light_np.reparentTo(parent_node) + + # 创建聚光灯对象 + light = SpotLight() + light.direction = Vec3(0, 0, -1) + light.fov = 70 + light.set_color_from_temperature(5 * 1000.0) + light.energy = 5000 + light.radius = 1000 + light.casts_shadows = True + light.shadow_map_resolution = 256 + light.setPos(0, 0, 0) # 相对于父节点的位置 + + # 添加到渲染管线 + render_pipeline.add_light(light) + + # 设置节点属性和标签 + light_np.setTag("light_type", "spot_light") + light_np.setTag("is_scene_element", "1") + light_np.setTag("light_energy", str(light.energy)) + light_np.setTag("created_by_user", "1") + + # 保存光源对象引用 + light_np.setPythonTag("rp_light_object", light) + + # 添加到管理列表 + self.world.Spotlight.append(light_np) + + # 在Qt树中创建对应的项目 + new_qt_item = QTreeWidgetItem(parent_item, [light_name]) + new_qt_item.setData(0, Qt.UserRole, light_np) + new_qt_item.setData(0, Qt.UserRole + 1, "LIGHT_NODE") + + # 展开父节点 + parent_item.setExpanded(True) + + print(f"✅ 成功创建聚光灯: {light_name}") + + except Exception as e: + print(f"❌ 创建聚光灯失败: {str(e)}") + return None + + def _createPointLightNode(self, parent_node, parent_item): + """创建点光源节点""" + from RenderPipelineFile.rpcore import PointLight + from QMeta3D.Meta3DWorld import get_render_pipeline + from panda3d.core import Vec3, NodePath + + try: + render_pipeline = get_render_pipeline() + + # 生成唯一的节点名称 + light_name = self._generateUniqueNodeName(f"Pointlight_{len(self.world.Pointlight)}", parent_node) + + # 创建挂载节点 + light_np = NodePath(light_name) + light_np.reparentTo(parent_node) + + # 创建点光源对象 + light = PointLight() + light.setPos(0, 0, 0) # 相对于父节点的位置 + light.energy = 5000 + light.radius = 1000 + light.inner_radius = 0.4 + light.set_color_from_temperature(5 * 1000.0) + light.casts_shadows = True + light.shadow_map_resolution = 256 + + # 添加到渲染管线 + render_pipeline.add_light(light) + + # 设置节点属性和标签 + light_np.setTag("light_type", "point_light") + light_np.setTag("is_scene_element", "1") + light_np.setTag("light_energy", str(light.energy)) + light_np.setTag("created_by_user", "1") + + # 保存光源对象引用 + light_np.setPythonTag("rp_light_object", light) + + # 添加到管理列表 + self.world.Pointlight.append(light_np) + + # 在Qt树中创建对应的项目 + new_qt_item = QTreeWidgetItem(parent_item, [light_name]) + new_qt_item.setData(0, Qt.UserRole, light_np) + new_qt_item.setData(0, Qt.UserRole + 1, "LIGHT_NODE") + + # 展开父节点 + parent_item.setExpanded(True) + + print(f"✅ 成功创建点光源: {light_name}") + return (light_np, new_qt_item) + + except Exception as e: + print(f"❌ 创建点光源失败: {str(e)}") + return None + + +