diff --git a/.gitignore b/.gitignore index aa6cef21..efd048cc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,57 @@ __pycache__/ *.pyc .idea/ +【模板】模型及算法与功能对应清单.docx +【模板】GY知识-产品模块架构及功能清单和主要应用场景说明.docx +1.json +2.json +3.json +4.json +6.json +功能清单.md +模型及算法与功能对应清单(完整版).md +维修系统使用说明.md +audio_demo_config.json +create_test_audio.py +demo_universal_dialog.py +Dock按钮样式统一说明.md +Dock面板标题样式线完成说明.md +exam_result_20250926_004454.json +exam_result_20250926_010628.json +exam_result_20250926_011746.json +exam_result_20250926_090330.json +exam_result_20250926_111756.json +exam_test_config.json +Figma布局优化说明.md +Figma设计分析与菜单栏优化文档.md +Figma样式优化完成说明.md +manual_tools_config.json +pm.txt +simple_menubar_test.py +test_audio.py +test_basic_audio.py +test_exam_tool_error.py +test_full_audio_playback.py +test_menu_visibility.py +test_mode_selection.py +test_mp3_audio.py +test_pygame_audio.py +test_score_config.py +test_simple_audio.py +tool_test_config.json +UI设计文档.md +UniversalMessageDialog使用说明.md +.codex/settings/kiroCodex-settings.json +data/projects.json +RenderPipelineFile/config/daytime.yaml +Resources/audio/README.md +Resources/model/a.glb +Resources/model/b.glb +Resources/model/c.glb +Resources/model/JQB_auto_converted.glb +Resources/model/Untitled.glb +Resources/models/Women_1.glb +Subjects/test_maintenance_config.json +Resources/models/Women_1.glb +Resources/models/Women_1.glb +Resources/models/Women_1.glb diff --git a/.idea/EG.iml b/.idea/EG.iml index fa7a6154..bb7fb320 100644 --- a/.idea/EG.iml +++ b/.idea/EG.iml @@ -1,8 +1,10 @@ - - + + + + diff --git a/.idea/misc.xml b/.idea/misc.xml index 51b9fc17..c76b5032 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -3,5 +3,5 @@ - + \ No newline at end of file diff --git a/.venv/Scripts/python.exe b/.venv/Scripts/python.exe deleted file mode 100644 index 42356dc4..00000000 Binary files a/.venv/Scripts/python.exe and /dev/null differ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..1fc3378b --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,112 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## 项目概述 + +这是一个基于Panda3D的3D渲染引擎和场景编辑器,集成了PyQt5界面和多种高级功能: + +- 3D场景编辑器(模型导入、材质系统、碰撞检测) +- GUI元素管理(2D/3D GUI组件) +- 项目管理系统(场景保存/加载、项目打包) +- Cesium地图集成 +- 渲染管线增强(RenderPipelineFile) + +## 运行和构建命令 + +### 启动应用程序 +```bash +python Start_Run.py [project_path] +``` +或者直接: +```bash +python main.py +``` + +### 依赖安装 +```bash +# 主要依赖 +pip install -r requirements/requirements.txt + +# Conda环境依赖 +pip install -r requirements/conda-requirements.txt +``` + +### 工具脚本 +```bash +# 安装FBX到GLTF转换工具 +./install_fbx2gltf.sh +``` + +## 核心架构 + +### 主要模块结构 +``` +EG/ +├── main.py # 应用程序入口点 +├── Start_Run.py # 启动脚本(路径配置) +├── core/ # 核心功能模块 +│ ├── world.py # 3D世界核心(继承Panda3DWorld) +│ ├── scene_manager.py # 场景和模型管理 +│ ├── selection.py # 对象选择系统 +│ ├── event_handler.py # 事件处理 +│ └── tool_manager.py # 工具系统 +├── gui/ # GUI元素管理 +│ └── gui_manager.py # 2D/3D GUI组件 +├── ui/ # 用户界面 +│ ├── widgets.py # 自定义Qt组件 +│ ├── property_panel.py # 属性面板 +│ └── interface_manager.py # 界面管理 +├── scene/ # 场景相关 +│ └── scene_manager.py # 场景管理器 +├── project/ # 项目管理 +│ └── project_manager.py # 项目生命周期 +├── RenderPipelineFile/ # 渲染管线扩展 +└── QPanda3D/ # Panda3D Qt集成 +``` + +### 核心设计模式 + +1. **模块化架构**: 每个功能模块独立,通过管理器类协调 +2. **事件驱动**: EventHandler统一处理用户交互和系统事件 +3. **组件系统**: SelectionSystem、ToolManager等可插拔组件 +4. **MVC分离**: UI组件、核心逻辑和数据管理分离 + +### 主要依赖集成 + +- **Panda3D 1.10.15**: 3D渲染引擎 +- **PyQt5**: GUI框架 +- **QPanda3D**: Panda3D的Qt集成 +- **RenderPipeline**: 高级渲染功能 + +## 开发指南 + +### 添加新功能模块 +1. 在对应目录下创建新的Python文件 +2. 继承相应的基类(如Panda3DWorld用于3D功能) +3. 在main.py中集成新模块 +4. 更新界面管理器以添加UI控制 + +### 材质和渲染 +- 材质系统集成在scene/scene_manager.py +- 支持PBR材质和自定义着色器 +- RenderPipelineFile提供高级渲染特性 + + +### GUI开发 +- 使用PyQt5构建主界面 +- 3D GUI元素通过gui/gui_manager.py管理 +- 自定义组件在ui/widgets.py中定义 + +## 文件约定 + +- Python文件使用UTF-8编码 +- 中文注释和文档字符串 +- 模块顶部包含功能描述注释 +- 类和方法使用描述性命名 + +## 注意事项 + +- 项目依赖多个大型库(Panda3D、PyQt5、RenderPipeline) +- Cesium集成需要WebEngine支持 +- 某些功能可能需要特定的系统配置 \ No newline at end of file diff --git a/QPanda3D/Panda3DWorld.py b/QPanda3D/Panda3DWorld.py index 78193075..e717b68c 100644 --- a/QPanda3D/Panda3DWorld.py +++ b/QPanda3D/Panda3DWorld.py @@ -70,6 +70,14 @@ class Panda3DWorld(ShowBase): loadPrcFileData("", f"win-size {width} {height}") loadPrcFileData("", "win-fixed-size #f") # 允许窗口调整大小 + # 🚀 VR性能优化配置 + loadPrcFileData("", "prefer-single-buffer true") # 减少缓冲区交换开销 + loadPrcFileData("", "gl-force-flush false") # 避免强制glFlush导致的性能损失 + loadPrcFileData("", "sync-video false") # 禁用默认VSync,让OpenVR控制 + loadPrcFileData("", "support-stencil false") # 禁用不必要的模板缓冲区 + loadPrcFileData("", "clock-mode non-real-time") # 禁用Panda3D帧率控制,让OpenVR控制 + # loadPrcFileData("", "gl-debug true") # 调试时可启用OpenGL调试 + if (is_fullscreen): loadPrcFileData("", "fullscreen #t") @@ -134,7 +142,7 @@ class Panda3DWorld(ShowBase): #render_pipeline.set_camera(self.cam) - #添加渲染效果�� + #添加渲染效果 #self.cam = self.render_pipeline._showbase.cam #self.camNode = self.cam.node() #self.camLens = self.camNode.get_lens() @@ -152,6 +160,19 @@ class Panda3DWorld(ShowBase): self.mouse_controller = CustomMouseController(self) self.mouse_controller.setUp() + # 添加错误处理钩子 + self.accept("transform_state_error", self._handle_transform_error) + + def _handle_transform_error(self): + """处理TransformState相关的错误""" + try: + from panda3d.core import TransformState, RenderState + TransformState.clear_cache() + RenderState.clear_cache() + print("已清理TransformState和RenderState缓存") + except Exception as e: + print(f"清理缓存时出错: {e}") + def render_pipeline(self): """获取 RenderPipeline 实例""" return self._render_pipeline @@ -190,4 +211,4 @@ def resize_buffer(self, width: int, height: int): self.camLens.set_film_size(width, height) # 或 set_aspect_ratio(width / height) # 强制更新窗口(有时在 Qt 内嵌时需要) - self.graphicsEngine.open_windows() + self.graphicsEngine.open_windows() \ No newline at end of file diff --git a/QPanda3D/QPanda3DWidget.py b/QPanda3D/QPanda3DWidget.py index 61b531b1..672ad897 100644 --- a/QPanda3D/QPanda3DWidget.py +++ b/QPanda3D/QPanda3DWidget.py @@ -12,6 +12,7 @@ from PyQt5 import QtWidgets, QtGui from PyQt5.QtCore import * from PyQt5.QtGui import * from PyQt5.QtWidgets import * +from direct.task.TaskManagerGlobal import taskMgr # Panda imports from panda3d.core import Texture, WindowProperties, CallbackGraphicsWindow @@ -32,24 +33,34 @@ class QPanda3DSynchronizer(QTimer): self.setInterval(int(dt)) self.timeout.connect(self.tick) - # def tick(self): - # taskMgr.step() - # self.qPanda3DWidget.update() - def tick(self): try: + # 在渲染前清理可能损坏的TransformState对象 + from panda3d.core import TransformState + TransformState.clear_cache() + taskMgr.step() self.qPanda3DWidget.update() except AssertionError as e: - if "has_mat()" in str(e): - print("⚠️ 检测到变换矩阵错误,跳过此帧") - # 继续运行而不是崩溃 + # 专门处理 TransformState has_mat() 断言错误 + if "has_mat" in str(e): + print(f"警告: 检测到TransformState断言错误,已静默处理: {e}") + # 尝试恢复渲染状态 + try: + # 强制清理缓存并重试 + from panda3d.core import TransformState, RenderState + TransformState.clear_cache() + RenderState.clear_cache() + taskMgr.step() + self.qPanda3DWidget.update() + except: + pass else: + # 重新抛出其他断言错误 raise except Exception as e: - print(f"❌ 渲染循环错误: {e}") - import traceback - traceback.print_exc() + # 静默处理其他所有异常 + print(f"警告: 检测到异常,已静默处理: {e}") def get_panda_key_modifiers(evt): @@ -248,7 +259,7 @@ class QPanda3DWidget(QWidget): # # data = tex.getRamImage().getData() # width = tex.getXSize() - # height = tex.getYSize() + # height = tex.getYSize() # expected_len = width * height * 4 # # if len(data) != expected_len: @@ -341,10 +352,4 @@ class QPanda3DWidget(QWidget): print(f"Panda3D 窗口尺寸已同步为: {adjusted_width} x {adjusted_height}") except Exception as e: - print(f"同步 Panda3D 窗口尺寸失败: {str(e)}") - - - - - - + print(f"同步 Panda3D 窗口尺寸失败: {str(e)}") \ No newline at end of file diff --git a/RenderPipelineFile/config/daytime.yaml b/RenderPipelineFile/config/daytime.yaml index 9c9622cd..54447c55 100644 --- a/RenderPipelineFile/config/daytime.yaml +++ b/RenderPipelineFile/config/daytime.yaml @@ -5,7 +5,7 @@ control_points: clouds: - cloud_brightness: [[[0.4558541267,0.9574780059],[0.2744727256,0.8944309678],[0.1938559693,0.5249266862],[0.8905932438,0.2375366569],[0.6429901728,0.9589492375],[0.7581600000,0.8914912023],[0.2226478119,0.7859268915],[0.8406909789,0.4897360704],[0.5451055662,0.9618768328],[0.9462571977,0.0000000000],[1.0000000000,0.0000000000],[0.3646833013,0.9472140762],[0.3186180422,0.9325513197],[0.0823264879,0.0000000000],[0.4932562620,0.9526462396]]] + cloud_brightness: [[[0.4558541267,0.9574780059],[0.2744727256,0.8944309678],[0.1938559693,0.5249266862],[0.8905932438,0.2375366569],[0.6429901728,0.9589492375],[0.7581600000,0.8914912023],[0.2226478119,0.7859268915],[0.8406909789,0.4897360704],[0.5451055662,0.9618768328],[0.9462571977,0.0000000000],[1.0000000000,0.0000000000],[0.3646833013,0.9472140762],[0.3186180422,0.9325513197],[0.0823264879,0.0000000000]]] color_correction: camera_iso: [[[0.4708024067,0.2757660168]]] camera_shutter: [[[0.5134061147,0.0552217053]]] @@ -17,8 +17,8 @@ control_points: scattering: sun_intensity: [[[0.0000000000,0.0000000000],[0.0041666667,0.0000000000],[0.0083333333,0.0000000000],[0.0125000000,0.0000000000],[0.0166666667,0.0000000000],[0.0208333333,0.0000000000],[0.0250000000,0.0000000000],[0.0291666667,0.0000000000],[0.0333333333,0.0000000000],[0.0375000000,0.0000000000],[0.0416666667,0.0000000000],[0.0458333333,0.0000000000],[0.0500000000,0.0000000000],[0.0541666667,0.0000000000],[0.0583333333,0.0000000000],[0.0625000000,0.0000000000],[0.0666666667,0.0000000000],[0.0708333333,0.0000000000],[0.0750000000,0.0000000000],[0.0791666667,0.0000000000],[0.0833333333,0.0000000000],[0.0875000000,0.0000000000],[0.0916666667,0.0000000000],[0.0958333333,0.0000000000],[0.1000000000,0.0000000000],[0.1041666667,0.0000000000],[0.1083333333,0.0000000000],[0.1125000000,0.0000000000],[0.1166666667,0.0000000000],[0.1208333333,0.0000000000],[0.1250000000,0.0000000000],[0.1291666667,0.0000000000],[0.1333333333,0.0000000000],[0.1375000000,0.0000000000],[0.1416666667,0.0000000000],[0.1458333333,0.0000000000],[0.1500000000,0.0000000000],[0.1541666667,0.0000000000],[0.1583333333,0.0000028805],[0.1625000000,0.0003577724],[0.1666666667,0.0013331400],[0.1708333333,0.0029671803],[0.1750000000,0.0052963381],[0.1791666667,0.0083550556],[0.1833333333,0.0121755589],[0.1875000000,0.0167876159],[0.1916666667,0.0222183530],[0.1958333333,0.0284919947],[0.2000000000,0.0356297193],[0.2041666667,0.0436494349],[0.2083333333,0.0525656099],[0.2125000000,0.0623891610],[0.2166666667,0.0731272461],[0.2208333333,0.0847831708],[0.2250000000,0.0973563167],[0.2291666667,0.1108419698],[0.2333333333,0.1252313631],[0.2375000000,0.1405115250],[0.2416666667,0.1566653434],[0.2458333333,0.1736715009],[0.2500000000,0.1915046014],[0.2541666667,0.2101350464],[0.2583333333,0.2295292930],[0.2625000000,0.2496498145],[0.2666666667,0.2704552670],[0.2708333333,0.2919006662],[0.2750000000,0.3139375192],[0.2791666667,0.3365139497],[0.2833333333,0.3595750662],[0.2875000000,0.3830630359],[0.2916666667,0.4069173972],[0.2958333333,0.4310753462],[0.3000000000,0.4554720417],[0.3041666667,0.4800408236],[0.3083333333,0.5047136020],[0.3125000000,0.5294212108],[0.3166666667,0.5540936424],[0.3208333333,0.5786605298],[0.3250000000,0.6030514553],[0.3291666667,0.6271963182],[0.3333333333,0.6510256858],[0.3375000000,0.6744711982],[0.3416666667,0.6974659988],[0.3458333333,0.7199450163],[0.3500000000,0.7418453485],[0.3541666667,0.7631067095],[0.3583333333,0.7836717291],[0.3625000000,0.8034862953],[0.3666666667,0.8224999302],[0.3708333333,0.8406661079],[0.3750000000,0.8579425235],[0.3791666667,0.8742914270],[0.3833333333,0.8896799131],[0.3875000000,0.9040801386],[0.3916666667,0.9174695289],[0.3958333333,0.9298310650],[0.4000000000,0.9411533765],[0.4041666667,0.9514309312],[0.4083333333,0.9606641691],[0.4125000000,0.9688595571],[0.4166666667,0.9760296330],[0.4208333333,0.9821930708],[0.4250000000,0.9873746114],[0.4291666667,0.9916050060],[0.4333333333,0.9949209310],[0.4375000000,0.9973647924],[0.4416666667,0.9989845508],[0.4458333333,0.9998334497],[0.4500000000,0.9999696949],[0.4541666667,0.9994560801],[0.4583333333,0.9983595429],[0.4625000000,0.9967506613],[0.4666666667,0.9947030614],[0.4708333333,0.9922927758],[0.4750000000,0.9895975125],[0.4791666667,0.9866958610],[0.4833333333,0.9836664262],[0.4875000000,0.9805868867],[0.4916666667,0.9775330316],[0.4958333333,0.9745777179],[0.5000000000,0.9717898417],[0.5041666667,0.9692332877],[0.5083333333,0.9669658924],[0.5125000000,0.9650384806],[0.5089595376,0.9690650222],[0.5208333333,0.9623666659],[0.5250000000,0.9616814371],[0.5291666667,0.9614534423],[0.5333333333,0.9616877089],[0.5375000000,0.9623790807],[0.5416666667,0.9635123329],[0.5458333333,0.9650624244],[0.5500000000,0.9669949804],[0.5541666667,0.9692669864],[0.5583333333,0.9718275065],[0.5625000000,0.9746185969],[0.5666666667,0.9775762863],[0.5708333333,0.9806315864],[0.5750000000,0.9837115661],[0.5791666667,0.9867403433],[0.5833333333,0.9896401655],[0.5875000000,0.9923323562],[0.5916666667,0.9947382579],[0.5958333333,0.9967800977],[0.6000000000,0.9983817820],[0.6041666667,0.9994696263],[0.6083333333,0.9999730028],[0.6125000000,0.9998249266],[0.6166666667,0.9989625601],[0.6208333333,0.9973276624],[0.6250000000,0.9948669567],[0.6291666667,0.9915324664],[0.6333333333,0.9872817545],[0.6375000000,0.9820781426],[0.6416666667,0.9758908775],[0.6458333333,0.9686952146],[0.6500000000,0.9604725211],[0.6541666667,0.9512102537],[0.6583333333,0.9409019858],[0.6625000000,0.9295473441],[0.6666666667,0.9171518878],[0.6708333333,0.9037270619],[0.6750000000,0.8892899902],[0.6791666667,0.8738633008],[0.6833333333,0.8574749656],[0.6875000000,0.8401579787],[0.6916666667,0.8219502453],[0.6958333333,0.8028941798],[0.7000000000,0.7830364456],[0.7041666667,0.7624277344],[0.7083333333,0.7411222520],[0.7125000000,0.7191776044],[0.7166666667,0.6966542563],[0.7208333333,0.6736152714],[0.7250000000,0.6501259629],[0.7291666667,0.6262533880],[0.7333333333,0.6020661121],[0.7375000000,0.5776338043],[0.7416666667,0.5530267796],[0.7458333333,0.5283156992],[0.7500000000,0.5035711751],[0.7541666667,0.4788634341],[0.7583333333,0.4542618347],[0.7625000000,0.4298347613],[0.7666666667,0.4056490351],[0.7708333333,0.3817697830],[0.7750000000,0.3582600107],[0.7791666667,0.3351803495],[0.7833333333,0.3125888445],[0.7875000000,0.2905406366],[0.7916666667,0.2690876955],[0.7958333333,0.2482787388],[0.8000000000,0.2281588906],[0.8041666667,0.2087696425],[0.8083333333,0.1901486315],[0.8125000000,0.1723295359],[0.8166666667,0.1553419918],[0.8208333333,0.1392115328],[0.8250000000,0.1239595144],[0.8291666667,0.1096030703],[0.8333333333,0.0961551918],[0.8375000000,0.0836246599],[0.8416666667,0.0720161369],[0.8458333333,0.0613302273],[0.8500000000,0.0515635598],[0.8541666667,0.0427088803],[0.8583333333,0.0347551990],[0.8625000000,0.0276878920],[0.8666666667,0.0214889271],[0.8708333333,0.0161369711],[0.8750000000,0.0116076130],[0.8791666667,0.0078735477],[0.8833333333,0.0049047927],[0.8875000000,0.0026688977],[0.8916666667,0.0011311782],[0.8958333333,0.0002549473],[0.9000000000,0.0000000000],[0.9041666667,0.0000000000],[0.9083333333,0.0000000000],[0.9125000000,0.0000000000],[0.9166666667,0.0000000000],[0.9208333333,0.0000000000],[0.9250000000,0.0000000000],[0.9291666667,0.0000000000],[0.9333333333,0.0000000000],[0.9375000000,0.0000000000],[0.9416666667,0.0000000000],[0.9458333333,0.0000000000],[0.9500000000,0.0000000000],[0.9541666667,0.0000000000],[0.9583333333,0.0000000000],[0.9625000000,0.0000000000],[0.9666666667,0.0000000000],[0.9708333333,0.0000000000],[0.9750000000,0.0000000000],[0.9791666667,0.0000000000],[0.9833333333,0.0000000000],[0.9875000000,0.0000000000],[0.9916666667,0.0000000000],[0.9958333333,0.0000000000]]] sun_color: [[[0.5010435645,0.5818710306],[0.0433100000,0.8999700000],[0.8635787716,0.9130000000],[0.1785000000,0.8973600000],[0.8099800000,0.8651100000],[0.2360800000,0.7712700000],[0.6583432177,0.8485126184],[0.1266806142,0.9648102053],[0.9558541267,0.9090909091],[0.5568400771,0.7353760446]],[[0.5001318426,0.5160300000],[0.0572700000,0.6541600000],[0.2395000000,0.5976800000],[0.8104600000,0.6009000000],[0.6967400000,0.5483900000]],[[0.0862400000,0.4257800000],[0.4955600000,0.4033000000],[0.8234200000,0.4340200000]]] - sun_azimuth: [[[0.5000000000,0.4833333333]]] - sun_altitude: [[[0.5000000000,0.9777777778]]] + sun_azimuth: [[[0.5000000000,0.6250000000]]] + sun_altitude: [[[0.5000000000,0.9555555556]]] extinction: [[[0.4913294798,0.6378830084]]] volumetrics: fog_ramp_size: [[[0.5510597303,0.7409470752]]] diff --git a/RenderPipelineFile/rpcore/render_pipeline.py b/RenderPipelineFile/rpcore/render_pipeline.py index a7ac29df..e4c46dbb 100644 --- a/RenderPipelineFile/rpcore/render_pipeline.py +++ b/RenderPipelineFile/rpcore/render_pipeline.py @@ -351,7 +351,11 @@ class RenderPipeline(RPObject): continue material = state.get_attrib(MaterialAttrib).get_material() - shading_model = material.emission.x + if material.emission is not None: + shading_model = material.emission.x + else: + shading_model = 0.0 + # SHADING_MODEL_TRANSPARENT if shading_model == 3: diff --git a/RenderPipelineFile/rpcore/util/movement_controller.py b/RenderPipelineFile/rpcore/util/movement_controller.py index 72161c7b..6fb2f4b9 100644 --- a/RenderPipelineFile/rpcore/util/movement_controller.py +++ b/RenderPipelineFile/rpcore/util/movement_controller.py @@ -60,7 +60,7 @@ class MovementController(object): self.keyboard_hpr_speed = 0.4 self.use_hpr = False self.smoothness = 6.0 - self.bobbing_amount = 1.5 + self.bobbing_amount = 0.0 self.bobbing_speed = 0.5 def set_initial_position(self, pos, target): @@ -138,8 +138,8 @@ class MovementController(object): self.showbase.accept("j", self.print_position) # mouse - self.showbase.accept("mouse1", self.set_mouse_enabled, [True]) - self.showbase.accept("mouse1-up", self.set_mouse_enabled, [False]) + self.showbase.accept("mouse3", self.set_mouse_enabled, [True]) + self.showbase.accept("mouse3-up", self.set_mouse_enabled, [False]) # arrow mouse navigation self.showbase.accept("arrow_up", self.set_hpr_movement, [1, 1]) diff --git a/RenderPipelineFile/samples/.gitignore b/RenderPipelineFile/samples/.gitignore deleted file mode 100644 index ba1fdf8a..00000000 --- a/RenderPipelineFile/samples/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -*.pyc -*.pyd - -*.blend1 -03-Forest -06-Map -*.blend1 -*.blend2 diff --git a/RenderPipelineFile/samples/00-Loading the pipeline/main-possibility2.py b/RenderPipelineFile/samples/00-Loading the pipeline/main-possibility2.py deleted file mode 100644 index ce8e6915..00000000 --- a/RenderPipelineFile/samples/00-Loading the pipeline/main-possibility2.py +++ /dev/null @@ -1,28 +0,0 @@ -""" - -This is an alternative possibility of initializing the RenderPipeline, which -makes it possible to construct the ShowBase object manually - -""" - -import sys -from direct.showbase.ShowBase import ShowBase - -# Insert the pipeline path to the system path, this is required to be -# able to import the pipeline classes. In case you placed the render -# pipeline in a subfolder of your project, you have to adjust this. -sys.path.insert(0, "../../RenderPipeline") -sys.path.insert(0, "../../") - -# Import render pipeline classes -from rpcore import RenderPipeline - -# Construct and create the pipeline -render_pipeline = RenderPipeline() -render_pipeline.pre_showbase_init() - -# Construct and create the ShowBase -base = ShowBase() -render_pipeline.create(base) - -base.run() diff --git a/RenderPipelineFile/samples/00-Loading the pipeline/main-possibility3.py b/RenderPipelineFile/samples/00-Loading the pipeline/main-possibility3.py deleted file mode 100644 index 3bedec2e..00000000 --- a/RenderPipelineFile/samples/00-Loading the pipeline/main-possibility3.py +++ /dev/null @@ -1,28 +0,0 @@ -""" - -This is an alternative possibility of initializing the RenderPipeline, which -uses the (deprecated!) DirectStart interface. This should not be used anymore, -except for fast prototyping. - -""" - -import sys - -# Insert the pipeline path to the system path, this is required to be -# able to import the pipeline classes. In case you placed the render -# pipeline in a subfolder of your project, you have to adjust this. -sys.path.insert(0, "../../") -sys.path.insert(0, "../../RenderPipeline") - -# Import render pipeline classes -from rpcore import RenderPipeline - -# Construct and create the pipeline -render_pipeline = RenderPipeline() -render_pipeline.pre_showbase_init() - -# Import (deprecated!) DirectStart interface -import direct.directbase.DirectStart -render_pipeline.create(base) - -base.run() diff --git a/RenderPipelineFile/samples/00-Loading the pipeline/main.py b/RenderPipelineFile/samples/00-Loading the pipeline/main.py deleted file mode 100644 index 43ce052c..00000000 --- a/RenderPipelineFile/samples/00-Loading the pipeline/main.py +++ /dev/null @@ -1,39 +0,0 @@ -""" - -Simplest possible application using the render pipeline. - -This sample will not show any fancy rendering output, but you can base your own -applications on this skeleton. - -This is the preferred way of initializing the pipeline, however you can find -alternative ways in the other included files. - -""" - -import sys -from direct.showbase.ShowBase import ShowBase - - -class Application(ShowBase): - - def __init__(self): - # Notice that you must not call ShowBase.__init__ (or super), the - # render pipeline does that for you. If this is unconvenient for you, - # have a look at the other initialization possibilities. - - # Insert the pipeline path to the system path, this is required to be - # able to import the pipeline classes. In case you placed the render - # pipeline in a subfolder of your project, you have to adjust this. - sys.path.insert(0, "../../") - sys.path.insert(0, "../../RenderPipeline") - - # Import the main render pipeline class - from rpcore import RenderPipeline - - # Construct and create the pipeline - self.render_pipeline = RenderPipeline() - self.render_pipeline.create(self) - - # Done! You can start setting up your application stuff as regular now. - -Application().run() diff --git a/RenderPipelineFile/samples/00-Loading the pipeline/screenshot.png b/RenderPipelineFile/samples/00-Loading the pipeline/screenshot.png deleted file mode 100644 index d1db7da9..00000000 Binary files a/RenderPipelineFile/samples/00-Loading the pipeline/screenshot.png and /dev/null differ diff --git a/RenderPipelineFile/samples/01-Material-Demo/main.py b/RenderPipelineFile/samples/01-Material-Demo/main.py deleted file mode 100644 index 49534cb3..00000000 --- a/RenderPipelineFile/samples/01-Material-Demo/main.py +++ /dev/null @@ -1,72 +0,0 @@ -""" - -Material Demo - -This demonstrates the various materials the pipeline supports. -It is also a reference scene, for testing BRDF changes. - -""" - -from __future__ import print_function - -import os -import sys -from panda3d.core import Vec3, load_prc_file_data -from direct.showbase.ShowBase import ShowBase - -# Change to the current directory -os.chdir(os.path.dirname(os.path.realpath(__file__))) - -# Insert the pipeline path to the system path, this is required to be -# able to import the pipeline classes -pipeline_path = "../../" - -# Just a special case for my development setup, so I don't accidentally -# commit a wrong path. You can remove this in your own programs. -if not os.path.isfile(os.path.join(pipeline_path, "setup.py")): - pipeline_path = "../../RenderPipeline/" - -sys.path.insert(0, pipeline_path) - -# Import the render pipeline class -from rpcore import RenderPipeline - -# This is a helper class for better camera movement - see below. -from rpcore.util.movement_controller import MovementController - - -class Application(ShowBase): - def __init__(self): - # Setup window size and title - load_prc_file_data("", """ - # win-size 1600 900 - window-title Render Pipeline - Material Sample - """) - - # Construct the render pipeline - self.render_pipeline = RenderPipeline() - self.render_pipeline.create(self) - self.render_pipeline.daytime_mgr.time = "19:17" - # self.render_pipeline.daytime_mgr.time = "12:00" - - # Load the scene - model = self.loader.load_model("scene/TestScene.bam") - model.reparent_to(self.render) - - self.render_pipeline.prepare_scene(model) - - # Enable parallax mapping on the floor - # self.render_pipeline.set_effect( - # model.find("**/FloorPlane"), - # "effects/default.yaml", {"parallax_mapping": True}, 100) - - # Initialize movement controller, this is a convenience class - # to provide an improved camera control compared to Panda3Ds default - # mouse controller. - self.controller = MovementController(self) - self.controller.set_initial_position_hpr( - Vec3(-17.2912578583, -13.290019989, 6.88211250305), - Vec3(-39.7285499573, -14.6770210266, 0.0)) - self.controller.setup() - -Application().run() diff --git a/RenderPipelineFile/samples/01-Material-Demo/scene/.gitignore b/RenderPipelineFile/samples/01-Material-Demo/scene/.gitignore deleted file mode 100644 index 256bf264..00000000 --- a/RenderPipelineFile/samples/01-Material-Demo/scene/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.blend diff --git a/RenderPipelineFile/samples/01-Material-Demo/scene/TestScene.7z b/RenderPipelineFile/samples/01-Material-Demo/scene/TestScene.7z deleted file mode 100644 index 72c48efe..00000000 Binary files a/RenderPipelineFile/samples/01-Material-Demo/scene/TestScene.7z and /dev/null differ diff --git a/RenderPipelineFile/samples/01-Material-Demo/scene/TestScene.bam b/RenderPipelineFile/samples/01-Material-Demo/scene/TestScene.bam deleted file mode 100644 index a11116ea..00000000 Binary files a/RenderPipelineFile/samples/01-Material-Demo/scene/TestScene.bam and /dev/null differ diff --git a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/CobaltBasecolor.png b/RenderPipelineFile/samples/01-Material-Demo/scene/tex/CobaltBasecolor.png deleted file mode 100644 index ab9ede2b..00000000 Binary files a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/CobaltBasecolor.png and /dev/null differ diff --git a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/CobaltNormal.png b/RenderPipelineFile/samples/01-Material-Demo/scene/tex/CobaltNormal.png deleted file mode 100644 index 966ff811..00000000 Binary files a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/CobaltNormal.png and /dev/null differ diff --git a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/CobaltSpecular.png b/RenderPipelineFile/samples/01-Material-Demo/scene/tex/CobaltSpecular.png deleted file mode 100644 index 909e6f28..00000000 Binary files a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/CobaltSpecular.png and /dev/null differ diff --git a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/Concrete.png b/RenderPipelineFile/samples/01-Material-Demo/scene/tex/Concrete.png deleted file mode 100644 index c7697801..00000000 Binary files a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/Concrete.png and /dev/null differ diff --git a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/CopperBasecolor.png b/RenderPipelineFile/samples/01-Material-Demo/scene/tex/CopperBasecolor.png deleted file mode 100644 index 9b40b9c9..00000000 Binary files a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/CopperBasecolor.png and /dev/null differ diff --git a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/CopperNormal.png b/RenderPipelineFile/samples/01-Material-Demo/scene/tex/CopperNormal.png deleted file mode 100644 index e32fb325..00000000 Binary files a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/CopperNormal.png and /dev/null differ diff --git a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/CopperSpecular.png b/RenderPipelineFile/samples/01-Material-Demo/scene/tex/CopperSpecular.png deleted file mode 100644 index 98d3558f..00000000 Binary files a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/CopperSpecular.png and /dev/null differ diff --git a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/EmptyBaseColor.png b/RenderPipelineFile/samples/01-Material-Demo/scene/tex/EmptyBaseColor.png deleted file mode 100644 index 437c2444..00000000 Binary files a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/EmptyBaseColor.png and /dev/null differ diff --git a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/EmptyNormal.png b/RenderPipelineFile/samples/01-Material-Demo/scene/tex/EmptyNormal.png deleted file mode 100644 index 863dc2ec..00000000 Binary files a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/EmptyNormal.png and /dev/null differ diff --git a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/EmptyRoughness.png b/RenderPipelineFile/samples/01-Material-Demo/scene/tex/EmptyRoughness.png deleted file mode 100644 index 437c2444..00000000 Binary files a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/EmptyRoughness.png and /dev/null differ diff --git a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/EmptySpecular.png b/RenderPipelineFile/samples/01-Material-Demo/scene/tex/EmptySpecular.png deleted file mode 100644 index 26ee2247..00000000 Binary files a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/EmptySpecular.png and /dev/null differ diff --git a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/FLoorBG2DISP.png b/RenderPipelineFile/samples/01-Material-Demo/scene/tex/FLoorBG2DISP.png deleted file mode 100644 index 4a51469e..00000000 Binary files a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/FLoorBG2DISP.png and /dev/null differ diff --git a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/FLoorBG2SPEC.png b/RenderPipelineFile/samples/01-Material-Demo/scene/tex/FLoorBG2SPEC.png deleted file mode 100644 index 77157db5..00000000 Binary files a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/FLoorBG2SPEC.png and /dev/null differ diff --git a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/FloorBG.png b/RenderPipelineFile/samples/01-Material-Demo/scene/tex/FloorBG.png deleted file mode 100644 index 1fa7b6cb..00000000 Binary files a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/FloorBG.png and /dev/null differ diff --git a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/FloorBG2.png b/RenderPipelineFile/samples/01-Material-Demo/scene/tex/FloorBG2.png deleted file mode 100644 index 9666b211..00000000 Binary files a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/FloorBG2.png and /dev/null differ diff --git a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/FloorBG2NRM.png b/RenderPipelineFile/samples/01-Material-Demo/scene/tex/FloorBG2NRM.png deleted file mode 100644 index 2c62714c..00000000 Binary files a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/FloorBG2NRM.png and /dev/null differ diff --git a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/FloorBGNRM.png b/RenderPipelineFile/samples/01-Material-Demo/scene/tex/FloorBGNRM.png deleted file mode 100644 index c0ba85cc..00000000 Binary files a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/FloorBGNRM.png and /dev/null differ diff --git a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/FloorBGSPEC.png b/RenderPipelineFile/samples/01-Material-Demo/scene/tex/FloorBGSPEC.png deleted file mode 100644 index 3f322f7a..00000000 Binary files a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/FloorBGSPEC.png and /dev/null differ diff --git a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/GridBG.png b/RenderPipelineFile/samples/01-Material-Demo/scene/tex/GridBG.png deleted file mode 100644 index 8dd6369f..00000000 Binary files a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/GridBG.png and /dev/null differ diff --git a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/Leather.png b/RenderPipelineFile/samples/01-Material-Demo/scene/tex/Leather.png deleted file mode 100644 index b6f947d9..00000000 Binary files a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/Leather.png and /dev/null differ diff --git a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/LeatherNRM.png b/RenderPipelineFile/samples/01-Material-Demo/scene/tex/LeatherNRM.png deleted file mode 100644 index 4a52fe60..00000000 Binary files a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/LeatherNRM.png and /dev/null differ diff --git a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/LeatherSPEC.png b/RenderPipelineFile/samples/01-Material-Demo/scene/tex/LeatherSPEC.png deleted file mode 100644 index 1f4fe999..00000000 Binary files a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/LeatherSPEC.png and /dev/null differ diff --git a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/MetalBasecolor.png b/RenderPipelineFile/samples/01-Material-Demo/scene/tex/MetalBasecolor.png deleted file mode 100644 index 0fe6bdaa..00000000 Binary files a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/MetalBasecolor.png and /dev/null differ diff --git a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/MetalNormal.png b/RenderPipelineFile/samples/01-Material-Demo/scene/tex/MetalNormal.png deleted file mode 100644 index a96c3c71..00000000 Binary files a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/MetalNormal.png and /dev/null differ diff --git a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/MetalSpecular.png b/RenderPipelineFile/samples/01-Material-Demo/scene/tex/MetalSpecular.png deleted file mode 100644 index 96f38961..00000000 Binary files a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/MetalSpecular.png and /dev/null differ diff --git a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/RoughnessGradient.png b/RenderPipelineFile/samples/01-Material-Demo/scene/tex/RoughnessGradient.png deleted file mode 100644 index a2269ebd..00000000 Binary files a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/RoughnessGradient.png and /dev/null differ diff --git a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/Sign_MeasuredMetal.png b/RenderPipelineFile/samples/01-Material-Demo/scene/tex/Sign_MeasuredMetal.png deleted file mode 100644 index 5169a6b9..00000000 Binary files a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/Sign_MeasuredMetal.png and /dev/null differ diff --git a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/Sign_Metallic.png b/RenderPipelineFile/samples/01-Material-Demo/scene/tex/Sign_Metallic.png deleted file mode 100644 index bf1d7730..00000000 Binary files a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/Sign_Metallic.png and /dev/null differ diff --git a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/Sign_Nonmetallic.png b/RenderPipelineFile/samples/01-Material-Demo/scene/tex/Sign_Nonmetallic.png deleted file mode 100644 index 90189c8c..00000000 Binary files a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/Sign_Nonmetallic.png and /dev/null differ diff --git a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/Sign_materials.png b/RenderPipelineFile/samples/01-Material-Demo/scene/tex/Sign_materials.png deleted file mode 100644 index 2cba0df2..00000000 Binary files a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/Sign_materials.png and /dev/null differ diff --git a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/StoneMask.png b/RenderPipelineFile/samples/01-Material-Demo/scene/tex/StoneMask.png deleted file mode 100644 index bf794786..00000000 Binary files a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/StoneMask.png and /dev/null differ diff --git a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/Stones_01_Atlas_Diffuse_01.png b/RenderPipelineFile/samples/01-Material-Demo/scene/tex/Stones_01_Atlas_Diffuse_01.png deleted file mode 100644 index af506ff1..00000000 Binary files a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/Stones_01_Atlas_Diffuse_01.png and /dev/null differ diff --git a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/Stones_01_Atlas_Normal_02.png b/RenderPipelineFile/samples/01-Material-Demo/scene/tex/Stones_01_Atlas_Normal_02.png deleted file mode 100644 index 25cf9158..00000000 Binary files a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/Stones_01_Atlas_Normal_02.png and /dev/null differ diff --git a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/Stripes.png b/RenderPipelineFile/samples/01-Material-Demo/scene/tex/Stripes.png deleted file mode 100644 index 6d36024d..00000000 Binary files a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/Stripes.png and /dev/null differ diff --git a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/empty_basecolor.png b/RenderPipelineFile/samples/01-Material-Demo/scene/tex/empty_basecolor.png deleted file mode 100644 index 437c2444..00000000 Binary files a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/empty_basecolor.png and /dev/null differ diff --git a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/empty_normal.png b/RenderPipelineFile/samples/01-Material-Demo/scene/tex/empty_normal.png deleted file mode 100644 index 863dc2ec..00000000 Binary files a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/empty_normal.png and /dev/null differ diff --git a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/empty_roughness.png b/RenderPipelineFile/samples/01-Material-Demo/scene/tex/empty_roughness.png deleted file mode 100644 index 437c2444..00000000 Binary files a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/empty_roughness.png and /dev/null differ diff --git a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/empty_specular.png b/RenderPipelineFile/samples/01-Material-Demo/scene/tex/empty_specular.png deleted file mode 100644 index 26ee2247..00000000 Binary files a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/empty_specular.png and /dev/null differ diff --git a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/grass_diffuse.jpg b/RenderPipelineFile/samples/01-Material-Demo/scene/tex/grass_diffuse.jpg deleted file mode 100644 index 503fa784..00000000 Binary files a/RenderPipelineFile/samples/01-Material-Demo/scene/tex/grass_diffuse.jpg and /dev/null differ diff --git a/RenderPipelineFile/samples/01-Material-Demo/screenshot.png b/RenderPipelineFile/samples/01-Material-Demo/screenshot.png deleted file mode 100644 index 888155b6..00000000 Binary files a/RenderPipelineFile/samples/01-Material-Demo/screenshot.png and /dev/null differ diff --git a/RenderPipelineFile/samples/02-Roaming-Ralph/main.py b/RenderPipelineFile/samples/02-Roaming-Ralph/main.py deleted file mode 100644 index c78f5eb4..00000000 --- a/RenderPipelineFile/samples/02-Roaming-Ralph/main.py +++ /dev/null @@ -1,315 +0,0 @@ -""" - -Roaming Ralph Sample (modified) - -This is the default roaming ralph sample, with the render pipeline. -Using the render pipeline is only the matter of a few lines, which have -been explicitely marked. - -NOTICE: Since this is a straight copy of the standard roaming ralph sample, - this attempts to keep as close to the original code to make it easier - to see where to load the render pipeline. - - If you find a bug/suggestion in this code, then you should report - that to the sample included in Panda3D, and not this code. - - (and yeah, this code could surely be written in a much nicer way) - -""" - -import os -import sys - -from panda3d.core import CollisionTraverser, CollisionNode -from panda3d.core import CollisionHandlerQueue, CollisionRay -from panda3d.core import AmbientLight, DirectionalLight -from panda3d.core import PandaNode, NodePath, TextNode -from panda3d.core import Vec3, Vec4, BitMask32, load_prc_file_data -from direct.gui.OnscreenText import OnscreenText -from direct.actor.Actor import Actor -from direct.showbase.ShowBase import ShowBase - - -# Switch into the current directory -os.chdir(os.path.realpath(os.path.dirname(__file__))) - -SPEED = 0.5 - - -# Function to put instructions on the screen. -def addInstructions(pos, msg): - return OnscreenText(text=msg, style=1, fg=(1, 1, 1, 1), - pos=(-0.9, pos - 0.2), align=TextNode.ALeft, scale=.035) - - -class World(ShowBase): - - def __init__(self): - - # Setup window size, title and so on - load_prc_file_data("", """ - win-size 1600 900 - window-title Render Pipeline - Roaming Ralph Demo - """) - - # ------ Begin of render pipeline code ------ - - # Insert the pipeline path to the system path, this is required to be - # able to import the pipeline classes - pipeline_path = "../../" - - # Just a special case for my development setup, so I don't accidentally - # commit a wrong path. You can remove this in your own programs. - if not os.path.isfile(os.path.join(pipeline_path, "setup.py")): - pipeline_path = "../../RenderPipeline/" - - sys.path.insert(0, pipeline_path) - - from rpcore import RenderPipeline, SpotLight - self.render_pipeline = RenderPipeline() - self.render_pipeline.create(self) - - # ------ End of render pipeline code, thats it! ------ - - # Set time of day - self.render_pipeline.daytime_mgr.time = "7:40" - - # Use a special effect for rendering the scene, this is because the - # roaming ralph model has no normals or valid materials - self.render_pipeline.set_effect(render, "scene-effect.yaml", {}, sort=250) - - self.keyMap = {"left":0, "right":0, "forward":0, "backward":0, "cam-left":0, "cam-right":0} - self.speed = 1.0 - base.win.setClearColor(Vec4(0,0,0,1)) - - # Post the instructions - - self.inst1 = addInstructions(0.95, "[ESC] Quit") - self.inst4 = addInstructions(0.90, "[W] Run Ralph Forward") - self.inst4 = addInstructions(0.85, "[S] Run Ralph Backward") - self.inst2 = addInstructions(0.80, "[A] Rotate Ralph Left") - self.inst3 = addInstructions(0.75, "[D] Rotate Ralph Right") - self.inst6 = addInstructions(0.70, "[Left Arrow] Rotate Camera Left") - self.inst7 = addInstructions(0.65, "[Right Arrow] Rotate Camera Right") - - # Set up the environment - # - # This environment model contains collision meshes. If you look - # in the egg file, you will see the following: - # - # { Polyset keep descend } - # - # This tag causes the following mesh to be converted to a collision - # mesh -- a mesh which is optimized for collision, not rendering. - # It also keeps the original mesh, so there are now two copies --- - # one optimized for rendering, one for collisions. - - self.environ = loader.loadModel("resources/world") - self.environ.reparentTo(render) - self.environ.setPos(0,0,0) - - - # Remove wall nodes - self.environ.find("**/wall").remove_node() - - # Create the main character, Ralph - self.ralph = Actor("resources/ralph", - {"run":"resources/ralph-run", - "walk":"resources/ralph-walk"}) - self.ralph.reparentTo(render) - self.ralph.setScale(.2) - self.ralph.setPos(Vec3(-110.9, 29.4, 1.8)) - - # Create a floater object. We use the "floater" as a temporary - # variable in a variety of calculations. - - self.floater = NodePath(PandaNode("floater")) - self.floater.reparentTo(render) - - # Accept the control keys for movement and rotation - - self.accept("escape", sys.exit) - self.accept("a", self.setKey, ["left",1]) - self.accept("d", self.setKey, ["right",1]) - self.accept("w", self.setKey, ["forward",1]) - self.accept("s", self.setKey, ["backward",1]) - self.accept("arrow_left", self.setKey, ["cam-left",1]) - self.accept("arrow_right", self.setKey, ["cam-right",1]) - self.accept("a-up", self.setKey, ["left",0]) - self.accept("d-up", self.setKey, ["right",0]) - self.accept("w-up", self.setKey, ["forward",0]) - self.accept("s-up", self.setKey, ["backward",0]) - self.accept("arrow_left-up", self.setKey, ["cam-left",0]) - self.accept("arrow_right-up", self.setKey, ["cam-right",0]) - self.accept("=", self.adjustSpeed, [0.25]) - self.accept("+", self.adjustSpeed, [0.25]) - self.accept("-", self.adjustSpeed, [-0.25]) - - taskMgr.add(self.move,"moveTask") - - # Game state variables - self.isMoving = False - - # Set up the camera - - base.disableMouse() - base.camera.setPos(self.ralph.getX() + 10,self.ralph.getY() + 10, 2) - base.camLens.setFov(80) - - # We will detect the height of the terrain by creating a collision - # ray and casting it downward toward the terrain. One ray will - # start above ralph's head, and the other will start above the camera. - # A ray may hit the terrain, or it may hit a rock or a tree. If it - # hits the terrain, we can detect the height. If it hits anything - # else, we rule that the move is illegal. - self.cTrav = CollisionTraverser() - - self.ralphGroundRay = CollisionRay() - self.ralphGroundRay.setOrigin(0,0,1000) - self.ralphGroundRay.setDirection(0,0,-1) - self.ralphGroundCol = CollisionNode('ralphRay') - self.ralphGroundCol.addSolid(self.ralphGroundRay) - self.ralphGroundCol.setFromCollideMask(BitMask32.bit(0)) - self.ralphGroundCol.setIntoCollideMask(BitMask32.allOff()) - self.ralphGroundColNp = self.ralph.attachNewNode(self.ralphGroundCol) - self.ralphGroundHandler = CollisionHandlerQueue() - self.cTrav.addCollider(self.ralphGroundColNp, self.ralphGroundHandler) - - self.camGroundRay = CollisionRay() - self.camGroundRay.setOrigin(0,0,1000) - self.camGroundRay.setDirection(0,0,-1) - self.camGroundCol = CollisionNode('camRay') - self.camGroundCol.addSolid(self.camGroundRay) - self.camGroundCol.setFromCollideMask(BitMask32.bit(0)) - self.camGroundCol.setIntoCollideMask(BitMask32.allOff()) - self.camGroundColNp = base.camera.attachNewNode(self.camGroundCol) - self.camGroundHandler = CollisionHandlerQueue() - self.cTrav.addCollider(self.camGroundColNp, self.camGroundHandler) - - # Uncomment this line to see the collision rays - #self.ralphGroundColNp.show() - #self.camGroundColNp.show() - - # Uncomment this line to show a visual representation of the - # collisions occuring - #self.cTrav.showCollisions(render) - - # Create some lighting - ambientLight = AmbientLight("ambientLight") - ambientLight.setColor(Vec4(.3, .3, .3, 1)) - directionalLight = DirectionalLight("directionalLight") - directionalLight.setDirection(Vec3(-5, -5, -5)) - directionalLight.setColor(Vec4(1, 1, 1, 1)) - directionalLight.setSpecularColor(Vec4(1, 1, 1, 1)) - render.setLight(render.attachNewNode(ambientLight)) - render.setLight(render.attachNewNode(directionalLight)) - - #Records the state of the arrow keys - def setKey(self, key, value): - self.keyMap[key] = value - - # Adjust movement speed - def adjustSpeed(self, delta): - newSpeed = self.speed + delta - if 0 <= newSpeed <= 3: - self.speed = newSpeed - - # Accepts arrow keys to move either the player or the menu cursor, - # Also deals with grid checking and collision detection - def move(self, task): - - # If the camera-left key is pressed, move camera left. - # If the camera-right key is pressed, move camera right. - - base.camera.lookAt(self.ralph) - if (self.keyMap["cam-left"]!=0): - base.camera.setX(base.camera, +20 * globalClock.getDt()) - if (self.keyMap["cam-right"]!=0): - base.camera.setX(base.camera, -20 * globalClock.getDt()) - - # save ralph's initial position so that we can restore it, - # in case he falls off the map or runs into something. - - startpos = self.ralph.getPos() - - # If a move-key is pressed, move ralph in the specified direction. - - if (self.keyMap["left"]!=0): - self.ralph.setH(self.ralph.getH() + 300 * globalClock.getDt()) - elif (self.keyMap["right"]!=0): - self.ralph.setH(self.ralph.getH() - 300 * globalClock.getDt()) - if (self.keyMap["forward"]!=0): - self.ralph.setY(self.ralph, -25 * self.speed * globalClock.getDt()) - elif (self.keyMap["backward"]!=0): - self.ralph.setY(self.ralph, 25 * self.speed * globalClock.getDt()) - - # If ralph is moving, loop the run animation. - # If he is standing still, stop the animation. - - if (self.keyMap["forward"]!=0) or (self.keyMap["backward"]!=0) or \ - (self.keyMap["left"]!=0) or (self.keyMap["right"]!=0): - if self.isMoving is False: - self.ralph.loop("run") - self.isMoving = True - else: - if self.isMoving: - self.ralph.stop() - self.ralph.pose("walk",5) - self.isMoving = False - - # If the camera is too far from ralph, move it closer. - # If the camera is too close to ralph, move it farther. - - camvec = self.ralph.getPos() - base.camera.getPos() - camvec.setZ(0) - camdist = camvec.length() - camvec.normalize() - if (camdist > 10.0): - base.camera.setPos(base.camera.getPos() + camvec*(camdist-10)) - camdist = 10.0 - if (camdist < 5.0): - base.camera.setPos(base.camera.getPos() - camvec*(5-camdist)) - camdist = 5.0 - - # Now check for collisions. - - self.cTrav.traverse(render) - - # Adjust ralph's Z coordinate. If ralph's ray hit terrain, - # update his Z. If it hit anything else, or didn't hit anything, put - # him back where he was last frame. - - entries = [] - for i in range(self.ralphGroundHandler.getNumEntries()): - entry = self.ralphGroundHandler.getEntry(i) - entries.append(entry) - if (len(entries)>0) and (entries[0].getIntoNode().getName() == "terrain"): - self.ralph.setZ(entries[0].getSurfacePoint(render).getZ()) - else: - self.ralph.setPos(startpos) - - # Keep the camera at one foot above the terrain, - # or two feet above ralph, whichever is greater. - - entries = [] - for i in range(self.camGroundHandler.getNumEntries()): - entry = self.camGroundHandler.getEntry(i) - entries.append(entry) - if (len(entries)>0) and (entries[0].getIntoNode().getName() == "terrain"): - base.camera.setZ(entries[0].getSurfacePoint(render).getZ()+1.0) - if (base.camera.getZ() < self.ralph.getZ() + 2.0): - base.camera.setZ(self.ralph.getZ() + 2.0) - - # The camera should look in ralph's direction, - # but it should also try to stay horizontal, so look at - # a floater which hovers above ralph's head. - - self.floater.setPos(self.ralph.getPos()) - self.floater.setZ(self.ralph.getZ() + 2.0) - base.camera.lookAt(self.floater) - - return task.cont - - -w = World().run() - diff --git a/RenderPipelineFile/samples/02-Roaming-Ralph/resources/ground.jpg b/RenderPipelineFile/samples/02-Roaming-Ralph/resources/ground.jpg deleted file mode 100644 index fa8e9af5..00000000 Binary files a/RenderPipelineFile/samples/02-Roaming-Ralph/resources/ground.jpg and /dev/null differ diff --git a/RenderPipelineFile/samples/02-Roaming-Ralph/resources/hedge.jpg b/RenderPipelineFile/samples/02-Roaming-Ralph/resources/hedge.jpg deleted file mode 100644 index 15408467..00000000 Binary files a/RenderPipelineFile/samples/02-Roaming-Ralph/resources/hedge.jpg and /dev/null differ diff --git a/RenderPipelineFile/samples/02-Roaming-Ralph/resources/ralph-run.egg.pz b/RenderPipelineFile/samples/02-Roaming-Ralph/resources/ralph-run.egg.pz deleted file mode 100644 index 4dce4115..00000000 Binary files a/RenderPipelineFile/samples/02-Roaming-Ralph/resources/ralph-run.egg.pz and /dev/null differ diff --git a/RenderPipelineFile/samples/02-Roaming-Ralph/resources/ralph-walk.egg.pz b/RenderPipelineFile/samples/02-Roaming-Ralph/resources/ralph-walk.egg.pz deleted file mode 100644 index 8012c7c2..00000000 Binary files a/RenderPipelineFile/samples/02-Roaming-Ralph/resources/ralph-walk.egg.pz and /dev/null differ diff --git a/RenderPipelineFile/samples/02-Roaming-Ralph/resources/ralph.egg.pz b/RenderPipelineFile/samples/02-Roaming-Ralph/resources/ralph.egg.pz deleted file mode 100644 index a59183c2..00000000 Binary files a/RenderPipelineFile/samples/02-Roaming-Ralph/resources/ralph.egg.pz and /dev/null differ diff --git a/RenderPipelineFile/samples/02-Roaming-Ralph/resources/ralph.jpg b/RenderPipelineFile/samples/02-Roaming-Ralph/resources/ralph.jpg deleted file mode 100644 index bc648ced..00000000 Binary files a/RenderPipelineFile/samples/02-Roaming-Ralph/resources/ralph.jpg and /dev/null differ diff --git a/RenderPipelineFile/samples/02-Roaming-Ralph/resources/rock03.jpg b/RenderPipelineFile/samples/02-Roaming-Ralph/resources/rock03.jpg deleted file mode 100644 index e4006f02..00000000 Binary files a/RenderPipelineFile/samples/02-Roaming-Ralph/resources/rock03.jpg and /dev/null differ diff --git a/RenderPipelineFile/samples/02-Roaming-Ralph/resources/tree.jpg b/RenderPipelineFile/samples/02-Roaming-Ralph/resources/tree.jpg deleted file mode 100644 index 1340ba04..00000000 Binary files a/RenderPipelineFile/samples/02-Roaming-Ralph/resources/tree.jpg and /dev/null differ diff --git a/RenderPipelineFile/samples/02-Roaming-Ralph/resources/world.egg.pz b/RenderPipelineFile/samples/02-Roaming-Ralph/resources/world.egg.pz deleted file mode 100644 index a5304ef9..00000000 Binary files a/RenderPipelineFile/samples/02-Roaming-Ralph/resources/world.egg.pz and /dev/null differ diff --git a/RenderPipelineFile/samples/02-Roaming-Ralph/scene-effect.yaml b/RenderPipelineFile/samples/02-Roaming-Ralph/scene-effect.yaml deleted file mode 100644 index 5924529b..00000000 --- a/RenderPipelineFile/samples/02-Roaming-Ralph/scene-effect.yaml +++ /dev/null @@ -1,13 +0,0 @@ - -# Special effect file for rendering the roaming ralph scene, since roaming -# Ralph has no normals or material information - -fragment: - material: - m.shading_model = SHADING_MODEL_DEFAULT; - m.basecolor = texture(p3d_Texture0, texcoord).xyz; - m.roughness = 0.9; - m.specular_ior = 1.51; - m.metallic = 0; - m.normal = vOutput.normal; - m.shading_model_param0 = 0.0; // unused diff --git a/RenderPipelineFile/samples/02-Roaming-Ralph/screenshot.png b/RenderPipelineFile/samples/02-Roaming-Ralph/screenshot.png deleted file mode 100644 index 57f777b5..00000000 Binary files a/RenderPipelineFile/samples/02-Roaming-Ralph/screenshot.png and /dev/null differ diff --git a/RenderPipelineFile/samples/03-Lights/main.py b/RenderPipelineFile/samples/03-Lights/main.py deleted file mode 100644 index f27adc12..00000000 --- a/RenderPipelineFile/samples/03-Lights/main.py +++ /dev/null @@ -1,147 +0,0 @@ -""" - -Lights sample - -This sample shows how to setup multiple lights and load them from a .bam file. - -""" - -# Disable the "xxx has no yyy member" error, pylint seems to be unable to detect -# the properties of a nodepath -# pylint: disable=no-member - -from __future__ import print_function - -import os -import sys -import math -from random import randint -from panda3d.core import Vec3, load_prc_file_data, Material -from direct.showbase.ShowBase import ShowBase -from direct.interval.IntervalGlobal import Sequence - -# Switch into the current directory -os.chdir(os.path.realpath(os.path.dirname(__file__))) - - -class MainApp(ShowBase): - - def __init__(self): - # Setup window size and title - load_prc_file_data("", """ - win-size 900 600 - window-title Render Pipeline - Lights demo - """) - - # ------ Begin of render pipeline code ------ - - # Insert the pipeline path to the system path, this is required to be - # able to import the pipeline classes - pipeline_path = "../../" - - # Just a special case for my development setup, so I don't accidentally - # commit a wrong path. You can remove this in your own programs. - if not os.path.isfile(os.path.join(pipeline_path, "setup.py")): - pipeline_path = "../../RenderPipeline/" - - sys.path.insert(0, pipeline_path) - - from rpcore import RenderPipeline, SpotLight - self.render_pipeline = RenderPipeline() - self.render_pipeline.create(self) - - # Import the movement controller, this is a convenience class - # to provide an improved camera control compared to Panda3Ds default - # mouse controller. - from rpcore.util.movement_controller import MovementController - - # ------ End of render pipeline code, thats it! ------ - - # Set time of day - self.render_pipeline.daytime_mgr.time = "5:20" - - # Configuration variables - self.half_energy = 5000 - self.lamp_fov = 70 - self.lamp_radius = 10 - - # Load the scene - model = self.loader.load_model("scene/Scene.bam") - model.reparent_to(self.render) - - # Animate balls, this is for testing the motion blur - blend_type = "noBlend" - np = model.find("**/MBRotate") - np.hprInterval(1.5, Vec3(360, 360, 0), Vec3(0, 0, 0), blendType=blend_type).loop() - - np = model.find("**/MBUpDown") - np_pos = np.get_pos() - Vec3(0, 0, 2) - Sequence( - np.posInterval(0.15, np_pos + Vec3(0, 0, 6), np_pos, blendType=blend_type), - np.posInterval(0.15, np_pos, np_pos + Vec3(0, 0, 6), blendType=blend_type)).loop() - - np = model.find("**/MBFrontBack") - np_pos = np.get_pos() - Vec3(0, 0, 2) - Sequence( - np.posInterval(0.15, np_pos + Vec3(0, 6, 0), np_pos, blendType=blend_type), - np.posInterval(0.15, np_pos, np_pos + Vec3(0, 6, 0), blendType=blend_type)).loop() - - np = model.find("**/MBScale") - Sequence( - np.scaleInterval(0.2, Vec3(1.5), Vec3(1), blendType=blend_type), - np.scaleInterval(0.2, Vec3(1), Vec3(1.5), blendType=blend_type)).loop() - - # Generate temperature lamps - # This shows how to procedurally create lamps. In this case, we - # base the lights positions on empties created in blender. - self._lights = [] - light_key = lambda light: int(light.get_name().split("LampLum")[-1]) - lumlamps = sorted(model.find_all_matches("**/LampLum*"), key=light_key) - for lumlamp in lumlamps: - lum = float(lumlamp.get_name()[len("LampLum"):]) - light = SpotLight() - light.direction = (0, -1.5, -1) - light.fov = self.lamp_fov - light.set_color_from_temperature(lum * 1000.0) - light.energy = self.half_energy - light.pos = lumlamp.get_pos(self.render) - light.radius = self.lamp_radius - light.casts_shadows = False - light.shadow_map_resolution = 256 - self.render_pipeline.add_light(light) - - # Put Pandas on the edges - if lumlamp in lumlamps[0:2] + lumlamps[-2:]: - panda = self.loader.load_model("panda") - panda.reparent_to(self.render) - panda_mat = Material("default") - panda_mat.emission = 0 - panda.set_material(panda_mat) - panda.set_pos(light.pos) - panda.set_z(0.65) - panda.set_h(180 + randint(-60, 60)) - panda.set_scale(0.2) - panda.set_y(panda.get_y() - 3.0) - - self._lights.append(light) - - self.render_pipeline.prepare_scene(model) - - # Init movement controller - self.controller = MovementController(self) - self.controller.set_initial_position(Vec3(23.9, 42.5, 13.4), Vec3(23.8, 33.4, 10.8)) - self.controller.setup() - - self.addTask(self.update, "update") - - def update(self, task): - """ Update method """ - frame_time = self.taskMgr.globalClock.get_frame_time() - - # Make the lights glow - for i, light in enumerate(self._lights): - brightness = math.sin(0.4 * i + frame_time) - light.energy = max(0, self.half_energy / 2.0 + brightness * self.half_energy) - return task.cont - -MainApp().run() diff --git a/RenderPipelineFile/samples/03-Lights/scene/Scene.bam b/RenderPipelineFile/samples/03-Lights/scene/Scene.bam deleted file mode 100644 index 1750497f..00000000 Binary files a/RenderPipelineFile/samples/03-Lights/scene/Scene.bam and /dev/null differ diff --git a/RenderPipelineFile/samples/03-Lights/scene/Scene.blend b/RenderPipelineFile/samples/03-Lights/scene/Scene.blend deleted file mode 100644 index 67c96e7b..00000000 Binary files a/RenderPipelineFile/samples/03-Lights/scene/Scene.blend and /dev/null differ diff --git a/RenderPipelineFile/samples/03-Lights/scene/tex/BallDiffuse.png b/RenderPipelineFile/samples/03-Lights/scene/tex/BallDiffuse.png deleted file mode 100644 index 1210474b..00000000 Binary files a/RenderPipelineFile/samples/03-Lights/scene/tex/BallDiffuse.png and /dev/null differ diff --git a/RenderPipelineFile/samples/03-Lights/scene/tex/BallRoughnesss.png b/RenderPipelineFile/samples/03-Lights/scene/tex/BallRoughnesss.png deleted file mode 100644 index 23ca130f..00000000 Binary files a/RenderPipelineFile/samples/03-Lights/scene/tex/BallRoughnesss.png and /dev/null differ diff --git a/RenderPipelineFile/samples/03-Lights/scene/tex/BoxDiffuse.png b/RenderPipelineFile/samples/03-Lights/scene/tex/BoxDiffuse.png deleted file mode 100644 index 39962173..00000000 Binary files a/RenderPipelineFile/samples/03-Lights/scene/tex/BoxDiffuse.png and /dev/null differ diff --git a/RenderPipelineFile/samples/03-Lights/scene/tex/BoxDisplace.png b/RenderPipelineFile/samples/03-Lights/scene/tex/BoxDisplace.png deleted file mode 100644 index 2ad814aa..00000000 Binary files a/RenderPipelineFile/samples/03-Lights/scene/tex/BoxDisplace.png and /dev/null differ diff --git a/RenderPipelineFile/samples/03-Lights/scene/tex/BoxNormal.png b/RenderPipelineFile/samples/03-Lights/scene/tex/BoxNormal.png deleted file mode 100644 index e00316c4..00000000 Binary files a/RenderPipelineFile/samples/03-Lights/scene/tex/BoxNormal.png and /dev/null differ diff --git a/RenderPipelineFile/samples/03-Lights/scene/tex/BoxSpecular.png b/RenderPipelineFile/samples/03-Lights/scene/tex/BoxSpecular.png deleted file mode 100644 index 6568ccc8..00000000 Binary files a/RenderPipelineFile/samples/03-Lights/scene/tex/BoxSpecular.png and /dev/null differ diff --git a/RenderPipelineFile/samples/03-Lights/scene/tex/BrickDiffuse.png b/RenderPipelineFile/samples/03-Lights/scene/tex/BrickDiffuse.png deleted file mode 100644 index 07ab787c..00000000 Binary files a/RenderPipelineFile/samples/03-Lights/scene/tex/BrickDiffuse.png and /dev/null differ diff --git a/RenderPipelineFile/samples/03-Lights/scene/tex/BrickNrm.png b/RenderPipelineFile/samples/03-Lights/scene/tex/BrickNrm.png deleted file mode 100644 index 050a3b28..00000000 Binary files a/RenderPipelineFile/samples/03-Lights/scene/tex/BrickNrm.png and /dev/null differ diff --git a/RenderPipelineFile/samples/03-Lights/scene/tex/BrickSpec.png b/RenderPipelineFile/samples/03-Lights/scene/tex/BrickSpec.png deleted file mode 100644 index bf91d0bf..00000000 Binary files a/RenderPipelineFile/samples/03-Lights/scene/tex/BrickSpec.png and /dev/null differ diff --git a/RenderPipelineFile/samples/03-Lights/scene/tex/CobaltDiffuse.png b/RenderPipelineFile/samples/03-Lights/scene/tex/CobaltDiffuse.png deleted file mode 100644 index e5853d8f..00000000 Binary files a/RenderPipelineFile/samples/03-Lights/scene/tex/CobaltDiffuse.png and /dev/null differ diff --git a/RenderPipelineFile/samples/03-Lights/scene/tex/CobaltNormal.png b/RenderPipelineFile/samples/03-Lights/scene/tex/CobaltNormal.png deleted file mode 100644 index 966ff811..00000000 Binary files a/RenderPipelineFile/samples/03-Lights/scene/tex/CobaltNormal.png and /dev/null differ diff --git a/RenderPipelineFile/samples/03-Lights/scene/tex/CobaltSpecular.png b/RenderPipelineFile/samples/03-Lights/scene/tex/CobaltSpecular.png deleted file mode 100644 index 909e6f28..00000000 Binary files a/RenderPipelineFile/samples/03-Lights/scene/tex/CobaltSpecular.png and /dev/null differ diff --git a/RenderPipelineFile/samples/03-Lights/scene/tex/ConcreteDiffuse.png b/RenderPipelineFile/samples/03-Lights/scene/tex/ConcreteDiffuse.png deleted file mode 100644 index 608a5f24..00000000 Binary files a/RenderPipelineFile/samples/03-Lights/scene/tex/ConcreteDiffuse.png and /dev/null differ diff --git a/RenderPipelineFile/samples/03-Lights/scene/tex/ConcreteDisp.png b/RenderPipelineFile/samples/03-Lights/scene/tex/ConcreteDisp.png deleted file mode 100644 index ef3f4e13..00000000 Binary files a/RenderPipelineFile/samples/03-Lights/scene/tex/ConcreteDisp.png and /dev/null differ diff --git a/RenderPipelineFile/samples/03-Lights/scene/tex/ConcreteNormal.png b/RenderPipelineFile/samples/03-Lights/scene/tex/ConcreteNormal.png deleted file mode 100644 index 416b9697..00000000 Binary files a/RenderPipelineFile/samples/03-Lights/scene/tex/ConcreteNormal.png and /dev/null differ diff --git a/RenderPipelineFile/samples/03-Lights/scene/tex/ConcreteRoughness.png b/RenderPipelineFile/samples/03-Lights/scene/tex/ConcreteRoughness.png deleted file mode 100644 index 170d8fd1..00000000 Binary files a/RenderPipelineFile/samples/03-Lights/scene/tex/ConcreteRoughness.png and /dev/null differ diff --git a/RenderPipelineFile/samples/03-Lights/scene/tex/CopperDiffuse.png b/RenderPipelineFile/samples/03-Lights/scene/tex/CopperDiffuse.png deleted file mode 100644 index 0ebf7fd1..00000000 Binary files a/RenderPipelineFile/samples/03-Lights/scene/tex/CopperDiffuse.png and /dev/null differ diff --git a/RenderPipelineFile/samples/03-Lights/scene/tex/CopperNormal.png b/RenderPipelineFile/samples/03-Lights/scene/tex/CopperNormal.png deleted file mode 100644 index e32fb325..00000000 Binary files a/RenderPipelineFile/samples/03-Lights/scene/tex/CopperNormal.png and /dev/null differ diff --git a/RenderPipelineFile/samples/03-Lights/scene/tex/CopperSpecular.png b/RenderPipelineFile/samples/03-Lights/scene/tex/CopperSpecular.png deleted file mode 100644 index 98d3558f..00000000 Binary files a/RenderPipelineFile/samples/03-Lights/scene/tex/CopperSpecular.png and /dev/null differ diff --git a/RenderPipelineFile/samples/03-Lights/scene/tex/EmptyBaseColor.png b/RenderPipelineFile/samples/03-Lights/scene/tex/EmptyBaseColor.png deleted file mode 100644 index 437c2444..00000000 Binary files a/RenderPipelineFile/samples/03-Lights/scene/tex/EmptyBaseColor.png and /dev/null differ diff --git a/RenderPipelineFile/samples/03-Lights/scene/tex/EmptyNormal.png b/RenderPipelineFile/samples/03-Lights/scene/tex/EmptyNormal.png deleted file mode 100644 index 863dc2ec..00000000 Binary files a/RenderPipelineFile/samples/03-Lights/scene/tex/EmptyNormal.png and /dev/null differ diff --git a/RenderPipelineFile/samples/03-Lights/scene/tex/EmptyRoughness.png b/RenderPipelineFile/samples/03-Lights/scene/tex/EmptyRoughness.png deleted file mode 100644 index 437c2444..00000000 Binary files a/RenderPipelineFile/samples/03-Lights/scene/tex/EmptyRoughness.png and /dev/null differ diff --git a/RenderPipelineFile/samples/03-Lights/scene/tex/EmptySpecular.png b/RenderPipelineFile/samples/03-Lights/scene/tex/EmptySpecular.png deleted file mode 100644 index 26ee2247..00000000 Binary files a/RenderPipelineFile/samples/03-Lights/scene/tex/EmptySpecular.png and /dev/null differ diff --git a/RenderPipelineFile/samples/03-Lights/scene/tex/GridBG.png b/RenderPipelineFile/samples/03-Lights/scene/tex/GridBG.png deleted file mode 100644 index 8dd6369f..00000000 Binary files a/RenderPipelineFile/samples/03-Lights/scene/tex/GridBG.png and /dev/null differ diff --git a/RenderPipelineFile/samples/03-Lights/scene/tex/MetalDiffuse.png b/RenderPipelineFile/samples/03-Lights/scene/tex/MetalDiffuse.png deleted file mode 100644 index 9ec43978..00000000 Binary files a/RenderPipelineFile/samples/03-Lights/scene/tex/MetalDiffuse.png and /dev/null differ diff --git a/RenderPipelineFile/samples/03-Lights/scene/tex/MetalNormal.png b/RenderPipelineFile/samples/03-Lights/scene/tex/MetalNormal.png deleted file mode 100644 index a96c3c71..00000000 Binary files a/RenderPipelineFile/samples/03-Lights/scene/tex/MetalNormal.png and /dev/null differ diff --git a/RenderPipelineFile/samples/03-Lights/scene/tex/MetalSpecular.png b/RenderPipelineFile/samples/03-Lights/scene/tex/MetalSpecular.png deleted file mode 100644 index 96f38961..00000000 Binary files a/RenderPipelineFile/samples/03-Lights/scene/tex/MetalSpecular.png and /dev/null differ diff --git a/RenderPipelineFile/samples/03-Lights/scene/tex/Sign_materials.png b/RenderPipelineFile/samples/03-Lights/scene/tex/Sign_materials.png deleted file mode 100644 index 2cba0df2..00000000 Binary files a/RenderPipelineFile/samples/03-Lights/scene/tex/Sign_materials.png and /dev/null differ diff --git a/RenderPipelineFile/samples/03-Lights/scene/tex/empty_basecolor.png b/RenderPipelineFile/samples/03-Lights/scene/tex/empty_basecolor.png deleted file mode 100644 index 437c2444..00000000 Binary files a/RenderPipelineFile/samples/03-Lights/scene/tex/empty_basecolor.png and /dev/null differ diff --git a/RenderPipelineFile/samples/03-Lights/scene/tex/empty_normal.png b/RenderPipelineFile/samples/03-Lights/scene/tex/empty_normal.png deleted file mode 100644 index 863dc2ec..00000000 Binary files a/RenderPipelineFile/samples/03-Lights/scene/tex/empty_normal.png and /dev/null differ diff --git a/RenderPipelineFile/samples/03-Lights/scene/tex/empty_roughness.png b/RenderPipelineFile/samples/03-Lights/scene/tex/empty_roughness.png deleted file mode 100644 index 437c2444..00000000 Binary files a/RenderPipelineFile/samples/03-Lights/scene/tex/empty_roughness.png and /dev/null differ diff --git a/RenderPipelineFile/samples/03-Lights/scene/tex/empty_specular.png b/RenderPipelineFile/samples/03-Lights/scene/tex/empty_specular.png deleted file mode 100644 index 26ee2247..00000000 Binary files a/RenderPipelineFile/samples/03-Lights/scene/tex/empty_specular.png and /dev/null differ diff --git a/RenderPipelineFile/samples/03-Lights/scene/tex/pattern.png b/RenderPipelineFile/samples/03-Lights/scene/tex/pattern.png deleted file mode 100644 index ccfb77a3..00000000 Binary files a/RenderPipelineFile/samples/03-Lights/scene/tex/pattern.png and /dev/null differ diff --git a/RenderPipelineFile/samples/03-Lights/screenshot.png b/RenderPipelineFile/samples/03-Lights/screenshot.png deleted file mode 100644 index 372b7f63..00000000 Binary files a/RenderPipelineFile/samples/03-Lights/screenshot.png and /dev/null differ diff --git a/RenderPipelineFile/samples/04-Material-Blending/main.py b/RenderPipelineFile/samples/04-Material-Blending/main.py deleted file mode 100644 index 296373b2..00000000 --- a/RenderPipelineFile/samples/04-Material-Blending/main.py +++ /dev/null @@ -1,89 +0,0 @@ -""" - -Material blending sample - -This shows how to use the material blend effect which comes with the pipeline, -and supports blending for up to 4 materials. - -""" - -from __future__ import print_function - -import os -import sys -from panda3d.core import Vec3, load_prc_file_data, TextureAttrib -from direct.showbase.ShowBase import ShowBase - -# Switch into the current directory -os.chdir(os.path.realpath(os.path.dirname(__file__))) - -class Application(ShowBase): - - """ Main Testing Showbase """ - - def __init__(self): - - # Setup window size, title and so on - load_prc_file_data("", """ - win-size 1600 900 - window-title Render Pipeline - Material blending example - """) - - # ------ Begin of render pipeline code ------ - - # Insert the pipeline path to the system path, this is required to be - # able to import the pipeline classes - pipeline_path = "../../" - - # Just a special case for my development setup, so I don't accidentally - # commit a wrong path. You can remove this in your own programs. - if not os.path.isfile(os.path.join(pipeline_path, "setup.py")): - pipeline_path = "../../RenderPipeline/" - - sys.path.insert(0, pipeline_path) - - from rpcore import RenderPipeline, SpotLight - self.render_pipeline = RenderPipeline() - self.render_pipeline.create(self) - - # This is a helper class for better camera movement - its not really - # a rendering element, but it included for convenience - from rpcore.util.movement_controller import MovementController - - # ------ End of render pipeline code, thats it! ------ - - # Set time of day - self.render_pipeline.daytime_mgr.time = "12:43" - - # Load the scene - model = loader.loadModel("scene/Scene.bam") - model.reparent_to(render) - - # Set the material blending effect on the terrain - terrain = model.find("**/Terrain") - self.render_pipeline.set_effect(terrain, "effects/material_blend4.yaml", { - "parallax_mapping": False, # Not supported - "alpha_testing": False, - "normal_mapping": False, # The effect does its own normal mapping - }, 100) - - # Configure the effect - terrain.set_shader_input("detail_scale_factor", 4.0) - - # Detailmap blending factors. - # Blending is calculated as (detailmap + ) ^ - # The base map has no blending since it is used as a filling material - # and blending the base map would cause spots with no material at all. - terrain.set_shader_input("material_0_pow", 10.0) - terrain.set_shader_input("material_0_add", 0.5) - terrain.set_shader_input("material_1_pow", 10.0) - terrain.set_shader_input("material_1_add", 0.5) - terrain.set_shader_input("material_2_pow", 10.0) - terrain.set_shader_input("material_2_add", 0.5) - - # Init movement controller - self.controller = MovementController(self) - self.controller.set_initial_position(Vec3(-15.2, -9.0, 11.8), Vec3(-12.3, -7.0, 9.7)) - self.controller.setup() - -Application().run() diff --git a/RenderPipelineFile/samples/04-Material-Blending/scene/Scene.bam b/RenderPipelineFile/samples/04-Material-Blending/scene/Scene.bam deleted file mode 100644 index de4886e6..00000000 Binary files a/RenderPipelineFile/samples/04-Material-Blending/scene/Scene.bam and /dev/null differ diff --git a/RenderPipelineFile/samples/04-Material-Blending/scene/Scene.blend b/RenderPipelineFile/samples/04-Material-Blending/scene/Scene.blend deleted file mode 100644 index 802f3e41..00000000 Binary files a/RenderPipelineFile/samples/04-Material-Blending/scene/Scene.blend and /dev/null differ diff --git a/RenderPipelineFile/samples/04-Material-Blending/scene/tex/Material1Blendmap.png b/RenderPipelineFile/samples/04-Material-Blending/scene/tex/Material1Blendmap.png deleted file mode 100644 index 89563853..00000000 Binary files a/RenderPipelineFile/samples/04-Material-Blending/scene/tex/Material1Blendmap.png and /dev/null differ diff --git a/RenderPipelineFile/samples/04-Material-Blending/scene/tex/Material1Diffuse.png b/RenderPipelineFile/samples/04-Material-Blending/scene/tex/Material1Diffuse.png deleted file mode 100644 index 71d24be1..00000000 Binary files a/RenderPipelineFile/samples/04-Material-Blending/scene/tex/Material1Diffuse.png and /dev/null differ diff --git a/RenderPipelineFile/samples/04-Material-Blending/scene/tex/Material1Normal.png b/RenderPipelineFile/samples/04-Material-Blending/scene/tex/Material1Normal.png deleted file mode 100644 index a872ba07..00000000 Binary files a/RenderPipelineFile/samples/04-Material-Blending/scene/tex/Material1Normal.png and /dev/null differ diff --git a/RenderPipelineFile/samples/04-Material-Blending/scene/tex/Material1Specular.png b/RenderPipelineFile/samples/04-Material-Blending/scene/tex/Material1Specular.png deleted file mode 100644 index 22b4f5c4..00000000 Binary files a/RenderPipelineFile/samples/04-Material-Blending/scene/tex/Material1Specular.png and /dev/null differ diff --git a/RenderPipelineFile/samples/04-Material-Blending/scene/tex/Material2Blendmap.jpg b/RenderPipelineFile/samples/04-Material-Blending/scene/tex/Material2Blendmap.jpg deleted file mode 100644 index 0264c105..00000000 Binary files a/RenderPipelineFile/samples/04-Material-Blending/scene/tex/Material2Blendmap.jpg and /dev/null differ diff --git a/RenderPipelineFile/samples/04-Material-Blending/scene/tex/Material2Diffuse.png b/RenderPipelineFile/samples/04-Material-Blending/scene/tex/Material2Diffuse.png deleted file mode 100644 index 1efdc2f1..00000000 Binary files a/RenderPipelineFile/samples/04-Material-Blending/scene/tex/Material2Diffuse.png and /dev/null differ diff --git a/RenderPipelineFile/samples/04-Material-Blending/scene/tex/Material2Normal.png b/RenderPipelineFile/samples/04-Material-Blending/scene/tex/Material2Normal.png deleted file mode 100644 index d7dd915d..00000000 Binary files a/RenderPipelineFile/samples/04-Material-Blending/scene/tex/Material2Normal.png and /dev/null differ diff --git a/RenderPipelineFile/samples/04-Material-Blending/scene/tex/Material2Specular.png b/RenderPipelineFile/samples/04-Material-Blending/scene/tex/Material2Specular.png deleted file mode 100644 index efb05350..00000000 Binary files a/RenderPipelineFile/samples/04-Material-Blending/scene/tex/Material2Specular.png and /dev/null differ diff --git a/RenderPipelineFile/samples/04-Material-Blending/scene/tex/Material3Blendmap.jpg b/RenderPipelineFile/samples/04-Material-Blending/scene/tex/Material3Blendmap.jpg deleted file mode 100644 index 8b3dd94a..00000000 Binary files a/RenderPipelineFile/samples/04-Material-Blending/scene/tex/Material3Blendmap.jpg and /dev/null differ diff --git a/RenderPipelineFile/samples/04-Material-Blending/scene/tex/Material3Diffuse.png b/RenderPipelineFile/samples/04-Material-Blending/scene/tex/Material3Diffuse.png deleted file mode 100644 index 623b178c..00000000 Binary files a/RenderPipelineFile/samples/04-Material-Blending/scene/tex/Material3Diffuse.png and /dev/null differ diff --git a/RenderPipelineFile/samples/04-Material-Blending/scene/tex/Material3Normal.png b/RenderPipelineFile/samples/04-Material-Blending/scene/tex/Material3Normal.png deleted file mode 100644 index b873baaf..00000000 Binary files a/RenderPipelineFile/samples/04-Material-Blending/scene/tex/Material3Normal.png and /dev/null differ diff --git a/RenderPipelineFile/samples/04-Material-Blending/scene/tex/Material3Specular.png b/RenderPipelineFile/samples/04-Material-Blending/scene/tex/Material3Specular.png deleted file mode 100644 index 29484104..00000000 Binary files a/RenderPipelineFile/samples/04-Material-Blending/scene/tex/Material3Specular.png and /dev/null differ diff --git a/RenderPipelineFile/samples/04-Material-Blending/scene/tex/Material4Diffuse.png b/RenderPipelineFile/samples/04-Material-Blending/scene/tex/Material4Diffuse.png deleted file mode 100644 index 0c63caa5..00000000 Binary files a/RenderPipelineFile/samples/04-Material-Blending/scene/tex/Material4Diffuse.png and /dev/null differ diff --git a/RenderPipelineFile/samples/04-Material-Blending/scene/tex/Material4Normal.png b/RenderPipelineFile/samples/04-Material-Blending/scene/tex/Material4Normal.png deleted file mode 100644 index 971be7c4..00000000 Binary files a/RenderPipelineFile/samples/04-Material-Blending/scene/tex/Material4Normal.png and /dev/null differ diff --git a/RenderPipelineFile/samples/04-Material-Blending/scene/tex/Material4Specular.png b/RenderPipelineFile/samples/04-Material-Blending/scene/tex/Material4Specular.png deleted file mode 100644 index 3501a4f3..00000000 Binary files a/RenderPipelineFile/samples/04-Material-Blending/scene/tex/Material4Specular.png and /dev/null differ diff --git a/RenderPipelineFile/samples/04-Material-Blending/scene/tex/MaterialMap.png b/RenderPipelineFile/samples/04-Material-Blending/scene/tex/MaterialMap.png deleted file mode 100644 index 59ed9c46..00000000 Binary files a/RenderPipelineFile/samples/04-Material-Blending/scene/tex/MaterialMap.png and /dev/null differ diff --git a/RenderPipelineFile/samples/04-Material-Blending/scene/tex/empty_basecolor.png b/RenderPipelineFile/samples/04-Material-Blending/scene/tex/empty_basecolor.png deleted file mode 100644 index 437c2444..00000000 Binary files a/RenderPipelineFile/samples/04-Material-Blending/scene/tex/empty_basecolor.png and /dev/null differ diff --git a/RenderPipelineFile/samples/04-Material-Blending/scene/tex/empty_normal.png b/RenderPipelineFile/samples/04-Material-Blending/scene/tex/empty_normal.png deleted file mode 100644 index 863dc2ec..00000000 Binary files a/RenderPipelineFile/samples/04-Material-Blending/scene/tex/empty_normal.png and /dev/null differ diff --git a/RenderPipelineFile/samples/04-Material-Blending/scene/tex/empty_roughness.png b/RenderPipelineFile/samples/04-Material-Blending/scene/tex/empty_roughness.png deleted file mode 100644 index 437c2444..00000000 Binary files a/RenderPipelineFile/samples/04-Material-Blending/scene/tex/empty_roughness.png and /dev/null differ diff --git a/RenderPipelineFile/samples/04-Material-Blending/scene/tex/empty_specular.png b/RenderPipelineFile/samples/04-Material-Blending/scene/tex/empty_specular.png deleted file mode 100644 index 26ee2247..00000000 Binary files a/RenderPipelineFile/samples/04-Material-Blending/scene/tex/empty_specular.png and /dev/null differ diff --git a/RenderPipelineFile/samples/04-Material-Blending/screenshot.png b/RenderPipelineFile/samples/04-Material-Blending/screenshot.png deleted file mode 100644 index 19622e5d..00000000 Binary files a/RenderPipelineFile/samples/04-Material-Blending/screenshot.png and /dev/null differ diff --git a/RenderPipelineFile/samples/05-Quboid/.gitignore b/RenderPipelineFile/samples/05-Quboid/.gitignore deleted file mode 100644 index 627c8f8b..00000000 --- a/RenderPipelineFile/samples/05-Quboid/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ - -models/Scene.blend -models/surrounding/Scene.blend* - diff --git a/RenderPipelineFile/samples/05-Quboid/Sound-license.txt b/RenderPipelineFile/samples/05-Quboid/Sound-license.txt deleted file mode 100644 index b6f21ab5..00000000 --- a/RenderPipelineFile/samples/05-Quboid/Sound-license.txt +++ /dev/null @@ -1,5 +0,0 @@ -stonrotate.wav by http://www.freesound.org/usersViewSingle.php?id=386635 -rock_Scrape.wav by http://www.freesound.org/usersViewSingle.php?id=634166 -kr2.wav ,kr1.wav ... kr5.wav by http://www.freesound.org/usersViewSingle.php?id=96 -kr6.wav by http://www.freesound.org/usersViewSingle.php?id=288 -ambient_uboot.wav by http://www.freesound.org/usersViewSingle.php?id=22256 diff --git a/RenderPipelineFile/samples/05-Quboid/levels/level1 b/RenderPipelineFile/samples/05-Quboid/levels/level1 deleted file mode 100644 index ec47ef8a..00000000 --- a/RenderPipelineFile/samples/05-Quboid/levels/level1 +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/RenderPipelineFile/samples/05-Quboid/levels/level_0 b/RenderPipelineFile/samples/05-Quboid/levels/level_0 deleted file mode 100644 index c44aeebe..00000000 --- a/RenderPipelineFile/samples/05-Quboid/levels/level_0 +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/RenderPipelineFile/samples/05-Quboid/levels/level_1 b/RenderPipelineFile/samples/05-Quboid/levels/level_1 deleted file mode 100644 index 2b576684..00000000 --- a/RenderPipelineFile/samples/05-Quboid/levels/level_1 +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/RenderPipelineFile/samples/05-Quboid/levels/level_10 b/RenderPipelineFile/samples/05-Quboid/levels/level_10 deleted file mode 100644 index 3fa38b30..00000000 --- a/RenderPipelineFile/samples/05-Quboid/levels/level_10 +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/RenderPipelineFile/samples/05-Quboid/levels/level_11 b/RenderPipelineFile/samples/05-Quboid/levels/level_11 deleted file mode 100644 index b41d21de..00000000 --- a/RenderPipelineFile/samples/05-Quboid/levels/level_11 +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/RenderPipelineFile/samples/05-Quboid/levels/level_12 b/RenderPipelineFile/samples/05-Quboid/levels/level_12 deleted file mode 100644 index 430fbbed..00000000 --- a/RenderPipelineFile/samples/05-Quboid/levels/level_12 +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/RenderPipelineFile/samples/05-Quboid/levels/level_13 b/RenderPipelineFile/samples/05-Quboid/levels/level_13 deleted file mode 100644 index f6e39ba1..00000000 --- a/RenderPipelineFile/samples/05-Quboid/levels/level_13 +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/RenderPipelineFile/samples/05-Quboid/levels/level_14 b/RenderPipelineFile/samples/05-Quboid/levels/level_14 deleted file mode 100644 index 69a03ae8..00000000 --- a/RenderPipelineFile/samples/05-Quboid/levels/level_14 +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/RenderPipelineFile/samples/05-Quboid/levels/level_15 b/RenderPipelineFile/samples/05-Quboid/levels/level_15 deleted file mode 100644 index c15539ad..00000000 --- a/RenderPipelineFile/samples/05-Quboid/levels/level_15 +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/RenderPipelineFile/samples/05-Quboid/levels/level_16 b/RenderPipelineFile/samples/05-Quboid/levels/level_16 deleted file mode 100644 index bdbf13d7..00000000 --- a/RenderPipelineFile/samples/05-Quboid/levels/level_16 +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/RenderPipelineFile/samples/05-Quboid/levels/level_17 b/RenderPipelineFile/samples/05-Quboid/levels/level_17 deleted file mode 100644 index d0baaaed..00000000 --- a/RenderPipelineFile/samples/05-Quboid/levels/level_17 +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/RenderPipelineFile/samples/05-Quboid/levels/level_18 b/RenderPipelineFile/samples/05-Quboid/levels/level_18 deleted file mode 100644 index 4773c3f8..00000000 --- a/RenderPipelineFile/samples/05-Quboid/levels/level_18 +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/RenderPipelineFile/samples/05-Quboid/levels/level_19 b/RenderPipelineFile/samples/05-Quboid/levels/level_19 deleted file mode 100644 index fc152d23..00000000 --- a/RenderPipelineFile/samples/05-Quboid/levels/level_19 +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/RenderPipelineFile/samples/05-Quboid/levels/level_2 b/RenderPipelineFile/samples/05-Quboid/levels/level_2 deleted file mode 100644 index 601dee3d..00000000 --- a/RenderPipelineFile/samples/05-Quboid/levels/level_2 +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/RenderPipelineFile/samples/05-Quboid/levels/level_20 b/RenderPipelineFile/samples/05-Quboid/levels/level_20 deleted file mode 100644 index dbd99f25..00000000 --- a/RenderPipelineFile/samples/05-Quboid/levels/level_20 +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/RenderPipelineFile/samples/05-Quboid/levels/level_21 b/RenderPipelineFile/samples/05-Quboid/levels/level_21 deleted file mode 100644 index 2020d3f0..00000000 --- a/RenderPipelineFile/samples/05-Quboid/levels/level_21 +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/RenderPipelineFile/samples/05-Quboid/levels/level_22 b/RenderPipelineFile/samples/05-Quboid/levels/level_22 deleted file mode 100644 index 9e73278a..00000000 --- a/RenderPipelineFile/samples/05-Quboid/levels/level_22 +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/RenderPipelineFile/samples/05-Quboid/levels/level_23 b/RenderPipelineFile/samples/05-Quboid/levels/level_23 deleted file mode 100644 index 7883283a..00000000 --- a/RenderPipelineFile/samples/05-Quboid/levels/level_23 +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/RenderPipelineFile/samples/05-Quboid/levels/level_24 b/RenderPipelineFile/samples/05-Quboid/levels/level_24 deleted file mode 100644 index b740dbe6..00000000 --- a/RenderPipelineFile/samples/05-Quboid/levels/level_24 +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/RenderPipelineFile/samples/05-Quboid/levels/level_25 b/RenderPipelineFile/samples/05-Quboid/levels/level_25 deleted file mode 100644 index a39db75e..00000000 --- a/RenderPipelineFile/samples/05-Quboid/levels/level_25 +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/RenderPipelineFile/samples/05-Quboid/levels/level_26 b/RenderPipelineFile/samples/05-Quboid/levels/level_26 deleted file mode 100644 index 82f7da48..00000000 --- a/RenderPipelineFile/samples/05-Quboid/levels/level_26 +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/RenderPipelineFile/samples/05-Quboid/levels/level_27 b/RenderPipelineFile/samples/05-Quboid/levels/level_27 deleted file mode 100644 index 151d4ef5..00000000 --- a/RenderPipelineFile/samples/05-Quboid/levels/level_27 +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/RenderPipelineFile/samples/05-Quboid/levels/level_28 b/RenderPipelineFile/samples/05-Quboid/levels/level_28 deleted file mode 100644 index ca7b86a1..00000000 --- a/RenderPipelineFile/samples/05-Quboid/levels/level_28 +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/RenderPipelineFile/samples/05-Quboid/levels/level_29 b/RenderPipelineFile/samples/05-Quboid/levels/level_29 deleted file mode 100644 index a11ad2b7..00000000 --- a/RenderPipelineFile/samples/05-Quboid/levels/level_29 +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - diff --git a/RenderPipelineFile/samples/05-Quboid/levels/level_3 b/RenderPipelineFile/samples/05-Quboid/levels/level_3 deleted file mode 100644 index 0c8f529a..00000000 --- a/RenderPipelineFile/samples/05-Quboid/levels/level_3 +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/RenderPipelineFile/samples/05-Quboid/levels/level_30 b/RenderPipelineFile/samples/05-Quboid/levels/level_30 deleted file mode 100644 index fe2cdea6..00000000 --- a/RenderPipelineFile/samples/05-Quboid/levels/level_30 +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/RenderPipelineFile/samples/05-Quboid/levels/level_31 b/RenderPipelineFile/samples/05-Quboid/levels/level_31 deleted file mode 100644 index dbd99f25..00000000 --- a/RenderPipelineFile/samples/05-Quboid/levels/level_31 +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/RenderPipelineFile/samples/05-Quboid/levels/level_32 b/RenderPipelineFile/samples/05-Quboid/levels/level_32 deleted file mode 100644 index 8c66f63e..00000000 --- a/RenderPipelineFile/samples/05-Quboid/levels/level_32 +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/RenderPipelineFile/samples/05-Quboid/levels/level_33 b/RenderPipelineFile/samples/05-Quboid/levels/level_33 deleted file mode 100644 index cca6ecde..00000000 --- a/RenderPipelineFile/samples/05-Quboid/levels/level_33 +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/RenderPipelineFile/samples/05-Quboid/levels/level_4 b/RenderPipelineFile/samples/05-Quboid/levels/level_4 deleted file mode 100644 index 4ba8eda5..00000000 --- a/RenderPipelineFile/samples/05-Quboid/levels/level_4 +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/RenderPipelineFile/samples/05-Quboid/levels/level_5 b/RenderPipelineFile/samples/05-Quboid/levels/level_5 deleted file mode 100644 index 1b6cc560..00000000 --- a/RenderPipelineFile/samples/05-Quboid/levels/level_5 +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/RenderPipelineFile/samples/05-Quboid/levels/level_6 b/RenderPipelineFile/samples/05-Quboid/levels/level_6 deleted file mode 100644 index 74d20922..00000000 --- a/RenderPipelineFile/samples/05-Quboid/levels/level_6 +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/RenderPipelineFile/samples/05-Quboid/levels/level_7 b/RenderPipelineFile/samples/05-Quboid/levels/level_7 deleted file mode 100644 index 06c25f98..00000000 --- a/RenderPipelineFile/samples/05-Quboid/levels/level_7 +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/RenderPipelineFile/samples/05-Quboid/levels/level_8 b/RenderPipelineFile/samples/05-Quboid/levels/level_8 deleted file mode 100644 index c92fc564..00000000 --- a/RenderPipelineFile/samples/05-Quboid/levels/level_8 +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/RenderPipelineFile/samples/05-Quboid/levels/level_9 b/RenderPipelineFile/samples/05-Quboid/levels/level_9 deleted file mode 100644 index 96095a49..00000000 --- a/RenderPipelineFile/samples/05-Quboid/levels/level_9 +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/RenderPipelineFile/samples/05-Quboid/levels/levelconvert.py b/RenderPipelineFile/samples/05-Quboid/levels/levelconvert.py deleted file mode 100644 index b7761d81..00000000 --- a/RenderPipelineFile/samples/05-Quboid/levels/levelconvert.py +++ /dev/null @@ -1,51 +0,0 @@ -from sys import argv -if len(argv) >1: - level = int(argv[1]) - -data=open("./level_"+str(level)) -inputData = data.read().replace("\n","").strip().replace(" ","").lstrip("<").rstrip(">").split("><") -data.close() -output = open("./level_"+str(level),"w" ) - -xmin=1000 -ymin=1000 -for tileData in inputData: - data = tileData.split(",") - for i in data: - if i.startswith("Pos="): - i = i.split("=")[1].split("_") - x,y=int(i[0]), int(i[1]) - if x < xmin: - xmin = x - if y < ymin: - ymin = y - - -lastx = 0 -for tileData in inputData: - - data = tileData.split(",") - output.write("<"+data[0]+",") - data = data[1].split("=")[1].split("_") - output.write("Pos="+str( int(data[0])-xmin ) +"_" +str( int(data[1])-ymin )+">") - print int(data[0])-xmin , lastx - if int(data[0])-xmin > lastx: - print "inserting newline" - lastx=int(data[0])-xmin - output.write("\n") - -""" -#print data -for x in range(0,len(data)): - for y in range(0,len(data[x])): - if data[x][y] == "0": - continue - elif data[x][y] == "1": - output.write("") - elif data[x][y] == "2": - output.write("") - elif data[x][y] == "3": - output.write("") - output.write("\n") -""" -output.close() diff --git a/RenderPipelineFile/samples/05-Quboid/main.py b/RenderPipelineFile/samples/05-Quboid/main.py deleted file mode 100644 index 46634bc5..00000000 --- a/RenderPipelineFile/samples/05-Quboid/main.py +++ /dev/null @@ -1,20 +0,0 @@ - - -# This is just the launcher, have a look at src/GameControl.py - -import os -import sys - -print("This sample is currently broken! Please wait until it is fixed. You can " - "run it on your own risk by commenting line 10 in this file.") -# sys.exit(0) - -# Change to the current directory -os.chdir(os.path.join(os.path.dirname(os.path.realpath(__file__)))) - -# Append the current directory to the path -sys.path.insert(0, os.getcwd()) - -from src.GameControl import GameControl -game = GameControl() -game.run() diff --git a/RenderPipelineFile/samples/05-Quboid/res/Roboto-Light.ttf b/RenderPipelineFile/samples/05-Quboid/res/Roboto-Light.ttf deleted file mode 100644 index 664e1b2f..00000000 Binary files a/RenderPipelineFile/samples/05-Quboid/res/Roboto-Light.ttf and /dev/null differ diff --git a/RenderPipelineFile/samples/05-Quboid/res/icon.ico b/RenderPipelineFile/samples/05-Quboid/res/icon.ico deleted file mode 100644 index 13312843..00000000 Binary files a/RenderPipelineFile/samples/05-Quboid/res/icon.ico and /dev/null differ diff --git a/RenderPipelineFile/samples/05-Quboid/res/icon.png b/RenderPipelineFile/samples/05-Quboid/res/icon.png deleted file mode 100644 index a9164794..00000000 Binary files a/RenderPipelineFile/samples/05-Quboid/res/icon.png and /dev/null differ diff --git a/RenderPipelineFile/samples/05-Quboid/res/menu.png b/RenderPipelineFile/samples/05-Quboid/res/menu.png deleted file mode 100644 index f9def276..00000000 Binary files a/RenderPipelineFile/samples/05-Quboid/res/menu.png and /dev/null differ diff --git a/RenderPipelineFile/samples/05-Quboid/res/select.png b/RenderPipelineFile/samples/05-Quboid/res/select.png deleted file mode 100644 index 53682e51..00000000 Binary files a/RenderPipelineFile/samples/05-Quboid/res/select.png and /dev/null differ diff --git a/RenderPipelineFile/samples/05-Quboid/res/start_game.png b/RenderPipelineFile/samples/05-Quboid/res/start_game.png deleted file mode 100644 index 5598ea79..00000000 Binary files a/RenderPipelineFile/samples/05-Quboid/res/start_game.png and /dev/null differ diff --git a/RenderPipelineFile/samples/05-Quboid/sounds/drop1.wav b/RenderPipelineFile/samples/05-Quboid/sounds/drop1.wav deleted file mode 100644 index 98366be4..00000000 Binary files a/RenderPipelineFile/samples/05-Quboid/sounds/drop1.wav and /dev/null differ diff --git a/RenderPipelineFile/samples/05-Quboid/sounds/drop2.wav b/RenderPipelineFile/samples/05-Quboid/sounds/drop2.wav deleted file mode 100644 index 8c2d053a..00000000 Binary files a/RenderPipelineFile/samples/05-Quboid/sounds/drop2.wav and /dev/null differ diff --git a/RenderPipelineFile/samples/05-Quboid/sounds/drop3.wav b/RenderPipelineFile/samples/05-Quboid/sounds/drop3.wav deleted file mode 100644 index 3bf8f36d..00000000 Binary files a/RenderPipelineFile/samples/05-Quboid/sounds/drop3.wav and /dev/null differ diff --git a/RenderPipelineFile/samples/05-Quboid/sounds/drop4.wav b/RenderPipelineFile/samples/05-Quboid/sounds/drop4.wav deleted file mode 100644 index 84b11a40..00000000 Binary files a/RenderPipelineFile/samples/05-Quboid/sounds/drop4.wav and /dev/null differ diff --git a/RenderPipelineFile/samples/05-Quboid/sounds/finish.wav b/RenderPipelineFile/samples/05-Quboid/sounds/finish.wav deleted file mode 100644 index 7fde8297..00000000 Binary files a/RenderPipelineFile/samples/05-Quboid/sounds/finish.wav and /dev/null differ diff --git a/RenderPipelineFile/samples/05-Quboid/sounds/kr1.wav b/RenderPipelineFile/samples/05-Quboid/sounds/kr1.wav deleted file mode 100644 index 4d4321f9..00000000 Binary files a/RenderPipelineFile/samples/05-Quboid/sounds/kr1.wav and /dev/null differ diff --git a/RenderPipelineFile/samples/05-Quboid/sounds/kr2.wav b/RenderPipelineFile/samples/05-Quboid/sounds/kr2.wav deleted file mode 100644 index 4e3e3fec..00000000 Binary files a/RenderPipelineFile/samples/05-Quboid/sounds/kr2.wav and /dev/null differ diff --git a/RenderPipelineFile/samples/05-Quboid/sounds/kr3.wav b/RenderPipelineFile/samples/05-Quboid/sounds/kr3.wav deleted file mode 100644 index 88e3a2b1..00000000 Binary files a/RenderPipelineFile/samples/05-Quboid/sounds/kr3.wav and /dev/null differ diff --git a/RenderPipelineFile/samples/05-Quboid/sounds/kr4.wav b/RenderPipelineFile/samples/05-Quboid/sounds/kr4.wav deleted file mode 100644 index d60e2a87..00000000 Binary files a/RenderPipelineFile/samples/05-Quboid/sounds/kr4.wav and /dev/null differ diff --git a/RenderPipelineFile/samples/05-Quboid/sounds/kr5.wav b/RenderPipelineFile/samples/05-Quboid/sounds/kr5.wav deleted file mode 100644 index 0e718577..00000000 Binary files a/RenderPipelineFile/samples/05-Quboid/sounds/kr5.wav and /dev/null differ diff --git a/RenderPipelineFile/samples/05-Quboid/sounds/kr6.wav b/RenderPipelineFile/samples/05-Quboid/sounds/kr6.wav deleted file mode 100644 index bc4ecd39..00000000 Binary files a/RenderPipelineFile/samples/05-Quboid/sounds/kr6.wav and /dev/null differ diff --git a/RenderPipelineFile/samples/05-Quboid/sounds/nyon.wav b/RenderPipelineFile/samples/05-Quboid/sounds/nyon.wav deleted file mode 100644 index 289ad84d..00000000 Binary files a/RenderPipelineFile/samples/05-Quboid/sounds/nyon.wav and /dev/null differ diff --git a/RenderPipelineFile/samples/05-Quboid/sounds/rock_Scrape.wav b/RenderPipelineFile/samples/05-Quboid/sounds/rock_Scrape.wav deleted file mode 100644 index d11fd8bd..00000000 Binary files a/RenderPipelineFile/samples/05-Quboid/sounds/rock_Scrape.wav and /dev/null differ diff --git a/RenderPipelineFile/samples/05-Quboid/sounds/stonerotate.wav b/RenderPipelineFile/samples/05-Quboid/sounds/stonerotate.wav deleted file mode 100644 index 8662f250..00000000 Binary files a/RenderPipelineFile/samples/05-Quboid/sounds/stonerotate.wav and /dev/null differ diff --git a/RenderPipelineFile/samples/05-Quboid/src/CamControl.py b/RenderPipelineFile/samples/05-Quboid/src/CamControl.py deleted file mode 100644 index dee8c7f7..00000000 --- a/RenderPipelineFile/samples/05-Quboid/src/CamControl.py +++ /dev/null @@ -1,36 +0,0 @@ - -from direct.task.Task import Task -from direct.interval.LerpInterval import LerpFunc - -class CamControl: - """ - All it does is taking the cubeclass-instance as input. And the camera will - haunt the cube till all eternitiy. - """ - def __init__(self,cube): - base.disableMouse() - self.cube=cube - self.ZOOMLEVEL = .37 - base.camera.setX(25*self.ZOOMLEVEL) - base.camera.setY(5*self.ZOOMLEVEL) - taskMgr.add(self.cameraMovement,"CameraTask") - - def setZoom(self,zoomlevel): - self.ZOOMLEVEL = zoomlevel - - def zoomIn(self): - i = LerpFunc(self.setZoom, fromData = self.ZOOMLEVEL, toData = self.ZOOMLEVEL*0.7, duration = 1.0,blendType = "easeInOut") - i.start() - - def zoomOut(self): - i = LerpFunc(self.setZoom, fromData = self.ZOOMLEVEL, toData = self.ZOOMLEVEL*1.3, duration = 1.0,blendType = "easeInOut") - i.start() - - def cameraMovement(self,task): - x,y,z = self.cube.getCube().getPos(render) - #smoothly follow the cube... - base.camera.setX( base.camera.getX() - ((base.camera.getX()-x-18*self.ZOOMLEVEL)*.5*globalClock.getDt() ) ) - base.camera.setY( base.camera.getY() - ((base.camera.getY()-y+5*self.ZOOMLEVEL )*.5*globalClock.getDt() ) ) - base.camera.setZ(15*self.ZOOMLEVEL) - base.camera.setHpr(75,-37,0) - return Task.cont diff --git a/RenderPipelineFile/samples/05-Quboid/src/CheckMove.py b/RenderPipelineFile/samples/05-Quboid/src/CheckMove.py deleted file mode 100644 index 32810540..00000000 --- a/RenderPipelineFile/samples/05-Quboid/src/CheckMove.py +++ /dev/null @@ -1,53 +0,0 @@ - -from random import randint - -def checkMove(level, pos , sounds = None): - """ - the game rules for valid movements are implemented here, - return value of 0 means, everything is fine, 1 means loosing for some ridiculus reason, - a return value of 2 means, level won. - as you can imagine. there is a lot more space for new and whicked ways to loose. - """ - x1,y1,x2,y2 = pos - - tile1 = level.find("=Pos="+str(x1)+"_"+str(y1)) - tile2 = None - if x2 != None : - tile2 = level.find("=Pos="+str(x2)+"_"+str(y2)) - #print pos - if sounds != None: - soundChecks(tile1,tile2,sounds) - """ - if tile1.getTag("Type") == "weaktile": - if sounds != None: - sounds.playSound("kr"+str(randint(0,1))+".wav") - "print crackling sound" - """ - - if tile1.isEmpty() == True: - print("you lost!") - return 1 - - if x2 != None and tile2.isEmpty() == True : #means. if there is supposed to be a tile under the cube but there is none.. - print("you lost!..once more") - return 1 - - if tile1.getTag("Type") == "weaktile" and tile2 == None : - print("loosing due to broken weaktile") - return 1 - - if tile1.getTag("Type") == "goal" and x2 == None: - print("you won, switching to next level!") - return 2 - - else: - return 0 - -def soundChecks(tile1,tile2,sounds): - #check for crackling sound - if tile1.isEmpty() == False: - if tile2 != None: - if tile2.isEmpty()==False: - if tile1.getTag("Type") == "weaktile" or tile2.getTag("Type") == "weaktile" : - sounds.playSound("kr"+str(randint(0,1)+1)+".wav") - diff --git a/RenderPipelineFile/samples/05-Quboid/src/Cube.py b/RenderPipelineFile/samples/05-Quboid/src/Cube.py deleted file mode 100644 index 3d372f9c..00000000 --- a/RenderPipelineFile/samples/05-Quboid/src/Cube.py +++ /dev/null @@ -1,378 +0,0 @@ - -from direct.interval.LerpInterval import LerpPosInterval, LerpHprInterval, LerpScaleInterval -from direct.interval.LerpInterval import LerpPosHprInterval,LerpColorScaleInterval, LerpFunc -from direct.interval.MetaInterval import Sequence,Parallel -from direct.interval.FunctionInterval import Func,Wait - -from direct.task.Task import Task -from panda3d.core import Vec3 - -from functools import partial -from random import random -from .CheckMove import * -from .Sound import * - -class Cube: - def __init__(self,level): - self.level = level - self.sounds = Sounds() - self.cube = loader.loadModel("./models/Cuboid.bam") - self.cube.setPythonTag("TilePos", [0,0,None,None] ) #tells us which 2 tiles the block is one, x1,y1,x2,y2. - #x2 and y2 are NONE if it's standing upright - self.cube.setPythonTag("Animated", False) # i so dont want to pass around global parameters - self.cube.reparentTo(render) - - self.moves = 0 - self.falls = 0 - self.shard_node = render.attach_new_node("shards") - - def resetStats(self): - self.moves = 0 - self.falls = 0 - - def enableGameControl(self): - """ - assigns the keyboard-events to the block's movement - """ - base.accept("arrow_up", self.rotateCube, [ "up" ]) - base.accept("arrow_down", self.rotateCube, [ "down" ]) - base.accept("arrow_left", self.rotateCube, [ "left" ]) - base.accept("arrow_right", self.rotateCube, [ "right" ]) - - def disableGameControl(self): - """ - clears the keyboard-events so the keys can be used to navigate the menu or other stuff. - """ - base.ignore("arrow_up") - base.ignore("arrow_down") - base.ignore("arrow_left") - base.ignore("arrow_right") - - def setPos(self, x1,y1=None ): - """ - sets the block to the position you desire. - you can either throw in 2 integers which make the tile positions. or you pass a tuple/list with at least 2 items. - in both cases the it is x y coordinates. the block will be roated into a standing position if it isn't already. - """ - if type(x1) == list or type(x1) == tuple: - y1=x1[1] - x1=x1[0] - if self.getCubeTiles()[3]: - self.cube.setHpr(render,0,0,0) - - self.cube.setPos(render,x1,y1,1) - self.setCubeTiles(x1,y1) - - def getCube(self): - """ - returns the cube's node path. - """ - return self.cube - - def getCubeTiles(self): - """ - internal convenience functions. returns a list with [x1,y1,x2,y2] - """ - return self.cube.getPythonTag("TilePos") - - def setCubeTiles(self,x1,y1,x2=None,y2=None): - """ - internal convenience-function which sets the tile coordinates for the tiles the block is on. inputs are x1,y1,x2,y2 . x2 and y2 are optional. - """ - self.cube.setPythonTag("TilePos", [x1,y1,x2,y2] ) - - def isAnimated(self): - """ - returns True if the block is currently playing an animation.. DONT TOUCH IT while it is animated or it will turn into evil-rotation cube. - """ - return self.cube.getPythonTag("Animated") - - def setAnimated(self,param): - """ - internal function: sets and clears the "isAnimated" tag - """ - self.cube.setPythonTag("Animated", param) - - def fadeInCube(self,duration = 2.5): - """ - helper function. simply moves the block from above onto the map. using the duration specified as parameter. - if no parameter is supplied, it will assume the default-time of 1.2 seconds or so. - """ - self.setAnimated(True) - x1,y1,x2,y2 = self.cube.getPythonTag("TilePos") - #animate the cube, once finished clear the animation tag - self.cube.setZ(render,15) - self.cube.wrtReparentTo(render) - - print("Starting fade in sequence") - Sequence( LerpPosInterval(self.cube, duration ,(x1, y1,1), blendType="easeOut" ) , - Func(self.setAnimated,False), - Func(lambda:self.level.stopAnimatedTile(x1,y1) ) ).start() - - def animate(self,myInterval): - """ - internal function which accepts an interval as input, plays it. and automatically resets the "animated" tag once it finished. - you have to check yourself if the tag is set before playing the animation. use isAnimated() for that. - """ - #print "animating cube" - seq = Sequence( myInterval , Func(self.setAnimated,False) ) - #seq.start() - return seq - - def resetCube(self,task=None): - """ - resets the cube to the statring position, including the fly-in-from-above animation. - """ - print("Resetting cube") - x,y,z = self.level.getPosFromTile(self.level.getStartTile()) - self.setPos(x,y) - self.cube.setHpr(render,0,0,0) - #self.level.stopAnimatedTile(x,y ) - print("Fading in cube") - self.fadeInCube() - - def levelUp(self, task=None): - """ - calls the levelClass to load the next level and resets the cube. - """ - if self.level.loadLevel() == 0: #if level loaded successfully... - self.resetCube() - - self.level.main.gui.set_stage(self.level.LevelNr + 1) - - def rotateCube(self,direction): - """ - rotates the cube according to the direction passed as first argument. accepted values are "up" "down" "left" "right" - """ - #first check if we are allowed to move the block at all.. - if self.isAnimated() : - print("canceling rotation of cube") - return - - #cleaning up from last rotation (we cant clean those up at the end of this function cause the interval needs the dummynode for rotation) - self.cube.wrtReparentTo(render) - try: - dummy.remove_node() - except: - pass - - self.setAnimated(True) - duration = 0.2 - x1,y1,x2,y2 = self.getCubeTiles() - - self.level.animateTile(x1,y1) - self.level.animateTile(x2,y2) - - dummy = render.attachNewNode("dummy") - dummy.reparentTo(self.cube) - dummy.setZ(render,0) - dummy.wrtReparentTo(render) - dummy.setHpr(0,0,0) - dest_hpr = Vec3(0) - - if self.cube.getZ(render) > .7: - #case1 : cube is standing upright - #ok... since we rotate relative there are rounding errors... since there !might! be some uebernoob playing the game - #needing one gazillion rotations to reach the goal it might happen those rounding errors actually get visible - #so let him enjoy and reset the cube every-time it's standing straight-up. - - self.cube.setZ(render,1) #how comes this is one?.. well.. i know.. because of my learnings.. - self.cube.setX(render, round(self.cube.getX(render),0) ) - self.cube.setY(render, round(self.cube.getY(render),0) ) - self.cube.setH(render, round(self.cube.getH(render),0) ) - self.cube.setP(render, round(self.cube.getP(render),0) ) - self.cube.setR(render, round(self.cube.getR(render),0) ) - - - if direction == "right": - dummy.setY(dummy,.5) - self.cube.wrtReparentTo(dummy) - dest_hpr = Vec3(0, -90, 0) - self.setCubeTiles( x1, y1+1 ,x1 , y1+2 ) - - if direction == "left": - dummy.setY(dummy,-.5) - self.cube.wrtReparentTo(dummy) - dest_hpr = Vec3(0, 90, 0) - self.setCubeTiles( x1, y1-2 ,x1 , y1-1 ) - - if direction == "up": - dummy.setX(dummy,-.5) - self.cube.wrtReparentTo(dummy) - dest_hpr = Vec3(0, 0, -90) - self.setCubeTiles( x1-2, y1 ,x1-1 , y1 ) - - if direction == "down": - dummy.setX(dummy,.5) - self.cube.wrtReparentTo(dummy) - dest_hpr = Vec3(0, 0, 90) - self.setCubeTiles( x1+1, y1 ,x1+2 , y1 ) - - elif x1 == x2 : #if aligned to y-axis - if direction == "right": - dummy.setY(dummy,1) - self.cube.wrtReparentTo(dummy) - dest_hpr = Vec3(0, -90, 0) - self.setCubeTiles( x1, y1+2 ) - - if direction == "left": - dummy.setY(dummy,-1) - self.cube.wrtReparentTo(dummy) - dest_hpr = Vec3(0, 90, 0) - self.setCubeTiles( x1, y1-1) - - if direction == "up": - dummy.setX(dummy,-.5) - self.cube.wrtReparentTo(dummy) - dest_hpr = Vec3(0, 0, -90) - self.setCubeTiles( x1-1, y1, x2-1, y2 ) - - if direction == "down": - dummy.setX(dummy,.5) - self.cube.wrtReparentTo(dummy) - dest_hpr = Vec3(0, 0, 90) - self.setCubeTiles( x1+1, y1, x2+1, y2 ) - - - elif y1==y2 : #if it is alligned to x-axis.. - - if direction == "right": - dummy.setY(dummy,.5) - self.cube.wrtReparentTo(dummy) - dest_hpr = Vec3(0, -90, 0) - self.setCubeTiles( x1, y1+1, x2, y2+1 ) - - if direction == "left": - dummy.setY(dummy,-.5) - self.cube.wrtReparentTo(dummy) - dest_hpr = Vec3(0, 90, 0) - self.setCubeTiles( x1, y1-1, x2, y2-1 ) - - if direction == "up": - dummy.setX(dummy,-1) - self.cube.wrtReparentTo(dummy) - dest_hpr = Vec3(0, 0, -90) - self.setCubeTiles( x1-1, y1 ) - - if direction == "down": - dummy.setX(dummy,1) - self.cube.wrtReparentTo(dummy) - dest_hpr = Vec3(0, 0, 90) - self.setCubeTiles( x1+2, y1 ) - - else: - print("Invalid move. Waiting ..") - - - print("Rotating!") - anim = self.animate( LerpHprInterval(dummy, duration, dest_hpr,(0,0,0) ) ) - - - #this sorta.. doesnt belong here.. but i dunno where to put it yet. - x1,y1,x2,y2 = self.getCubeTiles() - # self.level.tintTile(x1,y1) #this is fun to play with if your cube is invisible... - # self.level.tintTile(x2,y2) - self.level.stopAnimatedTile(x1,y1) #stops the tile-animation for the tiles below the block - self.level.stopAnimatedTile(x2,y2) - - #cheking what consequences your move had... muhahaa... if you get a 1 .. i've got bad news for you - checkresult = checkMove(self.level.levelNode,self.getCubeTiles(),self.sounds) - if checkresult == 1: - self.falls +=1 - self.moves +=1 - - # Force to the corner when the cuboid falls down - side_force = 1.7 - - force = Vec3(0, 0, 0) - if direction == "up": - force = Vec3(-side_force, 0, 0) - elif direction == "down": - force = Vec3(side_force, 0, 0) - elif direction == "left": - force = Vec3(0, -side_force, 0) - elif direction == "right": - force = Vec3(0, side_force, 0) - - dummy.set_hpr(render, Vec3(0, 0, 0)) - - del anim - - self.setAnimated(True) - final_hpr = dest_hpr * 3.0 + Vec3(random(), random(), random()) * 360.0 * 0.0 - anim = LerpFunc(self.animateCube, fromData=0, toData=1, duration=1.3, blendType='noBlend', extraArgs=[dummy.get_pos(render), Vec3(0), final_hpr, dummy, force]) - taskMgr.doMethodLater( anim.getDuration(), self.resetCube , "resetTask") - - elif checkresult == 2: - - #ok.. once reached the goal, move the block down, fading it out. thenload the new level etc. - anim.pop() - anim.append( Func(lambda: self.level.fadeOutLevel() ) ) - - Sequence(Wait(0.3), Func(lambda *args: self.cube.hide())).start() - - taskMgr.doMethodLater( anim.getDuration()+2 , self.levelUp , "lvlup") - taskMgr.doMethodLater( anim.getDuration()+2 , lambda *args: self.cube.show(), "show cube") - taskMgr.doMethodLater( anim.getDuration()+2 , lambda *args: self.shard_node.node().remove_all_children(), "clear shards") - - Sequence(Wait(0.2), Func(lambda *args: self.sounds.playSound("finish.wav"))).start() - - self.moves = 0 - self.falls = 0 - - cube_min, cube_max = Vec3(-0.5, -0.5, -1), Vec3(0.5, 0.5, 1) - self.shard_node.set_pos(dummy.get_pos(render) + Vec3(-0.5, 0, 0)) - shard_size = (cube_max - cube_min) / 5.0 - - self.shard_node.hide() - Sequence(Wait(0.22), Func(lambda *args: self.shard_node.show())).start() - - for i in range(5): - for j in range(5): - for k in range(5): - shard = loader.loadModel("models/CubeShard.bam") - shard.reparent_to(self.shard_node) - shard.set_x(i * shard_size.x + 0.1) - shard.set_y(j * shard_size.y + 0.1) - shard.set_z(k * shard_size.z + 0.2) - shard.set_scale(0.8 + random()) - - force = Vec3(i-2 - 0.15, j-2 - 0.15, k-2 + 2.6) - force.normalize() - force *= 12.0 * (1 + random() * 0.5) - - d_hpr = Vec3(random(), random(), random()) * 360.0 * (3.0 + random()) - - shard_anim = Sequence( - Wait(0.22), - LerpFunc(self.animateShard, fromData=0, toData=2, duration=2.0, blendType='noBlend', extraArgs=[shard.get_pos(), d_hpr, shard, force]), - LerpHprInterval(shard, 1.0 + random(), d_hpr * 1.6, d_hpr, blendType='noBlend'), - ) - shard_anim.start() - - elif checkresult == 0: - #how lame... just a ..move.. - print("playing sound") - self.moves += 1 - self.sounds.playSound("stonerotate.wav") - print("moves:",self.moves ," falls:",self.falls) - #last but not least.. we start the animation .. did you know that the pc knows you'r failing before you actually do? .. scary.. - anim.start() - - def animateShard(self, t, initial_pos, dest_hpr, shard, force): - - z_force = -(t**2) * 9.81 * 1.7 - regular_force = force * t - - dest_pos = initial_pos + regular_force + Vec3(0, 0, z_force) - - shard.set_pos(dest_pos) - shard.set_hpr(dest_hpr * t) - - def animateCube(self, t, initial_pos, initial_hpr, dest_hpr, cube, force): - z_force = -(t**2) * 9.81 * 1.7 - regular_force = force * t - dest_pos = initial_pos + regular_force + Vec3(0, 0, z_force) - cube.set_pos(render, dest_pos) - cube.set_hpr(render, initial_hpr * (1 - t) + dest_hpr * t) - # cube.set_hpr(render, initial_hpr) diff --git a/RenderPipelineFile/samples/05-Quboid/src/GUI.py b/RenderPipelineFile/samples/05-Quboid/src/GUI.py deleted file mode 100644 index c56fe2c3..00000000 --- a/RenderPipelineFile/samples/05-Quboid/src/GUI.py +++ /dev/null @@ -1,60 +0,0 @@ - - -from panda3d.core import TextNode, Vec4 -from direct.interval.LerpInterval import LerpColorScaleInterval -from direct.interval.MetaInterval import Sequence,Parallel -from direct.interval.FunctionInterval import Func,Wait - -class GUI(object): - - """ Handles the gui elements like showing the current stage """ - - def __init__(self, control): - - self.control = control - self.gui_node = pixel2d.attach_new_node("GUI") - self.gui_node.hide() - self.gui_node.set_transparency(True) - - roboto_light = loader.loadFont("res/Roboto-Light.ttf") - roboto_light.set_scale_factor(1) - roboto_light.set_pixels_per_unit(120) - - self.text_stage = TextNode("TextStage") - self.text_stage.set_text("Stage 1") - self.text_stage.set_align(TextNode.A_left) - self.text_stage.set_text_color(1, 1, 1, 1) - self.text_stage.set_font(roboto_light) - self.text_stage_np = self.gui_node.attach_new_node(self.text_stage) - self.text_stage_np.set_scale(80.0) - self.text_stage_np.set_pos(60, 0, -120) - - self.anim = None - - def show(self): - self.gui_node.show() - if self.anim is not None: - self.anim.finish() - self.anim = Sequence( - LerpColorScaleInterval(self.gui_node, 0.2, Vec4(1), Vec4(1, 1, 1, 0), blendType="easeInOut") - ) - self.anim.start() - - def hide(self): - self.gui_node.hide() - if self.anim is not None: - self.anim.finish() - self.anim = Sequence( - LerpColorScaleInterval(self.gui_node, 0.2, Vec4(1, 1, 1, 0), Vec4(1), blendType="easeInOut") - ) - self.anim.start() - - - def set_stage(self, stage_nr): - print(("Setting stage", stage_nr)) - - Sequence( - LerpColorScaleInterval(self.text_stage_np, 0.4, Vec4(1, 1, 1, 0), Vec4(1), blendType="easeInOut"), - Func(lambda *args: self.text_stage.set_text("Stage " + str(stage_nr))), - LerpColorScaleInterval(self.text_stage_np, 0.4, Vec4(1), Vec4(1, 1, 1, 0), blendType="easeInOut"), - ).start() diff --git a/RenderPipelineFile/samples/05-Quboid/src/GameControl.py b/RenderPipelineFile/samples/05-Quboid/src/GameControl.py deleted file mode 100644 index bb26b82c..00000000 --- a/RenderPipelineFile/samples/05-Quboid/src/GameControl.py +++ /dev/null @@ -1,100 +0,0 @@ - -import os -import sys - - -from direct.showbase.ShowBase import ShowBase -from panda3d.core import load_prc_file_data - -from .Cube import Cube -from .Menu import Menu -from .LevelLoader import Level -from .CamControl import CamControl -from .GUI import GUI - -class GameControl(ShowBase): - """ - controlling the game itself, menu, editors level selection... it's sorta a fake-fsm. - did i mention i dont like fsm's and prefer totaly whicked logic instead? - """ - def __init__(self): - - load_prc_file_data("", "textures-power-2 none") - load_prc_file_data("", "win-size 1600 900") - # load_prc_file_data("", "fullscreen #t") - load_prc_file_data("", "window-title cuboid") - load_prc_file_data("", "icon-filename res/icon.ico") - - # I found openal works better for me - load_prc_file_data("", "audio-library-name p3openal_audio") - - # ------ Begin of render pipeline code ------ - - # Insert the pipeline path to the system path, this is required to be - # able to import the pipeline classes - pipeline_path = "../../" - - # Just a special case for my development setup, so I don't accidentally - # commit a wrong path. You can remove this in your own programs. - if not os.path.isfile(os.path.join(pipeline_path, "setup.py")): - pipeline_path = "../../RenderPipeline/" - - sys.path.insert(0, pipeline_path) - - # Use the utility script to import the render pipeline classes - from rpcore import RenderPipeline - - self.render_pipeline = RenderPipeline() - # self.render_pipeline.mount_mgr.mount() - # self.render_pipeline.load_settings("/$$rpconfig/pipeline.yaml") - # self.render_pipeline.settings["pipeline.display_debugger"] = False - # self.render_pipeline.set_empty_loading_screen() - self.render_pipeline.create(self) - - # [Optional] use the default skybox, you can use your own skybox as well - # self.render_pipeline.create_default_skybox() - - # ------ End of render pipeline code, thats it! ------ - - - # Set time of day - self.render_pipeline.daytime_mgr.time = 0.812 - - self.menu = Menu(self) - self.level = Level(self) - self.cube = Cube(self.level) - self.camControl = CamControl(self.cube) - self.gui = GUI(self) - self.menu.showMenu() - base.accept("i",self.camControl.zoomIn) - base.accept("o",self.camControl.zoomOut) - - def startGame(self,level=0): - #debug purpose only: to directly play a certian lvl number - from sys import argv - if len(argv) >1: - level = int(argv[1]) - - self.menu.hideMenu() - self.level.loadLevel(level) - self.cube.resetCube() - self.cube.resetStats() - self.cube.enableGameControl() - base.accept("escape", self.pauseGame) - - def pauseGame(self): - self.cube.disableGameControl() - self.menu.showMenu() - self.menu.showResume() - #base.accept("escape", self.resumeGame ) - - def resumeGame(self): - self.menu.hideMenu() - self.menu.hideResume() - self.cube.enableGameControl() - base.accept("escape", self.pauseGame) - - def levelEnd(self): - self.cube.disableGameControl() - self.menu.showMenu() - diff --git a/RenderPipelineFile/samples/05-Quboid/src/LevelGenerator.py b/RenderPipelineFile/samples/05-Quboid/src/LevelGenerator.py deleted file mode 100644 index 511e9535..00000000 --- a/RenderPipelineFile/samples/05-Quboid/src/LevelGenerator.py +++ /dev/null @@ -1,117 +0,0 @@ -from random import randint - -class LevelGenerator: - """ - this class produces a new map . i admit. it's logic is quite twisted but it's neccessary or the levels would be as simple as the logic you used. - and we dont want the players to suffer death from boredom. doctors dont recommend it. - """ - def __init__(self): - self.tilemap ={} - self.cubeTiles=[0,0,None,None] - self.movementHistory=["0_0_None_None"] - - def generateLevel(self,effektiveMoves = 20): - self.movementHistory=["0_0_None_None"] - self.cubeTiles=[0,0,None,None] - count = 0 - failcount = 0 - directions = ["up","down","left" , "right" ] - while ( len( self.movementHistory ) < effektiveMoves ): - print("step1:",self.movementHistory) - self.fakeMove( directions[randint(0,3)] ) - print("step2:",self.movementHistory) - indexnumber = self.movementHistory.index(self.movementHistory[-1]) - print("index:", indexnumber) - while len(self.movementHistory) > indexnumber+1: - print(self.movementHistory.pop()) - - while (not "None" in self.movementHistory[-1]): - self.movementHistory.pop() - - print(len(self.movementHistory),self.movementHistory[-1]) - return self.createLevelFromHistory(self.movementHistory) - - - def createLevelFromHistory(self,history): - tiles = [] - for number in range(0,len(history) ): - tilestring = history[number].split("_") - strpos1 = "Pos="+tilestring[0]+"_"+tilestring[1] - if strpos1 not in tiles: - tiles.append(strpos1) - if tilestring[2] != "None": - strpos2 = "Pos="+tilestring[2]+"_"+tilestring[3] - if strpos2 not in tiles: - tiles.append(strpos2) - myMapString = "" - for number in range(1,len(tiles)-1 ): - myMapString += "" - myMapString += "" - return myMapString - - def getCubeTiles(self): - tiles = self.movementHistory[-1].split("_") - if tiles[2] == "None": - self.cubeTiles = [int(tiles[0]),int(tiles[1]),None,None] - else: - self.cubeTiles = [int(tiles[0]),int(tiles[1]),int(tiles[2]),int(tiles[3])] - return self.cubeTiles - - - def setCubeTiles(self,x1,y1,x2=None,y2=None): - newPositionString = str(x1)+"_"+str(y1)+"_"+str(x2)+"_"+str(y2) # looks like 12_5_None_None - self.cubeTiles= [x1,y1,x2,y2] - self.movementHistory.append(newPositionString) - print("new move was successful", self.cubeTiles) - - - def fakeMoveTest(self, x1,y1,x2=None,y2=None ): - for i in ["up","down","left","right"]: - if self.fakeMoveTestCases(i,x1,y1,x2,y2) == 1: - return 1 - return 0 - - - def fakeMove(self,direction): - """ - fakes the rotation of the cube. - """ - x1,y1,x2,y2 = self.getCubeTiles() - print("current position", x1,y1,x2,y2) - if self.cubeTiles[2] == None : - #case1 : cube is standing upright - - if direction == "right": - self.setCubeTiles( x1, y1+1 ,x1 , y1+2 ) - if direction == "left": - self.setCubeTiles( x1, y1-2 ,x1 , y1-1 ) - if direction == "up": - self.setCubeTiles( x1-2, y1 ,x1-1 , y1 ) - if direction == "down": - self.setCubeTiles( x1+1, y1 ,x1+2 , y1 ) - - elif x1 == x2: #if aligned to y-axis (heck i know but precision issues... you know?) - if direction == "right": - self.setCubeTiles( x1, y1+2 ) - if direction == "left": - self.setCubeTiles( x1, y1-1) - if direction == "up": - self.setCubeTiles( x1-1, y1, x2-1, y2 ) - if direction == "down": - self.setCubeTiles( x1+1, y1, x2+1, y2 ) - - - elif y1 == y2 : #if it is alligned to x-axis.. (math sux i know but we need tollerance) - if direction == "right": - self.setCubeTiles( x1, y1+1, x2, y2+1 ) - if direction == "left": - self.setCubeTiles( x1, y1-1, x2, y2-1 ) - if direction == "up": - self.setCubeTiles( x1-1, y1 ) - if direction == "down": - self.setCubeTiles( x1+2, y1 ) - - #this sorta.. doesnt belong here.. but i dunno where to put it yet. - #x1,y1,x2,y2 = self.getCubeTiles() - #checkresult = checkMove(self.level.levelNode,self.getCubeTiles(),self.sounds) - diff --git a/RenderPipelineFile/samples/05-Quboid/src/LevelLoader.py b/RenderPipelineFile/samples/05-Quboid/src/LevelLoader.py deleted file mode 100644 index 2006757e..00000000 --- a/RenderPipelineFile/samples/05-Quboid/src/LevelLoader.py +++ /dev/null @@ -1,287 +0,0 @@ - -""" -i dont really know how to save maps in a best possible way. but i guess i'll just store the same informationas in the tags in clear-text. -<"Type"="tile","Pos"="12_2"> -pos is the X_Y coordinate. Type can be lots of different types. "tile" would be the standard tile, -"weakTile" is one that only carries half the cuboid. "fragile" could be one that breaks after using it once -"switch" which triggers events... stuff like that. can be extended quite esily. - -other tags are "Start" and "Goal" -further tags could be "ID" which could come in handy when using switches. -notice the naming. tag names start capital, the values lowercase. - -""" -from panda3d.core import NodePath, Vec3 -from direct.showbase.Loader import Loader -from direct.interval.LerpInterval import LerpPosQuatInterval,LerpPosInterval, LerpHprInterval ,LerpPosHprInterval ,LerpColorScaleInterval -from direct.interval.LerpInterval import LerpFunc -from direct.interval.FunctionInterval import Func,Wait -from direct.interval.MetaInterval import Sequence -from random import random -from panda3d.core import AmbientLight,DirectionalLight -from panda3d.core import RigidBodyCombiner -from .Sound import * -from .LevelGenerator import LevelGenerator -from panda3d.core import OmniBoundingVolume - -class Level: - def __init__(self,main): - self.main = main - self.sounds = Sounds() - self.loadBackground() - self.levelGen = LevelGenerator() - print("initializing levelnode") - self.LevelNr = 0 - - def loadBackground(self,background="./models/surrounding/Scene.bam"): - base.setBackgroundColor(1,1,1) - self.background = loader.loadModel(background) - self.background.reparentTo(render) - self.background.setZ(-10) - self.background.setScale(.3) - self.background.setH(-90) - - def loadLevel(self, levelnr=None): - """ - will load the level with the nr. if no number is specified it will load lastlevelnumber+1.. or tries to do so. - if it fails it will either generate a new level or return to the menue (havent decided yet) - """ - if levelnr == None: - self.LevelNr +=1 - else: - self.LevelNr = levelnr - print("trying to load level nr:",self.LevelNr) - #handle unloading of the map here. - try: - tiles = self.levelNode.findAllMatches("=Pos") - except: - tiles = [] - print("no tiles for removal found") - for tile in tiles: - x,y,z = self.getPosFromTile(tile) - self.stopAnimatedTile(x,y) - tile.remove_node() - - try: - self.levelNode.remove_node() - except: - print("failed to remove old level node.. maybe there was none?") - - print("loading level....") - - try: - data=open("./levels/level_"+str(self.LevelNr)).read() - except: - print("sorry, failed to load mapfile level"+str(self.LevelNr)) - - #data = self.levelGen.generateLevel() - #print ".... level generator should kick in here.. but it's not yet ready.." - self.main.levelEnd() - return 1 - - #data=open("./levels/level_"+str(self.LevelNr-1)).read() - #if self.newMap == None: - # self.createNewLevel(level) - #data=open("./levels/level_temp").read() - #return - - - - self.levelNode = self.loadLevelData( data ) - print(self.levelNode, "returning levelNode") - self.levelNode.reparentTo(render) - self.fadeInLevel() - return 0 - #return self.levelNode - - #startPos = self.LevelNode.find("=Type=start").getTag("Pos") - #startPos = startPos.split("_") - - def animateTile(self,x,y): - """ - the floating animation of tiles. - """ - Time = 2 - tile = self.getTileFromPos(x,y) - if tile: - if tile.getPythonTag("Seq").isPlaying() ==False: - seq = Sequence( - LerpPosInterval( tile, Time+(0.2*Time*random()),(x,y,.35),(x,y,0) , blendType="easeInOut") , - LerpPosInterval( tile, Time+(0.2*Time*random()),(x,y, 0),(x,y,.35) , blendType="easeInOut") ) - tile.setPythonTag("Seq", seq ) - seq.loop() - - - def stopAnimatedTile(self,x,y,now=None): - tile = self.getTileFromPos(x,y) - if tile: - if tile.hasPythonTag("Seq") and now == None: - sequence = tile.getPythonTag("Seq") - sequence.pause() - sequence = Sequence( Wait(.1), LerpPosQuatInterval(tile,.1,(x,y,0),(0,0,0),blendType='easeIn') ) - sequence.start() - tile.setPythonTag("Seq",sequence) - elif tile.hasPythonTag("Seq") and now != None: - sequence = tile.getPythonTag("Seq") - sequence.pause() - - def animateTiles(self,task=None): - """ - calls animateTile on all tiles. note the plural in the name - """ - tiles = self.levelNode.findAllMatches("=Pos") - for tile in tiles: - x,y,z = self.getPosFromTile(tile) - self.animateTile(x,y) - - - def loadTile(self,data): - data = data.split(",") - tile = None - for i in data: - #check for the type and load tile - if i.startswith("Type="): - i = i.split("=") - tile = loader.loadModel("./models/"+i[1]+".bam") - tile.setTag(i[0],i[1]) - break - - for i in data: - if i.startswith("Pos=") and tile != None: - i = i.split("=") - tile.setTag(i[0], i[1]) - break - - if tile: - if tile.hasTag("Pos") and tile.hasTag("Type"): - return tile - else: - return None - else: - return None - - - def loadLevelData(self,inputData): - """ - processes the level asloaded from the file. it seperates the input data until the data for each tile is ready. - each tile data will be passed to loadTile(). - it returns a rigidNode optimized nodepath. - """ - rigidNode = RigidBodyCombiner("LevelNode") - levelNode = NodePath(rigidNode) - #rigidNode.reparentTo(levelNode) - #this looks heavy but all it does is deleting whitespaces and seperating the content for each tile into a list. - inputData = inputData.replace("\n","").strip().replace(" ","").lstrip("<").rstrip(">").split("><") - - for tileData in inputData: - tile = self.loadTile(tileData) - if tile != None: - tile.reparentTo(levelNode) - tile.setPos( self.getPosFromTile(tile) ) - tile.setZ(tile,0.00000001) #workaround for rigid body combiner so it does not assume the (0,0) tile as static - else: - print("ERROR, could not load tile with data: ",tileData) - rigidNode.collect() - inode = rigidNode.getInternalScene().node() #workaround for a boundingvolume issue with rigidbodycombiner - inode.setBounds(OmniBoundingVolume()) #still workaround - inode.setFinal(True) #still workaround - #levelNode.analyze() - return levelNode - - def getTileFromPos(self,x,y): - """ - returns the nodePath of a tile with the given tile number - """ - if type(x) == list or type(x) == tuple: - y=x[1] - x=x[0] - tile = self.levelNode.find("=Pos="+str(x)+"_"+str(y)) - if tile.isEmpty(): - return None - else: - return tile - - def getPosFromTile(self,tile): - """ - returns the tile position given a tile's nodePath, or None if the nodepath has no position tags - """ - if tile.hasTag("Pos"): - pos = tile.getTag("Pos").split("_") - x,y = int(pos[0]) , int(pos[1]) - return (x,y,0) - else: - print("ERROR, supplied tile has no 'Pos' tag") - return None - - def getStartTile(self): - tile = self.levelNode.find("=Type=start") - if tile.isEmpty() == True: - print("Start-Tile was not found.. I'm prediction an application crash within the next 50ms...") - print("oh.. and feel free to add propper exception handling here so we get back to some menue or so, instead of crashin") - return tile - - - def fadeOutLevel(self,fadeTime=1): - """ - the level-falls-apart animation. - """ - tiles = self.levelNode.findAllMatches("=Pos") - for tile in tiles: - x,y,z = self.getPosFromTile(tile) - self.stopAnimatedTile(x,y,True) - tile.setPos(x,y,0) - #tile.setHpr(random()*360,random()*360,random()*360) - # seq = LerpPosHprInterval(tile,fadeTime+(0.3*fadeTime*random()),(x,y,-15),(random()*360 -180,random()*360-180,random()*360-180), blendType='easeIn') - # seq.start() - - final_hpr = Vec3(random(), random(), random()) * 360.0 - force = (Vec3(random(), random(), random())-0.5) * 5.0 - force.z = 0 - seq = LerpFunc(self.tileGravityAnimation, fromData=0, toData=1, duration=1.0, blendType='noBlend', extraArgs=[tile.get_pos(render), Vec3(0), final_hpr, tile, force]) - tile.setPythonTag("Seq", seq) - seq.start() - - - def tileGravityAnimation(self, t, initial_pos, initial_hpr, dest_hpr, tile, force): - """ - Animates a tile by applying gravity - """ - z_force = -(t**2) * 9.81 * 1.7 - regular_force = force * t - dest_pos = initial_pos + regular_force + Vec3(0, 0, z_force) - tile.set_pos(render, dest_pos) - tile.set_hpr(render, initial_hpr * (1 - t) + dest_hpr * t) - - def fadeInLevel(self, fadeTime = 1.6): - """ - fade-in animation. the parameter it takes is the time in seconds.changing it might cause animation glitches with the cube-fade-in animation - """ - self.sounds.playSound("nyon.wav") - tiles = self.levelNode.findAllMatches("=Pos") - for tile in tiles: - - x,y,z = self.getPosFromTile(tile) - tile.setPos(x,y,-15) - tile.setHpr(random()*360-180,random()*360-180,random()*360-180) - seq = LerpPosQuatInterval(tile,fadeTime+(0.3*fadeTime*random()),(x,y,0),(0,0,0),blendType='easeOut') - tile.setPythonTag("Seq", seq ) - seq.start() - Sequence(Wait(fadeTime*1.4), Func(lambda:self.animateTiles())).start() - - def tintTile(self,x,y): - """ - the 'i was so bored and was looking for something colorful' function. feel free to enable it in the Cube.py file - """ - if x != None and y != None: - tile=self.getTileFromPos(x,y) - - #tile.setColorScale(0,0,0,1) - if tile != None: - if tile.hasPythonTag("Colorlerp"): - tile.getPythonTag("Colorlerp").pause() - seq = Sequence( - Wait(.1), - LerpColorScaleInterval(tile,.1,(random(),random(),random(),1) ), - LerpColorScaleInterval(tile,10,(1,1,1,1) ) ) - tile.setPythonTag("Colorlerp",seq) - seq.start() diff --git a/RenderPipelineFile/samples/05-Quboid/src/Menu.py b/RenderPipelineFile/samples/05-Quboid/src/Menu.py deleted file mode 100644 index 46b7137b..00000000 --- a/RenderPipelineFile/samples/05-Quboid/src/Menu.py +++ /dev/null @@ -1,145 +0,0 @@ - - -from panda3d.core import SamplerState -from direct.gui.DirectFrame import DirectFrame -from direct.gui.DirectButton import DirectButton -from direct.interval.LerpInterval import LerpColorScaleInterval -from direct.interval.MetaInterval import Sequence -from direct.interval.FunctionInterval import Func -from direct.gui.DirectGuiBase import DGG -from sys import exit - -class Menu(object): - def __init__(self,main): - self.main=main - - wx = base.win.get_x_size() - wy = base.win.get_y_size() - kx = 1920 - ky = 1080 - self.myFrame = DirectFrame(frameColor=(1,1,1,1), - frameSize=(0, kx,0, ky)) - - menu_tex = loader.loadTexture("res/menu.png") - menu_tex.set_minfilter(SamplerState.FT_nearest) - menu_tex.set_magfilter(SamplerState.FT_linear) - self.myFrame["frameTexture"] = menu_tex - self.myFrame.reparentTo(base.pixel2d) - self.myFrame.set_pos( (wx-kx) / 2, 0, -(wy+ky) / 2) - self.myFrame.set_transparency(True) - - self.startButton = DirectButton( - text = "", - text_scale=1.0, - text_fg=(0.2,0.2,0.2,1), - frameTexture="res/start_game.png", - frameColor=(1,1,1,1), - frameSize=(-64, 64, -20, 20), - command=self.main.startGame, - relief=DGG.FLAT, - rolloverSound=None, - clickSound=None, - parent=self.myFrame, - scale=2.0, - pos=(wx/2 + 160, 0, wy/2 + 50) - ) - self.startButton.setTransparency(1) - - self.exitButton = DirectButton( - text = ("Exit Game"), - pos=(0,0,-.8), - text_scale=.1, - frameColor=(0,0,0,0), - relief=1, - frameVisibleScale=(2,3), - command=exit, - rolloverSound=None, - clickSound=None, - parent=self.myFrame, - ) - self.exitButton.setTransparency(1) - - self.resumeButton = DirectButton( - text = ("Resume"), - pos=(.0,0,.3), - text_scale=.1, - frameColor=(0,0,0,0), - relief=1, - frameVisibleScale=(2,3), - command=self.main.resumeGame, - rolloverSound=None, - clickSound=None, - parent=self.myFrame, - ) - - self.resumeButton.setTransparency(1) - self.resumeButton.hide() - - self.selectFrame= DirectFrame( frameColor=(1,1,1,1) , frameSize=(-64, 64, -20, 20) , frameTexture="res/select.png") - self.selectFrame.setTransparency(1) - self.selectFrame.reparentTo(self.startButton) - self.entries = [self.exitButton,self.startButton,self.resumeButton] - self.activeEntry = 1 - - - - - def clearKeys(self): - base.ignore("arrow_up") - base.ignore("arrow_down") - base.ignore("arrow_left") - base.ignore("arrow_right") - base.ignore("escape") - base.ignore("enter") - - def execSelection(self): - self.entries[self.activeEntry]["command"]() - - - def selectDown(self): - if self.activeEntry == 0: - self.activeEntry = len(self.entries)-1 - else: - self.activeEntry -=1 - - if self.entries[self.activeEntry].isHidden(): - self.selectDown() - return - self.selectFrame.reparentTo(self.entries[self.activeEntry]) - - def selectUp(self): - - if self.activeEntry == len(self.entries)-1: - self.activeEntry=0 - else: - self.activeEntry +=1 - if self.entries[self.activeEntry].isHidden() : - self.selectUp() - return - self.selectFrame.reparentTo(self.entries[self.activeEntry]) - - def hideMenu(self): - self.clearKeys() - self.main.gui.show() - seq= Sequence( LerpColorScaleInterval(self.myFrame, 0.4 ,(1,1,1,0)) , Func(self.myFrame.hide) ) - seq.start() - - def hideResume(self): - seq= Sequence( LerpColorScaleInterval(self.resumeButton, .5 ,(1,1,1,0)) , Func(self.resumeButton.hide)) - seq.start() - - def showResume(self): - self.resumeButton.show() - #seq= Sequence( LerpColorScaleInterval(self.resumeButton, 1 ,(1,1,1,1)) ) - #seq.start() - - def showMenu(self): - self.clearKeys() - base.accept("arrow_up" , self.selectUp ) - base.accept("arrow_down" , self.selectDown ) - base.accept("escape", exit) - base.accept("enter",self.execSelection) - self.myFrame.show() - self.main.gui.hide() - seq= Sequence( LerpColorScaleInterval(self.myFrame, .5 ,(1,1,1,1)) ) - seq.start() diff --git a/RenderPipelineFile/samples/05-Quboid/src/Sound.py b/RenderPipelineFile/samples/05-Quboid/src/Sound.py deleted file mode 100644 index a6f4d35f..00000000 --- a/RenderPipelineFile/samples/05-Quboid/src/Sound.py +++ /dev/null @@ -1,26 +0,0 @@ - - -class Sounds(object): - def __init__(self): - self.soundlist = [ - ["stonerotate.wav",1], - ["finish.wav",1], - ["kr1.wav",.3], - ["kr2.wav",.3], - ["drop4.wav",1], - ["drop3.wav",1], - ["drop2.wav",1], - ["drop1.wav",1], - ["nyon.wav",1], - ] - self.soundDict={} - - for sound in self.soundlist: - self.soundDict[sound[0]] = loader.loadSfx("./sounds/"+sound[0]) - self.soundDict[sound[0]].setVolume(sound[1]) - - def playSound(self,soundname): - if soundname in self.soundDict: - self.soundDict[soundname].play() - else: - print("sound not found",soundname) diff --git a/RenderPipelineFile/samples/06-Car/main.py b/RenderPipelineFile/samples/06-Car/main.py deleted file mode 100644 index da6715f8..00000000 --- a/RenderPipelineFile/samples/06-Car/main.py +++ /dev/null @@ -1,84 +0,0 @@ - -# Disable the "xxx has no yyy member" error, pylint seems to be unable to detect -# the properties of a nodepath -# pylint: disable=no-member - -from __future__ import print_function - -import os -import sys -import math -from random import random, randint, seed -from panda3d.core import Vec3, load_prc_file_data -from direct.showbase.ShowBase import ShowBase - -# Change to the current directory -os.chdir(os.path.dirname(os.path.realpath(__file__))) - -class MainApp(ShowBase): - def __init__(self): - - # Setup window size, title and so on - load_prc_file_data("", """ - win-size 1600 900 - window-title Render Pipeline - Car Demo - """) - - # ------ Begin of render pipeline code ------ - - # Insert the pipeline path to the system path, this is required to be - # able to import the pipeline classes - pipeline_path = "../../" - - # Just a special case for my development setup, so I don't accidentally - # commit a wrong path. You can remove this in your own programs. - if not os.path.isfile(os.path.join(pipeline_path, "setup.py")): - pipeline_path = "../../RenderPipeline/" - - sys.path.insert(0, pipeline_path) - - from rpcore import RenderPipeline, SpotLight - self.render_pipeline = RenderPipeline() - self.render_pipeline.create(self) - - # This is a helper class for better camera movement - its not really - # a rendering element, but it included for convenience - from rpcore.util.movement_controller import MovementController - - # ------ End of render pipeline code, thats it! ------ - - self.render_pipeline.daytime_mgr.time = "20:08" - - # Load the scene - model = loader.loadModel("scene/scene.bam") - # model = loader.loadModel("scene2/Scene.bam") - model_0 = self.loader.loadModel("/home/tiger/下载/Benci/source/s65/s65/s65.fbx") - model_0.reparentTo(self.render) - model_0.setScale(0.01) - model_0.setPos(-8, 42, 0) - model_0.setHpr(0, 90, 0) - - model.reparent_to(render) - self.render_pipeline.prepare_scene(model) - - # Init movement controller - self.controller = MovementController(self) - self.controller.set_initial_position( - Vec3(-7.5, -5.3, 1.8), Vec3(-5.9, -4.0, 1.6)) - self.controller.setup() - - base.accept("l", self.tour) - - def tour(self): - """ Camera flythrough """ - mopath = ( - (Vec3(-10.8645000458, 9.76458263397, 2.13306283951), Vec3(-133.556228638, -4.23447799683, 0.0)), - (Vec3(-10.6538448334, -5.98406457901, 1.68028640747), Vec3(-59.3999938965, -3.32706642151, 0.0)), - (Vec3(9.58458328247, -5.63625621796, 2.63269257545), Vec3(58.7906494141, -9.40668964386, 0.0)), - (Vec3(6.8135137558, 11.0153560638, 2.25509500504), Vec3(148.762527466, -6.41223621368, 0.0)), - (Vec3(-9.07093334198, 3.65908527374, 1.42396306992), Vec3(245.362503052, -3.59927511215, 0.0)), - (Vec3(-8.75390911102, -3.82727789879, 0.990055501461), Vec3(296.090484619, -0.604830980301, 0.0)), - ) - self.controller.play_motion_path(mopath, 3.0) - -MainApp().run() diff --git a/RenderPipelineFile/samples/06-Car/scene/Scene.blend b/RenderPipelineFile/samples/06-Car/scene/Scene.blend deleted file mode 100644 index 541c888c..00000000 Binary files a/RenderPipelineFile/samples/06-Car/scene/Scene.blend and /dev/null differ diff --git a/RenderPipelineFile/samples/06-Car/scene/scene.bam b/RenderPipelineFile/samples/06-Car/scene/scene.bam deleted file mode 100644 index 8dc54969..00000000 Binary files a/RenderPipelineFile/samples/06-Car/scene/scene.bam and /dev/null differ diff --git a/RenderPipelineFile/samples/06-Car/scene/tex/AirPattern.png b/RenderPipelineFile/samples/06-Car/scene/tex/AirPattern.png deleted file mode 100644 index e6b28085..00000000 Binary files a/RenderPipelineFile/samples/06-Car/scene/tex/AirPattern.png and /dev/null differ diff --git a/RenderPipelineFile/samples/06-Car/scene/tex/AirPatternBW.png b/RenderPipelineFile/samples/06-Car/scene/tex/AirPatternBW.png deleted file mode 100644 index bb60ff4b..00000000 Binary files a/RenderPipelineFile/samples/06-Car/scene/tex/AirPatternBW.png and /dev/null differ diff --git a/RenderPipelineFile/samples/06-Car/scene/tex/BrushedMetal.png b/RenderPipelineFile/samples/06-Car/scene/tex/BrushedMetal.png deleted file mode 100644 index 522629b7..00000000 Binary files a/RenderPipelineFile/samples/06-Car/scene/tex/BrushedMetal.png and /dev/null differ diff --git a/RenderPipelineFile/samples/06-Car/scene/tex/CarPaintDiff.png b/RenderPipelineFile/samples/06-Car/scene/tex/CarPaintDiff.png deleted file mode 100644 index 6a53101e..00000000 Binary files a/RenderPipelineFile/samples/06-Car/scene/tex/CarPaintDiff.png and /dev/null differ diff --git a/RenderPipelineFile/samples/06-Car/scene/tex/CarPaintNrm.png b/RenderPipelineFile/samples/06-Car/scene/tex/CarPaintNrm.png deleted file mode 100644 index b27c33b2..00000000 Binary files a/RenderPipelineFile/samples/06-Car/scene/tex/CarPaintNrm.png and /dev/null differ diff --git a/RenderPipelineFile/samples/06-Car/scene/tex/CarPaintSpec.png b/RenderPipelineFile/samples/06-Car/scene/tex/CarPaintSpec.png deleted file mode 100644 index 2a6a5512..00000000 Binary files a/RenderPipelineFile/samples/06-Car/scene/tex/CarPaintSpec.png and /dev/null differ diff --git a/RenderPipelineFile/samples/06-Car/scene/tex/Cerberus_A.tga b/RenderPipelineFile/samples/06-Car/scene/tex/Cerberus_A.tga deleted file mode 100644 index aa9fffaf..00000000 Binary files a/RenderPipelineFile/samples/06-Car/scene/tex/Cerberus_A.tga and /dev/null differ diff --git a/RenderPipelineFile/samples/06-Car/scene/tex/Cerberus_M.tga b/RenderPipelineFile/samples/06-Car/scene/tex/Cerberus_M.tga deleted file mode 100644 index dfe249a6..00000000 Binary files a/RenderPipelineFile/samples/06-Car/scene/tex/Cerberus_M.tga and /dev/null differ diff --git a/RenderPipelineFile/samples/06-Car/scene/tex/Cerberus_N.tga b/RenderPipelineFile/samples/06-Car/scene/tex/Cerberus_N.tga deleted file mode 100644 index 60d77b46..00000000 Binary files a/RenderPipelineFile/samples/06-Car/scene/tex/Cerberus_N.tga and /dev/null differ diff --git a/RenderPipelineFile/samples/06-Car/scene/tex/Cerberus_R.tga b/RenderPipelineFile/samples/06-Car/scene/tex/Cerberus_R.tga deleted file mode 100644 index 26c3859f..00000000 Binary files a/RenderPipelineFile/samples/06-Car/scene/tex/Cerberus_R.tga and /dev/null differ diff --git a/RenderPipelineFile/samples/06-Car/scene/tex/ConcreteDiff.png b/RenderPipelineFile/samples/06-Car/scene/tex/ConcreteDiff.png deleted file mode 100644 index 8e756b65..00000000 Binary files a/RenderPipelineFile/samples/06-Car/scene/tex/ConcreteDiff.png and /dev/null differ diff --git a/RenderPipelineFile/samples/06-Car/scene/tex/ConcreteNormal.png b/RenderPipelineFile/samples/06-Car/scene/tex/ConcreteNormal.png deleted file mode 100644 index 416b9697..00000000 Binary files a/RenderPipelineFile/samples/06-Car/scene/tex/ConcreteNormal.png and /dev/null differ diff --git a/RenderPipelineFile/samples/06-Car/scene/tex/ConcreteNrm.png b/RenderPipelineFile/samples/06-Car/scene/tex/ConcreteNrm.png deleted file mode 100644 index 7e8b7795..00000000 Binary files a/RenderPipelineFile/samples/06-Car/scene/tex/ConcreteNrm.png and /dev/null differ diff --git a/RenderPipelineFile/samples/06-Car/scene/tex/ConcreteRoughness.png b/RenderPipelineFile/samples/06-Car/scene/tex/ConcreteRoughness.png deleted file mode 100644 index 170d8fd1..00000000 Binary files a/RenderPipelineFile/samples/06-Car/scene/tex/ConcreteRoughness.png and /dev/null differ diff --git a/RenderPipelineFile/samples/06-Car/scene/tex/ConcreteSpec.png b/RenderPipelineFile/samples/06-Car/scene/tex/ConcreteSpec.png deleted file mode 100644 index 28e7cc23..00000000 Binary files a/RenderPipelineFile/samples/06-Car/scene/tex/ConcreteSpec.png and /dev/null differ diff --git a/RenderPipelineFile/samples/06-Car/scene/tex/FloorDiff.png b/RenderPipelineFile/samples/06-Car/scene/tex/FloorDiff.png deleted file mode 100644 index b15142c0..00000000 Binary files a/RenderPipelineFile/samples/06-Car/scene/tex/FloorDiff.png and /dev/null differ diff --git a/RenderPipelineFile/samples/06-Car/scene/tex/FloorNrm.png b/RenderPipelineFile/samples/06-Car/scene/tex/FloorNrm.png deleted file mode 100644 index 81f69168..00000000 Binary files a/RenderPipelineFile/samples/06-Car/scene/tex/FloorNrm.png and /dev/null differ diff --git a/RenderPipelineFile/samples/06-Car/scene/tex/FloorRough.png b/RenderPipelineFile/samples/06-Car/scene/tex/FloorRough.png deleted file mode 100644 index 93f044e7..00000000 Binary files a/RenderPipelineFile/samples/06-Car/scene/tex/FloorRough.png and /dev/null differ diff --git a/RenderPipelineFile/samples/06-Car/scene/tex/MarbleTex.png b/RenderPipelineFile/samples/06-Car/scene/tex/MarbleTex.png deleted file mode 100644 index 569cff68..00000000 Binary files a/RenderPipelineFile/samples/06-Car/scene/tex/MarbleTex.png and /dev/null differ diff --git a/RenderPipelineFile/samples/06-Car/scene/tex/Rock.png b/RenderPipelineFile/samples/06-Car/scene/tex/Rock.png deleted file mode 100644 index 887fe400..00000000 Binary files a/RenderPipelineFile/samples/06-Car/scene/tex/Rock.png and /dev/null differ diff --git a/RenderPipelineFile/samples/06-Car/scene/tex/RockNrm.png b/RenderPipelineFile/samples/06-Car/scene/tex/RockNrm.png deleted file mode 100644 index b7cf7bf4..00000000 Binary files a/RenderPipelineFile/samples/06-Car/scene/tex/RockNrm.png and /dev/null differ diff --git a/RenderPipelineFile/samples/06-Car/scene/tex/Tire.png b/RenderPipelineFile/samples/06-Car/scene/tex/Tire.png deleted file mode 100644 index d829d3a6..00000000 Binary files a/RenderPipelineFile/samples/06-Car/scene/tex/Tire.png and /dev/null differ diff --git a/RenderPipelineFile/samples/06-Car/scene/tex/TireNrm.png b/RenderPipelineFile/samples/06-Car/scene/tex/TireNrm.png deleted file mode 100644 index 8979d1bb..00000000 Binary files a/RenderPipelineFile/samples/06-Car/scene/tex/TireNrm.png and /dev/null differ diff --git a/RenderPipelineFile/samples/06-Car/scene/tex/TireRimDiff.png b/RenderPipelineFile/samples/06-Car/scene/tex/TireRimDiff.png deleted file mode 100644 index ad2dd072..00000000 Binary files a/RenderPipelineFile/samples/06-Car/scene/tex/TireRimDiff.png and /dev/null differ diff --git a/RenderPipelineFile/samples/06-Car/scene/tex/TireRimSpec.png b/RenderPipelineFile/samples/06-Car/scene/tex/TireRimSpec.png deleted file mode 100644 index ec4fdc67..00000000 Binary files a/RenderPipelineFile/samples/06-Car/scene/tex/TireRimSpec.png and /dev/null differ diff --git a/RenderPipelineFile/samples/06-Car/scene/tex/TireSpec.png b/RenderPipelineFile/samples/06-Car/scene/tex/TireSpec.png deleted file mode 100644 index 89d156c8..00000000 Binary files a/RenderPipelineFile/samples/06-Car/scene/tex/TireSpec.png and /dev/null differ diff --git a/RenderPipelineFile/samples/06-Car/scene/tex/WallTileRough.png b/RenderPipelineFile/samples/06-Car/scene/tex/WallTileRough.png deleted file mode 100644 index c741aa3b..00000000 Binary files a/RenderPipelineFile/samples/06-Car/scene/tex/WallTileRough.png and /dev/null differ diff --git a/RenderPipelineFile/samples/06-Car/scene/tex/alduin.jpg b/RenderPipelineFile/samples/06-Car/scene/tex/alduin.jpg deleted file mode 100644 index 7acb6911..00000000 Binary files a/RenderPipelineFile/samples/06-Car/scene/tex/alduin.jpg and /dev/null differ diff --git a/RenderPipelineFile/samples/06-Car/scene/tex/alduin.png b/RenderPipelineFile/samples/06-Car/scene/tex/alduin.png deleted file mode 100644 index 7dcdea03..00000000 Binary files a/RenderPipelineFile/samples/06-Car/scene/tex/alduin.png and /dev/null differ diff --git a/RenderPipelineFile/samples/06-Car/scene/tex/alduin_n.jpg b/RenderPipelineFile/samples/06-Car/scene/tex/alduin_n.jpg deleted file mode 100644 index bdab0653..00000000 Binary files a/RenderPipelineFile/samples/06-Car/scene/tex/alduin_n.jpg and /dev/null differ diff --git a/RenderPipelineFile/samples/06-Car/scene/tex/alduineyes.jpg b/RenderPipelineFile/samples/06-Car/scene/tex/alduineyes.jpg deleted file mode 100644 index 4cebf88d..00000000 Binary files a/RenderPipelineFile/samples/06-Car/scene/tex/alduineyes.jpg and /dev/null differ diff --git a/RenderPipelineFile/samples/06-Car/scene/tex/alduineyes_g.jpg b/RenderPipelineFile/samples/06-Car/scene/tex/alduineyes_g.jpg deleted file mode 100644 index 23e1d2fe..00000000 Binary files a/RenderPipelineFile/samples/06-Car/scene/tex/alduineyes_g.jpg and /dev/null differ diff --git a/RenderPipelineFile/samples/06-Car/scene/tex/black.tga b/RenderPipelineFile/samples/06-Car/scene/tex/black.tga deleted file mode 100644 index d47a40dc..00000000 Binary files a/RenderPipelineFile/samples/06-Car/scene/tex/black.tga and /dev/null differ diff --git a/RenderPipelineFile/samples/06-Car/scene/tex/carbonFiber.tga b/RenderPipelineFile/samples/06-Car/scene/tex/carbonFiber.tga deleted file mode 100644 index af186173..00000000 Binary files a/RenderPipelineFile/samples/06-Car/scene/tex/carbonFiber.tga and /dev/null differ diff --git a/RenderPipelineFile/samples/06-Car/scene/tex/empty_basecolor.png b/RenderPipelineFile/samples/06-Car/scene/tex/empty_basecolor.png deleted file mode 100644 index 437c2444..00000000 Binary files a/RenderPipelineFile/samples/06-Car/scene/tex/empty_basecolor.png and /dev/null differ diff --git a/RenderPipelineFile/samples/06-Car/scene/tex/empty_normal.png b/RenderPipelineFile/samples/06-Car/scene/tex/empty_normal.png deleted file mode 100644 index 863dc2ec..00000000 Binary files a/RenderPipelineFile/samples/06-Car/scene/tex/empty_normal.png and /dev/null differ diff --git a/RenderPipelineFile/samples/06-Car/scene/tex/empty_roughness.png b/RenderPipelineFile/samples/06-Car/scene/tex/empty_roughness.png deleted file mode 100644 index 437c2444..00000000 Binary files a/RenderPipelineFile/samples/06-Car/scene/tex/empty_roughness.png and /dev/null differ diff --git a/RenderPipelineFile/samples/06-Car/scene/tex/empty_specular.png b/RenderPipelineFile/samples/06-Car/scene/tex/empty_specular.png deleted file mode 100644 index 26ee2247..00000000 Binary files a/RenderPipelineFile/samples/06-Car/scene/tex/empty_specular.png and /dev/null differ diff --git a/RenderPipelineFile/samples/06-Car/scene/tex/floor_spec.png b/RenderPipelineFile/samples/06-Car/scene/tex/floor_spec.png deleted file mode 100644 index 37be52e8..00000000 Binary files a/RenderPipelineFile/samples/06-Car/scene/tex/floor_spec.png and /dev/null differ diff --git a/RenderPipelineFile/samples/06-Car/scene/tex/glass0.tga b/RenderPipelineFile/samples/06-Car/scene/tex/glass0.tga deleted file mode 100644 index 3242ce70..00000000 Binary files a/RenderPipelineFile/samples/06-Car/scene/tex/glass0.tga and /dev/null differ diff --git a/RenderPipelineFile/samples/06-Car/scene/tex/grille1.tga b/RenderPipelineFile/samples/06-Car/scene/tex/grille1.tga deleted file mode 100644 index 4fa8e708..00000000 Binary files a/RenderPipelineFile/samples/06-Car/scene/tex/grille1.tga and /dev/null differ diff --git a/RenderPipelineFile/samples/06-Car/scene/tex/grille2.tga b/RenderPipelineFile/samples/06-Car/scene/tex/grille2.tga deleted file mode 100644 index a758253a..00000000 Binary files a/RenderPipelineFile/samples/06-Car/scene/tex/grille2.tga and /dev/null differ diff --git a/RenderPipelineFile/samples/06-Car/scene/tex/interior_lod0.tga b/RenderPipelineFile/samples/06-Car/scene/tex/interior_lod0.tga deleted file mode 100644 index 04527494..00000000 Binary files a/RenderPipelineFile/samples/06-Car/scene/tex/interior_lod0.tga and /dev/null differ diff --git a/RenderPipelineFile/samples/06-Car/scene/tex/lights.tga b/RenderPipelineFile/samples/06-Car/scene/tex/lights.tga deleted file mode 100644 index fe56b100..00000000 Binary files a/RenderPipelineFile/samples/06-Car/scene/tex/lights.tga and /dev/null differ diff --git a/RenderPipelineFile/samples/06-Car/scene/tex/nodamage.tga b/RenderPipelineFile/samples/06-Car/scene/tex/nodamage.tga deleted file mode 100644 index 81a3b31e..00000000 Binary files a/RenderPipelineFile/samples/06-Car/scene/tex/nodamage.tga and /dev/null differ diff --git a/RenderPipelineFile/samples/06-Car/scene/tex/oilfilm.png b/RenderPipelineFile/samples/06-Car/scene/tex/oilfilm.png deleted file mode 100644 index 712715c7..00000000 Binary files a/RenderPipelineFile/samples/06-Car/scene/tex/oilfilm.png and /dev/null differ diff --git a/RenderPipelineFile/samples/06-Car/scene/tex/painting-roughness.png b/RenderPipelineFile/samples/06-Car/scene/tex/painting-roughness.png deleted file mode 100644 index f7b4fb09..00000000 Binary files a/RenderPipelineFile/samples/06-Car/scene/tex/painting-roughness.png and /dev/null differ diff --git a/RenderPipelineFile/samples/06-Car/scene/tex/painting.png b/RenderPipelineFile/samples/06-Car/scene/tex/painting.png deleted file mode 100644 index 5e13dbca..00000000 Binary files a/RenderPipelineFile/samples/06-Car/scene/tex/painting.png and /dev/null differ diff --git a/RenderPipelineFile/samples/06-Car/scene/tex/pipeline_logo_text.png b/RenderPipelineFile/samples/06-Car/scene/tex/pipeline_logo_text.png deleted file mode 100644 index 9c8cd24c..00000000 Binary files a/RenderPipelineFile/samples/06-Car/scene/tex/pipeline_logo_text.png and /dev/null differ diff --git a/RenderPipelineFile/samples/06-Car/scene/tex/tireA0.tga b/RenderPipelineFile/samples/06-Car/scene/tex/tireA0.tga deleted file mode 100644 index 473bc66a..00000000 Binary files a/RenderPipelineFile/samples/06-Car/scene/tex/tireA0.tga and /dev/null differ diff --git a/RenderPipelineFile/samples/06-Car/scene/tex/undercarriage.tga b/RenderPipelineFile/samples/06-Car/scene/tex/undercarriage.tga deleted file mode 100644 index 70e60903..00000000 Binary files a/RenderPipelineFile/samples/06-Car/scene/tex/undercarriage.tga and /dev/null differ diff --git a/RenderPipelineFile/samples/06-Car/scene/tex/wheel.tga b/RenderPipelineFile/samples/06-Car/scene/tex/wheel.tga deleted file mode 100644 index 315d590e..00000000 Binary files a/RenderPipelineFile/samples/06-Car/scene/tex/wheel.tga and /dev/null differ diff --git a/RenderPipelineFile/samples/06-Car/scene/tex/wheel0.tga b/RenderPipelineFile/samples/06-Car/scene/tex/wheel0.tga deleted file mode 100644 index f4fd1114..00000000 Binary files a/RenderPipelineFile/samples/06-Car/scene/tex/wheel0.tga and /dev/null differ diff --git a/RenderPipelineFile/samples/06-Car/screenshot.png b/RenderPipelineFile/samples/06-Car/screenshot.png deleted file mode 100644 index 289cc840..00000000 Binary files a/RenderPipelineFile/samples/06-Car/screenshot.png and /dev/null differ diff --git a/RenderPipelineFile/samples/07-Shading-Models/main.py b/RenderPipelineFile/samples/07-Shading-Models/main.py deleted file mode 100644 index 8c6b76eb..00000000 --- a/RenderPipelineFile/samples/07-Shading-Models/main.py +++ /dev/null @@ -1,103 +0,0 @@ -""" - -Shading Models - -This shows the different shading models supported in the pipeline. - -""" - -from __future__ import print_function - -import os -import sys -import math -from random import random, randint, seed -from panda3d.core import Vec3, load_prc_file_data -from direct.showbase.ShowBase import ShowBase - -# Change to the current directory -os.chdir(os.path.dirname(os.path.realpath(__file__))) - -class MainApp(ShowBase): - def __init__(self): - - # Setup window size, title and so on - load_prc_file_data("", """ - win-size 1600 900 - window-title Render Pipeline - Shading Models Demo - """) - - # ------ Begin of render pipeline code ------ - - # Insert the pipeline path to the system path, this is required to be - # able to import the pipeline classes - pipeline_path = "../../" - - # Just a special case for my development setup, so I don't accidentally - # commit a wrong path. You can remove this in your own programs. - if not os.path.isfile(os.path.join(pipeline_path, "setup.py")): - pipeline_path = "../../RenderPipeline/" - - sys.path.insert(0, pipeline_path) - - from rpcore import RenderPipeline, SpotLight - self.render_pipeline = RenderPipeline() - self.render_pipeline.create(self) - - # This is a helper class for better camera movement - its not really - # a rendering element, but it included for convenience - from rpcore.util.movement_controller import MovementController - - # ------ End of render pipeline code, thats it! ------ - - # Set time of day - self.render_pipeline.daytime_mgr.time = 0.769 - - # Load the scene - model = loader.loadModel("scene/TestScene.bam") - model.reparent_to(render) - - self.render_pipeline.prepare_scene(model) - - # Init movement controller - self.controller = MovementController(self) - self.controller.set_initial_position( - Vec3(6.6, -18.8, 4.5), Vec3(4.7, -16.7, 3.4)) - self.controller.setup() - - base.accept("l", self.tour) - base.accept("r", self.reload_shaders) - - def reload_shaders(self): - self.render_pipeline.reload_shaders() - self.render_pipeline.prepare_scene(render) - - def tour(self): - mopath = ( - (Vec3(3.97601628304, -15.5422525406, 1.73230814934), Vec3(49.2462043762, -11.7619161606, 0.0)), - (Vec3(4.37102460861, -6.52981519699, 2.84148645401), Vec3(138.54864502, -15.7908058167, 0.0)), - (Vec3(-5.88968038559, -13.9816446304, 2.44033527374), Vec3(302.348571777, -13.2863616943, 0.0)), - (Vec3(5.23844909668, -18.1897411346, 4.54698801041), Vec3(402.91229248, -14.7019147873, 0.0)), - (Vec3(-7.27328443527, -0.466051012278, 3.30696845055), Vec3(607.032165527, -19.6019115448, 0.0)), - (Vec3(5.33415555954, 1.92750489712, 2.53945565224), Vec3(484.103546143, -11.9796953201, 0.0)), - (Vec3(-0.283608138561, -6.86583900452, 1.43702816963), Vec3(354.63848877, -4.79302883148, 0.0)), - (Vec3(-11.7576808929, 7.0855755806, 3.40899515152), Vec3(272.73840332, -12.959692955, 0.0)), - (Vec3(7.75462722778, 13.220041275, 3.97876667976), Vec3(126.342140198, -19.4930171967, 0.0)), - (Vec3(-2.10827493668, 4.78230571747, 1.27567899227), Vec3(-40.1353683472, -5.77301359177, 0.0)), - (Vec3(8.67115211487, 16.9084873199, 3.72598099709), Vec3(89.5658569336, -10.9996757507, 0.0)), - (Vec3(-8.1254825592, 26.6411190033, 3.21335697174), Vec3(268.092102051, -12.1974525452, 0.0)), - (Vec3(7.89382314682, 45.8911399841, 4.47727441788), Vec3(498.199554443, -11.3263425827, 0.0)), - (Vec3(-2.33054184914, 43.8977775574, 1.86498868465), Vec3(551.198242188, 13.6092195511, 0.0)), - (Vec3(4.80335664749, 36.9664497375, 6.16300296783), Vec3(810.128417969, -10.6730031967, 0.0)), - (Vec3(45.0654678345, 7.54712438583, 22.2645874023), Vec3(808.238586426, -17.5330123901, 0.0)), - (Vec3(5.99377584457, -12.3760728836, 4.53536558151), Vec3(806.978820801, -2.39745855331, 0.0)), - (Vec3(6.05853939056, -1.72227275372, 4.53848743439), Vec3(809.65637207, -2.39745855331, 0.0)), - (Vec3(4.81568479538, 8.28769683838, 4.48393821716), Vec3(809.65637207, -2.39745855331, 0.0)), - (Vec3(5.82831144333, 17.5230751038, 4.52401590347), Vec3(809.65637207, -2.39745855331, 0.0)), - (Vec3(4.09594917297, 26.7909412384, 4.44915866852), Vec3(809.65637207, -2.39745855331, 0.0)), - (Vec3(4.39108037949, 37.2143096924, 5.16932630539), Vec3(809.65637207, -2.39745855331, 0.0)), - ) - self.controller.play_motion_path(mopath, 2.3) - - -MainApp().run() diff --git a/RenderPipelineFile/samples/07-Shading-Models/scene/.gitignore b/RenderPipelineFile/samples/07-Shading-Models/scene/.gitignore deleted file mode 100644 index 256bf264..00000000 --- a/RenderPipelineFile/samples/07-Shading-Models/scene/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.blend diff --git a/RenderPipelineFile/samples/07-Shading-Models/scene/TestScene.bam b/RenderPipelineFile/samples/07-Shading-Models/scene/TestScene.bam deleted file mode 100644 index 017e2941..00000000 Binary files a/RenderPipelineFile/samples/07-Shading-Models/scene/TestScene.bam and /dev/null differ diff --git a/RenderPipelineFile/samples/07-Shading-Models/scene/tex/ConcreteNrm.png b/RenderPipelineFile/samples/07-Shading-Models/scene/tex/ConcreteNrm.png deleted file mode 100644 index cdeee803..00000000 Binary files a/RenderPipelineFile/samples/07-Shading-Models/scene/tex/ConcreteNrm.png and /dev/null differ diff --git a/RenderPipelineFile/samples/07-Shading-Models/scene/tex/ConcreteRoughness.png b/RenderPipelineFile/samples/07-Shading-Models/scene/tex/ConcreteRoughness.png deleted file mode 100644 index 2b4926f9..00000000 Binary files a/RenderPipelineFile/samples/07-Shading-Models/scene/tex/ConcreteRoughness.png and /dev/null differ diff --git a/RenderPipelineFile/samples/07-Shading-Models/scene/tex/ConcreteSpecular.png b/RenderPipelineFile/samples/07-Shading-Models/scene/tex/ConcreteSpecular.png deleted file mode 100644 index d6824b23..00000000 Binary files a/RenderPipelineFile/samples/07-Shading-Models/scene/tex/ConcreteSpecular.png and /dev/null differ diff --git a/RenderPipelineFile/samples/07-Shading-Models/scene/tex/FaceDiffuse.png b/RenderPipelineFile/samples/07-Shading-Models/scene/tex/FaceDiffuse.png deleted file mode 100644 index 800910e4..00000000 Binary files a/RenderPipelineFile/samples/07-Shading-Models/scene/tex/FaceDiffuse.png and /dev/null differ diff --git a/RenderPipelineFile/samples/07-Shading-Models/scene/tex/WallTex.png b/RenderPipelineFile/samples/07-Shading-Models/scene/tex/WallTex.png deleted file mode 100644 index e3094fc8..00000000 Binary files a/RenderPipelineFile/samples/07-Shading-Models/scene/tex/WallTex.png and /dev/null differ diff --git a/RenderPipelineFile/samples/07-Shading-Models/scene/tex/empty_basecolor.png b/RenderPipelineFile/samples/07-Shading-Models/scene/tex/empty_basecolor.png deleted file mode 100644 index 437c2444..00000000 Binary files a/RenderPipelineFile/samples/07-Shading-Models/scene/tex/empty_basecolor.png and /dev/null differ diff --git a/RenderPipelineFile/samples/07-Shading-Models/scene/tex/empty_normal.png b/RenderPipelineFile/samples/07-Shading-Models/scene/tex/empty_normal.png deleted file mode 100644 index 863dc2ec..00000000 Binary files a/RenderPipelineFile/samples/07-Shading-Models/scene/tex/empty_normal.png and /dev/null differ diff --git a/RenderPipelineFile/samples/07-Shading-Models/scene/tex/empty_roughness.png b/RenderPipelineFile/samples/07-Shading-Models/scene/tex/empty_roughness.png deleted file mode 100644 index 437c2444..00000000 Binary files a/RenderPipelineFile/samples/07-Shading-Models/scene/tex/empty_roughness.png and /dev/null differ diff --git a/RenderPipelineFile/samples/07-Shading-Models/scene/tex/empty_specular.png b/RenderPipelineFile/samples/07-Shading-Models/scene/tex/empty_specular.png deleted file mode 100644 index 26ee2247..00000000 Binary files a/RenderPipelineFile/samples/07-Shading-Models/scene/tex/empty_specular.png and /dev/null differ diff --git a/RenderPipelineFile/samples/07-Shading-Models/scene/tex/pattern.png b/RenderPipelineFile/samples/07-Shading-Models/scene/tex/pattern.png deleted file mode 100644 index ccfb77a3..00000000 Binary files a/RenderPipelineFile/samples/07-Shading-Models/scene/tex/pattern.png and /dev/null differ diff --git a/RenderPipelineFile/samples/07-Shading-Models/scene/tex/roughness-tex.png b/RenderPipelineFile/samples/07-Shading-Models/scene/tex/roughness-tex.png deleted file mode 100644 index f8dc2bd4..00000000 Binary files a/RenderPipelineFile/samples/07-Shading-Models/scene/tex/roughness-tex.png and /dev/null differ diff --git a/RenderPipelineFile/samples/07-Shading-Models/scene/tex/tree_stem_01.png b/RenderPipelineFile/samples/07-Shading-Models/scene/tex/tree_stem_01.png deleted file mode 100644 index d7b614ce..00000000 Binary files a/RenderPipelineFile/samples/07-Shading-Models/scene/tex/tree_stem_01.png and /dev/null differ diff --git a/RenderPipelineFile/samples/07-Shading-Models/screenshot.png b/RenderPipelineFile/samples/07-Shading-Models/screenshot.png deleted file mode 100644 index e85aaad3..00000000 Binary files a/RenderPipelineFile/samples/07-Shading-Models/screenshot.png and /dev/null differ diff --git a/RenderPipelineFile/samples/08-Terrain/main.py b/RenderPipelineFile/samples/08-Terrain/main.py deleted file mode 100644 index 952617d8..00000000 --- a/RenderPipelineFile/samples/08-Terrain/main.py +++ /dev/null @@ -1,117 +0,0 @@ -""" - -Terrain Demo - -Shows how to use the shader terrain mesh in the pipeline - -""" - -from __future__ import print_function - -import os -import sys -from panda3d.core import Vec3, load_prc_file_data, ShaderTerrainMesh -from direct.showbase.ShowBase import ShowBase - -# Change to the current directory -os.chdir(os.path.dirname(os.path.realpath(__file__))) - -# Insert the pipeline path to the system path, this is required to be -# able to import the pipeline classes -pipeline_path = "../../" - -# Just a special case for my development setup, so I don't accidentally -# commit a wrong path. You can remove this in your own programs. -if not os.path.isfile(os.path.join(pipeline_path, "setup.py")): - pipeline_path = "../../RenderPipeline/" - -sys.path.insert(0, pipeline_path) - -from rpcore import RenderPipeline - -# This is a helper class for better camera movement - its not really -# a rendering element, but it included for convenience -from rpcore.util.movement_controller import MovementController - -class Application(ShowBase): - def __init__(self): - - # Setup window size, title and so on - load_prc_file_data("", """ - win-size 1600 900 - window-title Render Pipeline by tobspr - stm-max-chunk-count 2048 - gl-coordinate-system default - stm-max-views 20 - notify-level-linmath error - """) - - self.render_pipeline = RenderPipeline() - self.render_pipeline.create(self) - - # Set time of day - self.render_pipeline.daytime_mgr.time = "04:25" - - # Add some environment probe to provide better reflections - probe = self.render_pipeline.add_environment_probe() - probe.set_pos(0, 0, 600) - probe.set_scale(8192 * 2, 8192 * 2, 1000) - - self.terrain_np = render.attach_new_node("terrain") - - heightfield = loader.loadTexture("resources/heightfield.png") - - for x in range(3): - for y in range(3): - terrain_node = ShaderTerrainMesh() - terrain_node.heightfield = heightfield - terrain_node.target_triangle_width = 6.0 - terrain_node.generate() - - terrain_n = self.terrain_np.attach_new_node(terrain_node) - terrain_n.set_scale(8192, 8192, 600) - terrain_n.set_pos(-4096 + (x - 1) * 8192, -4096 + (y - 1) * 8192, 0) - - # Init movement controller - self.controller = MovementController(self) - self.controller.set_initial_position(Vec3(-12568, -11736, 697), Vec3(-12328, -11357, 679)) - self.controller.setup() - - self.accept("r", self.reload_shaders) - self.reload_shaders() - - def reload_shaders(self): - self.render_pipeline.reload_shaders() - - # Set the terrain effect - self.render_pipeline.set_effect(self.terrain_np, "effects/terrain-effect.yaml", {}, 100) - base.accept("l", self.tour) - - - def tour(self): - control_points = ( - (Vec3(2755.62084961, 6983.76708984, 506.219055176), Vec3(-179.09147644, 4.30751991272, 0.0)), - (Vec3(3153.70068359, 5865.30859375, 560.780822754), Vec3(-225.239028931, 3.43641376495, 0.0)), - (Vec3(2140.57080078, 5625.22753906, 598.345031738), Vec3(-196.022613525, 1.91196918488, 0.0)), - (Vec3(2598.85961914, 3820.56958008, 627.692993164), Vec3(-272.410125732, 3.32752752304, 0.0)), - (Vec3(1894.64526367, 3597.39257812, 647.455078125), Vec3(-198.227630615, 0.605303645134, 0.0)), - (Vec3(1998.48425293, 1870.05358887, 639.38458252), Vec3(-121.682502747, 0.169744253159, 0.0)), - (Vec3(3910.55297852, 2370.70922852, 518.356567383), Vec3(-130.42376709, 3.65418696404, -0.000583928485867)), - (Vec3(4830.80761719, 1542.30749512, 501.019073486), Vec3(21.7212314606, 8.00974178314, 0.0)), - (Vec3(4324.81982422, 2259.2409668, 713.095458984), Vec3(-19.1500263214, -1.68137300014, 0.0)), - (Vec3(4584.38720703, 3417.75073242, 612.79510498), Vec3(-39.4675331116, -4.94804239273, 0.0)), - (Vec3(4569.68261719, 4840.80908203, 540.924926758), Vec3(-83.4887771606, 4.08973407745, 0.0)), - (Vec3(5061.6484375, 5490.93896484, 694.142578125), Vec3(-135.778717041, 0.605291008949, 0.0)), - (Vec3(6273.03222656, 6343.10546875, 769.735290527), Vec3(-200.826278687, -1.35470712185, 0.0)), - (Vec3(7355.02832031, 6314.39892578, 1164.65527344), Vec3(-262.645324707, -12.2435855865, 0.0)), - (Vec3(6321.42431641, 4453.68310547, 898.895202637), Vec3(-322.574157715, -2.22581481934, 0.0)), - (Vec3(4820.63916016, 4637.07128906, 793.165222168), Vec3(-330.291748047, 0.931967377663, 0.0)), - (Vec3(2621.3125, 4790.99072266, 691.223144531), Vec3(-423.059020996, 3.10974740982, 0.0)), - (Vec3(2166.64697266, 5683.75195312, 685.415039062), Vec3(-497.871612549, -1.57248008251, 0.0)), - (Vec3(2426.64135742, 7216.48974609, 993.247558594), Vec3(-524.17388916, -6.69025611877, 0.0)), - (Vec3(5014.83398438, 6712.60253906, 963.192810059), Vec3(-596.072387695, -3.31469583511, 0.00446693599224)), - (Vec3(6107.69970703, 5460.03662109, 820.63104248), Vec3(-650.567199707, 1.04085958004, 0.0)), - ) - self.controller.play_motion_path(control_points, 3.0) - -Application().run() diff --git a/RenderPipelineFile/samples/08-Terrain/resources/heightfield.png b/RenderPipelineFile/samples/08-Terrain/resources/heightfield.png deleted file mode 100644 index dfa23ac8..00000000 Binary files a/RenderPipelineFile/samples/08-Terrain/resources/heightfield.png and /dev/null differ diff --git a/RenderPipelineFile/samples/08-Terrain/screenshot.png b/RenderPipelineFile/samples/08-Terrain/screenshot.png deleted file mode 100644 index 38caf584..00000000 Binary files a/RenderPipelineFile/samples/08-Terrain/screenshot.png and /dev/null differ diff --git a/RenderPipelineFile/samples/09-Benchmark/main.py b/RenderPipelineFile/samples/09-Benchmark/main.py deleted file mode 100644 index 10a0038c..00000000 --- a/RenderPipelineFile/samples/09-Benchmark/main.py +++ /dev/null @@ -1,113 +0,0 @@ -""" - -Benchmark - -This is a benchmark to test the power of the GPU - -""" - -from __future__ import print_function, division - -import os -import sys -from panda3d.core import Vec3, load_prc_file_data, PNMImage -from direct.showbase.ShowBase import ShowBase - -# Switch into the current directory -os.chdir(os.path.realpath(os.path.dirname(__file__))) - - -class MainApp(ShowBase): - - """ Main Testing Showbase """ - - def __init__(self): - - # Setup window size, title and so on - load_prc_file_data("", """ - win-size 1600 900 - window-title Render Pipeline - Benchmark - """) - - # ------ Begin of render pipeline code ------ - - # Insert the pipeline path to the system path, this is required to be - # able to import the pipeline classes - pipeline_path = "../../" - - # Just a special case for my development setup, so I don't accidentally - # commit a wrong path. You can remove this in your own programs. - if not os.path.isfile(os.path.join(pipeline_path, "setup.py")): - pipeline_path = "../../RenderPipeline/" - - sys.path.insert(0, pipeline_path) - - from rpcore import RenderPipeline, PointLight - self.render_pipeline = RenderPipeline() - self.render_pipeline.create(self) - - # This is a helper class for better camera movement - its not really - # a rendering element, but it included for convenience - from rpcore.util.movement_controller import MovementController - - # ------ End of render pipeline code, thats it! ------ - - # Set time of day - self.render_pipeline.daytime_mgr.time = "17:41" - - self.camLens.set_fov(90) - - model = self.loader.load_model("scene/Scene.bam") - model.reparent_to(self.render) - - model.flatten_strong() - num_rows = 255 - - img = PNMImage("scene/lights.png") - - for x in range(num_rows): - for y in range(num_rows): - light = PointLight() - # light.direction = (0, 0, -1) - # light.fov = 60 - # light.set_color_from_temperature(randint(2000, 20000)) - light.color = img.get_xel(x * 1, y * 1) - light.energy = 5000 * (x / num_rows) - light.pos = Vec3(-(x - num_rows // 2) / num_rows * 1000.0, - (y - num_rows // 2) / num_rows * 1000.0, 2) - light.radius = 4 - light.inner_radius = 0.5 - light.casts_shadows = False - light.shadow_map_resolution = 256 - self.render_pipeline.add_light(light) - - self.render_pipeline.prepare_scene(model) - - # Init movement controller - self.controller = MovementController(self) - self.controller.set_initial_position(Vec3(0, 450, 200), Vec3(0)) - self.controller.setup() - - self.accept("l", self.benchmark) - - def benchmark(self): - mopath = ( - (Vec3(0.0, 450.0, 200.0), Vec3(180.0, -23.9624938965, 0.0)), - (Vec3(-190.848297119, 304.510772705, 90.5852050781), Vec3(209.767547607, -19.2802791595, 0.0)), - (Vec3(-220.74269104, 10.6886262894, 38.7188148499), Vec3(278.595062256, -16.6669464111, -0.00123210949823)), - (Vec3(-51.2080802917, -188.072463989, 50.2380104065), Vec3(364.747375488, -22.7647132874, 0.0)), - (Vec3(211.633651733, -190.621276855, 216.169631958), Vec3(413.887451172, -40.1869468689, -0.000118153897347)), - (Vec3(320.780090332, 303.404388428, 341.834014893), Vec3(495.000030518, -41.1669464111, 0.00174981483724)), - (Vec3(125.150436401, 294.57989502, 218.834960938), Vec3(444.363800049, 3.80416536331, 0.0)), - (Vec3(-355.501434326, 153.010559082, 68.0701370239), Vec3(611.234924316, -11.5491724014, 0.000359044410288)), - (Vec3(-118.283355713, -115.640907288, 6.09887886047), Vec3(637.222473145, -8.82695007324, 0.0)), - (Vec3(80.3096160889, 12.4637413025, 26.0630741119), Vec3(676.439758301, -24.3980617523, 0.0)), - (Vec3(69.6195449829, 152.581176758, 14.8633785248), Vec3(881.898925781, -15.3602952957, 0.0)), - (Vec3(-202.29776001, 109.818962097, 94.7222290039), Vec3(962.381530762, -27.7736206055, 0.00155594921671)), - (Vec3(6.89826059341, -412.195037842, 221.591659546), Vec3(1080.42749023, -31.1491756439, 0.0)), - (Vec3(362.657867432, -34.1290054321, 216.362884521), Vec3(1166.81677246, -35.0691833496, 0.0)), - (Vec3(-0.339450836182, 452.040649414, 199.996627808), Vec3(180.0, -23.9624938965, 0.0)), - ) - self.controller.play_motion_path(mopath, 3.0) - -MainApp().run() diff --git a/RenderPipelineFile/samples/09-Benchmark/scene/Scene.bam b/RenderPipelineFile/samples/09-Benchmark/scene/Scene.bam deleted file mode 100644 index 683ab56a..00000000 Binary files a/RenderPipelineFile/samples/09-Benchmark/scene/Scene.bam and /dev/null differ diff --git a/RenderPipelineFile/samples/09-Benchmark/scene/Scene.blend b/RenderPipelineFile/samples/09-Benchmark/scene/Scene.blend deleted file mode 100644 index 27d9e899..00000000 Binary files a/RenderPipelineFile/samples/09-Benchmark/scene/Scene.blend and /dev/null differ diff --git a/RenderPipelineFile/samples/09-Benchmark/scene/lights.png b/RenderPipelineFile/samples/09-Benchmark/scene/lights.png deleted file mode 100644 index 1a21a74e..00000000 Binary files a/RenderPipelineFile/samples/09-Benchmark/scene/lights.png and /dev/null differ diff --git a/RenderPipelineFile/samples/09-Benchmark/scene/tex/empty_basecolor.png b/RenderPipelineFile/samples/09-Benchmark/scene/tex/empty_basecolor.png deleted file mode 100644 index 437c2444..00000000 Binary files a/RenderPipelineFile/samples/09-Benchmark/scene/tex/empty_basecolor.png and /dev/null differ diff --git a/RenderPipelineFile/samples/09-Benchmark/scene/tex/empty_normal.png b/RenderPipelineFile/samples/09-Benchmark/scene/tex/empty_normal.png deleted file mode 100644 index 863dc2ec..00000000 Binary files a/RenderPipelineFile/samples/09-Benchmark/scene/tex/empty_normal.png and /dev/null differ diff --git a/RenderPipelineFile/samples/09-Benchmark/scene/tex/empty_roughness.png b/RenderPipelineFile/samples/09-Benchmark/scene/tex/empty_roughness.png deleted file mode 100644 index 437c2444..00000000 Binary files a/RenderPipelineFile/samples/09-Benchmark/scene/tex/empty_roughness.png and /dev/null differ diff --git a/RenderPipelineFile/samples/09-Benchmark/scene/tex/empty_specular.png b/RenderPipelineFile/samples/09-Benchmark/scene/tex/empty_specular.png deleted file mode 100644 index 26ee2247..00000000 Binary files a/RenderPipelineFile/samples/09-Benchmark/scene/tex/empty_specular.png and /dev/null differ diff --git a/RenderPipelineFile/samples/10-Plugin-Showcase-AO/main.py b/RenderPipelineFile/samples/10-Plugin-Showcase-AO/main.py deleted file mode 100644 index 8267bca8..00000000 --- a/RenderPipelineFile/samples/10-Plugin-Showcase-AO/main.py +++ /dev/null @@ -1,72 +0,0 @@ -""" - -Material Demo - -This demonstrates the various materials the pipeline supports. -It is also a reference scene, for testing BRDF changes. - -""" - -from __future__ import print_function - -import os -import sys -from panda3d.core import Vec3, load_prc_file_data -from direct.showbase.ShowBase import ShowBase - -# Change to the current directory -os.chdir(os.path.dirname(os.path.realpath(__file__))) - -# Insert the pipeline path to the system path, this is required to be -# able to import the pipeline classes -pipeline_path = "../../" - -# Just a special case for my development setup, so I don't accidentally -# commit a wrong path. You can remove this in your own programs. -if not os.path.isfile(os.path.join(pipeline_path, "setup.py")): - pipeline_path = "../../RenderPipeline/" - -sys.path.insert(0, pipeline_path) - -# Import the render pipeline class -from rpcore import RenderPipeline - -# This is a helper class for better camera movement - see below. -from rpcore.util.movement_controller import MovementController - - -class Application(ShowBase): - def __init__(self): - # Setup window size and title - load_prc_file_data("", """ - # win-size 1600 900 - window-title Render Pipeline - Plugin Showcase - AO - """) - - # Construct the render pipeline - self.render_pipeline = RenderPipeline() - self.render_pipeline.create(self) - self.render_pipeline.daytime_mgr.time = "19:17" - # self.render_pipeline.daytime_mgr.time = "12:00" - - # Load the scene - model = self.loader.load_model("scene/Scene.bam") - model.reparent_to(self.render) - self.render_pipeline.prepare_scene(model) - - # probe = self.render_pipeline.add_environment_probe() - # probe.set_pos(0, 0, 4) - # probe.set_scale(42, 42, 8) - # probe.parallax_correction = True - # probe.border_smoothness = 0.001 - - # Initialize movement controller, this is a convenience class - # to provide an improved camera control compared to Panda3Ds default - # mouse controller. - self.controller = MovementController(self) - self.controller.set_initial_position_hpr( - Vec3(-17.2912578583, -13.290019989, 6.88211250305), - Vec3(-39.7285499573, -14.6770210266, 0.0)) - self.controller.setup() - -Application().run() diff --git a/RenderPipelineFile/samples/10-Plugin-Showcase-AO/scene/Scene.bam b/RenderPipelineFile/samples/10-Plugin-Showcase-AO/scene/Scene.bam deleted file mode 100644 index 59ea84be..00000000 Binary files a/RenderPipelineFile/samples/10-Plugin-Showcase-AO/scene/Scene.bam and /dev/null differ diff --git a/RenderPipelineFile/samples/10-Plugin-Showcase-AO/scene/Scene.blend b/RenderPipelineFile/samples/10-Plugin-Showcase-AO/scene/Scene.blend deleted file mode 100644 index b423b329..00000000 Binary files a/RenderPipelineFile/samples/10-Plugin-Showcase-AO/scene/Scene.blend and /dev/null differ diff --git a/RenderPipelineFile/samples/10-Plugin-Showcase-AO/scene/tex/empty_basecolor.png b/RenderPipelineFile/samples/10-Plugin-Showcase-AO/scene/tex/empty_basecolor.png deleted file mode 100644 index 437c2444..00000000 Binary files a/RenderPipelineFile/samples/10-Plugin-Showcase-AO/scene/tex/empty_basecolor.png and /dev/null differ diff --git a/RenderPipelineFile/samples/10-Plugin-Showcase-AO/scene/tex/empty_normal.png b/RenderPipelineFile/samples/10-Plugin-Showcase-AO/scene/tex/empty_normal.png deleted file mode 100644 index 863dc2ec..00000000 Binary files a/RenderPipelineFile/samples/10-Plugin-Showcase-AO/scene/tex/empty_normal.png and /dev/null differ diff --git a/RenderPipelineFile/samples/10-Plugin-Showcase-AO/scene/tex/empty_roughness.png b/RenderPipelineFile/samples/10-Plugin-Showcase-AO/scene/tex/empty_roughness.png deleted file mode 100644 index 437c2444..00000000 Binary files a/RenderPipelineFile/samples/10-Plugin-Showcase-AO/scene/tex/empty_roughness.png and /dev/null differ diff --git a/RenderPipelineFile/samples/10-Plugin-Showcase-AO/scene/tex/empty_specular.png b/RenderPipelineFile/samples/10-Plugin-Showcase-AO/scene/tex/empty_specular.png deleted file mode 100644 index 26ee2247..00000000 Binary files a/RenderPipelineFile/samples/10-Plugin-Showcase-AO/scene/tex/empty_specular.png and /dev/null differ diff --git a/RenderPipelineFile/samples/10-Plugin-Showcase-AO/scene/tex/skin_diffuse.jpg b/RenderPipelineFile/samples/10-Plugin-Showcase-AO/scene/tex/skin_diffuse.jpg deleted file mode 100644 index a85021c5..00000000 Binary files a/RenderPipelineFile/samples/10-Plugin-Showcase-AO/scene/tex/skin_diffuse.jpg and /dev/null differ diff --git a/RenderPipelineFile/samples/10-Plugin-Showcase-AO/scene/tex/wood_diffuse.jpg b/RenderPipelineFile/samples/10-Plugin-Showcase-AO/scene/tex/wood_diffuse.jpg deleted file mode 100644 index 43bb395c..00000000 Binary files a/RenderPipelineFile/samples/10-Plugin-Showcase-AO/scene/tex/wood_diffuse.jpg and /dev/null differ diff --git a/RenderPipelineFile/samples/11-Instancing/main.py b/RenderPipelineFile/samples/11-Instancing/main.py deleted file mode 100644 index c485d9d0..00000000 --- a/RenderPipelineFile/samples/11-Instancing/main.py +++ /dev/null @@ -1,109 +0,0 @@ -""" - -Material Demo - -This demonstrates the various materials the pipeline supports. -It is also a reference scene, for testing BRDF changes. - -""" - -from __future__ import print_function - -import os -import sys -import struct -from panda3d.core import Vec3, load_prc_file_data, Texture, GeomEnums -from panda3d.core import OmniBoundingVolume -from direct.showbase.ShowBase import ShowBase - -# Change to the current directory -os.chdir(os.path.dirname(os.path.realpath(__file__))) - -# Insert the pipeline path to the system path, this is required to be -# able to import the pipeline classes -pipeline_path = "../../" - -# Just a special case for my development setup, so I don't accidentally -# commit a wrong path. You can remove this in your own programs. -if not os.path.isfile(os.path.join(pipeline_path, "setup.py")): - pipeline_path = "../../RenderPipeline/" - -sys.path.insert(0, pipeline_path) - -# Import the render pipeline class -from rpcore import RenderPipeline - -# This is a helper class for better camera movement - see below. -from rpcore.util.movement_controller import MovementController - - -class Application(ShowBase): - def __init__(self): - # Setup window size and title - load_prc_file_data("", """ - # win-size 1600 900 - window-title Render Pipeline - Instancing Example - """) - - # Construct the render pipeline - self.render_pipeline = RenderPipeline() - self.render_pipeline.create(self) - self.render_pipeline.daytime_mgr.time = "19:17" - # self.render_pipeline.daytime_mgr.time = "12:00" - - # Load the scene - model = self.loader.load_model("scene/Scene.bam") - model.reparent_to(self.render) - - # Find the prefab object, we are going to in instance this object - # multiple times - prefab = model.find("**/InstancedObjectPrefab") - - # Collect all instances - matrices = [] - for elem in model.find_all_matches("**/PREFAB*"): - matrices.append(elem.get_mat(self.render)) - elem.remove_node() - - print("Loaded", len(matrices), "instances!") - - # Allocate storage for the matrices, each matrix has 16 elements, - # but because one pixel has four components, we need amount * 4 pixels. - buffer_texture = Texture() - buffer_texture.setup_buffer_texture(len(matrices) * 4, Texture.T_float, Texture.F_rgba32, GeomEnums.UH_static) - - float_size = len(struct.pack("f", 0.0)) - floats = [] - - # Serialize matrices to floats - ram_image = buffer_texture.modify_ram_image() - - for idx, mat in enumerate(matrices): - for i in range(4): - for j in range(4): - floats.append(mat.get_cell(i, j)) - - # Write the floats to the texture - data = struct.pack("f" * len(floats), *floats) - ram_image.set_subdata(0, len(data), data) - - # Load the effect - self.render_pipeline.set_effect(prefab, "effects/basic_instancing.yaml", {}) - - prefab.set_shader_input("InstancingData", buffer_texture) - prefab.set_instance_count(len(matrices)) - - # We have do disable culling, so that all instances stay visible - prefab.node().set_bounds(OmniBoundingVolume()) - prefab.node().set_final(True) - - # Initialize movement controller, this is a convenience class - # to provide an improved camera control compared to Panda3Ds default - # mouse controller. - self.controller = MovementController(self) - self.controller.set_initial_position_hpr( - Vec3(-23.2, -32.5, 5.3), - Vec3(-33.8, -8.3, 0.0)) - self.controller.setup() - -Application().run() diff --git a/RenderPipelineFile/samples/11-Instancing/scene/Scene.bam b/RenderPipelineFile/samples/11-Instancing/scene/Scene.bam deleted file mode 100644 index dbb0d22a..00000000 Binary files a/RenderPipelineFile/samples/11-Instancing/scene/Scene.bam and /dev/null differ diff --git a/RenderPipelineFile/samples/11-Instancing/scene/Scene.blend b/RenderPipelineFile/samples/11-Instancing/scene/Scene.blend deleted file mode 100644 index c723cd0b..00000000 Binary files a/RenderPipelineFile/samples/11-Instancing/scene/Scene.blend and /dev/null differ diff --git a/RenderPipelineFile/samples/11-Instancing/scene/tex/empty_basecolor.png b/RenderPipelineFile/samples/11-Instancing/scene/tex/empty_basecolor.png deleted file mode 100644 index 437c2444..00000000 Binary files a/RenderPipelineFile/samples/11-Instancing/scene/tex/empty_basecolor.png and /dev/null differ diff --git a/RenderPipelineFile/samples/11-Instancing/scene/tex/empty_normal.png b/RenderPipelineFile/samples/11-Instancing/scene/tex/empty_normal.png deleted file mode 100644 index 863dc2ec..00000000 Binary files a/RenderPipelineFile/samples/11-Instancing/scene/tex/empty_normal.png and /dev/null differ diff --git a/RenderPipelineFile/samples/11-Instancing/scene/tex/empty_roughness.png b/RenderPipelineFile/samples/11-Instancing/scene/tex/empty_roughness.png deleted file mode 100644 index 437c2444..00000000 Binary files a/RenderPipelineFile/samples/11-Instancing/scene/tex/empty_roughness.png and /dev/null differ diff --git a/RenderPipelineFile/samples/11-Instancing/scene/tex/empty_specular.png b/RenderPipelineFile/samples/11-Instancing/scene/tex/empty_specular.png deleted file mode 100644 index 26ee2247..00000000 Binary files a/RenderPipelineFile/samples/11-Instancing/scene/tex/empty_specular.png and /dev/null differ diff --git a/RenderPipelineFile/samples/11-Instancing/scene/tex/wood_diffuse.jpg b/RenderPipelineFile/samples/11-Instancing/scene/tex/wood_diffuse.jpg deleted file mode 100644 index 43bb395c..00000000 Binary files a/RenderPipelineFile/samples/11-Instancing/scene/tex/wood_diffuse.jpg and /dev/null differ diff --git a/RenderPipelineFile/samples/README.md b/RenderPipelineFile/samples/README.md deleted file mode 100644 index 968adcdc..00000000 --- a/RenderPipelineFile/samples/README.md +++ /dev/null @@ -1,14 +0,0 @@ -## Render Pipeline Samples - -The samples are not included in this repository to keep the size small. - -#### Automatic Download (recommended) -You can run `python download_samples.py` to automatically download the samples. - -#### Manual Download -If you want to manually download the samples (for whatever reason), you can -find them here: -https://github.com/tobspr/RenderPipeline-Samples - -After downloading, place the downloaded contents in this folder to run the samples. diff --git a/RenderPipelineFile/samples/download_samples.py b/RenderPipelineFile/samples/download_samples.py deleted file mode 100644 index d7473118..00000000 --- a/RenderPipelineFile/samples/download_samples.py +++ /dev/null @@ -1,44 +0,0 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -# Script to download the Render Pipeline samples - -import os -import sys - -sys.path.insert(0, "../") -sys.path.insert(0, "../rpcore/util") - -from submodule_downloader import download_submodule # noqa - -if __name__ == "__main__": - - # Make sure we are in the right directory - main_dir = os.path.dirname(os.path.realpath(__file__)) - os.chdir(main_dir) - - # Now extract the samples - download_submodule("tobspr", "RenderPipeline-Samples", ".", ["README.md", "LICENSE"]) diff --git a/RenderPipelineFile/toolkit/plugin_configurator/main.py b/RenderPipelineFile/toolkit/plugin_configurator/main.py index 9410f080..fd393ae8 100644 --- a/RenderPipelineFile/toolkit/plugin_configurator/main.py +++ b/RenderPipelineFile/toolkit/plugin_configurator/main.py @@ -49,6 +49,8 @@ from rplibs.pyqt_imports import * #noqa # Load the generated UI Layout from ui.main_window_generated import Ui_MainWindow # noqa +from ui.widgets import UniversalMessageDialog + from rpcore.pluginbase.manager import PluginManager # noqa from rpcore.util.network_communication import NetworkCommunication # noqa from rpcore.mount_manager import MountManager # noqa @@ -119,9 +121,13 @@ class PluginConfigurator(QMainWindow, Ui_MainWindow): self, "Warning", msg, QMessageBox.Yes, QMessageBox.No) if reply == QMessageBox.Yes: - QMessageBox.information( - self, "Success", - "Settings have been reset! You may have to restart the pipeline.") + UniversalMessageDialog.show_success( + self, + "Success", + "Settings have been reset! You may have to restart the pipeline.", + show_cancel=False, + confirm_text="OK" + ) self._plugin_mgr.reset_plugin_settings(self._current_plugin) # Save config diff --git a/RenderPipelineFile/samples/05-Quboid/src/__init__.py b/Resources/a similarity index 100% rename from RenderPipelineFile/samples/05-Quboid/src/__init__.py rename to Resources/a diff --git a/Resources/icons/heightmap.png b/Resources/icons/heightmap.png new file mode 100644 index 00000000..33b6c701 Binary files /dev/null and b/Resources/icons/heightmap.png differ diff --git a/_tmp_material.py b/_tmp_material.py deleted file mode 100644 index 8e4fa492..00000000 --- a/_tmp_material.py +++ /dev/null @@ -1,6 +0,0 @@ -# Autogenerated -name = 'Plastic-R0.0' -roughness = 0.0 -ior = 1.51 -basecolor = (1, 0, 0) -mat_type = 'default' diff --git a/core/Command_System.py b/core/Command_System.py new file mode 100644 index 00000000..fd8d245b --- /dev/null +++ b/core/Command_System.py @@ -0,0 +1,648 @@ +from abc import ABC, abstractmethod +from collections import deque +from typing import List +from panda3d.core import NodePath, Point3 + + +class Command(ABC): + """ + 抽象命令类,所有具体命令都需要继承此类 + """ + + @abstractmethod + def execute(self): + """ + 执行命令 + """ + pass + + @abstractmethod + def undo(self): + """ + 撤销命令 + """ + pass + + @abstractmethod + def redo(self): + """ + 重做命令 + """ + pass + + +class CommandManager: + """ + 命令管理器,负责管理命令的执行、撤销和重做 + """ + + def __init__(self, max_history: int = 100): + # 用于存储已执行的命令的历史记录 + self._undo_stack: deque = deque(maxlen=max_history) + # 用于存储已撤销的命令,支持重做 + self._redo_stack: deque = deque(maxlen=max_history) + # 最大历史记录数 + self._max_history = max_history + + def execute_command(self, command: Command): + """ + 执行命令,并将其添加到撤销栈中 + """ + try: + command.execute() + self._undo_stack.append(command) + # 清空重做栈,因为执行新命令后就无法重做之前的命令了 + self._redo_stack.clear() + except Exception as e: + print(f"执行命令时出错: {e}") + raise + + def undo(self) -> bool: + """ + 撤销上一个命令 + 返回是否成功撤销 + """ + if not self._undo_stack: + return False + + try: + command = self._undo_stack.pop() + command.undo() + self._redo_stack.append(command) + return True + except Exception as e: + print(f"撤销命令时出错: {e}") + # 如果撤销失败,将命令放回撤销栈 + self._undo_stack.append(command) + return False + + def redo(self) -> bool: + """ + 重做上一个被撤销的命令 + 返回是否成功重做 + """ + if not self._redo_stack: + return False + + try: + command = self._redo_stack.pop() + command.redo() + self._undo_stack.append(command) + return True + except Exception as e: + print(f"重做命令时出错: {e}") + # 如果重做失败,将命令放回重做栈 + self._redo_stack.append(command) + return False + + def can_undo(self) -> bool: + """ + 检查是否可以撤销 + """ + return len(self._undo_stack) > 0 + + def can_redo(self) -> bool: + """ + 检查是否可以重做 + """ + return len(self._redo_stack) > 0 + + def clear_history(self): + """ + 清空所有历史记录 + """ + self._undo_stack.clear() + self._redo_stack.clear() + + def get_undo_count(self) -> int: + """ + 获取可撤销的命令数量 + """ + return len(self._undo_stack) + + def get_redo_count(self) -> int: + """ + 获取可重做的命令数量 + """ + return len(self._redo_stack) + + +# 示例命令实现 +class MoveNodeCommand(Command): + """ + 移动节点命令示例 + """ + + def __init__(self, node: NodePath, old_pos, new_pos): + self.node = node + self.old_pos = old_pos + self.new_pos = new_pos + + def execute(self): + """ + 执行移动操作 + """ + self.node.setPos(self.new_pos) + + def undo(self): + """ + 撤销移动操作 + """ + self.node.setPos(self.old_pos) + + def redo(self): + """ + 重做移动操作 + """ + self.node.setPos(self.new_pos) + + +class DeleteNodeCommand(Command): + """ + 删除节点命令示例 + """ + + def __init__(self, node: NodePath, parent_node: NodePath,world=None): + self.node = node + self.parent_node = parent_node + self.world = world + + self.node_name = node.getName() + self.node_pos = node.getPos() + self.node_hpr = node.getHpr() + self.node_scale = node.getScale() + + # 保存节点类型信息 + self.node_type = "NODE" + if node.hasTag("tree_item_type"): + self.node_type = node.getTag("tree_item_type") + elif node.hasTag("gui_type"): + gui_type = node.getTag("gui_type") + if gui_type == "button": + self.node_type = "GUI_BUTTON" + elif gui_type == "label": + self.node_type = "GUI_LABEL" + elif gui_type == "entry": + self.node_type = "GUI_ENTRY" + elif gui_type == "2d_image": + self.node_type = "GUI_IMAGE" + elif gui_type == "3d_text": + self.node_type = "GUI_3DTEXT" + elif gui_type == "3d_image": + self.node_type = "GUI_3DIMAGE" + elif gui_type == "video_screen": + self.node_type = "GUI_VIDEO_SCREEN" + elif gui_type == "2d_video_screen": + self.node_type = "GUI_2D_VIDEO_SCREEN" + elif node.hasTag("light_type"): + self.node_type = "LIGHT_NODE" + elif node.hasTag("element_type") and node.getTag("element_type") == "cesium_tileset": + self.node_type = "CESIUM_TILESET_NODE" + elif node.hasTag("is_scene_element"): + self.node_type = "SCENE_NODE" + + self.node_tags = {} + if hasattr(node,'hasTag'): + for tag_key in node.getNetTag('tags').split(',') if node.hasTag('tags') else []: + if node.hasTag(tag_key): + self.node_tags[tag_key] = node.getTag(tag_key) + else: + try: + if hasattr(node,'getTag'): + common_tags = ['is_scene_element','tree_item_type','gui_type','light_type', + 'element_type','file','model_path','video_path','image_path', + 'gui_text','name','created_by_user'] + for tag in common_tags: + if node.hasTag(tag): + self.node_tags[tag] = node.getTag(tag) + except: + pass + + self.node_python_tags = {} + if hasattr(node,'getPythonTagKeys'): + try: + for tag_key in node.getPythonTagKeys(): + self.node_python_tags[tag_key] = node.getPythonTag(tag_key) + except Exception as e: + pass + + # 对于特定类型的节点,保存额外的数据 + self.extra_data = {} + if self.node_type in ["GUI_BUTTON", "GUI_LABEL", "GUI_ENTRY", "GUI_IMAGE", + "GUI_3DTEXT", "GUI_3DIMAGE", "GUI_VIDEO_SCREEN", "GUI_2D_VIDEO_SCREEN"]: + if node.hasTag("gui_text"): + self.extra_data["gui_text"] = node.getTag("gui_text") + if node.hasTag("video_path"): + self.extra_data["video_path"] = node.getTag("video_path") + if node.hasTag("image_path"): + self.extra_data["image_path"] = node.getTag("image_path") + elif self.node_type == "LIGHT_NODE": + if node.hasTag("light_type"): + self.extra_data["light_type"] = node.getTag("light_type") + rp_light = node.getPythonTag("rp_light_object") + if rp_light: + self.extra_data["light_data"] = { + 'energy': getattr(rp_light, 'energy', 5000), + 'radius': getattr(rp_light, 'radius', 1000), + 'fov': getattr(rp_light, 'fov', 70) if hasattr(rp_light, 'fov') else 70, + 'inner_radius': getattr(rp_light, 'inner_radius', 0.4) if hasattr(rp_light, + 'inner_radius') else 0.4, + 'casts_shadows': getattr(rp_light, 'casts_shadows', True), + 'shadow_map_resolution': getattr(rp_light, 'shadow_map_resolution', 256) + } + elif self.node_type == "CESIUM_TILESET_NODE": + if node.hasTag("tileset_url"): + self.extra_data["tileset_url"] = node.getTag("tileset_url") + + def execute(self): + """ + 执行删除操作 + """ + # 从world的相应列表中移除节点引用 + if self.world and hasattr(self.world, 'scene_manager'): + scene_manager = self.world.scene_manager + if self.node_type == "LIGHT_NODE": + if self.node.hasTag("light_type"): + light_type = self.node.getTag("light_type") + if light_type == "spot_light" and hasattr(scene_manager, + 'Spotlight') and self.node in scene_manager.Spotlight: + scene_manager.Spotlight.remove(self.node) + elif light_type == "point_light" and hasattr(scene_manager, + 'Pointlight') and self.node in scene_manager.Pointlight: + scene_manager.Pointlight.remove(self.node) + elif self.node_type == "IMPORTED_MODEL_NODE" and hasattr(scene_manager, + 'models') and self.node in scene_manager.models: + scene_manager.models.remove(self.node) + elif self.node_type.startswith("GUI_") and hasattr(self.world, + 'gui_elements') and self.node in self.world.gui_elements: + self.world.gui_elements.remove(self.node) + elif self.node_type == "CESIUM_TILESET_NODE": + # 从tilesets列表中移除 + if hasattr(scene_manager, 'tilesets'): + tilesets_to_remove = [] + for i, tileset_info in enumerate(scene_manager.tilesets): + if tileset_info.get('node') == self.node: + tilesets_to_remove.append(i) + for i in reversed(tilesets_to_remove): + del scene_manager.tilesets[i] + + # 从场景图中移除节点 + if self.node and not self.node.isEmpty(): + self.node.removeNode() + + def undo(self): + """ + 撤销删除操作(重新创建节点) + """ + try: + # 使用场景管理器重建节点 + if self.world and hasattr(self.world, 'scene_manager'): + scene_manager = self.world.scene_manager + + # 创建节点数据字典 + node_data = { + 'name': self.node_name, + 'node_type': self.node_type, + 'pos': (self.node_pos.x, self.node_pos.y, self.node_pos.z), + 'hpr': (self.node_hpr.x, self.node_hpr.y, self.node_hpr.z), + 'scale': (self.node_scale.x, self.node_scale.y, self.node_scale.z), + 'tags': self.node_tags + } + + # 添加额外数据 + if self.extra_data: + if self.node_type.startswith("GUI_"): + node_data['gui_data'] = self.extra_data + elif self.node_type == "LIGHT_NODE": + node_data['light_data'] = self.extra_data.get('light_data', {}) + elif self.node_type == "CESIUM_TILESET_NODE": + node_data['tileset_url'] = self.extra_data.get('tileset_url', '') + + # 重建节点 + new_node = scene_manager.recreateNodeFromData(node_data, self.parent_node) + + if new_node: + print(f"✅ 成功撤销删除操作,节点 {self.node_name} 已恢复") + # 更新节点引用 + self.node = new_node + else: + print(f"❌ 撤销删除操作失败,无法重建节点 {self.node_name}") + else: + print("❌ 无法撤销删除操作,缺少场景管理器引用") + + except Exception as e: + print(f"❌ 撤销删除操作时出错: {e}") + import traceback + traceback.print_exc() + + def redo(self): + """ + 重做删除操作 + """ + self.execute() + + +class RotateNodeCommand(Command): + """ + 旋转节点命令 + """ + + def __init__(self, node: NodePath, old_hpr, new_hpr): + self.node = node + self.old_hpr = old_hpr + self.new_hpr = new_hpr + + def execute(self): + """ + 执行旋转操作 + """ + self.node.setHpr(self.new_hpr) + + def undo(self): + """ + 撤销旋转操作 + """ + self.node.setHpr(self.old_hpr) + + def redo(self): + """ + 重做旋转操作 + """ + self.node.setHpr(self.new_hpr) + + +class ScaleNodeCommand(Command): + """ + 缩放节点命令 + """ + + def __init__(self, node: NodePath, old_scale, new_scale): + self.node = node + self.old_scale = old_scale + self.new_scale = new_scale + + def execute(self): + """ + 执行缩放操作 + """ + self.node.setScale(self.new_scale) + + def undo(self): + """ + 撤销缩放操作 + """ + self.node.setScale(self.old_scale) + + def redo(self): + """ + 重做缩放操作 + """ + self.node.setScale(self.new_scale) + + +class CreateNodeCommand(Command): + """ + 创建节点命令 + """ + + def __init__(self, node_creator_func,parent_node, *args, **kwargs): + self.node_creator_func = node_creator_func + self.parent_node = parent_node + self.args = args + self.kwargs = kwargs + self.created_node = None + + def execute(self): + """ + 执行创建节点操作 + """ + self.created_node = self.node_creator_func(self.parent_node,*self.args, **self.kwargs) + return self.created_node + + def undo(self): + """ + 撤销创建节点操作 + """ + if self.created_node: + self.created_node.removeNode() + + def redo(self): + """ + 重做创建节点操作 + """ + self.execute() + + +class ReparentNodeCommand(Command): + """ + 重新设置节点父子关系命令 - 增强版(同时处理Panda3D和Qt树) + """ + + def __init__(self, node: NodePath, old_parent: NodePath, new_parent: NodePath, + old_parent_item=None, new_parent_item=None, is_2d_gui=False, world=None): + self.node = node + self.old_parent = old_parent + self.new_parent = new_parent + self.old_parent_item = old_parent_item # Qt树中的旧父节点项 + self.new_parent_item = new_parent_item # Qt树中的新父节点项 + self.is_2d_gui = is_2d_gui + self.world = world + + # 保存节点在操作前的世界坐标和局部坐标,以便在撤销/重做时保持位置不变 + self.world_pos = node.getPos(self.world.render if self.world else node.getParent()) + self.world_hpr = node.getHpr(self.world.render if self.world else node.getParent()) + self.world_scale = node.getScale(self.world.render if self.world else node.getParent()) + # 同时保存局部坐标,因为在父节点改变后可能需要恢复 + self.local_pos = node.getPos() + self.local_hpr = node.getHpr() + self.local_scale = node.getScale() + + def _updateQtTree(self, node_item, new_parent_item): + """更新Qt树控件中的节点位置""" + if not node_item or not new_parent_item: + return + + # 从当前父节点中移除 + current_parent = node_item.parent() + if current_parent: + current_parent.removeChild(node_item) + else: + # 如果是顶级项目 + tree_widget = node_item.treeWidget() + if tree_widget: + index = tree_widget.indexOfTopLevelItem(node_item) + if index >= 0: + tree_widget.takeTopLevelItem(index) + + # 添加到新父节点 + new_parent_item.addChild(node_item) + + def execute(self): + """ + 执行重新父化操作 + """ + # 更新Panda3D节点父子关系 + if self.is_2d_gui and self.world: + # 2D GUI元素需要特殊处理 + if self.new_parent and not self.new_parent.isEmpty(): + if hasattr(self.new_parent, 'getTag') and self.new_parent.getTag("is_gui_element") == "1": + # 目标是GUI元素,直接重新父化 + self.node.wrtReparentTo(self.new_parent) + else: + # 目标是3D节点,保持GUI特性,重新父化到aspect2d + self.node.wrtReparentTo(self.world.aspect2d) + print(f"2D GUI元素保持在aspect2d下") + else: + # 如果新父节点为None,重新父化到aspect2d + self.node.wrtReparentTo(self.world.aspect2d) + print(f"2D GUI元素重新父化到aspect2d") + else: + # 普通3D节点的处理 + if self.new_parent and not self.new_parent.isEmpty(): + self.node.wrtReparentTo(self.new_parent) + else: + # 如果新父节点为空,将其父化到render节点 + if self.world: + self.node.wrtReparentTo(self.world.render) + else: + # 备用方案 + from panda3d.core import NodePath + self.node.wrtReparentTo(NodePath("render")) + + def undo(self): + """ + 撤销重新父化操作 + """ + # 在改变父节点前保存当前的缩放值 + current_scale = self.node.getScale() + + # 恢复Panda3D节点父子关系 + if self.is_2d_gui and self.world: + # 2D GUI元素需要特殊处理 + if self.old_parent and not self.old_parent.isEmpty(): + if hasattr(self.old_parent, 'getTag') and self.old_parent.getTag("is_gui_element") == "1": + # 原父节点是GUI元素,直接重新父化 + self.node.wrtReparentTo(self.old_parent) + else: + # 原父节点是3D节点,保持GUI特性,重新父化到aspect2d + self.node.wrtReparentTo(self.world.aspect2d) + print(f"2D GUI元素恢复到aspect2d下") + else: + # 如果原父节点为空,重新父化到aspect2d + self.node.wrtReparentTo(self.world.aspect2d) + print(f"2D GUI元素恢复到aspect2d") + else: + # 普通3D节点的处理 + if self.old_parent and not self.old_parent.isEmpty(): + self.node.wrtReparentTo(self.old_parent) + else: + # 如果原父节点为空,将其父化到render节点 + if self.world: + self.node.wrtReparentTo(self.world.render) + else: + # 备用方案 + from panda3d.core import NodePath + self.node.wrtReparentTo(NodePath("render")) + + # 恢复局部坐标(不是世界坐标),因为父节点已经改变 + self.node.setPos(self.local_pos) + self.node.setHpr(self.local_hpr) + # 特别处理缩放,确保GUI元素的缩放不会异常变化 + if not self.is_2d_gui or abs(current_scale.length() - self.local_scale.length()) > 0.001: + self.node.setScale(self.local_scale) + + def redo(self): + """ + 重做重新父化操作 + """ + # 在改变父节点前保存当前的缩放值 + current_scale = self.node.getScale() + + # 重新执行Panda3D节点父子关系更新 + if self.is_2d_gui and self.world: + # 2D GUI元素需要特殊处理 + if self.new_parent and not self.new_parent.isEmpty(): + if hasattr(self.new_parent, 'getTag') and self.new_parent.getTag("is_gui_element") == "1": + # 目标是GUI元素,直接重新父化 + self.node.wrtReparentTo(self.new_parent) + else: + # 目标是3D节点,保持GUI特性,重新父化到aspect2d + self.node.wrtReparentTo(self.world.aspect2d) + print(f"2D GUI元素保持在aspect2d下") + else: + # 如果新父节点为None,重新父化到aspect2d + self.node.wrtReparentTo(self.world.aspect2d) + print(f"2D GUI元素重新父化到aspect2d") + else: + # 普通3D节点的处理 + if self.new_parent and not self.new_parent.isEmpty(): + self.node.wrtReparentTo(self.new_parent) + else: + # 如果新父节点为空,将其父化到render节点 + if self.world: + self.node.wrtReparentTo(self.world.render) + else: + # 备用方案 + from panda3d.core import NodePath + self.node.wrtReparentTo(NodePath("render")) + + # 恢复局部坐标(不是世界坐标),因为父节点已经改变 + self.node.setPos(self.local_pos) + self.node.setHpr(self.local_hpr) + # 特别处理缩放,确保GUI元素的缩放不会异常变化 + if not self.is_2d_gui or abs(current_scale.length() - self.local_scale.length()) > 0.001: + self.node.setScale(self.local_scale) + + +class CompositeCommand(Command): + """ + 组合命令类,用于同时执行多个命令 + """ + def __init__(self,commands:List[Command]): + self.commands = commands + + def execute(self): + """ + 执行所有命令 + """ + for command in self.commands: + command.execute() + + def undo(self): + """ + 撤销所有命令(逆序执行) + """ + for command in reversed(self.commands): + command.undo() + + def redo(self): + """ + 重做所有命令 + """ + for command in self.commands: + command.redo() + +class MoveLightCommand(Command): + def __init__(self, node, old_pos, new_pos, light_object=None): + self.node = node + self.old_pos = Point3(old_pos) + self.new_pos = Point3(new_pos) + self.light_object = light_object + + def execute(self): # 将原来的 do() 改为 execute() + if self.light_object: + self.light_object.pos = self.new_pos + if self.node: + self.node.setPos(self.new_pos) + + def undo(self): + if self.light_object: + self.light_object.pos = self.old_pos + if self.node: + self.node.setPos(self.old_pos) + + def redo(self): + self.execute() # 调用 execute() 而不是 do() + + + diff --git a/core/CustomMouseController.py b/core/CustomMouseController.py index 87405be1..6ac00945 100644 --- a/core/CustomMouseController.py +++ b/core/CustomMouseController.py @@ -1,4 +1,3 @@ -from PyQt5.QtCore import showbase from direct.task.TaskManagerGlobal import taskMgr diff --git a/core/InfoPanelManager.py b/core/InfoPanelManager.py index 35000ffe..71a43eb8 100644 --- a/core/InfoPanelManager.py +++ b/core/InfoPanelManager.py @@ -2,6 +2,7 @@ from xml.sax.handler import property_encoding from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import QMessageBox from direct.gui.DirectGui import DirectFrame, DirectLabel from direct.showbase.ShowBaseGlobal import aspect2d from panda3d.core import TextNode, Vec4, NodePath @@ -50,7 +51,7 @@ class InfoPanelManager(DirectObject): title, content = self._getPanelContent(panel_id) # 创建主节点,便于统一管理 - panel_node = parent_node.attachNewNode(f"info_panel_{panel_id}") + panel_node = parent_node.attachNewNode(f"{panel_id}") panel_node.setPos(position[0], 0, position[1]) # 创建主面板框架(带边框效果) @@ -186,10 +187,9 @@ class InfoPanelManager(DirectObject): panel_node.setTag("is_gui_element",'1') panel_node.setTag("tree_item_type","INFO_PANEL") panel_node.setTag("supports_3d_position_editing","1") - # 如果有背景图片,保存背景图片路径 if bg_image: - panel_node.setTag("bg_image_path", bg_image) + panel_node.setTag("image_path", bg_image) if not visible: @@ -197,6 +197,8 @@ class InfoPanelManager(DirectObject): # 将面板添加到场景树 #self._addPanelToSceneTree(panel_node, panel_id) + if hasattr(self.world, 'gui_elements'): + self.world.gui_elements.append(panel_node) return panel_node @@ -756,7 +758,7 @@ class InfoPanelManager(DirectObject): title, content = self._getPanelContent(panel_id) # 创建主节点,便于统一管理 - panel_node = parent_node.attachNewNode(f"info_panel_3d_{panel_id}") + panel_node = parent_node.attachNewNode(f"{panel_id}") panel_node.setPos(position[0], position[1], position[2]) # 直接创建文字节点,不创建面板背景和边框 @@ -820,6 +822,8 @@ class InfoPanelManager(DirectObject): # 将面板添加到场景树 #self._addPanelToSceneTree(panel_node, panel_id) + if hasattr(self.world, 'gui_elements'): + self.world.gui_elements.append(panel_node) return panel_node @@ -1266,6 +1270,102 @@ class InfoPanelManager(DirectObject): traceback.print_exc() return None + def onCreateSampleInfoPanel(self): + """创建示例天气信息面板(模拟数据)""" + try: + # 获取中文字体 + from panda3d.core import TextNode + font = self.world.getChineseFont() if self.world.getChineseFont() else None + + # 使用唯一的面板ID + import time + unique_id = f"weather_info_{int(time.time())}" + + # 创建示例面板 + weather_panel = self.createInfoPanel( + panel_id=unique_id, # 使用唯一ID + position=(1.32, 0.68), + size=(1, 0.6), + bg_color=(0.15, 0.25, 0.35, 0), # 蓝色背景 + border_color=(0.3, 0.5, 0.7, 0), # 蓝色边框 + title_color=(0.7, 0.9, 1.0, 1.0), # 浅蓝色标题 + content_color=(0.95, 0.95, 0.95, 1.0), + font=font, + bg_image="/home/tiger/图片/内部信息框2@2x.png" + ) + weather_panel.setTag("name",unique_id) + + # 更新面板标题 + self.updatePanelContent(unique_id, title="北京天气") + + self._addPanelToSceneTree(weather_panel, unique_id) + # 立即显示加载中信息 + self.updatePanelContent(unique_id, content="正在获取天气数据...") + + self.registerDataSource(unique_id, self.getRealWeatherData, update_interval=5.0) + + print("✓ 示例天气信息面板已创建") + + except Exception as e: + print(f"✗ 创建示例天气信息面板失败: {e}") + import traceback + traceback.print_exc() + QMessageBox.critical(self, "错误", f"创建示例天气信息面板时出错: {str(e)}") + + def getRealWeatherData(self): + """获取真实天气数据""" + try: + import requests + import json + from datetime import datetime + + # 请求天气数据 + url = "https://wttr.in/Beijing?format=j1" + response = requests.get(url, timeout=10) + response.raise_for_status() + + # 解析JSON数据 + weather_data = response.json() + + # 提取当前天气信息 + current_condition = weather_data['current_condition'][0] + weather_desc = current_condition['weatherDesc'][0]['value'] + temp_c = current_condition['temp_C'] + feels_like = current_condition['FeelsLikeC'] + humidity = current_condition['humidity'] + pressure = current_condition['pressure'] + visibility = current_condition['visibility'] + wind_speed = current_condition['windspeedKmph'] + wind_dir = current_condition['winddir16Point'] + + # 提取空气质量(如果可用) + air_quality = "N/A" + if 'air_quality' in weather_data and weather_data['air_quality']: + if 'us-epa-index' in current_condition: + air_quality_index = current_condition['air_quality_index'] + air_quality = f"指数: {air_quality_index}" + + # 获取更新时间 + update_time = datetime.now().strftime("%Y-%m-%d %H:%M") + + # 格式化显示内容 + content = f"天气状况: {weather_desc}\n温度: {temp_c}°C (体感 {feels_like}°C)\n湿度: {humidity}%\n气压: {pressure} hPa\n能见度: {visibility} km\n风速: {wind_speed} km/h ({wind_dir})\n空气质量: {air_quality}\n更新时间: {update_time}" + + return content + + except requests.exceptions.Timeout: + return "错误: 获取天气数据超时" + except requests.exceptions.ConnectionError: + return "错误: 网络连接失败" + except requests.exceptions.HTTPError as e: + return f"HTTP错误: {e}" + except json.JSONDecodeError: + return "错误: 无法解析天气数据" + except KeyError as e: + return f"错误: 天气数据格式不正确 (缺少字段: {e})" + except Exception as e: + return f"获取天气数据失败: {str(e)}" + # 示例数据源函数 def getRealtimeData(): diff --git a/core/alvr_streamer.py b/core/alvr_streamer.py deleted file mode 100644 index 2783c5bd..00000000 --- a/core/alvr_streamer.py +++ /dev/null @@ -1,509 +0,0 @@ -""" -ALVR串流处理器 - -负责与ALVR服务器通信和视频流传输 -支持Quest等VR头显的无线串流 -""" - -import socket -import struct -import threading -import json -import time -import subprocess -import psutil -from direct.showbase.DirectObject import DirectObject -from panda3d.core import Texture, PNMImage - - -class ALVRStreamer(DirectObject): - """ALVR串流处理器""" - - def __init__(self, world, vr_manager): - super().__init__() - self.world = world - self.vr_manager = vr_manager - - # ALVR服务器配置 - self.alvr_server_ip = "127.0.0.1" - self.alvr_server_port = 9943 - self.alvr_streaming_port = 9944 - - # 连接状态 - self.connected = False - self.streaming = False - self.server_socket = None - self.streaming_socket = None - - # 流媒体配置 - self.stream_width = 2880 # Quest 2 推荐分辨率 - self.stream_height = 1700 - self.stream_fps = 72 - self.bitrate = 150 # Mbps - self.codec = "h264" - - # 线程管理 - self.connection_thread = None - self.streaming_thread = None - self.running = False - - # 性能统计 - self.frame_count = 0 - self.last_fps_time = time.time() - self.current_fps = 0 - self.latency = 0 - - print("✓ ALVR串流处理器初始化完成") - - def initialize(self): - """初始化ALVR串流""" - try: - # 检查ALVR服务器是否运行 - if not self._check_alvr_server(): - print("ALVR服务器未运行,尝试启动...") - if not self._start_alvr_server(): - print("无法启动ALVR服务器") - return False - - # 连接到ALVR服务器 - if not self._connect_to_server(): - print("无法连接到ALVR服务器") - return False - - # 配置流媒体设置 - self._configure_streaming() - - # 启动串流线程 - self._start_streaming_threads() - - print("✓ ALVR串流初始化成功") - return True - - except Exception as e: - print(f"ALVR初始化错误: {str(e)}") - return False - - def _check_alvr_server(self): - """检查ALVR服务器是否运行""" - try: - # 检查进程 - for proc in psutil.process_iter(['pid', 'name', 'cmdline']): - if 'alvr' in proc.info['name'].lower(): - return True - - # 尝试连接端口 - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.settimeout(2) - result = sock.connect_ex((self.alvr_server_ip, self.alvr_server_port)) - sock.close() - - return result == 0 - - except Exception as e: - print(f"检查ALVR服务器错误: {str(e)}") - return False - - def _start_alvr_server(self): - """启动ALVR服务器""" - try: - # 尝试启动ALVR服务器 - # 这里需要根据实际的ALVR安装路径调整 - alvr_paths = [ - "/usr/local/bin/alvr_server", - "/usr/bin/alvr_server", - "C:/Program Files/ALVR/alvr_server.exe", - "C:/ALVR/alvr_server.exe" - ] - - for path in alvr_paths: - try: - subprocess.Popen([path], shell=True) - time.sleep(3) # 等待服务器启动 - if self._check_alvr_server(): - return True - except FileNotFoundError: - continue - - return False - - except Exception as e: - print(f"启动ALVR服务器错误: {str(e)}") - return False - - def _connect_to_server(self): - """连接到ALVR服务器""" - try: - # 创建TCP连接 - self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.server_socket.settimeout(5) - self.server_socket.connect((self.alvr_server_ip, self.alvr_server_port)) - - # 发送握手消息 - handshake_data = { - "type": "handshake", - "client_name": "Panda3D_VR_Engine", - "version": "1.0", - "capabilities": { - "video": True, - "audio": True, - "tracking": True, - "haptics": True - } - } - - self._send_message(handshake_data) - - # 接收响应 - response = self._receive_message() - if response and response.get("type") == "handshake_response": - self.connected = True - print("✓ 已连接到ALVR服务器") - return True - - return False - - except Exception as e: - print(f"连接ALVR服务器错误: {str(e)}") - return False - - def _send_message(self, data): - """发送消息到ALVR服务器""" - try: - message = json.dumps(data).encode('utf-8') - length = struct.pack('= 1.0: - self.current_fps = self.frame_count - self.frame_count = 0 - self.last_fps_time = current_time - - def start_streaming(self): - """开始串流""" - if not self.connected: - print("未连接到ALVR服务器") - return False - - start_message = {"type": "start_streaming"} - self._send_message(start_message) - return True - - def stop_streaming(self): - """停止串流""" - if not self.connected: - return - - stop_message = {"type": "stop_streaming"} - self._send_message(stop_message) - self.streaming = False - - def send_haptic_feedback(self, controller_id, duration, intensity): - """发送触觉反馈""" - if not self.connected: - return - - haptic_message = { - "type": "haptic_feedback", - "controller_id": controller_id, - "duration": duration, - "intensity": intensity - } - - self._send_message(haptic_message) - - def get_streaming_status(self): - """获取串流状态""" - return { - "connected": self.connected, - "streaming": self.streaming, - "fps": self.current_fps, - "latency": self.latency, - "resolution": f"{self.stream_width}x{self.stream_height}", - "bitrate": self.bitrate - } - - def set_stream_quality(self, width, height, fps, bitrate): - """设置串流质量""" - self.stream_width = width - self.stream_height = height - self.stream_fps = fps - self.bitrate = bitrate - - # 如果正在串流,重新配置 - if self.streaming: - self._configure_streaming() - - def shutdown(self): - """关闭串流""" - print("关闭ALVR串流...") - - self.running = False - self.streaming = False - - # 发送断开消息 - if self.connected: - disconnect_message = {"type": "disconnect"} - self._send_message(disconnect_message) - - # 关闭连接 - if self.server_socket: - self.server_socket.close() - - if self.streaming_socket: - self.streaming_socket.close() - - # 等待线程结束 - if self.connection_thread: - self.connection_thread.join(timeout=2) - - if self.streaming_thread: - self.streaming_thread.join(timeout=2) - - self.connected = False - print("✓ ALVR串流已关闭") - - def is_connected(self): - """检查是否连接""" - return self.connected - - def is_streaming(self): - """检查是否在串流""" - return self.streaming \ No newline at end of file diff --git a/core/assembly_interaction.py b/core/assembly_interaction.py new file mode 100644 index 00000000..c2917e8d --- /dev/null +++ b/core/assembly_interaction.py @@ -0,0 +1,2339 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import json +import os +from direct.showbase.DirectObject import DirectObject +from direct.task import Task +from panda3d.core import CollisionTraverser, CollisionNode, CollisionHandlerQueue, CollisionRay +from panda3d.core import BitMask32, Vec3, Point3, Plane +from PyQt5.QtWidgets import QMessageBox, QFileDialog, QDialog, QVBoxLayout, QHBoxLayout +from PyQt5.QtWidgets import QLabel, QPushButton, QTextEdit, QComboBox, QGroupBox +from PyQt5.QtCore import Qt + +# 导入维修系统GUI +try: + from core.maintenance_gui import MaintenanceGUI +except ImportError as e: + print(f"⚠️ 导入维修GUI失败: {e}") + MaintenanceGUI = None + + +class AssemblyInteractionManager(DirectObject): + """拆装交互管理器(优化版)""" + + def __init__(self, world): + DirectObject.__init__(self) + self.world = world + self.config_data = None + self.current_step = 0 + self.total_steps = 0 + self.is_active = False + + # --- 模式控制 --- + self.mode = "training" # 默认训练模式,可选 "training" 或 "exam" + + # --- 拖拽相关(优化) --- + self.dragging_model = None + self.drag_offset = Vec3(0, 0, 0) + self.drag_plane = Plane(Vec3(0, 1, 0), Point3(0, 0, 0)) # 初始化一个平面 + + # --- 安装/拆卸状态管理 --- + self.model_original_positions = {} # 记录模型的原始位置 + self.model_current_states = {} # 记录模型当前状态 ('installed' 或 'removed') + + # --- UI组件 --- + self.step_dialog = None + + # --- 维修系统GUI --- + self.maintenance_gui = None + if MaintenanceGUI: + self.maintenance_gui = MaintenanceGUI(world) + + # --- 操作权限控制 --- + self.operation_enabled = True # 是否允许进行操作 + + # --- 考核相关 --- + self.exam_score = 0 # 考核总分 + self.exam_max_score = 0 # 考核满分 + self.step_scores = {} # 每步得分记录 + + # --- 碰撞检测(优化) --- + self.picker_traverser = CollisionTraverser('picker_traverser') + self.collision_handler = CollisionHandlerQueue() + self.picker_ray_node = None + + print("拆装交互管理器初始化完成") + + def init_exam_mode(self): + """初始化考核模式""" + print("📝 初始化考核模式...") + + # 重置考核数据 + self.exam_score = 0 + self.exam_max_score = 0 + self.step_scores = {} + + # 计算总分 + steps = self.config_data.get('steps', []) + for i, step_data in enumerate(steps): + step_score = step_data.get('score', 10) # 默认每步10分 + self.exam_max_score += step_score + self.step_scores[i] = { + 'max_score': step_score, + 'current_score': step_score, # 初始满分,操作错误时扣分 + 'tool_error': False, # 是否有工具错误 + 'operation_attempts': 0 # 操作尝试次数 + } + + print(f"📝 考核模式初始化完成,总分: {self.exam_max_score} 分") + + def play_step_audio(self, step_data): + """播放步骤音频""" + try: + # 考核模式下不播放音频 + if self.mode == "exam": + print("🔇 考核模式,音频播放已禁用") + return + + # 检查是否启用了自动播放音频 + settings = self.config_data.get('settings', {}) + auto_play_audio = settings.get('auto_play_audio', True) + + if not auto_play_audio: + print("🔇 音频播放已禁用") + return + + # 获取音频文件路径 + audio_file = step_data.get('audio_file', '') + + if not audio_file or audio_file.strip() == '': + print("🔇 当前步骤没有配置音频文件") + return + + print(f"🔊 准备播放步骤音频: {audio_file}") + + import os + # 检查音频文件格式 + file_ext = os.path.splitext(audio_file)[1].lower() + print(f"🔊 音频文件格式: {file_ext}") + + # 对于MP3文件,优先使用pygame播放 + if file_ext in ['.mp3', '.m4a', '.aac']: + print("🔊 检测到MP3/M4A/AAC格式,优先尝试pygame播放") + if self.play_audio_with_pygame(audio_file): + return + print("🔊 pygame不可用,尝试备用播放方法") + self.play_audio_fallback(audio_file) + return + + # 检查音频文件是否存在 + import os + if not os.path.isfile(audio_file): + # 如果不是绝对路径,尝试在项目目录下查找 + if not os.path.isabs(audio_file): + # 尝试在当前目录、Resources目录等位置查找 + possible_paths = [ + audio_file, + os.path.join("Resources", audio_file), + os.path.join("Resources", "audio", audio_file), + os.path.join("audio", audio_file), + os.path.join("sounds", audio_file) + ] + + audio_file_found = None + for path in possible_paths: + if os.path.isfile(path): + audio_file_found = path + break + + if audio_file_found: + audio_file = audio_file_found + print(f"🔊 找到音频文件: {audio_file}") + else: + print(f"❌ 找不到音频文件: {audio_file}") + print(f" 尝试的路径: {possible_paths}") + return + else: + print(f"❌ 音频文件不存在: {audio_file}") + return + + # 使用Panda3D的音频系统播放音频 + if hasattr(self.world, 'loader') and self.world.loader: + try: + # 检查音频系统是否启用 + from panda3d.core import AudioManager + print(f"🔊 音频管理器状态: {AudioManager.createAudioManager()}") + + # 加载音频文件 + print(f"🔊 尝试加载音频文件: {audio_file}") + audio_sound = self.world.loader.loadSfx(audio_file) + + if audio_sound: + print(f"✅ 音频文件加载成功: {os.path.basename(audio_file)}") + print(f"🔊 音频状态: {audio_sound.status()}") + audio_length = audio_sound.length() + print(f"🔊 音频长度: {audio_length:.2f}秒") + + # 检查音频文件是否有效 + if audio_length <= 0: + print("⚠️ 音频长度为0,可能是格式不支持或文件损坏") + print("🔊 尝试使用pygame播放") + if not self.play_audio_with_pygame(audio_file): + self.play_audio_fallback(audio_file) + return + + # 设置音量(在播放前设置) + audio_sound.setVolume(0.8) + print(f"🔊 设置音量: 80%") + + # 播放音频 + audio_sound.play() + print(f"✅ 开始播放音频: {os.path.basename(audio_file)}") + + # 等待一小段时间让播放状态更新 + import time + time.sleep(0.1) + + final_status = audio_sound.status() + print(f"🔊 播放后状态: {final_status}") + + # 检查是否真的在播放 (状态码2表示PLAYING) + if final_status == 2: # PLAYING + print("✅ 音频正在播放中") + elif final_status == 1: # READY + print("⚠️ 音频处于就绪状态但未播放,尝试pygame") + if not self.play_audio_with_pygame(audio_file): + self.play_audio_fallback(audio_file) + else: + print(f"⚠️ 音频状态异常: {final_status}") + if not self.play_audio_with_pygame(audio_file): + self.play_audio_fallback(audio_file) + else: + print(f"❌ 无法加载音频文件: {audio_file}") + print("🔊 Panda3D加载失败,尝试pygame") + if not self.play_audio_with_pygame(audio_file): + self.play_audio_fallback(audio_file) + + except Exception as e: + print(f"❌ Panda3D音频播放失败: {e}") + import traceback + traceback.print_exc() + # 尝试使用pygame播放(备用方案) + print("🔊 Panda3D播放失败,尝试pygame") + if not self.play_audio_with_pygame(audio_file): + self.play_audio_fallback(audio_file) + else: + print("⚠️ 找不到Panda3D音频加载器,尝试pygame") + if not self.play_audio_with_pygame(audio_file): + self.play_audio_fallback(audio_file) + + except Exception as e: + print(f"❌ 音频播放失败: {e}") + import traceback + traceback.print_exc() + + def play_audio_with_pygame(self, audio_file): + """使用pygame播放音频(优先方法)""" + try: + import pygame + print("🔊 使用pygame播放音频") + + # 初始化pygame mixer + if not pygame.mixer.get_init(): + pygame.mixer.pre_init(frequency=22050, size=-16, channels=2, buffer=1024) + pygame.mixer.init() + print("🔊 pygame mixer初始化完成") + + # 停止当前播放的音频(如果有) + if pygame.mixer.music.get_busy(): + pygame.mixer.music.stop() + print("🔊 停止之前的音频") + + # 加载并播放音频文件 + pygame.mixer.music.load(audio_file) + pygame.mixer.music.set_volume(0.8) + pygame.mixer.music.play() + + print(f"✅ pygame开始播放音频: {os.path.basename(audio_file)}") + + # 在后台线程中监控播放完成 + def wait_for_pygame_completion(): + import time + while pygame.mixer.music.get_busy(): + time.sleep(0.1) + print(f"🎵 pygame音频播放完成: {os.path.basename(audio_file)}") + + import threading + threading.Thread(target=wait_for_pygame_completion, daemon=True).start() + + print("🎵 音频将在后台完整播放") + return True + + except ImportError: + print("⚠️ pygame未安装") + return False + except Exception as e: + print(f"⚠️ pygame播放失败: {e}") + import traceback + traceback.print_exc() + return False + + def play_audio_fallback(self, audio_file): + """备用音频播放方法""" + import os + print(f"🔊 尝试备用音频播放方法...") + + # 方法1: 使用系统默认播放器(最简单) + try: + print("🔊 尝试使用系统默认播放器") + if os.name == 'nt': # Windows + os.startfile(audio_file) + print(f"✅ 使用系统默认播放器播放: {os.path.basename(audio_file)}") + return + elif os.name == 'posix': # Linux/Mac + os.system(f'xdg-open "{audio_file}"') + print(f"✅ 使用系统默认播放器播放: {os.path.basename(audio_file)}") + return + except Exception as e: + print(f"⚠️ 系统默认播放器失败: {e}") + + # 方法2: 尝试使用Windows Media Player + try: + if os.name == 'nt': # Windows + print("🔊 尝试使用Windows Media Player") + cmd = f'start wmplayer "{audio_file}"' + os.system(cmd) + print(f"✅ 使用Windows Media Player播放音频: {os.path.basename(audio_file)}") + return + except Exception as e: + print(f"⚠️ Windows Media Player播放失败: {e}") + + # 方法3: 使用PowerShell播放WAV文件 + try: + if os.name == 'nt': + file_ext = os.path.splitext(audio_file)[1].lower() + if file_ext in ['.wav']: + print("🔊 尝试使用PowerShell播放WAV文件") + import subprocess + cmd = f'powershell -c "(New-Object Media.SoundPlayer \\"{audio_file}\\").PlaySync()"' + subprocess.run(cmd, shell=True) + print(f"✅ 使用PowerShell播放音频: {os.path.basename(audio_file)}") + return + except Exception as e: + print(f"⚠️ PowerShell播放失败: {e}") + + # 方法4: 简单的系统调用 + try: + print("🔊 尝试使用系统默认播放器") + if os.name == 'nt': # Windows + os.startfile(audio_file) + elif os.name == 'posix': # Linux/Mac + os.system(f'xdg-open "{audio_file}"') + print(f"✅ 使用系统默认播放器播放音频: {os.path.basename(audio_file)}") + except Exception as e: + print(f"❌ 所有备用音频播放方法都失败了: {e}") + print("💡 建议检查:") + print(" 1. 音频文件是否存在且格式正确") + print(" 2. 系统音频驱动是否正常") + print(" 3. 音频文件是否损坏") + + def set_operation_enabled(self, enabled): + """设置操作是否启用""" + self.operation_enabled = enabled + print(f"🔧 操作权限: {'启用' if enabled else '禁用'}") + + def check_operation_permission(self): + """检查操作权限""" + if not self.is_active: + print("⚠️ 交互模式未激活") + return False + + # 获取当前步骤数据 + if self.current_step >= self.total_steps: + print("⚠️ 所有步骤已完成") + return False + + step_data = self.config_data['steps'][self.current_step] + required_tool = step_data.get('required_tool', '无') + + # 获取当前选择的工具(优先从维修GUI获取) + current_tool = "无" + if self.maintenance_gui: + current_tool = self.maintenance_gui.get_current_tool() + elif hasattr(self, 'step_dialog') and self.step_dialog and hasattr(self.step_dialog, 'tool_combo'): + current_tool = self.step_dialog.tool_combo.currentText() + + # 使用 getChinese 处理工具名称 + if hasattr(self.world, 'getChinese'): + required_tool = self.world.getChinese(required_tool) + current_tool = self.world.getChinese(current_tool) + + print(f"🔧 工具检查: 需要'{required_tool}',当前'{current_tool}'") + + # 检查工具是否匹配 + tool_matches = self.check_tool_match(current_tool, required_tool) + + if self.mode == "training": + # 训练模式:显示GUI警告并阻止错误操作 + if not tool_matches: + print("⚠️ 训练模式 - 工具不匹配,无法进行操作") + + # 在维修GUI中显示警告 + if self.maintenance_gui: + warning_msg = f"请先选择 '{required_tool}' 工具!" + self.maintenance_gui.show_warning(warning_msg, 2.0) + # 也在Qt对话框中显示警告(仅在非维修系统模式下) + elif hasattr(self, 'step_dialog') and self.step_dialog: + QMessageBox.warning(self.step_dialog, "工具不匹配", + f"当前步骤需要使用 '{required_tool}' 工具,请先选择正确的工具!") + return False + return True + else: + # 考核模式:不显示提示,但记录工具错误并阻止操作 + if not tool_matches: + print(f"❌ 考核模式 - 工具错误:需要'{required_tool}',实际选择'{current_tool}',操作被阻止") + + # 记录工具错误(扣分) + self.record_tool_error() + return False + return True + + def check_tool_match(self, current_tool, required_tool): + """检查工具是否匹配""" + # 如果步骤不要求特定工具,任何工具都可以 + if required_tool == "无" or required_tool == "" or not required_tool: + return True + + # 精确匹配 + if current_tool == required_tool: + return True + + return False + + def record_tool_error(self): + """记录工具错误(考核模式下扣分)""" + if self.mode != "exam": + return + + # 获取当前步骤的考核记录 + if self.current_step in self.step_scores: + step_record = self.step_scores[self.current_step] + + # 如果是第一次工具错误,扣除一定分数 + if not step_record['tool_error']: + step_record['tool_error'] = True + # 扣除该步骤50%的分数作为工具错误惩罚 + # penalty = step_record['max_score'] * 0.5 + penalty = step_record['max_score'] + step_record['current_score'] = max(0, step_record['current_score'] - penalty) + + print(f"📝 考核记录:工具错误,扣除{penalty:.0f}分,当前步骤剩余{step_record['current_score']:.0f}分") + + # 增加操作尝试次数 + step_record['operation_attempts'] += 1 + + def start_interaction_mode(self, mode="training"): + """启动交互模式""" + try: + # 设置模式 + self.mode = mode + print(f"🎯 启动拆装交互模式: {self.mode}") + + # 如果没有配置数据,尝试加载 + if not self.config_data: + if not self.load_configuration(): + return False + + if not self.config_data or not self.config_data.get('steps'): + QMessageBox.warning(None, "警告", "没有找到有效的配置步骤!\n请先在'拆装配置'中设置步骤。") + return False + + self.total_steps = len(self.config_data['steps']) + self.current_step = 0 + self.is_active = True + + # 设置维修系统GUI + if self.maintenance_gui: + tools_list = self.config_data.get('tools', ['手']) + self.maintenance_gui.setup_gui(tools_list, mode) + self.maintenance_gui.show_gui() + print(f"✅ 维修系统GUI已启动,模式: {mode}") + + # 如果是考核模式,初始化考核相关数据 + + if self.mode == "exam": + self.init_exam_mode() + # 显示考核开始提示 + if self.maintenance_gui: + exam_start_msg = f"📝 考核模式已启动\n\n总分: {self.exam_max_score} 分\n步骤数: {self.total_steps}\n\n✅ 可以选择和切换工具\n❌ 不会显示步骤描述和语音提示\n❌ 使用错误工具会扣分" + self.maintenance_gui.update_step_info(exam_start_msg) + print("📝 考核开始提示已在GUI中显示") + + # 设置碰撞检测 + self.setup_picking() + + # 拦截Qt层面的鼠标事件处理 + print("🔄 拦截Qt层面的鼠标事件处理") + print(f"🔍 检查world对象: {type(self.world).__name__}") + print(f"🔍 是否有qtWidget属性: {hasattr(self.world, 'qtWidget')}") + if hasattr(self.world, 'qtWidget'): + print(f"🔍 qtWidget值: {self.world.qtWidget}") + print(f"🔍 qtWidget类型: {type(self.world.qtWidget)}") + + if hasattr(self.world, 'qtWidget') and self.world.qtWidget: + # 备份原有的鼠标事件处理方法 + self.original_mouse_press_event = self.world.qtWidget.mousePressEvent + self.original_mouse_release_event = self.world.qtWidget.mouseReleaseEvent + + print(f"🔍 原有鼠标事件方法: {self.original_mouse_press_event}") + + # 替换为我们的处理方法 + self.world.qtWidget.mousePressEvent = self.qt_mouse_press_event + self.world.qtWidget.mouseReleaseEvent = self.qt_mouse_release_event + print("✅ Qt鼠标事件已被拦截") + else: + print("⚠️ 找不到Qt部件,使用Panda3D事件系统") + # 作为备用方案,仍然绑定Panda3D事件 + self.accept("mouse1", self.on_mouse_down) + self.accept("mouse1-up", self.on_mouse_up) + + # 绑定键盘备用触发 + self.accept("space", self.trigger_drag_by_keyboard) + self.accept("s", self.show_models_status) # S键显示模型状态 + self.accept("r", self.record_current_positions) # R键记录当前位置为原始位置 + + print("✅ 拆装交互事件绑定完成") + print("💡 提示: 按S键可查看所有模型状态,按R键重新记录原始位置") + + print("拆装交互模式已启动,核心事件已绑定。") + print("请点击目标模型进行操作。") + + # 调试:列出场景中的所有模型 + self.list_scene_models() + + # 显示步骤指引界面(维修系统使用GUI界面,跳过Qt对话框) + if not self.maintenance_gui: + self.show_step_dialog() + + # 开始第一步 + self.start_current_step() + + print(f"共 {self.total_steps} 步") + return True + + except Exception as e: + QMessageBox.critical(None, "错误", f"启动交互模式失败: {str(e)}") + import traceback + traceback.print_exc() + return False + + def load_configuration(self): + """加载拆装配置""" + try: + file_path, _ = QFileDialog.getOpenFileName( + None, "选择拆装配置文件", "", + "JSON文件 (*.json);;所有文件 (*)" + ) + if not file_path: + return False + + with open(file_path, 'r', encoding='utf-8') as f: + self.config_data = json.load(f) + + self.total_steps = len(self.config_data.get('steps', [])) + self.current_step = 0 + + # 从配置文件中恢复原始位置信息 + self.load_original_positions_from_config() + + print(f"成功加载配置: {file_path}") + return True + + except Exception as e: + QMessageBox.critical(None, "错误", f"加载配置失败: {str(e)}") + return False + + def load_original_positions_from_config(self): + """从配置文件中加载模型的原始位置信息""" + print("📍 从配置文件恢复模型原始位置...") + + # 清空现有的位置记录 + self.model_original_positions.clear() + self.model_current_states.clear() + + # 从配置文件的models部分读取原始位置(models是数组格式) + models_config = self.config_data.get('models', []) + + for model_data in models_config: + model_name = model_data.get('name') + if not model_name: + continue + + # 尝试从配置文件中获取原始位置 + original_pos_data = model_data.get('original_pos') + + if original_pos_data and len(original_pos_data) >= 3: + # 如果配置文件中有原始位置,使用配置文件中的位置 + original_pos = Vec3(original_pos_data[0], original_pos_data[1], original_pos_data[2]) + self.model_original_positions[model_name] = original_pos + self.model_current_states[model_name] = 'installed' + print(f" 📍 {model_name}: 从配置文件恢复原始位置 {original_pos}") + else: + # 如果配置文件中没有原始位置,使用当前场景中的位置作为原始位置 + target_node = self.find_model_node(model_name) + if target_node and not target_node.isEmpty(): + current_pos = target_node.getPos() + self.model_original_positions[model_name] = current_pos + self.model_current_states[model_name] = 'installed' + print(f" 📍 {model_name}: 使用当前场景位置作为原始位置 {current_pos}") + else: + print(f" ❌ {model_name}: 在场景中找不到对应模型") + + # 如果配置文件中没有models部分,从步骤中提取模型信息 + if not models_config: + print(" 配置文件中没有models部分,从步骤中提取模型信息...") + for step_data in self.config_data.get('steps', []): + target_model_name = step_data.get('target_model') + if target_model_name and target_model_name not in self.model_original_positions: + target_node = self.find_model_node(target_model_name) + if target_node and not target_node.isEmpty(): + current_pos = target_node.getPos() + self.model_original_positions[target_model_name] = current_pos + self.model_current_states[target_model_name] = 'installed' + print(f" 📍 {target_model_name}: 从场景记录原始位置 {current_pos}") + + print(f"📍 原始位置恢复完成,共记录 {len(self.model_original_positions)} 个模型") + + # 检查配置文件的步骤完整性 + self.check_step_completeness() + + def check_step_completeness(self): + """检查配置文件的步骤完整性""" + print("\n🔍 检查步骤配置完整性...") + + steps = self.config_data.get('steps', []) + if not steps: + print("⚠️ 配置文件中没有步骤") + return + + # 统计每个模型的拆卸和安装步骤 + model_operations = {} + + for i, step in enumerate(steps): + target_model = step.get('target_model') + step_type = self.normalize_step_type(step) + + if target_model: + if target_model not in model_operations: + model_operations[target_model] = {'disassemble': [], 'assemble': []} + + model_operations[target_model][step_type].append(i + 1) + + # 检查每个模型是否有配对的拆卸和安装步骤 + incomplete_models = [] + for model_name, operations in model_operations.items(): + disassemble_count = len(operations['disassemble']) + assemble_count = len(operations['assemble']) + + print(f" 📋 {model_name}: 拆卸步骤 {disassemble_count} 个, 安装步骤 {assemble_count} 个") + + if disassemble_count > 0 and assemble_count == 0: + incomplete_models.append(model_name) + print(f" ⚠️ 只有拆卸步骤,缺少安装步骤") + elif disassemble_count != assemble_count: + print(f" ⚠️ 拆卸和安装步骤数量不匹配") + + if incomplete_models: + print(f"\n⚠️ 发现 {len(incomplete_models)} 个模型缺少安装步骤:") + for model in incomplete_models: + print(f" - {model}") + print("💡 建议为每个拆卸的模型添加对应的安装步骤") + else: + print("\n✅ 所有模型都有完整的拆卸和安装步骤配置") + + def setup_picking(self): + """设置用于鼠标拾取的碰撞检测系统""" + try: + # 创建一个碰撞射线 + picker_ray = CollisionRay() + + # 创建一个用于放置射线的碰撞节点 + picker_ray_node = CollisionNode('mouse_ray') + picker_ray_node.addSolid(picker_ray) + + # 设置碰撞掩码,确保它只与可拾取的物体碰撞 + # 这里我们假设可拾取的物体有第1位的碰撞比特 + picker_ray_node.setFromCollideMask(BitMask32.bit(1)) + picker_ray_node.setIntoCollideMask(BitMask32.allOff()) + + # 将碰撞节点附加到相机上,这样射线就会跟随相机移动 + self.picker_ray_node = self.world.camera.attachNewNode(picker_ray_node) + + # 将碰撞节点添加到遍历器中,并指定一个处理器来存储碰撞结果 + self.picker_traverser.addCollider(self.picker_ray_node, self.collision_handler) + + print("射线拾取系统设置完成") + + except Exception as e: + print(f"设置碰撞检测失败: {e}") + + def show_step_dialog(self): + """显示步骤指引对话框""" + if self.step_dialog: + self.step_dialog.close() + + # 获取主窗口作为父窗口 + parent_window = None + if hasattr(self.world, 'main_window') and self.world.main_window: + parent_window = self.world.main_window + + self.step_dialog = StepGuideDialog(self, parent_window) + self.step_dialog.show() + + def start_current_step(self): + """开始当前步骤""" + if self.current_step >= self.total_steps: + self.finish_interaction() + return + + step_data = self.config_data['steps'][self.current_step] + + # 兼容中文和英文配置格式 + operation_type = self.normalize_operation_type(step_data) + step_type = self.normalize_step_type(step_data) + target_model_name = step_data.get('target_model', '未设置') + + # 调试信息:显示字段映射结果 + print(f"🔄 字段映射: '{step_data.get('type', 'N/A')}' -> '{step_type}'") + print(f"🔄 操作映射: '{step_data.get('interaction_type', 'N/A')}' -> '{operation_type}'") + + # 使用 getChinese 处理步骤名称 + step_name = step_data.get('name', '未命名') + if hasattr(self.world, 'getChinese'): + step_name = self.world.getChinese(step_name) + target_model_name = self.world.getChinese(target_model_name) + + print(f"\n=== 开始第 {self.current_step + 1} 步: {step_name} ===") + print(f"目标模型: {target_model_name}") + print(f"操作类型: {operation_type}") + print(f"步骤类型: {step_type}") + + # 显示容差信息 + if step_type == 'assemble': + snap_tolerance = step_data.get('snap_tolerance') or step_data.get('tolerance', 5.0) + print(f"吸附容差: {snap_tolerance:.1f} 单位") + else: + disassemble_threshold = step_data.get('disassemble_threshold', 5.0) + print(f"拆卸阈值: {disassemble_threshold:.1f} 单位(距离原位)") + + # 检查模型的原始位置记录 + if target_model_name: + if target_model_name in self.model_original_positions: + original_pos = self.model_original_positions[target_model_name] + print(f"📍 模型 '{target_model_name}' 的原始位置: {original_pos}") + else: + # 如果没有记录,使用当前位置作为原始位置(但会给出警告) + target_node = self.find_model_node(target_model_name) + if target_node and not target_node.isEmpty(): + current_pos = target_node.getPos() + self.model_original_positions[target_model_name] = current_pos + self.model_current_states[target_model_name] = 'installed' + print(f"⚠️ 模型 '{target_model_name}' 没有预设原始位置,使用当前位置: {current_pos}") + print(" 建议在配置文件中预设模型的original_position以确保一致性") + + # 如果是安装步骤,确保模型显示 + if step_type == 'assemble' and target_model_name: + target_node = self.find_model_node(target_model_name) + if target_node and not target_node.isEmpty(): + if target_node.isHidden(): + print(f"👁️ 安装步骤开始 - 显示隐藏的模型: {target_model_name}") + target_node.show() + else: + print(f"✅ 模型 '{target_model_name}' 已经可见") + + # 播放步骤音频(如果有配置) + self.play_step_audio(step_data) + + # 更新维修系统GUI的步骤信息 + if self.maintenance_gui: + step_name = step_data.get('name', f'步骤 {self.current_step + 1}') + step_description = step_data.get('description', '') + + # 使用 getChinese 处理文本 + if hasattr(self.world, 'getChineseFont'): + step_name = self.world.getChineseFont() + step_description = self.world.getChineseFont() + + if self.mode == "exam": + # 考核模式:只显示步骤编号和名称,不显示描述 + step_info = f"Step {self.current_step + 1}/{self.total_steps}: {step_name}" + print(f"📝 考核模式步骤信息: {step_info}") + else: + # 训练模式:显示完整信息 + step_info = f"Step: {self.current_step + 1}/{self.total_steps}: {step_name}" + if step_description: + step_info += f"\n{step_description}" + print(f"📚 训练模式步骤信息: {step_info}") + + self.maintenance_gui.update_step_info(step_info) + + # 更新Qt步骤对话框(仅在非维修系统模式下) + if self.step_dialog and not self.maintenance_gui: + self.step_dialog.update_step_info(step_data, self.current_step + 1, self.total_steps) + + # 准备交互(主要是确保目标模型可以被拾取) + self.prepare_interaction(step_data) + + def prepare_interaction(self, step_data): + """准备交互,主要是设置目标模型的碰撞掩码""" + target_model_name = step_data.get('target_model') + if not target_model_name: + print("警告: 当前步骤没有指定目标模型") + return + + target_node = self.find_model_node(target_model_name) + if not target_node: + print(f"警告: 找不到目标模型 '{target_model_name}'") + return + + # 给目标模型设置碰撞掩码,使其可以被射线拾取 + # BitMask32.bit(1) 对应 setup_picking 中设置的 from_collide_mask + target_node.setCollideMask(BitMask32.bit(1)) + print(f"已为模型 '{target_model_name}' 设置碰撞掩码,等待用户点击。") + + def find_model_node(self, model_name): + """在场景中查找指定名称的模型节点""" + # find() 是Panda3D提供的更高效的节点查找方法 + return self.world.render.find(f"**/{model_name}") + + def list_scene_models(self): + """列出场景中的所有模型(调试用)""" + print("\n📋 场景中的模型列表:") + all_nodes = self.world.render.findAllMatches("**") + model_count = 0 + for node_path in all_nodes: + node_name = node_path.getName() + # 过滤掉系统节点和碰撞节点 + if (node_name and + node_name not in ['render', 'camera', 'aspect2d', 'pixel2d', 'cam'] and + not node_name.startswith("modelCollision_") and + not node_name.startswith("collision") and + not node_name.startswith("picker")): + print(f" - {node_name}") + model_count += 1 + print(f"总共找到 {model_count} 个模型节点\n") + + def show_models_status(self): + """显示所有模型的当前状态""" + if not self.model_original_positions: + print("📊 没有记录任何模型状态") + return + + print("\n📊 模型状态信息:") + for model_name, original_pos in self.model_original_positions.items(): + current_state = self.model_current_states.get(model_name, 'unknown') + model_node = self.find_model_node(model_name) + + if model_node and not model_node.isEmpty(): + current_pos = model_node.getPos() + distance_from_original = (current_pos - original_pos).length() + + status_icon = "🔧" if current_state == 'installed' else "📦" if current_state == 'removed' else "❓" + print(f" {status_icon} {model_name}:") + print(f" 状态: {current_state}") + print(f" 原始位置: {original_pos}") + print(f" 当前位置: {current_pos}") + print(f" 距离原位: {distance_from_original:.2f}") + else: + print(f" ❌ {model_name}: 找不到模型节点") + print() + + def record_current_positions(self): + """将当前所有模型的位置记录为原始位置""" + print("\n📍 重新记录所有模型的原始位置...") + + # 从步骤中提取所有模型 + all_models = set() + for step_data in self.config_data.get('steps', []): + target_model = step_data.get('target_model') + if target_model: + all_models.add(target_model) + + # 记录每个模型的当前位置为原始位置 + for model_name in all_models: + target_node = self.find_model_node(model_name) + if target_node and not target_node.isEmpty(): + current_pos = target_node.getPos() + self.model_original_positions[model_name] = current_pos + self.model_current_states[model_name] = 'installed' + print(f" 📍 {model_name}: 记录新的原始位置 {current_pos}") + else: + print(f" ❌ {model_name}: 在场景中找不到对应模型") + + print(f"📍 重新记录完成,共记录 {len(self.model_original_positions)} 个模型") + print("💡 提示: 如需永久保存这些位置,请在配置界面中重新保存配置文件") + + def normalize_step_type(self, step_data): + """标准化步骤类型,兼容中文和英文配置""" + # 优先使用英文字段 + step_type = step_data.get('step_type') + if step_type: + return step_type + + # 兼容中文字段 + type_field = step_data.get('type', '') + if type_field == '拆卸': + return 'disassemble' + elif type_field == '安装': + return 'assemble' + else: + return 'disassemble' # 默认值 + + def normalize_operation_type(self, step_data): + """标准化操作类型,兼容中文和英文配置""" + # 优先使用英文字段 + operation_type = step_data.get('operation_type') + if operation_type: + return operation_type + + # 兼容中文字段 + interaction_type = step_data.get('interaction_type', '') + if interaction_type == '鼠标拖拽': + return 'drag' + elif interaction_type == '点击触发': + return 'click' + else: + return 'drag' # 默认值 + + def analyze_final_state(self): + """分析最终状态,检查哪些模型没有正确回到原位""" + print("\n📊 最终状态分析:") + + if not self.model_original_positions: + print("没有模型状态记录") + return + + incorrect_models = [] + for model_name, original_pos in self.model_original_positions.items(): + model_node = self.find_model_node(model_name) + if model_node and not model_node.isEmpty(): + current_pos = model_node.getPos() + distance = (current_pos - original_pos).length() + current_state = self.model_current_states.get(model_name, 'unknown') + + # 检查是否应该在原位但实际不在原位的模型 + if current_state == 'installed' and distance > 1.0: # 容差1.0单位 + incorrect_models.append({ + 'name': model_name, + 'expected_pos': original_pos, + 'actual_pos': current_pos, + 'distance': distance + }) + print(f"❌ {model_name}: 应该在原位但偏离了 {distance:.2f} 单位") + print(f" 期望位置: {original_pos}") + print(f" 实际位置: {current_pos}") + elif current_state == 'installed' and distance <= 1.0: + print(f"✅ {model_name}: 正确安装在原位 (偏差 {distance:.2f})") + elif current_state == 'removed': + print(f"📦 {model_name}: 已拆卸 (距离原位 {distance:.2f})") + + if incorrect_models: + print(f"\n⚠️ 发现 {len(incorrect_models)} 个模型没有正确回到原位") + print("💡 建议检查安装步骤的配置和吸附容差设置") + else: + print("\n✅ 所有模型都处于正确状态") + + def qt_mouse_press_event(self, event): + """Qt鼠标按下事件处理(选择性拦截版)""" + from PyQt5.QtCore import Qt + + # 只拦截左键,其他按键使用原有处理方式 + if event.button() == Qt.LeftButton: + print('🖱️ 左键被拦截处理') + print(f'🖱️ 左键点击位置: ({event.x()}, {event.y()})') + + # 检查是否点击在GUI区域 + if self.maintenance_gui and self.is_click_in_gui_area(event.x(), event.y()): + print('🎯 点击在GUI区域,直接处理工具按钮点击') + # 直接处理工具按钮点击,不依赖DirectGUI事件 + button_clicked = self.handle_gui_button_click(event.x(), event.y()) + if button_clicked: + print(f'🎯 工具按钮点击已处理: {button_clicked}') + event.accept() + return + + # 如果不是按钮点击,让原有事件处理器处理 + if hasattr(self, 'original_mouse_press_event'): + self.original_mouse_press_event(event) + else: + event.ignore() + return + + # 点击在3D区域,进行拦截处理 + print('🎯 点击在3D区域,进行拦截处理') + self.handle_qt_mouse_click(event.x(), event.y()) + event.accept() + else: + # 其他按键使用原有处理方式 + if hasattr(self, 'original_mouse_press_event'): + self.original_mouse_press_event(event) + else: + event.ignore() + + def is_click_in_gui_area(self, click_x, click_y): + """检查鼠标点击是否在GUI区域(高精度版本)""" + try: + if not self.maintenance_gui: + return False + + # 获取窗口尺寸用于坐标转换 + if hasattr(self.world, 'qtWidget') and self.world.qtWidget: + widget_width = self.world.qtWidget.width() + widget_height = self.world.qtWidget.height() + else: + # 使用默认窗口尺寸 + widget_width = 1380 + widget_height = 750 + + # 使用与maintenance_gui完全相同的坐标转换算法 + aspect_x = (click_x / widget_width) * 2.67 - 1.33 + aspect_y = 1.0 - (click_y / widget_height) * 2.0 + + # 额外的坐标验证(与Panda3D标准坐标系对比) + # Panda3D aspect2d坐标系: x范围[-1.33, 1.33], y范围[-1.0, 1.0] + normalized_x = (click_x / widget_width) * 2.0 - 1.0 # 标准化到[-1, 1] + normalized_y = 1.0 - (click_y / widget_height) * 2.0 # 标准化到[-1, 1],Y轴翻转 + aspect_ratio = widget_width / widget_height + panda_x = normalized_x * aspect_ratio # 应用宽高比 + panda_y = normalized_y + + print(f"🔍 精确GUI区域检查: Qt({click_x}, {click_y}) -> aspect2d({aspect_x:.3f}, {aspect_y:.3f})") + print(f"🔍 验证坐标转换: 标准化({normalized_x:.3f}, {normalized_y:.3f}) -> Panda3D({panda_x:.3f}, {panda_y:.3f})") + print(f"📏 窗口信息: 尺寸({widget_width}x{widget_height}), 宽高比({aspect_ratio:.3f})") + + # 快速检查工具按钮区域 + if hasattr(self.maintenance_gui, 'available_tools') and self.maintenance_gui.available_tools: + # 工具按钮区域(底部条带) + tools_area_left = -1.1 + tools_area_right = 0.5 + tools_area_bottom = -1.0 + tools_area_top = -0.5 + + if (tools_area_left <= aspect_x <= tools_area_right and + tools_area_bottom <= aspect_y <= tools_area_top): + print(f"✅ 点击在工具按钮区域内") + return True + + # 检查其他GUI区域(步骤显示、当前工具、警告等) + other_gui_areas = [] + + # 步骤显示区域(顶部) + if hasattr(self.maintenance_gui, 'step_text') and self.maintenance_gui.step_text: + other_gui_areas.append({ + 'name': '步骤显示区域', + 'left': -1.8, 'right': 1.8, + 'bottom': 0.5, 'top': 1.1 + }) + + # 当前工具显示区域(右侧) + if hasattr(self.maintenance_gui, 'current_tool_text') and self.maintenance_gui.current_tool_text: + other_gui_areas.append({ + 'name': '当前工具显示区域', + 'left': 0.2, 'right': 1.8, + 'bottom': 0.6, 'top': 1.0 + }) + + # 警告显示区域(中央) + if hasattr(self.maintenance_gui, 'warning_text') and self.maintenance_gui.warning_text: + other_gui_areas.append({ + 'name': '警告显示区域', + 'left': -1.5, 'right': 1.5, + 'bottom': 0.0, 'top': 0.6 + }) + + # 检查其他GUI区域 + for area in other_gui_areas: + if (area['left'] <= aspect_x <= area['right'] and + area['bottom'] <= aspect_y <= area['top']): + print(f"✅ 点击在GUI区域内: {area['name']}") + return True + + print("❌ 点击不在任何GUI区域内") + return False + + except Exception as e: + print(f"❌ 检查GUI区域失败: {e}") + import traceback + traceback.print_exc() + return False + + def handle_gui_button_click(self, click_x, click_y): + """直接处理GUI按钮点击""" + try: + if not self.maintenance_gui: + return None + + # 获取窗口尺寸用于坐标转换 + if hasattr(self.world, 'qtWidget') and self.world.qtWidget: + widget_width = self.world.qtWidget.width() + widget_height = self.world.qtWidget.height() + else: + widget_width = 1380 + widget_height = 750 + + # 使用与maintenance_gui完全相同的坐标转换算法 + aspect_x = (click_x / widget_width) * 2.67 - 1.33 + aspect_y = 1.0 - (click_y / widget_height) * 2.0 + + print(f"🎯 直接处理按钮点击:Qt({click_x}, {click_y}) -> aspect2d({aspect_x:.3f}, {aspect_y:.3f})") + + # 检查工具按钮点击 + if hasattr(self.maintenance_gui, 'available_tools') and self.maintenance_gui.available_tools: + button_width = 0.4 + button_height = 0.25 + button_spacing = 0.45 + start_x = -0.8 + start_y = -0.75 + click_padding = 0.05 # 点击容差 + + for i, tool_data in enumerate(self.maintenance_gui.available_tools): + # 获取工具名称 + if isinstance(tool_data, dict): + tool_name = tool_data.get('name', str(tool_data)) + else: + tool_name = str(tool_data) + + # 计算按钮位置 + button_x = start_x + i * button_spacing + button_y = start_y + + # 计算按钮边界(包含容差) + left = button_x - (button_width/2 + click_padding) + right = button_x + (button_width/2 + click_padding) + bottom = button_y - (button_height/2 + click_padding) + top = button_y + (button_height/2 + click_padding) + + print(f"🔍 检查按钮{i} '{tool_name}': 范围x[{left:.3f}, {right:.3f}], y[{bottom:.3f}, {top:.3f}]") + + # 检查点击是否在按钮范围内 + if (left <= aspect_x <= right and bottom <= aspect_y <= top): + print(f"✅ 直接匹配按钮{i} '{tool_name}'!调用工具选择") + # 直接调用维修GUI的工具选择方法 + self.maintenance_gui.on_tool_selected(tool_name) + return tool_name + + print("❌ 点击不在任何按钮范围内") + return None + + except Exception as e: + print(f"❌ 直接处理按钮点击失败: {e}") + import traceback + traceback.print_exc() + return None + + def qt_mouse_release_event(self, event): + """Qt鼠标释放事件处理(维修系统优化版)""" + from PyQt5.QtCore import Qt + + # 只拦截左键释放,其他按键使用原有处理方式 + if event.button() == Qt.LeftButton: + print('🖱️ 左键释放被拦截处理') + + # 维修系统模式:先让原有处理器处理 + if self.maintenance_gui: + if hasattr(self, 'original_mouse_release_event'): + self.original_mouse_release_event(event) + + # 处理3D交互的鼠标释放 + self.on_mouse_up() + event.accept() + else: + # 其他按键释放使用原有处理方式 + if hasattr(self, 'original_mouse_release_event'): + self.original_mouse_release_event(event) + else: + event.ignore() + + def handle_qt_mouse_click(self, x, y): + """处理Qt鼠标点击(转换为3D世界坐标)""" + try: + if not self.is_active: + print("⚠️ 交互模式未激活") + return + + # 检查操作权限 + if not self.check_operation_permission(): + return + + # 获取当前步骤的目标模型 + if self.current_step >= self.total_steps: + print("⚠️ 所有步骤已完成") + return + + step_data = self.config_data['steps'][self.current_step] + target_model_name = step_data.get('target_model') + + if not target_model_name: + print("⚠️ 当前步骤没有指定目标模型") + return + + print(f'🎯 检测点击目标: {target_model_name}') + + # 获取窗口尺寸进行坐标转换 + if hasattr(self.world, 'qtWidget') and self.world.qtWidget: + widget = self.world.qtWidget + win_width = widget.width() + win_height = widget.height() + + # 转换为Panda3D的标准化坐标 + mx = 2.0 * x / float(win_width) - 1.0 + my = 1.0 - 2.0 * y / float(win_height) + + # 执行射线检测 + self.perform_ray_cast(mx, my, target_model_name) + else: + print("❌ 找不到Qt部件,无法进行坐标转换") + + except Exception as e: + print(f"❌ Qt鼠标点击处理失败: {e}") + import traceback + traceback.print_exc() + + def perform_ray_cast(self, mx, my, target_model_name): + """执行射线检测""" + try: + # 获取鼠标射线 + from panda3d.core import Point3, Point2, CollisionTraverser, CollisionHandlerQueue + from panda3d.core import CollisionNode, CollisionRay, BitMask32 + + near_point = Point3() + far_point = Point3() + self.world.cam.node().getLens().extrude(Point2(mx, my), near_point, far_point) + + # 转换到世界坐标系 + world_near = self.world.render.getRelativePoint(self.world.camera, near_point) + world_far = self.world.render.getRelativePoint(self.world.camera, far_point) + + # 进行真正的射线检测 + picker = CollisionTraverser() + queue = CollisionHandlerQueue() + + # 创建射线碰撞检测 + pickerNode = CollisionNode('mouseRay') + pickerNP = self.world.camera.attachNewNode(pickerNode) + + # 设置射线的碰撞掩码,匹配模型的碰撞掩码(第2位) + pickerNode.setFromCollideMask(BitMask32.bit(1)) + + # 使用相机坐标系的点创建射线 + direction = far_point - near_point + direction.normalize() + pickerNode.addSolid(CollisionRay(near_point, direction)) + + picker.addCollider(pickerNP, queue) + picker.traverse(self.world.render) + + # 检查是否点击到了目标模型 + hit_target_model = False + hit_point = None + + if queue.getNumEntries() > 0: + # 遍历所有碰撞结果 + for i in range(queue.getNumEntries()): + entry = queue.getEntry(i) + hit_node_path = entry.getIntoNodePath() + hit_pos = entry.getSurfacePoint(self.world.render) + + # 检查是否击中了目标模型或其相关的碰撞体 + if self.is_target_model_hit(hit_node_path, target_model_name): + print(f"✅ 击中目标模型: {hit_node_path.getName()}") + hit_target_model = True + hit_point = hit_pos + break + + # 清理碰撞检测节点 + pickerNP.removeNode() + + if hit_target_model: + # 查找目标模型节点 + target_node = self.find_model_node(target_model_name) + if target_node and not target_node.isEmpty(): + # 根据操作类型决定行为 + step_data = self.config_data['steps'][self.current_step] + operation_type = self.normalize_operation_type(step_data) + + if operation_type == 'drag': + print(f'✅ 开始拖拽目标模型: {target_node.getName()}') + self.start_dragging(target_node, hit_point) + elif operation_type == 'click': + print(f'✅ 点击触发位移模型: {target_node.getName()}') + self.trigger_click_movement(target_node, step_data) + else: + print(f"❌ 未知的操作类型: {operation_type}") + else: + print(f"❌ 找不到目标模型节点: {target_model_name}") + else: + print(f"❌ 没有点击到目标模型 '{target_model_name}',请点击正确的模型") + + except Exception as e: + print(f"❌ 射线检测失败: {e}") + import traceback + traceback.print_exc() + + def is_target_model_hit(self, hit_node_path, target_model_name): + """检查击中的节点是否是目标模型""" + hit_name = hit_node_path.getName() + + # 直接匹配模型名称 + if hit_name == target_model_name: + return True + + # 检查是否是模型的碰撞体(通常以modelCollision_开头) + if hit_name.startswith("modelCollision_"): + collision_model_name = hit_name.replace("modelCollision_", "", 1) + if collision_model_name == target_model_name: + return True + + # 检查父节点是否是目标模型 + parent = hit_node_path.getParent() + while parent and not parent.isEmpty() and parent != self.world.render: + parent_name = parent.getName() + if parent_name == target_model_name: + return True + parent = parent.getParent() + + # 检查子节点路径 + current = hit_node_path + while current and not current.isEmpty(): + if current.getName() == target_model_name: + return True + current = current.getParent() + + return False + + def on_mouse_down(self): + """鼠标按下事件(优化版 v2:处理兄弟节点结构的碰撞体)""" + if not self.is_active or not self.world.mouseWatcherNode.hasMouse(): + return + # 获取鼠标位置并发射射线 + mouse_pos = self.world.mouseWatcherNode.getMouse() + picker_ray = self.picker_ray_node.node().getSolid(0) + picker_ray.setFromLens(self.world.camNode, mouse_pos.getX(), mouse_pos.getY()) + + # 执行遍历 + self.picker_traverser.traverse(self.world.render) + + if self.collision_handler.getNumEntries() > 0: + # 获取最近的碰撞点 + self.collision_handler.sortEntries() + entry = self.collision_handler.getEntry(0) + + hit_node_path = entry.getIntoNodePath() + hit_point = entry.getSurfacePoint(self.world.render) + + # --- 这是新的逻辑 --- + # 打印被击中的碰撞体名字,用于调试 + print(f"射线击中了碰撞节点: {hit_node_path.getName()}") + + # 根据命名约定推导模型名称 + hit_name = hit_node_path.getName() + derived_model_name = "" + + # 假设碰撞节点的命名规则是 "前缀 + 模型名" + if hit_name.startswith("modelCollision_"): + derived_model_name = hit_name.replace("modelCollision_", "", 1) + else: + derived_model_name = hit_name + + print(f"根据命名约定,推导出的模型名称为: {derived_model_name}") + + # 获取当前步骤的目标模型名称 + step_data = self.config_data['steps'][self.current_step] + target_model_name = step_data.get('target_model') + + # 检查推导出的名称是否是当前步骤需要操作的目标 + if derived_model_name == target_model_name: + print(f"✅ 击中正确!目标是 '{target_model_name}'") + + # 根据推导出的正确名称,去场景中查找可以被拖动的那个节点 + model_to_drag = self.find_model_node(derived_model_name) + + if model_to_drag and not model_to_drag.isEmpty(): + self.start_dragging(model_to_drag, hit_point) + else: + print(f"❌ 错误:虽然名称匹配,但在场景中找不到名为 '{derived_model_name}' 的可拖动节点!") + else: + print(f"❌ 击中的不是当前目标模型 (需要 '{target_model_name}', 击中了 '{derived_model_name}')") + def start_dragging(self, model_node, hit_point): + """开始拖拽模型(优化版:计算偏移量)""" + # 再次检查操作权限(双重保险) + if not self.check_operation_permission(): + print("⚠️ 权限检查失败,无法开始拖拽") + return + + self.dragging_model = model_node + + # 1. 定义一个拖拽平面。这里我们使用一个平行于XZ的平面(法线为Y轴), + # 并且该平面穿过模型的初始中心点。 + # 这意味着模型将在其初始高度上水平移动。 + # 您可以根据需要更改法线,例如 Vec3(0, 0, 1) 表示在XY平面上拖拽。 + plane_normal = Vec3(0, 1, 0) + self.drag_plane = Plane(plane_normal, model_node.getPos()) + + # 2. 计算鼠标射线与拖拽平面的交点。 + # 这个交点是鼠标在“拖拽世界”中的3D位置。 + plane_intersection_point = Point3() + if self.drag_plane.intersectsLine(plane_intersection_point, + self.world.camera.getPos(self.world.render), + hit_point): + # 3. 计算模型中心点和这个交点之间的偏移量。 + # 在整个拖拽过程中,我们将维持这个偏移量,以避免模型跳跃。 + self.drag_offset = model_node.getPos() - plane_intersection_point + print(f"开始拖拽模型: {model_node.getName()}") + print(f"初始位置: {model_node.getPos()}") + + # 启动拖拽任务 + self.world.taskMgr.add(self.drag_task, "drag_model_task") + else: + print("警告:无法计算拖拽平面的交点,拖拽取消。") + self.dragging_model = None + + def drag_task(self, task): + """拖拽任务(优化版:应用偏移量)""" + if not self.dragging_model or not self.world.mouseWatcherNode.hasMouse(): + # 如果模型被置空或鼠标不在窗口内,则停止任务 + return task.done + + # 获取最新的鼠标位置 + mouse_pos = self.world.mouseWatcherNode.getMouse() + + # 再次从相机发射射线以获取鼠标在3D空间中的指向 + near_point = Point3() + far_point = Point3() + self.world.cam.node().getLens().extrude(mouse_pos, near_point, far_point) + + # 将近点和远点转换到世界坐标系 + origin_point = self.world.render.getRelativePoint(self.world.camera, near_point) + direction_point = self.world.render.getRelativePoint(self.world.camera, far_point) + + # 计算射线与我们之前定义的拖拽平面的交点 + intersection_point = Point3() + if self.drag_plane.intersectsLine(intersection_point, origin_point, direction_point): + # 新的位置 = 当前鼠标在拖拽平面上的3D位置 + 初始偏移量 + new_pos = intersection_point + self.drag_offset + self.dragging_model.setPos(new_pos) + + # 初始化距离打印时间(如果还没有) + if not hasattr(self, '_last_distance_print_time'): + self._last_distance_print_time = 0 + + # 在拖拽过程中,实时显示距离原始位置的距离 + if hasattr(self, 'config_data') and self.current_step < len(self.config_data.get('steps', [])): + step_data = self.config_data['steps'][self.current_step] + step_type = self.normalize_step_type(step_data) + target_model_name = step_data.get('target_model', '') + + if (target_model_name in self.model_original_positions and + hasattr(self, '_last_distance_print_time')): + + # 每0.5秒显示一次距离信息 + current_time = task.time + if current_time - getattr(self, '_last_distance_print_time', 0) > 0.5: + original_pos = self.model_original_positions[target_model_name] + distance = (new_pos - original_pos).length() + + if step_type == 'assemble': + snap_tolerance = step_data.get('snap_tolerance') or step_data.get('tolerance', 5.0) + if distance <= snap_tolerance * 1.2: # 只在接近时显示 + print(f"📏 安装拖拽中 - 距离原位: {distance:.2f} (需要 <= {snap_tolerance:.2f})") + else: # 拆卸模式 + disassemble_threshold = step_data.get('disassemble_threshold', 5.0) + if distance >= disassemble_threshold * 0.8: # 接近阈值时显示 + print(f"📏 拆卸拖拽中 - 距离原位: {distance:.2f} (需要 >= {disassemble_threshold:.2f})") + + self._last_distance_print_time = current_time + + return task.cont + + def stop_dragging(self): + """停止拖拽""" + if not self.dragging_model: + return + + print(f"停止拖拽模型: {self.dragging_model.getName()}") + print(f"最终位置: {self.dragging_model.getPos()}") + + # 停止拖拽任务 + self.world.taskMgr.remove("drag_model_task") + + # 检查是否完成了当前步骤 + self.check_step_completion() + + # 清理拖拽状态 + self.dragging_model = None + + def on_mouse_up(self): + """鼠标抬起事件""" + self.stop_dragging() + + def check_step_completion(self): + """检查步骤是否完成""" + if not self.dragging_model: + return + + step_data = self.config_data['steps'][self.current_step] + step_type = self.normalize_step_type(step_data) + print(f"检查步骤完成: {step_type} ----------------------------------------") + target_model_name = step_data.get('target_model', '') + current_pos = self.dragging_model.getPos() + + if step_type == 'assemble': + # 安装模式:检查是否接近原始位置 + if target_model_name in self.model_original_positions: + original_pos = self.model_original_positions[target_model_name] + distance_to_original = (original_pos - current_pos).length() + # 吸附容差:优先使用配置的snap_tolerance,否则使用拆卸时的tolerance,最后默认5.0 + snap_tolerance = step_data.get('snap_tolerance') or step_data.get('tolerance', 5.0) + + print(f"🔧 安装模式 - 距离原始位置: {distance_to_original:.2f}") + print(f"🎯 吸附容差: {snap_tolerance:.2f}") + print(f"🔍 吸附条件: {distance_to_original:.2f} <= {snap_tolerance:.2f} = {distance_to_original <= snap_tolerance}") + + if distance_to_original <= snap_tolerance: + # 自动吸附到原始位置 + print(f"✨ 自动吸附到原始位置!") + print(f"📍 精确位置: {original_pos}") + self.dragging_model.setPos(original_pos) + self.model_current_states[target_model_name] = 'installed' + print(f"步骤 {self.current_step + 1} 完成!模型已安装到原始位置") + self.complete_current_step() + return + else: + print(f"距离原始位置还有 {distance_to_original:.2f} 单位,需要更接近才能自动吸附") + else: + print("⚠️ 没有找到模型的原始位置记录") + + else: + # 拆卸模式:检查是否远离原始位置 + if target_model_name in self.model_original_positions: + original_pos = self.model_original_positions[target_model_name] + distance_from_original = (current_pos - original_pos).length() + disassemble_threshold = step_data.get('disassemble_threshold', 5.0) # 拆卸距离阈值 + + print(f"🔧 拆卸模式 - 距离原始位置: {distance_from_original:.2f}") + print(f"🎯 拆卸阈值: {disassemble_threshold:.2f}") + print(f"🔍 拆卸条件: {distance_from_original:.2f} >= {disassemble_threshold:.2f} = {distance_from_original >= disassemble_threshold}") + + if distance_from_original >= disassemble_threshold: + print(f"步骤 {self.current_step + 1} 完成!模型已拆卸,距离原位 {distance_from_original:.2f} >= {disassemble_threshold:.2f}") + if target_model_name: + self.model_current_states[target_model_name] = 'removed' + self.complete_current_step() + return + else: + print(f"需要将模型拖拽到距离原位至少 {disassemble_threshold:.2f} 单位(当前 {distance_from_original:.2f})") + else: + # 如果没有原始位置记录,回退到原来的目标位置模式 + print("⚠️ 没有找到模型的原始位置记录,使用目标位置模式") + target_pos_data = step_data.get('target_position') + + if target_pos_data is None: + # 如果没有定义目标位置,拆卸后即完成 + print("拆卸模式 - 没有定义目标位置,视为完成。") + if target_model_name: + self.model_current_states[target_model_name] = 'removed' + self.complete_current_step() + return + + target_pos = Vec3(*target_pos_data) + distance = (target_pos - current_pos).length() + tolerance = step_data.get('tolerance', 5) + + if distance <= tolerance: + print(f"步骤 {self.current_step + 1} 完成!拆卸到目标位置 距离 {distance:.2f} < 容差 {tolerance}") + if target_model_name: + self.model_current_states[target_model_name] = 'removed' + self.complete_current_step() + else: + print(f"距离目标位置还有 {distance:.2f} 单位,未达到容差 {tolerance}") + + def complete_current_step(self): + """完成当前步骤""" + # 记录操作完成(考核模式下) + if self.mode == "exam" and self.current_step in self.step_scores: + self.step_scores[self.current_step]['operation_attempts'] += 1 + print(f"📝 考核记录:步骤 {self.current_step + 1} 完成,操作次数: {self.step_scores[self.current_step]['operation_attempts']}") + + # 在进入下一步之前,取消当前模型的碰撞,避免干扰 + step_data = self.config_data['steps'][self.current_step] + target_model_name = step_data.get('target_model') + if target_model_name: + node = self.find_model_node(target_model_name) + if node: + node.setCollideMask(BitMask32.allOff()) + + # 显示当前所有模型的状态 + self.show_models_status() + + self.current_step += 1 + + if self.current_step >= self.total_steps: + self.finish_interaction() + else: + self.start_current_step() + + def finish_interaction(self): + """完成所有交互""" + print("\n🎉 所有拆装步骤已完成!") + self.is_active = False + self.ignoreAll() + + # 清理维修系统GUI + if self.maintenance_gui: + self.maintenance_gui.cleanup_gui() + print("✅ 维修系统GUI已清理") + + # 恢复原有的Qt鼠标事件处理 + print("🔄 恢复原有的Qt鼠标事件处理") + if hasattr(self, 'original_mouse_press_event') and hasattr(self.world, 'qtWidget') and self.world.qtWidget: + self.world.qtWidget.mousePressEvent = self.original_mouse_press_event + self.world.qtWidget.mouseReleaseEvent = self.original_mouse_release_event + print("✅ 原有Qt鼠标事件处理已恢复") + else: + print("⚠️ 没有找到备份的Qt事件处理方法") + + if self.step_dialog: + self.step_dialog.close() + self.step_dialog = None + + # 根据模式显示不同的完成结果 + if self.mode == "exam": + # 考核模式:显示详细的考核结果 + self.show_exam_results() + else: + # 训练模式:显示训练完成提示 + print("\n🎉 训练模式完成!") + completion_msg = "🎉 训练完成!\n\n所有维修步骤已完成!\n现在可以尝试考核模式检验学习成果。" + + if self.maintenance_gui: + self.maintenance_gui.update_step_info(completion_msg) + print("📋 训练完成信息已在GUI中显示") + else: + QMessageBox.information(None, "训练完成", completion_msg) + + def stop_interaction(self): + """用户主动停止交互""" + print("\n🛑 用户主动停止拆装交互") + + try: + # 设置为非活动状态 + self.is_active = False + self.ignoreAll() + + # 清理维修系统GUI + if self.maintenance_gui: + self.maintenance_gui.cleanup_gui() + print("✅ 维修系统GUI已清理") + + # 恢复原有的Qt鼠标事件处理 + print("🔄 恢复原有的Qt鼠标事件处理") + if hasattr(self, 'original_mouse_press_event') and hasattr(self.world, 'qtWidget') and self.world.qtWidget: + self.world.qtWidget.mousePressEvent = self.original_mouse_press_event + self.world.qtWidget.mouseReleaseEvent = self.original_mouse_release_event + print("✅ 原有Qt鼠标事件处理已恢复") + else: + print("⚠️ 没有找到备份的Qt事件处理方法") + + # 关闭步骤对话框 + if self.step_dialog: + self.step_dialog.close() + self.step_dialog = None + + # 显示停止确认信息 + if self.mode == "exam": + stop_msg = "🛑 考核已停止\n\n您的考核进度没有被保存。" + else: + stop_msg = "🛑 训练已停止\n\n您可以随时重新开始训练。" + + if self.maintenance_gui: + # 如果GUI还在,显示停止信息 + self.maintenance_gui.update_step_info(stop_msg) + else: + QMessageBox.information(None, "操作停止", stop_msg) + + print("✅ 拆装交互已停止") + + except Exception as e: + print(f"❌ 停止拆装交互失败: {e}") + import traceback + traceback.print_exc() + + def show_exam_results(self): + """显示考核结果""" + try: + # 计算最终得分 + final_score = 0 + total_operations = 0 + total_errors = 0 + + for step_record in self.step_scores.values(): + final_score += step_record['current_score'] + total_operations += step_record['operation_attempts'] + if step_record['tool_error']: + total_errors += 1 + + # 计算得分率和准确率 + score_rate = (final_score / self.exam_max_score * 100) if self.exam_max_score > 0 else 0 + accuracy_rate = ((self.total_steps - total_errors) / self.total_steps * 100) if self.total_steps > 0 else 100 + + # 生成考核报告 + print(f"\n{'='*50}") + print(f"🎓 维修技能考核结果报告") + print(f"{'='*50}") + print(f"📊 总体成绩:") + print(f" 总得分: {final_score:.0f} / {self.exam_max_score:.0f} 分") + print(f" 得分率: {score_rate:.1f}%") + print(f" 操作准确率: {accuracy_rate:.1f}%") + print(f" 总操作次数: {total_operations}") + print(f" 错误次数: {total_errors}") + + # 评级系统 + if score_rate >= 90: + grade = "优秀" + grade_icon = "🏆" + grade_desc = "表现卓越,技能熟练" + elif score_rate >= 80: + grade = "良好" + grade_icon = "🥈" + grade_desc = "表现良好,基本掌握" + elif score_rate >= 60: + grade = "及格" + grade_icon = "✅" + grade_desc = "达到基本要求" + else: + grade = "不及格" + grade_icon = "❌" + grade_desc = "需要加强练习" + + print(f"\n🏅 评级: {grade_icon} {grade}") + print(f" 评语: {grade_desc}") + + # 详细步骤分析 + print(f"\n📋 详细步骤分析:") + print(f"{'-'*40}") + + perfect_steps = 0 + for step_idx, step_record in self.step_scores.items(): + step_data = self.config_data['steps'][step_idx] + step_name = step_data.get('name', f'步骤{step_idx+1}') + max_score = step_record['max_score'] + current_score = step_record['current_score'] + tool_error = step_record['tool_error'] + attempts = step_record['operation_attempts'] + + # 判断步骤完成质量 + if current_score >= max_score: + step_status = "🟢 完美" + perfect_steps += 1 + elif current_score >= max_score * 0.8: + step_status = "🟡 良好" + elif current_score > 0: + step_status = "🟠 一般" + else: + step_status = "🔴 失败" + + print(f"{step_name}:") + print(f" 状态: {step_status}") + print(f" 得分: {current_score:.0f}/{max_score:.0f} 分") + print(f" 工具使用: {'❌ 错误' if tool_error else '✅ 正确'}") + print(f" 操作次数: {attempts}") + print() + + # 生成改进建议 + suggestions = [] + if total_errors > 0: + suggestions.append("• 注意选择正确的工具进行操作") + if total_operations > self.total_steps * 1.5: + suggestions.append("• 减少不必要的操作,提高操作效率") + if perfect_steps < self.total_steps * 0.5: + suggestions.append("• 多练习以提高操作的精确度") + if score_rate < 80: + suggestions.append("• 建议重新学习相关理论知识") + + if suggestions: + print("💡 改进建议:") + for suggestion in suggestions: + print(f" {suggestion}") + print() + + print(f"{'='*50}") + + # 为GUI显示准备简化版本的报告 + gui_report = f"🎓 考核完成!\n\n" + gui_report += f"📊 成绩总览:\n" + gui_report += f"总得分: {final_score:.0f}/{self.exam_max_score:.0f} 分 ({score_rate:.1f}%)\n" + gui_report += f"评级: {grade_icon} {grade}\n" + gui_report += f"操作准确率: {accuracy_rate:.1f}%\n\n" + gui_report += f"完美完成步骤: {perfect_steps}/{self.total_steps}\n" + gui_report += f"总操作次数: {total_operations}\n" + gui_report += f"错误次数: {total_errors}" + + # 在维修GUI中显示详细考核结果 + if self.maintenance_gui: + # 创建详细的考核结果数据 + exam_result_data = { + 'final_score': final_score, + 'max_score': self.exam_max_score, + 'score_rate': score_rate, + 'accuracy_rate': accuracy_rate, + 'grade': grade, + 'grade_icon': grade_icon, + 'grade_desc': grade_desc, + 'total_operations': total_operations, + 'total_errors': total_errors, + 'perfect_steps': perfect_steps, + 'total_steps': self.total_steps, + 'step_details': [], + 'suggestions': suggestions + } + + # 添加每步详情 + for step_idx, step_record in self.step_scores.items(): + step_data = self.config_data['steps'][step_idx] + step_name = step_data.get('name', f'步骤{step_idx+1}') + max_score = step_record['max_score'] + current_score = step_record['current_score'] + tool_error = step_record['tool_error'] + attempts = step_record['operation_attempts'] + + # 判断步骤完成质量 + if current_score >= max_score: + step_status = "🟢 完美" + elif current_score >= max_score * 0.8: + step_status = "🟡 良好" + elif current_score > 0: + step_status = "🟠 一般" + else: + step_status = "🔴 失败" + + exam_result_data['step_details'].append({ + 'name': step_name, + 'status': step_status, + 'current_score': current_score, + 'max_score': max_score, + 'tool_error': tool_error, + 'attempts': attempts + }) + + # 使用GUI显示考核结果 + self.maintenance_gui.show_exam_results(exam_result_data) + print("📋 考核结果已在GUI中显示") + else: + # 如果没有GUI,使用简单的控制台输出 + print("⚠️ 维修GUI不可用,考核结果仅在控制台显示") + + # 保存考核结果到文件(可选) + self.save_exam_results(final_score, score_rate, grade) + + except Exception as e: + print(f"❌ 显示考核结果失败: {e}") + import traceback + traceback.print_exc() + QMessageBox.critical(None, "错误", f"显示考核结果失败: {str(e)}") + + def save_exam_results(self, final_score, score_rate, grade): + """保存考核结果到文件""" + try: + import datetime + import json + + # 生成考核记录 + exam_record = { + "timestamp": datetime.datetime.now().isoformat(), + "mode": "exam", + "final_score": final_score, + "max_score": self.exam_max_score, + "score_rate": score_rate, + "grade": grade, + "total_steps": self.total_steps, + "step_details": {} + } + + # 添加每步详情 + for step_idx, step_record in self.step_scores.items(): + step_name = self.config_data['steps'][step_idx].get('name', f'步骤{step_idx+1}') + exam_record["step_details"][step_name] = { + "max_score": step_record['max_score'], + "current_score": step_record['current_score'], + "tool_error": step_record['tool_error'], + "operation_attempts": step_record['operation_attempts'] + } + + # 保存到文件 + filename = f"exam_result_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.json" + filepath = os.path.join(".", filename) + + with open(filepath, 'w', encoding='utf-8') as f: + json.dump(exam_record, f, ensure_ascii=False, indent=2) + + print(f"📄 考核结果已保存到: {filepath}") + + except Exception as e: + print(f"⚠️ 保存考核结果失败: {e}") + # 不影响主流程,只是记录警告 + + def stop_interaction_mode(self): + """停止交互模式""" + self.is_active = False + self.ignoreAll() + + # 恢复原有的Qt鼠标事件处理 + print("🔄 恢复原有的Qt鼠标事件处理") + if hasattr(self, 'original_mouse_press_event') and hasattr(self.world, 'qtWidget') and self.world.qtWidget: + self.world.qtWidget.mousePressEvent = self.original_mouse_press_event + self.world.qtWidget.mouseReleaseEvent = self.original_mouse_release_event + print("✅ 原有Qt鼠标事件处理已恢复") + else: + print("⚠️ 没有找到备份的Qt事件处理方法") + + if self.step_dialog: + self.step_dialog.close() + self.step_dialog = None + + self.world.taskMgr.remove("drag_model_task") + print("拆装交互模式已停止") + + def trigger_drag_by_keyboard(self): + """通过键盘触发拖拽(备用方案)""" + if not self.is_active or self.dragging_model: + return + + print("⌨️ 使用键盘备用方案触发拖拽") + step_data = self.config_data['steps'][self.current_step] + target_model_name = step_data.get('target_model') + + if not target_model_name: + print("当前步骤没有指定目标模型") + return + + target_node = self.find_model_node(target_model_name) + if not target_node: + print(f"找不到目标模型: {target_model_name}") + return + + # 对于键盘触发,我们无法获取精确的hit_point, + # 所以直接使用模型的中心点作为模拟的点击点。 + self.start_dragging(target_node, target_node.getPos()) + + def trigger_click_movement(self, model_node, step_data): + """触发点击位移动画""" + try: + # 检查操作权限(双重保险) + if not self.check_operation_permission(): + print("⚠️ 权限检查失败,无法进行点击触发位移") + return + + step_type = self.normalize_step_type(step_data) + target_model_name = step_data.get('target_model', '') + + print(f"🎬 开始点击触发位移: {model_node.getName()}") + print(f"📋 步骤类型: {step_type}") + + if step_type == 'disassemble': + # 拆卸模式:移动到目标位置然后隐藏 + self.perform_disassemble_click_movement(model_node, step_data) + elif step_type == 'assemble': + # 安装模式:显示模型然后移动到原始位置 + self.perform_assemble_click_movement(model_node, step_data) + else: + print(f"❌ 未知的步骤类型: {step_type}") + + except Exception as e: + print(f"❌ 点击触发位移失败: {e}") + import traceback + traceback.print_exc() + + def perform_disassemble_click_movement(self, model_node, step_data): + """执行拆卸点击位移""" + target_model_name = step_data.get('target_model', '') + + # 记录当前位置为原始位置(如果还没有记录) + if target_model_name not in self.model_original_positions: + current_pos = model_node.getPos() + self.model_original_positions[target_model_name] = current_pos + print(f"📍 记录模型 '{target_model_name}' 的原始位置: {current_pos}") + + # 获取目标位移位置 + target_pos_data = step_data.get('target_position') + if not target_pos_data: + print("❌ 拆卸步骤缺少target_position配置") + return + + target_pos = Vec3(*target_pos_data) + print(f"🎯 目标位置: {target_pos}") + + # 获取动画时长 + animation_duration = step_data.get('animation_duration', 2.0) + + # 创建位移动画序列 + from direct.interval.IntervalGlobal import LerpPosInterval, Sequence, Func + + move_interval = LerpPosInterval( + model_node, + animation_duration, + target_pos, + name=f"disassemble_{target_model_name}" + ) + + # 动画完成后的回调 + def on_disassemble_complete(): + print(f"✅ 拆卸动画完成,隐藏模型: {target_model_name}") + model_node.hide() # 隐藏模型 + self.model_current_states[target_model_name] = 'removed' + self.complete_current_step() # 完成当前步骤 + + # 创建动画序列 + disassemble_sequence = Sequence( + move_interval, + Func(on_disassemble_complete), + name=f"disassemble_sequence_{target_model_name}" + ) + + print(f"🎬 开始拆卸动画,持续时间: {animation_duration}秒") + disassemble_sequence.start() + + def perform_assemble_click_movement(self, model_node, step_data): + """执行安装点击位移""" + target_model_name = step_data.get('target_model', '') + + # 确保模型可见(通常在步骤开始时已经显示,这里是双重保险) + if model_node.isHidden(): + print(f"👁️ 点击触发时显示模型: {target_model_name}") + model_node.show() + else: + print(f"✅ 模型 '{target_model_name}' 已经可见,开始安装动画") + + # 获取原始位置 + if target_model_name not in self.model_original_positions: + print(f"❌ 找不到模型 '{target_model_name}' 的原始位置") + return + + original_pos = self.model_original_positions[target_model_name] + print(f"🏠 原始位置: {original_pos}") + + # 获取动画时长 + animation_duration = step_data.get('animation_duration', 2.0) + + # 创建位移动画序列 + from direct.interval.IntervalGlobal import LerpPosInterval, Sequence, Func + + move_interval = LerpPosInterval( + model_node, + animation_duration, + original_pos, + name=f"assemble_{target_model_name}" + ) + + # 动画完成后的回调 + def on_assemble_complete(): + print(f"✅ 安装动画完成,模型回到原位: {target_model_name}") + model_node.setPos(original_pos) # 确保精确位置 + self.model_current_states[target_model_name] = 'installed' + self.complete_current_step() # 完成当前步骤 + + # 创建动画序列 + assemble_sequence = Sequence( + move_interval, + Func(on_assemble_complete), + name=f"assemble_sequence_{target_model_name}" + ) + + print(f"🎬 开始安装动画,持续时间: {animation_duration}秒") + assemble_sequence.start() + + +class StepGuideDialog(QDialog): + """步骤指引对话框(UI代码保持不变)""" + + def __init__(self, interaction_manager, parent=None): + super().__init__(parent) + self.interaction_manager = interaction_manager + self.current_required_tool = "无" # 当前步骤要求的工具 + self.mode = interaction_manager.mode # 获取模式 + + self.chinese_font = None + if hasattr(interaction_manager.world,'getChineseFont'): + self.chinese_font = interaction_manager.world.getChineseFont() + + self.setupUI() + + def setupUI(self): + if self.mode == "exam": + self.setWindowTitle("拆装考核") + else: + self.setWindowTitle("拆装步骤指引") + + self.setFixedSize(450, 400) + self.setWindowFlags(Qt.Window | Qt.WindowStaysOnTopHint) + layout = QVBoxLayout(self) + + # 步骤信息 + self.step_info_label = QLabel("准备开始...") + self.step_info_label.setStyleSheet("font-size: 14px; font-weight: bold; color: #2E86C1;") + if self.chinese_font: + self.step_info_label.setFont(self.chinese_font) + layout.addWidget(self.step_info_label) + + # 步骤描述(考核模式下隐藏) + if self.mode != "exam": + self.step_desc_text = QTextEdit() + self.step_desc_text.setMaximumHeight(100) + self.step_desc_text.setReadOnly(True) + if self.chinese_font: + self.step_desc_text.setFont(self.chinese_font) + layout.addWidget(self.step_desc_text) + else: + self.step_desc_text = None + + # 工具选择组 + tool_group = QGroupBox("工具选择") + if self.chinese_font: + tool_group.setFont(self.chinese_font) + tool_layout = QVBoxLayout(tool_group) + + # 在训练模式下显示要求的工具,考核模式下不显示 + if self.mode != "exam": + self.required_tool_label = QLabel("当前步骤要求工具: 无") + self.required_tool_label.setStyleSheet("font-weight: bold; color: #E74C3C;") + if self.chinese_font: + self.required_tool_label.setFont(self.chinese_font) + tool_layout.addWidget(self.required_tool_label) + else: + self.required_tool_label = None + + current_tool_layout = QHBoxLayout() + current_tool_label = QLabel("当前选择工具:") + if self.chinese_font: + current_tool_label.setFont(self.chinese_font) + current_tool_layout.addWidget(current_tool_label) + self.current_tool_combo = QComboBox() + self.current_tool_combo.addItem("无") # 默认只有"无"选项 + self.current_tool_combo.currentTextChanged.connect(self.on_tool_changed) + if self.chinese_font: + self.current_tool_combo.setFont(self.chinese_font) + current_tool_layout.addWidget(self.current_tool_combo) + tool_layout.addLayout(current_tool_layout) + + # 在训练模式下显示工具状态,考核模式下不显示 + if self.mode != "exam": + self.tool_status_label = QLabel("✅ 工具匹配,可以进行操作") + self.tool_status_label.setStyleSheet("color: #27AE60; font-weight: bold;") + if self.chinese_font: + self.tool_status_label.setFont(self.chinese_font) + tool_layout.addWidget(self.tool_status_label) + else: + self.tool_status_label = None + + layout.addWidget(tool_group) + + # 操作提示(考核模式下隐藏) + if self.mode != "exam": + self.operation_label = QLabel("操作提示:") + self.operation_label.setStyleSheet("font-weight: bold;") + if self.chinese_font: + self.operation_label.setFont(self.chinese_font) + layout.addWidget(self.operation_label) + self.operation_text = QTextEdit() + self.operation_text.setMaximumHeight(80) + self.operation_text.setReadOnly(True) + if self.chinese_font: + self.operation_text.setFont(self.chinese_font) + layout.addWidget(self.operation_text) + else: + self.operation_label = None + self.operation_text = None + + # 按钮 + button_layout = QHBoxLayout() + self.skip_button = QPushButton("跳过当前步骤") + if self.chinese_font: + self.skip_button.setFont(self.chinese_font) + self.skip_button.clicked.connect(self.skip_current_step) + button_layout.addWidget(self.skip_button) + self.stop_button = QPushButton("停止交互") + if self.chinese_font: + self.stop_button.setFont(self.chinese_font) + self.stop_button.clicked.connect(self.stop_interaction) + button_layout.addWidget(self.stop_button) + layout.addLayout(button_layout) + + # 初始化工具状态 + self.sync_current_tool() + self.update_tool_status() + + def sync_current_tool(self): + """初始化拆装工具状态""" + # 从配置文件中加载工具列表 + self.load_tools_from_config() + # 默认选择"无" + self.current_tool_combo.setCurrentText("无") + print(f"🔧 初始化拆装工具: 无") + + def load_tools_from_config(self): + """从配置文件中加载工具列表""" + # 清空现有工具选项(保留"无") + self.current_tool_combo.clear() + self.current_tool_combo.addItem("无") + + # 从配置文件中加载工具 + if hasattr(self.interaction_manager, 'config_data') and self.interaction_manager.config_data: + tools = self.interaction_manager.config_data.get('tools', []) + for tool in tools: + tool_name = tool.get('name', '') + if tool_name and tool_name != "无": + self.current_tool_combo.addItem(tool_name) + print(f"🔧 加载工具: {tool_name}") + + if len(tools) > 0: + print(f"🔧 从配置文件加载了 {len(tools)} 个工具") + else: + print("🔧 配置文件中没有定义工具") + else: + print("⚠️ 无法访问配置数据") + + def on_tool_changed(self, tool_name): + """工具改变时的处理""" + print(f"🔧 用户选择拆装工具: {tool_name}") + + # 拆装工具不需要同步到编辑器的工具管理器 + # 这里是独立的拆装工具选择 + + # 更新工具状态显示 + self.update_tool_status() + + def update_tool_status(self): + """更新工具状态显示""" + current_tool = self.current_tool_combo.currentText() + required_tool = self.current_required_tool + + # 检查工具是否匹配 + tool_matches = self.check_tool_permission(current_tool, required_tool) + + if self.mode == "training": + # 训练模式:显示工具匹配状态 + if tool_matches: + self.tool_status_label.setText("✅ 工具匹配,可以进行操作") + self.tool_status_label.setStyleSheet("color: #27AE60; font-weight: bold;") + # 启用交互操作 + self.interaction_manager.set_operation_enabled(True) + else: + self.tool_status_label.setText(f"❌ 工具不匹配,需要选择 '{required_tool}' 工具") + self.tool_status_label.setStyleSheet("color: #E74C3C; font-weight: bold;") + # 禁用交互操作 + self.interaction_manager.set_operation_enabled(False) + else: + # 考核模式:不显示工具状态,但仍然需要正确的工具才能操作 + # 这里不再总是启用操作,而是让权限检查在实际操作时进行 + self.interaction_manager.set_operation_enabled(True) + + def check_tool_permission(self, current_tool, required_tool): + """检查工具权限""" + # 如果步骤不要求特定工具,任何工具都可以 + if required_tool == "无" or required_tool == "" or not required_tool: + return True + + # 精确匹配 + if current_tool == required_tool: + return True + + return False + + def update_step_info(self, step_data, current_step, total_steps): + step_name = step_data.get('name', f'步骤 {current_step}') + + if self.mode == "exam": + # 考核模式:显示考核信息 + step_score = step_data.get('score', 10) + self.step_info_label.setText(f"第 {current_step}/{total_steps} 步: {step_name} (分值: {step_score})") + else: + # 训练模式:正常显示 + self.step_info_label.setText(f"第 {current_step}/{total_steps} 步: {step_name}") + + # 为动态文本设置中文字体 + if self.chinese_font: + self.step_info_label.setFont(self.chinese_font) + + # 步骤描述(考核模式下不显示) + if self.step_desc_text is not None: + step_desc = step_data.get('description', '无描述') + self.step_desc_text.setPlainText(step_desc) + if self.chinese_font: + self.step_desc_text.setFont(self.chinese_font) + + # 更新工具要求 + self.current_required_tool = step_data.get('required_tool', '无') + if self.required_tool_label is not None: + self.required_tool_label.setText(f"当前步骤要求工具: {self.current_required_tool}") + if self.chinese_font: + self.required_tool_label.setFont(self.chinese_font) + + # 更新工具状态 + self.update_tool_status() + + operation_type = self.interaction_manager.normalize_operation_type(step_data) + step_type = self.interaction_manager.normalize_step_type(step_data) + target_model = step_data.get('target_model', '未指定') + + if operation_type == 'drag': + if step_type == 'assemble': + snap_tolerance = step_data.get('snap_tolerance') or step_data.get('tolerance', 5.0) + operation_hint = f"🔧 安装操作\n请用鼠标左键点击并拖拽模型 '{target_model}' 到原始位置附近。\n当距离原始位置 {snap_tolerance:.1f} 单位内时,模型会自动吸附到正确位置。" + else: + disassemble_threshold = step_data.get('disassemble_threshold', 5.0) + operation_hint = f"🔧 拆卸操作\n请用鼠标左键点击并拖拽模型 '{target_model}' 脱离原位。\n拖拽到距离原始位置 {disassemble_threshold:.1f} 单位外即可完成。" + elif operation_type == 'click': + if step_type == 'assemble': + animation_duration = step_data.get('animation_duration', 2.0) + operation_hint = f"🔧 安装操作(点击触发)\n请点击模型 '{target_model}' 触发自动安装。\n模型将自动移动到原始位置(动画时长 {animation_duration:.1f}秒)。" + else: + target_pos_data = step_data.get('target_position') + animation_duration = step_data.get('animation_duration', 2.0) + if target_pos_data: + from panda3d.core import Vec3 + target_pos = Vec3(*target_pos_data) + operation_hint = f"🔧 拆卸操作(点击触发)\n请点击模型 '{target_model}' 触发自动拆卸。\n模型将自动移动到位置 {target_pos}(动画时长 {animation_duration:.1f}秒),然后隐藏。" + else: + operation_hint = f"🔧 拆卸操作(点击触发)\n请点击模型 '{target_model}' 触发自动拆卸。\n⚠️ 警告:缺少target_position配置" + else: + # 未知操作类型的回退提示 + if step_type == 'assemble': + operation_hint = f"🔧 安装操作\n请操作模型 '{target_model}' 将其安装到原位。" + else: + operation_hint = f"🔧 拆卸操作\n请操作模型 '{target_model}' 将其拆卸。" + + # 操作提示(考核模式下不显示) + if self.operation_text is not None: + self.operation_text.setPlainText(operation_hint) + if self.chinese_font: + self.operation_text.setFont(self.chinese_font) + + def skip_current_step(self): + if self.interaction_manager.is_active: + # 停止可能正在进行的拖拽 + if self.interaction_manager.dragging_model: + self.interaction_manager.stop_dragging() + self.interaction_manager.complete_current_step() + + def stop_interaction(self): + self.interaction_manager.stop_interaction_mode() + self.close() + + def closeEvent(self, event): + reply = QMessageBox.question(self, "确认", "是否要停止拆装交互?", + QMessageBox.Yes | QMessageBox.No) + if reply == QMessageBox.Yes: + self.stop_interaction() + event.accept() + else: + event.ignore() \ No newline at end of file diff --git a/core/collision_manager.py b/core/collision_manager.py index 6c4b3e73..6117ea30 100644 --- a/core/collision_manager.py +++ b/core/collision_manager.py @@ -19,10 +19,10 @@ class CollisionManager: # === 基础碰撞掩码定义 === # 每个掩码使用不同的位(bit)来标识,可以进行位运算组合 - 'TERRAIN': BitMask32.bit(0), # 地形/地面 - 通常用于地面碰撞检测 - 'UI_ELEMENT': BitMask32.bit(1), # UI元素 - 界面组件的碰撞检测 - 'CAMERA': BitMask32.bit(2), # 摄像机 - 相机的碰撞检测 - 'MODEL_COLLISION': BitMask32.bit(3), # 模型碰撞 - 通用模型间碰撞检测 + # 'TERRAIN': BitMask32.bit(0), # 地形/地面 - 通常用于地面碰撞检测 + # 'UI_ELEMENT': BitMask32.bit(1), # UI元素 - 界面组件的碰撞检测 + # 'CAMERA': BitMask32.bit(2), # 摄像机 - 相机的碰撞检测 + 'MODEL_COLLISION': BitMask32.bit(6), # 模型碰撞 - 通用模型间碰撞检测 } # 碰撞体形状类型 diff --git a/core/event_handler.py b/core/event_handler.py index be0f6cc1..89cb2ad9 100644 --- a/core/event_handler.py +++ b/core/event_handler.py @@ -188,15 +188,34 @@ class EventHandler: if self.world.selection.gizmo: #print("准备检查坐标轴点击...") try: + highlighted_axis = self.world.selection.gizmoHighlightAxis + if highlighted_axis: + print(f"✓ 检测到高亮轴: {highlighted_axis},直接开始拖拽") + # 直接使用高亮轴开始拖拽 + self.world.selection.startGizmoDrag(highlighted_axis, x, y) + pickerNP.removeNode() + return + + # 如果没有高亮轴,再尝试检测点击 gizmoAxis = self.world.selection.checkGizmoClick(x, y) if gizmoAxis: - #print(f"✓ 检测到坐标轴点击: {gizmoAxis}") + print(f"✓ 检测到坐标轴点击: {gizmoAxis}") # 开始坐标轴拖拽 self.world.selection.startGizmoDrag(gizmoAxis, x, y) pickerNP.removeNode() return else: print("× 没有点击到坐标轴") + + # gizmoAxis = self.world.selection.checkGizmoClick(x, y) + # if gizmoAxis: + # #print(f"✓ 检测到坐标轴点击: {gizmoAxis}") + # # 开始坐标轴拖拽 + # self.world.selection.startGizmoDrag(gizmoAxis, x, y) + # pickerNP.removeNode() + # return + # else: + # print("× 没有点击到坐标轴") except Exception as e: print(f"❌ 坐标轴点击检测出现异常: {str(e)}") import traceback diff --git a/core/maintenance_gui.py b/core/maintenance_gui.py new file mode 100644 index 00000000..74d0add6 --- /dev/null +++ b/core/maintenance_gui.py @@ -0,0 +1,1094 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +维修系统场景GUI界面 +包括步骤显示、工具选择按钮和警告提示 +""" + +from direct.gui.DirectGui import * +from direct.gui import DirectGuiGlobals as DGG +from direct.showbase.DirectObject import DirectObject +from direct.task import Task +from direct.task.TaskManagerGlobal import taskMgr +from panda3d.core import TextNode, Vec3, Vec4 + +# 引入全局变量 +import builtins + + +class MaintenanceGUI(DirectObject): + """维修系统GUI管理器""" + + def __init__(self, world): + DirectObject.__init__(self) + self.world = world + + self.chinese_font = None + if hasattr(self.world, 'getChineseFont'): + self.chinese_font = self.world.getChineseFont() + + # 获取GUI父节点引用 + self.aspect2d = None + print(f"🔍 初始化维修GUI,world类型: {type(world).__name__}") + print(f"🔍 world是否有aspect2d: {hasattr(world, 'aspect2d')}") + + if hasattr(world, 'aspect2d'): + self.aspect2d = world.aspect2d + print("✅ 从world获取aspect2d成功") + else: + print("❌ world没有aspect2d属性") + + # GUI元素 + self.step_text = None # 步骤显示文本 + self.current_tool_text = None # 当前工具显示文本 + self.warning_text = None # 警告文本 + self.tool_buttons = [] # 工具按钮列表 + self.stop_button = None # 停止按钮 + + # 状态 + self.current_tool = None # 当前选择的工具,初始为无 + self.available_tools = [] # 可用工具列表 + self.current_step_info = "" # 当前步骤信息 + self.mode = "training" # 模式:training 或 exam + + # 警告显示任务 + self.warning_task = None + + print("✅ 维修系统GUI初始化完成") + + # 添加测试快捷键 + self.accept("t", self.test_tool_selection) # 按T键测试工具选择 + self.accept("1", lambda: self.test_specific_button(0)) # 按1键测试第一个按钮 + self.accept("2", lambda: self.test_specific_button(1)) # 按2键测试第二个按钮 + self.accept("3", lambda: self.test_specific_button(2)) # 按3键测试第三个按钮 + self.accept("p", self.print_button_positions) # 按P键打印按钮位置 + + # 添加鼠标监控任务 + self.mouse_monitor_task = None + + def setup_gui(self, tools_list, mode="training"): + """设置GUI界面""" + try: + print(f"🎨 设置维修系统GUI,工具列表: {tools_list}, 模式: {mode}") + + self.available_tools = tools_list if tools_list else ["手"] + self.mode = mode + + # 重新尝试获取aspect2d引用 + self.get_aspect2d_reference() + + # 清理现有GUI + self.cleanup_gui() + + # 创建步骤显示文本 + self.create_step_text() + + # 创建工具选择按钮(训练和考核模式都需要) + self.create_tool_buttons() + self.create_current_tool_text() + + # 创建停止按钮(训练和考核模式都需要) + # self.create_stop_button() + + # 创建警告文本(仅训练模式需要) + if mode == "training": + self.create_warning_text() + + print("✅ 维修系统GUI设置完成") + + # 启动鼠标监控任务(训练和考核模式都需要) + self.start_mouse_monitor() + + except Exception as e: + print(f"❌ 设置维修系统GUI失败: {e}") + import traceback + traceback.print_exc() + + def start_mouse_monitor(self): + """启动鼠标监控任务""" + try: + # 停止可能存在的旧任务 + if self.mouse_monitor_task: + taskMgr.remove(self.mouse_monitor_task) + + # 启动新的监控任务 + self.mouse_monitor_task = taskMgr.add(self.monitor_mouse_clicks, "mouse_monitor") + print("✅ 鼠标监控任务已启动") + print(f"✅ 任务管理器中的任务: {[task.getName() for task in taskMgr.getAllTasks()]}") + + # 测试一下任务是否能正常运行 + self.test_mouse_monitor() + + except Exception as e: + print(f"❌ 启动鼠标监控失败: {e}") + import traceback + traceback.print_exc() + + def test_mouse_monitor(self): + """测试鼠标监控功能""" + try: + print("🧪 测试鼠标监控功能...") + + # 尝试直接调用监控函数 + result = self.monitor_mouse_clicks(None) + print(f"🧪 监控函数调用结果: {result}") + + except Exception as e: + print(f"❌ 测试鼠标监控失败: {e}") + import traceback + traceback.print_exc() + + def monitor_mouse_clicks(self, task): + """监控鼠标点击并手动检测按钮""" + try: + # 添加计数器来减少日志输出 + if not hasattr(self, '_monitor_counter'): + self._monitor_counter = 0 + self._monitor_counter += 1 + + # 每100帧输出一次状态 + if self._monitor_counter % 100 == 1: + print(f"🔄 鼠标监控运行中... (帧 {self._monitor_counter})") + + # 检查鼠标状态 + mouse_watcher = None + if hasattr(self.world, 'mouseWatcherNode'): + mouse_watcher = self.world.mouseWatcherNode + if self._monitor_counter % 100 == 1: + print(f"✅ 从world获取mouseWatcherNode: {mouse_watcher}") + else: + # 使用全局base对象 + try: + import builtins + if hasattr(builtins, 'base') and hasattr(builtins.base, 'mouseWatcherNode'): + mouse_watcher = builtins.base.mouseWatcherNode + if self._monitor_counter % 100 == 1: + print(f"✅ 从base获取mouseWatcherNode: {mouse_watcher}") + except: + if self._monitor_counter % 100 == 1: + print("❌ 无法获取mouseWatcherNode") + + if mouse_watcher: + has_mouse = mouse_watcher.hasMouse() + if has_mouse: + mouse_pos = mouse_watcher.getMouse() + + # 检查左键状态 + mouse_x = mouse_pos.getX() + mouse_y = mouse_pos.getY() + + # 检查左键是否被按下 + is_button_down = mouse_watcher.isButtonDown('mouse1') + + # 如果没有记录当前状态,初始化 + if not hasattr(self, '_last_button_state'): + self._last_button_state = False + + # 检测按钮从未按下到按下的状态变化(按下瞬间) + if is_button_down and not self._last_button_state: + print(f"🖱️ 鼠标监控:检测到按钮按下,位置({mouse_x:.3f}, {mouse_y:.3f})") + + # 检查这次点击是否在按钮区域 + clicked_button = self.check_button_click(mouse_x, mouse_y) + if clicked_button is not None: + print(f"🎯 鼠标监控检测到按钮点击: {clicked_button}") + self.on_tool_selected(clicked_button) + + # 更新按钮状态 + self._last_button_state = is_button_down + else: + if self._monitor_counter % 100 == 1: + print("❌ mouseWatcher为空") + + return task.cont if task else None + + except Exception as e: + print(f"❌ 鼠标监控异常: {e}") + import traceback + traceback.print_exc() + return task.cont if task else None + + def check_button_click(self, mouse_x, mouse_y): + """检查鼠标点击是否在按钮区域""" + try: + button_width = 0.4 # 与create_tool_buttons保持一致 + button_height = 0.25 # 与create_tool_buttons保持一致 + button_spacing = 0.45 # 与create_tool_buttons保持一致 + start_x = -0.8 # 与create_tool_buttons保持一致 + start_y = -0.75 # 与create_tool_buttons保持一致 + + print(f"🔍 检查按钮点击:鼠标({mouse_x:.3f}, {mouse_y:.3f})") + + for i, tool_data in enumerate(self.available_tools): + # 获取工具名称 + if isinstance(tool_data, dict): + tool_name = tool_data.get('name', str(tool_data)) + else: + tool_name = str(tool_data) + + # 计算按钮位置 + button_x = start_x + i * button_spacing + button_y = start_y + + # 计算按钮边界 + left = button_x - button_width/2 + right = button_x + button_width/2 + bottom = button_y - button_height/2 + top = button_y + button_height/2 + + print(f" 按钮{i} '{tool_name}': 中心({button_x:.3f}, {button_y:.3f}), 范围x[{left:.3f}, {right:.3f}], y[{bottom:.3f}, {top:.3f}]") + + # 检查鼠标是否在按钮范围内 + if (left <= mouse_x <= right and bottom <= mouse_y <= top): + print(f"✅ 点击在按钮{i} '{tool_name}' 范围内") + return tool_name + + print("❌ 点击不在任何按钮范围内") + return None + + except Exception as e: + print(f"❌ 检查按钮点击异常: {e}") + return None + + def handle_mouse_click(self, qt_x, qt_y): + """直接处理鼠标点击(从拆装交互系统调用)""" + try: + print(f"🖱️ 直接处理鼠标点击:Qt坐标({qt_x}, {qt_y})") + + # 将Qt坐标转换为aspect2d坐标 + # 获取窗口尺寸 + if hasattr(self.world, 'qtWidget') and self.world.qtWidget: + widget_width = self.world.qtWidget.width() + widget_height = self.world.qtWidget.height() + else: + widget_width = 1380 + widget_height = 750 + + # 坐标转换(与之前拆装交互系统中的算法一致) + aspect_x = (qt_x / widget_width) * 2.67 - 1.33 + aspect_y = 1.0 - (qt_y / widget_height) * 2.0 + + print(f"🔄 坐标转换:Qt({qt_x}, {qt_y}) -> aspect2d({aspect_x:.3f}, {aspect_y:.3f})") + + # 检查是否点击在按钮区域 + clicked_button = self.check_button_click(aspect_x, aspect_y) + if clicked_button: + print(f"🎯 直接处理:检测到按钮点击 '{clicked_button}'") + self.on_tool_selected(clicked_button) + return True + + return False + + except Exception as e: + print(f"❌ 直接处理鼠标点击失败: {e}") + import traceback + traceback.print_exc() + return False + + def get_aspect2d_reference(self): + """获取aspect2d引用""" + if self.aspect2d: + return # 已经有引用了 + + try: + print("🔍 重新尝试获取aspect2d引用...") + + # 方法1: 直接使用world对象(推荐) + if hasattr(self.world, 'aspect2d'): + self.aspect2d = self.world.aspect2d + print("✅ 从world获取aspect2d成功") + return + + # 方法2: 使用world对象作为父节点 + # 在main.py的Panda3DWorld中,world对象本身就是ShowBase的实例 + self.aspect2d = self.world.aspect2d if hasattr(self.world, 'aspect2d') else None + + if self.aspect2d: + print("✅ 使用world.aspect2d成功") + else: + print("❌ 无法获取aspect2d引用") + + except Exception as e: + print(f"❌ 获取aspect2d引用失败: {e}") + import traceback + traceback.print_exc() + + def create_step_text(self): + """创建步骤显示文本""" + try: + if not self.aspect2d: + print("❌ aspect2d引用不可用,无法创建步骤文本") + return + + self.step_text = DirectLabel( + text="准备开始维修训练...", + text_align=TextNode.ALeft, + text_scale=0.08, + text_fg=(1, 1, 1, 1), + text_bg=(0, 0, 0, 0.7), + frameColor=(0, 0, 0, 0.5), + frameSize=(-1.8, 1.8, -0.3, 0.3), + pos=(-1.7, 0, 0.8), + parent=self.aspect2d + ) + print("✅ 步骤显示文本创建成功") + + except Exception as e: + print(f"❌ 创建步骤文本失败: {e}") + import traceback + traceback.print_exc() + + def create_tool_buttons(self): + """创建工具选择按钮""" + try: + print(f"🔧 开始创建工具按钮...") + print(f" aspect2d引用: {self.aspect2d}") + print(f" 可用工具: {self.available_tools}") + print(f" 模式: {self.mode}") + + if not self.aspect2d: + print("❌ aspect2d引用不可用,无法创建工具按钮") + print(" 尝试重新获取aspect2d引用...") + self.get_aspect2d_reference() + if not self.aspect2d: + print("❌ 重新获取aspect2d失败") + return + + print(f"🔧 使用aspect2d创建工具按钮: {self.available_tools}") + + # 按钮布局参数(优化为更容易点击) + button_width = 0.4 # 增加宽度 + button_height = 0.25 # 增加高度,让按钮更容易点击 + button_spacing = 0.45 # 增加间距 + start_x = -0.8 # 进一步向右移动 + start_y = -0.75 # 向上移动一点 + + for i, tool in enumerate(self.available_tools): + # 处理工具数据格式 + if isinstance(tool, dict): + tool_name = tool.get('name', str(tool)) + tool_description = tool.get('description', '') + else: + tool_name = str(tool) + tool_description = '' + + print(f" 正在创建按钮 {i+1}/{len(self.available_tools)}: {tool_name}") + + # 计算按钮位置(横向排列) + pos_x = start_x + i * button_spacing + pos_y = start_y + + # 创建按钮 + button = DirectButton( + text=tool_name, + text_scale=0.06, + text_fg=(1, 1, 1, 1), + frameColor=(0.3, 0.3, 0.8, 0.8), + frameSize=(-button_width/2, button_width/2, -button_height/2, button_height/2), + pos=(pos_x, 0, pos_y), + command=self.on_tool_selected, + extraArgs=[tool_name], + parent=self.aspect2d, + # 按钮交互设置 + relief=DGG.RAISED, + borderWidth=(0.01, 0.01), + text_pos=(0, -0.02), + # 强制启用鼠标交互 + enableEdit=1, + # 音效设置 + clickSound=None, + rolloverSound=None, + # 确保按钮优先级和可见性 + sortOrder=1000, # 更高的排序值 + state=DGG.NORMAL, # 确保按钮状态正常 + # 鼠标事件设置 + suppressMouse=0 + ) + + # 手动绑定额外的事件监听 + button.bind(DGG.B1PRESS, self.button_press_handler, extraArgs=[tool_name]) + button.bind(DGG.B1RELEASE, self.button_release_handler, extraArgs=[tool_name]) + + self.tool_buttons.append(button) + print(f" ✅ 成功创建工具按钮: {tool_name} at ({pos_x}, {pos_y})") + + print(f"🎯 总共创建了 {len(self.tool_buttons)} 个工具按钮") + + # 设置默认选择的工具样式 + self.update_tool_button_styles() + + except Exception as e: + print(f"❌ 创建工具按钮失败: {e}") + import traceback + traceback.print_exc() + + def create_current_tool_text(self): + """创建当前工具显示文本""" + try: + if not self.aspect2d: + print("❌ aspect2d引用不可用,无法创建当前工具文本") + return + + self.current_tool_text = DirectLabel( + text=f"Tool: {self.current_tool}", + text_align=TextNode.ALeft, + text_scale=0.07, + text_fg=(1, 1, 0, 1), + text_bg=(0, 0, 0, 0.7), + frameColor=(0, 0, 0, 0.5), + frameSize=(-0.8, 0.8, -0.2, 0.2), + pos=(1.0, 0, 0.8), + parent=self.aspect2d + ) + print("✅ 当前工具文本创建成功") + + except Exception as e: + print(f"❌ 创建当前工具文本失败: {e}") + import traceback + traceback.print_exc() + + def create_stop_button(self): + """创建停止按钮""" + try: + if not self.aspect2d: + print("❌ aspect2d引用不可用,无法创建停止按钮") + return + + # 停止按钮位置:右下角 + self.stop_button = DirectButton( + text="停止", + text_align=TextNode.ACenter, + text_scale=0.06, + text_fg=(1, 1, 1, 1), + text_bg=(0, 0, 0, 0), + frameColor=(0.8, 0.2, 0.2, 0.9), # 红色背景 + frameSize=(-0.15, 0.15, -0.08, 0.08), + pos=(1.15, 0, -0.85), # 右下角位置 + parent=self.aspect2d, + command=self.on_stop_clicked, + relief=DGG.RAISED, + borderWidth=(0.01, 0.01), + rolloverSound=None, + clickSound=None, + pressEffect=1 + ) + + print("✅ 停止按钮创建成功") + + except Exception as e: + print(f"❌ 创建停止按钮失败: {e}") + import traceback + traceback.print_exc() + + def create_warning_text(self): + """创建警告文本""" + try: + if not self.aspect2d: + print("❌ aspect2d引用不可用,无法创建警告文本") + return + + self.warning_text = DirectLabel( + text="", + text_align=TextNode.ACenter, + text_scale=0.09, + text_fg=(1, 0, 0, 1), + text_bg=(1, 1, 0, 0.8), + frameColor=(1, 0, 0, 0.8), + frameSize=(-1.5, 1.5, -0.3, 0.3), + pos=(0, 0, 0.3), + parent=self.aspect2d + ) + # 默认隐藏警告文本 + self.warning_text.hide() + print("✅ 警告文本创建成功") + + except Exception as e: + print(f"❌ 创建警告文本失败: {e}") + import traceback + traceback.print_exc() + + def button_press_handler(self, tool_name, event=None): + """按钮按下事件处理""" + print(f"🖱️ 按钮按下事件:{tool_name}") + + def button_release_handler(self, tool_name, event=None): + """按钮释放事件处理""" + print(f"🖱️ 按钮释放事件:{tool_name}") + print(f"🔄 直接调用工具选择:{tool_name}") + self.on_tool_selected(tool_name) + + def on_tool_selected(self, tool): + """工具选择回调""" + try: + print(f"🎯 按钮点击事件触发!") + print(f"🔧 选择工具: {tool}") + + # 考核模式下额外提示 + if hasattr(self, 'mode') and self.mode == "exam": + print(f"📝 考核模式工具选择: {tool}") + + print(f"📍 当前工具变更: {self.current_tool} -> {tool}") + + self.current_tool = tool + + # 更新当前工具显示 + if self.current_tool_text: + self.current_tool_text['text'] = f"Tool: {tool}" + print(f"✅ 当前工具显示已更新") + + # 更新按钮样式 + self.update_tool_button_styles() + print(f"✅ 按钮样式已更新") + + # 通知拆装交互系统工具变更 + if hasattr(self.world, 'assembly_interaction') and self.world.assembly_interaction: + if hasattr(self.world.assembly_interaction, 'step_dialog') and self.world.assembly_interaction.step_dialog: + # 更新步骤对话框中的工具选择 + step_dialog = self.world.assembly_interaction.step_dialog + if hasattr(step_dialog, 'tool_combo'): + # 同步工具选择 + tool_index = -1 + for i in range(step_dialog.tool_combo.count()): + if step_dialog.tool_combo.itemText(i) == tool: + tool_index = i + break + if tool_index >= 0: + step_dialog.tool_combo.setCurrentIndex(tool_index) + print(f"✅ Qt对话框工具选择已同步") + + print(f"✅ 工具切换完成: {tool}") + + except Exception as e: + print(f"❌ 工具选择失败: {e}") + import traceback + traceback.print_exc() + + def on_stop_clicked(self): + """停止按钮点击回调""" + try: + print("🛑 用户点击停止按钮") + + # 通知拆装交互系统停止 + if hasattr(self.world, 'assembly_interaction') and self.world.assembly_interaction: + self.world.assembly_interaction.stop_interaction() + else: + print("⚠️ 拆装交互系统不存在,无法停止") + + except Exception as e: + print(f"❌ 停止操作失败: {e}") + import traceback + traceback.print_exc() + + def update_tool_button_styles(self): + """更新工具按钮样式""" + try: + for i, button in enumerate(self.tool_buttons): + # 处理工具数据格式 + tool_data = self.available_tools[i] + if isinstance(tool_data, dict): + tool_name = tool_data.get('name', str(tool_data)) + else: + tool_name = str(tool_data) + + if tool_name == self.current_tool: + # 选中状态:亮蓝色 + button['frameColor'] = (0.2, 0.8, 1.0, 0.9) + button['text_fg'] = (0, 0, 0, 1) + else: + # 未选中状态:深蓝色 + button['frameColor'] = (0.3, 0.3, 0.8, 0.8) + button['text_fg'] = (1, 1, 1, 1) + + except Exception as e: + print(f"❌ 更新按钮样式失败: {e}") + import traceback + traceback.print_exc() + + def update_step_info(self, step_text): + """更新步骤信息""" + try: + print(f"📋 更新步骤信息: {step_text}") + + self.current_step_info = step_text + + if self.step_text: + self.step_text['text'] = step_text + + except Exception as e: + print(f"❌ 更新步骤信息失败: {e}") + + def show_warning(self, message, duration=2.0): + """显示警告信息(仅训练模式)""" + try: + # 考核模式下不显示警告 + if hasattr(self, 'mode') and self.mode == "exam": + print(f"📝 考核模式:隐藏警告 - {message}") + return + + print(f"⚠️ 显示警告: {message}") + + if not self.warning_text: + return + + # 取消之前的警告任务 + if self.warning_task: + taskMgr.remove(self.warning_task) + self.warning_task = None + + # 显示警告文本 + self.warning_text['text'] = message + self.warning_text.show() + + # 设置自动隐藏任务 + self.warning_task = taskMgr.doMethodLater( + duration, + self.hide_warning, + 'hide_warning_task' + ) + + except Exception as e: + print(f"❌ 显示警告失败: {e}") + + def hide_warning(self, task=None): + """隐藏警告信息""" + try: + if self.warning_text: + self.warning_text.hide() + + if self.warning_task: + self.warning_task = None + + return Task.done + + except Exception as e: + print(f"❌ 隐藏警告失败: {e}") + return Task.done + + def test_tool_selection(self): + """测试工具选择功能(按T键触发)""" + if self.available_tools: + # 循环选择工具进行测试 + current_index = 0 + for i, tool_data in enumerate(self.available_tools): + if isinstance(tool_data, dict): + tool_name = tool_data.get('name', str(tool_data)) + else: + tool_name = str(tool_data) + + if tool_name == self.current_tool: + current_index = i + break + + # 选择下一个工具 + next_index = (current_index + 1) % len(self.available_tools) + next_tool_data = self.available_tools[next_index] + + if isinstance(next_tool_data, dict): + next_tool_name = next_tool_data.get('name', str(next_tool_data)) + else: + next_tool_name = str(next_tool_data) + + print(f"⌨️ 键盘测试:切换到工具 '{next_tool_name}'") + self.on_tool_selected(next_tool_name) + + def test_specific_button(self, button_index): + """测试特定按钮(通过索引)""" + if not hasattr(self, 'available_tools') or not self.available_tools: + print(f"⌨️ 按钮{button_index}: 没有可用工具") + return + + if button_index >= len(self.available_tools): + print(f"⌨️ 按钮{button_index}: 索引超出范围(最大{len(self.available_tools)-1})") + return + + # 获取工具名称 + tool_data = self.available_tools[button_index] + if isinstance(tool_data, dict): + tool_name = tool_data.get('name', str(tool_data)) + else: + tool_name = str(tool_data) + + print(f"⌨️ 按钮{button_index}测试:切换到工具 '{tool_name}'") + self.on_tool_selected(tool_name) + + def print_button_positions(self): + """打印所有按钮的位置信息""" + try: + if not hasattr(self, 'available_tools') or not self.available_tools: + print("📍 没有可用工具按钮") + return + + print("📍 ===== 按钮位置信息 =====") + + button_width = 0.4 + button_height = 0.25 + button_spacing = 0.45 + start_x = -0.8 + start_y = -0.75 + + for i, tool_data in enumerate(self.available_tools): + # 获取工具名称 + if isinstance(tool_data, dict): + tool_name = tool_data.get('name', str(tool_data)) + else: + tool_name = str(tool_data) + + # 计算按钮位置 + button_x = start_x + i * button_spacing + button_y = start_y + + # 计算按钮边界 + left = button_x - button_width/2 + right = button_x + button_width/2 + bottom = button_y - button_height/2 + top = button_y + button_height/2 + + print(f"📍 按钮{i} '{tool_name}':") + print(f" 中心: ({button_x:.3f}, {button_y:.3f})") + print(f" 范围: x[{left:.3f}, {right:.3f}], y[{bottom:.3f}, {top:.3f}]") + print(f" 尺寸: {button_width} x {button_height}") + + print("📍 ========================") + + except Exception as e: + print(f"❌ 打印按钮位置失败: {e}") + import traceback + traceback.print_exc() + + def show_exam_results(self, exam_data): + """显示考核结果(GUI界面)""" + try: + print("🎓 开始显示考核结果GUI...") + + # 清理现有GUI元素 + self.cleanup_gui() + + # 创建考核结果界面 + self.create_exam_result_gui(exam_data) + + print("✅ 考核结果GUI显示完成") + + except Exception as e: + print(f"❌ 显示考核结果GUI失败: {e}") + import traceback + traceback.print_exc() + + def create_exam_result_gui(self, exam_data): + """创建考核结果GUI界面""" + try: + if not self.aspect2d: + print("❌ aspect2d引用不可用,无法创建考核结果GUI") + return + + # 主背景 - 黑色背景 + from direct.gui.DirectGui import DirectFrame + self.exam_bg = DirectFrame( + frameColor=(0, 0, 0, 0.95), # 更深的黑色背景 + frameSize=(-2.0, 2.0, -1.0, 1.0), + pos=(0, 0, 0), + parent=self.aspect2d + ) + + # 标题 + self.exam_title = DirectLabel( + text="🎓 考核结果报告", + text_align=TextNode.ACenter, + text_scale=0.12, + text_fg=(1, 1, 0, 1), # 黄色文字 + text_bg=(0, 0, 0, 0), # 透明背景 + frameColor=(0, 0, 0, 0), # 透明边框 + frameSize=(-1.5, 1.5, -0.15, 0.15), + pos=(0, 0, 0.8), + parent=self.aspect2d + ) + + # 总体成绩区域 + score_text = f"📊 总得分: {exam_data['final_score']:.0f}/{exam_data['max_score']:.0f} 分 ({exam_data['score_rate']:.1f}%)\n" + score_text += f"🏅 评级: {exam_data['grade_icon']} {exam_data['grade']} - {exam_data['grade_desc']}\n" + score_text += f"🎯 操作准确率: {exam_data['accuracy_rate']:.1f}% | 完美步骤: {exam_data['perfect_steps']}/{exam_data['total_steps']}" + + # 根据成绩选择文字颜色(在黑色背景下更突出) + if exam_data['score_rate'] >= 90: + score_fg_color = (0.2, 1.0, 0.2, 1) # 亮绿色 + elif exam_data['score_rate'] >= 80: + score_fg_color = (0.8, 1.0, 0.2, 1) # 亮黄绿色 + elif exam_data['score_rate'] >= 60: + score_fg_color = (1.0, 0.8, 0.2, 1) # 亮橙色 + else: + score_fg_color = (1.0, 0.3, 0.3, 1) # 亮红色 + + self.exam_score = DirectLabel( + text=score_text, + text_align=TextNode.ACenter, + text_scale=0.07, + text_fg=score_fg_color, # 根据成绩动态变色 + text_bg=(0, 0, 0, 0), # 透明背景 + frameColor=(0, 0, 0, 0), # 透明边框 + frameSize=(-1.9, 1.9, -0.3, 0.3), + pos=(0, 0, 0.35), + parent=self.aspect2d + ) + + # 步骤详情区域 + steps_text = "📋 步骤详情:\n" + for step in exam_data['step_details']: + steps_text += f"{step['status']} {step['name']}: {step['current_score']:.0f}/{step['max_score']:.0f}分" + if step['tool_error']: + steps_text += " ❌工具错误" + steps_text += f" ({step['attempts']}次操作)\n" + + self.exam_steps = DirectLabel( + text=steps_text, + text_align=TextNode.ALeft, + text_scale=0.06, + text_fg=(0.9, 0.9, 0.9, 1), # 浅灰色文字 + text_bg=(0, 0, 0, 0), # 透明背景 + frameColor=(0, 0, 0, 0), # 透明边框 + frameSize=(-1.8, 1.8, -0.4, 0.4), + pos=(0, 0, -0.1), + parent=self.aspect2d + ) + + # 改进建议区域(如果有建议) + if exam_data['suggestions']: + suggestions_text = "💡 改进建议:\n" + for suggestion in exam_data['suggestions']: + suggestions_text += f"{suggestion}\n" + + self.exam_suggestions = DirectLabel( + text=suggestions_text, + text_align=TextNode.ALeft, + text_scale=0.05, + text_fg=(1, 0.8, 0.2, 1), # 橙黄色文字 + text_bg=(0, 0, 0, 0), # 透明背景 + frameColor=(0, 0, 0, 0), # 透明边框 + frameSize=(-1.8, 1.8, -0.25, 0.25), + pos=(0, 0, -0.7), + parent=self.aspect2d + ) + + # 倒计时显示 + self.exam_countdown_text = DirectLabel( + text="10秒后自动关闭", + text_align=TextNode.ACenter, + text_scale=0.06, + text_fg=(0.8, 0.8, 0.8, 1), # 灰色文字 + text_bg=(0, 0, 0, 0), # 透明背景 + frameColor=(0, 0, 0, 0), # 透明边框 + frameSize=(-0.8, 0.8, -0.1, 0.1), + pos=(0, 0, -0.8), + parent=self.aspect2d + ) + + # 启动10秒倒计时任务 + self.start_countdown_timer() + + print("✅ 考核结果GUI元素创建完成") + + except Exception as e: + print(f"❌ 创建考核结果GUI失败: {e}") + import traceback + traceback.print_exc() + + def close_exam_results(self): + """关闭考核结果界面""" + try: + print("🔄 关闭考核结果界面...") + + # 清理考核结果GUI元素 + if hasattr(self, 'exam_bg') and self.exam_bg: + self.exam_bg.removeNode() + self.exam_bg = None + + if hasattr(self, 'exam_title') and self.exam_title: + self.exam_title.removeNode() + self.exam_title = None + + if hasattr(self, 'exam_score') and self.exam_score: + self.exam_score.removeNode() + self.exam_score = None + + if hasattr(self, 'exam_steps') and self.exam_steps: + self.exam_steps.removeNode() + self.exam_steps = None + + if hasattr(self, 'exam_suggestions') and self.exam_suggestions: + self.exam_suggestions.removeNode() + self.exam_suggestions = None + + if hasattr(self, 'exam_countdown_text') and self.exam_countdown_text: + self.exam_countdown_text.removeNode() + self.exam_countdown_text = None + + # 停止倒计时任务 + if hasattr(self, 'exam_countdown_task') and self.exam_countdown_task: + taskMgr.remove(self.exam_countdown_task) + self.exam_countdown_task = None + + print("✅ 考核结果界面已关闭") + + except Exception as e: + print(f"❌ 关闭考核结果界面失败: {e}") + import traceback + traceback.print_exc() + + def start_countdown_timer(self): + """启动10秒倒计时""" + try: + self.countdown_seconds = 10 + print(f"⏰ 启动考核结果倒计时:{self.countdown_seconds}秒") + + # 启动倒计时任务 + self.exam_countdown_task = taskMgr.doMethodLater( + 1.0, # 每秒执行一次 + self.update_countdown, + 'exam_countdown_task' + ) + + except Exception as e: + print(f"❌ 启动倒计时失败: {e}") + import traceback + traceback.print_exc() + + def update_countdown(self, task): + """更新倒计时显示""" + try: + self.countdown_seconds -= 1 + + if self.countdown_seconds > 0: + # 更新倒计时显示 + if hasattr(self, 'exam_countdown_text') and self.exam_countdown_text: + self.exam_countdown_text['text'] = f"{self.countdown_seconds}秒后自动关闭" + + print(f"⏰ 倒计时:{self.countdown_seconds}秒") + + # 继续倒计时 + return task.again + else: + # 倒计时结束,自动关闭 + print("⏰ 倒计时结束,自动关闭考核结果界面") + self.close_exam_results() + return task.done + + except Exception as e: + print(f"❌ 更新倒计时失败: {e}") + import traceback + traceback.print_exc() + return task.done + + + def get_current_tool(self): + """获取当前选择的工具""" + return self.current_tool + + def set_mode(self, mode): + """设置模式""" + self.mode = mode + print(f"🎯 维修GUI模式设置为: {mode}") + + def cleanup_gui(self): + """清理GUI元素""" + try: + print("🧹 清理维修系统GUI") + + # 清理步骤文本 + if self.step_text: + self.step_text.destroy() + self.step_text = None + + # 清理当前工具文本 + if self.current_tool_text: + self.current_tool_text.destroy() + self.current_tool_text = None + + # 清理警告文本 + if self.warning_text: + self.warning_text.destroy() + self.warning_text = None + + # 清理工具按钮 + for button in self.tool_buttons: + if button: + button.destroy() + self.tool_buttons.clear() + + # 清理停止按钮 + if hasattr(self, 'stop_button') and self.stop_button: + self.stop_button.destroy() + self.stop_button = None + + # 取消警告任务 + if self.warning_task: + taskMgr.remove(self.warning_task) + self.warning_task = None + + # 取消鼠标监控任务 + if self.mouse_monitor_task: + taskMgr.remove(self.mouse_monitor_task) + self.mouse_monitor_task = None + + # 清理考核结果界面 + self.close_exam_results() + + print("✅ 维修系统GUI清理完成") + + except Exception as e: + print(f"❌ 清理维修系统GUI失败: {e}") + + def show_gui(self): + """显示GUI""" + try: + print(f"🎨 显示维修系统GUI...") + print(f" 模式: {self.mode}") + print(f" 步骤文本: {self.step_text is not None}") + print(f" 当前工具文本: {self.current_tool_text is not None}") + print(f" 工具按钮数量: {len(self.tool_buttons)}") + + if self.step_text: + self.step_text.show() + print(" ✅ 步骤文本已显示") + + # 当前工具文本和工具按钮在训练和考核模式都需要显示 + if self.current_tool_text: + self.current_tool_text.show() + print(" ✅ 当前工具文本已显示") + + for i, button in enumerate(self.tool_buttons): + button.show() + print(f" ✅ 工具按钮 {i+1} 已显示") + + # 停止按钮在训练和考核模式都需要显示 + if self.stop_button: + self.stop_button.show() + print(" ✅ 停止按钮已显示") + + print("✅ 维修系统GUI显示完成") + + except Exception as e: + print(f"❌ 显示维修系统GUI失败: {e}") + import traceback + traceback.print_exc() + + def hide_gui(self): + """隐藏GUI""" + try: + if self.step_text: + self.step_text.hide() + + if self.current_tool_text: + self.current_tool_text.hide() + + if self.warning_text: + self.warning_text.hide() + + for button in self.tool_buttons: + button.hide() + + if self.stop_button: + self.stop_button.hide() + + print("✅ 维修系统GUI隐藏") + + except Exception as e: + print(f"❌ 隐藏维修系统GUI失败: {e}") \ No newline at end of file diff --git a/core/selection.py b/core/selection.py index 8a3302aa..66295b8f 100644 --- a/core/selection.py +++ b/core/selection.py @@ -7,7 +7,6 @@ - 拖拽变换逻辑 - 射线检测和碰撞检测 """ -from PIL.ImageChops import lighter from direct.showbase.ShowBaseGlobal import globalClock from panda3d.core import (Vec3, Point3, Point2, LineSegs, ColorAttrib, RenderState, DepthTestAttrib, CollisionTraverser, CollisionHandlerQueue, @@ -16,7 +15,6 @@ from panda3d.core import (Vec3, Point3, Point2, LineSegs, ColorAttrib, RenderSta from direct.task.TaskManagerGlobal import taskMgr import math - class SelectionSystem: """选择和变换系统类""" @@ -125,11 +123,8 @@ class SelectionSystem: def createSelectionBox(self, nodePath): """为选中的节点创建选择框""" try: - #print(f" 开始创建选择框,目标节点: {nodePath.getName()}") - - # 如果已有选择框,先移除 if self.selectionBox: - print(" 移除现有选择框") + #print(" 移除现有选择框") self.selectionBox.removeNode() self.selectionBox = None @@ -137,20 +132,11 @@ class SelectionSystem: print(" 目标节点为空,取消创建") return - # 创建选择框作为render的子节点,但会实时跟踪目标节点 self.selectionBox = self.world.render.attachNewNode("selectionBox") - self.selectionBoxTarget = nodePath # 保存目标节点引用 - #print(f" 选择框节点创建完成: {self.selectionBox}") + self.selectionBoxTarget = nodePath - # 启动选择框更新任务 taskMgr.add(self.updateSelectionBoxTask, "updateSelectionBox") - #print(" 选择框更新任务已启动") - - # 初始更新选择框 - #print(" 开始初始化选择框几何体...") - #self.updateSelectionBoxGeometry() - - #print(f" ✓ 为节点 {nodePath.getName()} 创建了选择框") + self.updateSelectionBoxGeometry() except Exception as e: print(f" ✗ 创建选择框失败: {str(e)}") @@ -286,7 +272,7 @@ class SelectionSystem: def updateSelectionBoxTask(self, task): """选择框更新任务 - 平衡性能和实时性""" try: - update_interval = 0.1 + update_interval = 0.05 if not hasattr(self, '_last_selection_box_update'): self._last_selection_box_update = 0 @@ -919,13 +905,6 @@ class SelectionSystem: # 创建或获取材质 mat = Material() - # # 设置材质属性 - 使用自发光确保在RenderPipeline下可见 - # mat.setBaseColor(Vec4(color[0], color[1], color[2], color[3])) - # mat.setDiffuse(Vec4(0, 0, 0, 1)) - # #mat.setEmission(Vec4(color[0], color[1], color[2], 1.0)) # 自发光 - # mat.setEmission(Vec4(1,1,1,1.0)) # 自发光 - # mat.set_roughness(1) - # 设置材质属性 - 使用更自然的颜色,避免过亮的自发光 adjusted_color = Vec4( min(color[0]*20, 1.0), @@ -935,11 +914,7 @@ class SelectionSystem: ) mat.setBaseColor(adjusted_color) - # mat.setDiffuse(adjusted_color * 0.8) # 稍微降低漫反射亮度 - # mat.setAmbient(adjusted_color * 0.3) # 设置环境光反射 - # mat.setSpecular(Vec4(0.3, 0.3, 0.3, 1.0)) # 适度的镜面反射 - # mat.setShininess(25.0) # 适中的高光强度 - mat.setEmission(Vec4(1, 1, 1, 1.0)) # 自发光 + #mat.setEmission(Vec4(1, 1, 1, 1.0)) # 自发光 # 应用材质 handle_node.setMaterial(mat, 1) @@ -1528,8 +1503,14 @@ class SelectionSystem: self.dragStartMousePos = (mouseX, mouseY) + light_object = self.gizmoTarget.getPythonTag("rp_light_object") + if light_object: + self.gizmoTargetStartPos = Point3(light_object.pos) + else: + self.gizmoTargetStartPos = self.gizmoTarget.getPos() + # 保存开始拖拽时目标节点的位置和坐标轴的位置 - self.gizmoTargetStartPos = self.gizmoTarget.getPos() + #self.gizmoTargetStartPos = self.gizmoTarget.getPos() self.gizmoStartPos = self.gizmo.getPos(self.world.render) # 坐标轴的世界位置 # 添加对缩放的支持:保存初始缩放值 @@ -1900,6 +1881,43 @@ class SelectionSystem: """停止坐标轴拖拽""" print(f"停止坐标轴拖拽 - 轴: {self.dragGizmoAxis}") + if hasattr(self.world,'command_manager') and self.world.command_manager and self.gizmoTarget: + current_pos = self.gizmoTarget.getPos() + + if (hasattr(self,'gizmoTargetStartPos') and self.gizmoTargetStartPos and + (abs(current_pos.x-self.gizmoTargetStartPos.x)>0.001 or + abs(current_pos.y-self.gizmoTargetStartPos.y)>0.001 or + abs(current_pos.z-self.gizmoTargetStartPos.z)>0.001)): + from core.Command_System import MoveNodeCommand + from core.Command_System import MoveLightCommand + + light_object = self.gizmoTarget.getPythonTag("rp_light_object") + if light_object: + command = MoveLightCommand(self.gizmoTarget,self.gizmoTargetStartPos,current_pos,light_object) + else: + command = MoveNodeCommand(self.gizmoTarget,self.gizmoTargetStartPos,current_pos) + self.world.command_manager.execute_command(command) + # 如果是缩放操作且缩放发生了变化,则创建缩放命令 + elif (hasattr(self, 'gizmoTargetStartScale') and hasattr(self, 'gizmoTargetStartScale') and + self.gizmoTargetStartScale): + current_scale = self.gizmoTarget.getScale() + if (abs(current_scale.x - self.gizmoTargetStartScale.x) > 0.001 or + abs(current_scale.y - self.gizmoTargetStartScale.y) > 0.001 or + abs(current_scale.z - self.gizmoTargetStartScale.z) > 0.001): + from core.Command_System import ScaleNodeCommand + command = ScaleNodeCommand(self.gizmoTarget, self.gizmoTargetStartScale, current_scale) + self.world.command_manager.execute_command(command) + # 如果是旋转操作且旋转发生了变化,则创建旋转命令 + elif (hasattr(self, 'gizmoTargetStartHpr') and hasattr(self, 'gizmoTargetStartHpr') and + self.gizmoTargetStartHpr): + current_hpr = self.gizmoTarget.getHpr() + if (abs(current_hpr.x - self.gizmoTargetStartHpr.x) > 0.001 or + abs(current_hpr.y - self.gizmoTargetStartHpr.y) > 0.001 or + abs(current_hpr.z - self.gizmoTargetStartHpr.z) > 0.001): + from core.Command_System import RotateNodeCommand + command = RotateNodeCommand(self.gizmoTarget, self.gizmoTargetStartHpr, current_hpr) + self.world.command_manager.execute_command(command) + # 恢复所有轴的颜色 for axis_name in ["x", "y", "z"]: self.setGizmoAxisColor(axis_name, self.gizmo_colors[axis_name]) @@ -1925,18 +1943,18 @@ class SelectionSystem: try: if self.selectedNode == nodePath: return - print(f"\n=== 更新选择状态 ===") + #print(f"\n=== 更新选择状态 ===") # 如果正在删除节点,避免更新选择 if hasattr(self, '_deleting_node') and self._deleting_node: print("正在删除节点,跳过选择更新") - print("=== 选择状态更新完成 ===\n") + #print("=== 选择状态更新完成 ===\n") return node_name = "None" if nodePath and not nodePath.isEmpty(): node_name = nodePath.getName() - print(f"新选择的节点: {node_name}") + #print(f"新选择的节点: {node_name}") self.selectedNode = nodePath # 添加兼容性属性 @@ -1968,7 +1986,6 @@ class SelectionSystem: else: print("× 坐标轴创建失败") - print(f"✓ 选中了节点: {node_name}") else: print("清除选择...") self.clearSelectionBox() @@ -1982,50 +1999,68 @@ class SelectionSystem: self.world.interface_manager.treeWidget.setCurrentItem(None) print("✓ 树形控件选中状态已清空") - print("=== 选择状态更新完成 ===\n") + #print("=== 选择状态更新完成 ===\n") except Exception as e: print(f"更新选择状态失败{str(e)}") import traceback traceback.print_exc() - def _updateSelectionVisuals(self, nodePath): - """更新选择的视觉效果(选择框和坐标轴)""" - try: - if nodePath and not nodePath.isEmpty(): - node_name = nodePath.getName() - print(f"开始为节点 {node_name} 创建选择框和坐标轴...") + def _reparentTreeItem(self, item, new_parent_item): + """将树项重新父化到新的父项下""" + if not item or not new_parent_item: + return - # 创建选择框 - print("创建选择框...") - self.createSelectionBox(nodePath) - if self.selectionBox: - box_name = "Unknown" - if self.selectionBox and not self.selectionBox.isEmpty(): - box_name = self.selectionBox.getName() - print(f"✓ 选择框创建成功: {box_name}") - else: - print("× 选择框创建失败") + # 从当前父项中移除 + current_parent = item.parent() + if current_parent: + current_parent.removeChild(item) + else: + # 如果是顶级项 + index = self.indexOfTopLevelItem(item) + if index >= 0: + self.takeTopLevelItem(index) - # 创建坐标轴 - print("创建坐标轴...") - self.createGizmo(nodePath) - if self.gizmo: - gizmo_name = "Unknown" - if self.gizmo and not self.gizmo.isEmpty(): - gizmo_name = self.gizmo.getName() - print(f"✓ 坐标轴创建成功: {gizmo_name}") - else: - print("× 坐标轴创建失败") + # 添加到新父项 + new_parent_item.addChild(item) - print(f"✓ 选中了节点: {node_name}") - else: - print("清除选择...") - self.clearSelectionBox() - self.clearGizmo() - print("✓ 取消选择") - - except Exception as e: - print(f"更新选择视觉效果失败: {e}") + # def _updateSelectionVisuals(self, nodePath): + # """更新选择的视觉效果(选择框和坐标轴)""" + # try: + # if nodePath and not nodePath.isEmpty(): + # node_name = nodePath.getName() + # print(f"开始为节点 {node_name} 创建选择框和坐标轴...") + # + # # 创建选择框 + # print("创建选择框...") + # self.createSelectionBox(nodePath) + # if self.selectionBox: + # box_name = "Unknown" + # if self.selectionBox and not self.selectionBox.isEmpty(): + # box_name = self.selectionBox.getName() + # print(f"✓ 选择框创建成功: {box_name}") + # else: + # print("× 选择框创建失败") + # + # # 创建坐标轴 + # print("创建坐标轴...") + # self.createGizmo(nodePath) + # if self.gizmo: + # gizmo_name = "Unknown" + # if self.gizmo and not self.gizmo.isEmpty(): + # gizmo_name = self.gizmo.getName() + # print(f"✓ 坐标轴创建成功: {gizmo_name}") + # else: + # print("× 坐标轴创建失败") + # + # print(f"✓ 选中了节点: {node_name}") + # else: + # print("清除选择...") + # self.clearSelectionBox() + # self.clearGizmo() + # print("✓ 取消选择") + # + # except Exception as e: + # print(f"更新选择视觉效果失败: {e}") def getSelectedNode(self): """获取当前选中的节点""" @@ -2267,7 +2302,7 @@ class SelectionSystem: view_direction.normalize() # 计算合适的观察距离 - optimal_distance = max(size * 2.0, 5.0) + optimal_distance = max(size * 1, 1.0) # 计算目标摄像机位置 target_cam_pos = center + (view_direction * optimal_distance) diff --git a/core/vr_actions.py b/core/vr_actions.py new file mode 100644 index 00000000..378476db --- /dev/null +++ b/core/vr_actions.py @@ -0,0 +1,583 @@ +""" +VR动作系统模块 + +基于OpenVR Action系统,提供高级的输入处理和动作映射: +- VR动作清单管理 +- 按钮和轴输入处理 +- 触觉反馈 +- 动作集管理 +""" + +import json +import os +from pathlib import Path +from direct.showbase.DirectObject import DirectObject + +try: + import openvr + OPENVR_AVAILABLE = True +except ImportError: + OPENVR_AVAILABLE = False + + +class VRActionManager(DirectObject): + """VR动作管理器 - 处理OpenVR动作系统""" + + def __init__(self, vr_manager): + """初始化VR动作管理器 + + Args: + vr_manager: VR管理器实例 + """ + super().__init__() + + self.vr_manager = vr_manager + self.vr_input = None + self.action_set_handles = [] + self.action_handles = {} + + # 预定义的标准动作 + self.standard_actions = { + # 姿态动作 + 'pose': '/actions/default/in/Pose', + + # 按钮动作 + 'trigger': '/actions/default/in/Trigger', + 'grip': '/actions/default/in/Grip', + 'menu': '/actions/default/in/Menu', + 'system': '/actions/default/in/System', + 'trackpad_click': '/actions/default/in/TrackpadClick', + 'trackpad_touch': '/actions/default/in/TrackpadTouch', + 'a_button': '/actions/default/in/AButton', + 'b_button': '/actions/default/in/BButton', + + # 轴动作 + 'trackpad': '/actions/default/in/Trackpad', + 'joystick': '/actions/default/in/Joystick', + 'squeeze': '/actions/default/in/Squeeze', + + # 震动输出 + 'haptic': '/actions/default/out/Haptic' + } + + # 动作集 + self.default_action_set = '/actions/default' + + print("✓ VR动作管理器初始化完成") + + def initialize(self): + """初始化VR动作系统""" + if not OPENVR_AVAILABLE or not self.vr_manager.vr_system: + print("⚠️ VR系统不可用,无法初始化动作系统") + return False + + try: + print("🎮 正在初始化VR动作系统...") + + # 获取VR输入接口 + self.vr_input = openvr.VRInput() + if not self.vr_input: + print("❌ 无法获取VR输入接口") + return False + + # 创建动作清单文件 + manifest_path = self._create_action_manifest() + if not manifest_path: + print("❌ 无法创建动作清单") + return False + + # 加载动作清单 + error = self.vr_input.setActionManifestPath(manifest_path) + if error != openvr.VRInputError_None: + print(f"❌ 加载动作清单失败: {error}") + return False + + # 获取动作句柄 + self._load_action_handles() + + # 创建动作集 + self._setup_action_sets() + + print("✅ VR动作系统初始化成功") + return True + + except Exception as e: + print(f"❌ VR动作系统初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def _create_action_manifest(self): + """创建VR动作清单文件""" + try: + # 动作清单配置 + manifest_data = { + "actions": [ + # 姿态动作 + { + "name": "/actions/default/in/Pose", + "type": "pose" + }, + + # 数字动作(按钮) + { + "name": "/actions/default/in/Trigger", + "type": "boolean" + }, + { + "name": "/actions/default/in/Grip", + "type": "boolean" + }, + { + "name": "/actions/default/in/Menu", + "type": "boolean" + }, + { + "name": "/actions/default/in/System", + "type": "boolean" + }, + { + "name": "/actions/default/in/TrackpadClick", + "type": "boolean" + }, + { + "name": "/actions/default/in/TrackpadTouch", + "type": "boolean" + }, + { + "name": "/actions/default/in/AButton", + "type": "boolean" + }, + { + "name": "/actions/default/in/BButton", + "type": "boolean" + }, + + # 模拟动作(轴) + { + "name": "/actions/default/in/Trackpad", + "type": "vector2" + }, + { + "name": "/actions/default/in/Joystick", + "type": "vector2" + }, + { + "name": "/actions/default/in/Squeeze", + "type": "vector1" + }, + + # 震动输出 + { + "name": "/actions/default/out/Haptic", + "type": "vibration" + } + ], + + "action_sets": [ + { + "name": "/actions/default", + "usage": "single" + } + ], + + "default_bindings": [ + { + "controller_type": "vive_controller", + "binding_url": "bindings_vive.json" + }, + { + "controller_type": "oculus_touch", + "binding_url": "bindings_oculus.json" + }, + { + "controller_type": "knuckles", + "binding_url": "bindings_index.json" + } + ], + + "localization": [ + { + "language_tag": "zh_CN", + "/actions/default/in/Trigger": "扳机", + "/actions/default/in/Grip": "握把", + "/actions/default/in/Menu": "菜单", + "/actions/default/in/System": "系统", + "/actions/default/in/TrackpadClick": "触摸板点击", + "/actions/default/in/TrackpadTouch": "触摸板触摸", + "/actions/default/in/Pose": "手部姿态", + "/actions/default/out/Haptic": "震动反馈" + } + ] + } + + # 保存到临时目录 + manifest_dir = Path.cwd() / "vr_actions" + manifest_dir.mkdir(exist_ok=True) + + manifest_path = manifest_dir / "actions.json" + with open(manifest_path, 'w', encoding='utf-8') as f: + json.dump(manifest_data, f, indent=2, ensure_ascii=False) + + # 创建基本的绑定文件 + self._create_default_bindings(manifest_dir) + + print(f"✓ 动作清单已创建: {manifest_path}") + return str(manifest_path) + + except Exception as e: + print(f"❌ 创建动作清单失败: {e}") + return None + + def _create_default_bindings(self, manifest_dir): + """创建默认的控制器绑定文件""" + # Vive控制器绑定 + vive_bindings = { + "controller_type": "vive_controller", + "description": "Vive控制器绑定", + "name": "EG VR Editor - Vive", + "bindings": { + "/actions/default": { + "sources": [ + { + "inputs": { + "click": { + "output": "/actions/default/in/Trigger" + } + }, + "mode": "button", + "path": "/user/hand/left/input/trigger" + }, + { + "inputs": { + "click": { + "output": "/actions/default/in/Trigger" + } + }, + "mode": "button", + "path": "/user/hand/right/input/trigger" + }, + { + "inputs": { + "click": { + "output": "/actions/default/in/Grip" + } + }, + "mode": "button", + "path": "/user/hand/left/input/grip" + }, + { + "inputs": { + "click": { + "output": "/actions/default/in/Grip" + } + }, + "mode": "button", + "path": "/user/hand/right/input/grip" + }, + { + "inputs": { + "click": { + "output": "/actions/default/in/Menu" + } + }, + "mode": "button", + "path": "/user/hand/left/input/menu" + }, + { + "inputs": { + "position": { + "output": "/actions/default/in/Trackpad" + }, + "click": { + "output": "/actions/default/in/TrackpadClick" + }, + "touch": { + "output": "/actions/default/in/TrackpadTouch" + } + }, + "mode": "trackpad", + "path": "/user/hand/left/input/trackpad" + }, + { + "inputs": { + "position": { + "output": "/actions/default/in/Trackpad" + }, + "click": { + "output": "/actions/default/in/TrackpadClick" + }, + "touch": { + "output": "/actions/default/in/TrackpadTouch" + } + }, + "mode": "trackpad", + "path": "/user/hand/right/input/trackpad" + } + ], + "poses": [ + { + "output": "/actions/default/in/Pose", + "path": "/user/hand/left/pose/raw" + }, + { + "output": "/actions/default/in/Pose", + "path": "/user/hand/right/pose/raw" + } + ], + "haptics": [ + { + "output": "/actions/default/out/Haptic", + "path": "/user/hand/left/output/haptic" + }, + { + "output": "/actions/default/out/Haptic", + "path": "/user/hand/right/output/haptic" + } + ] + } + } + } + + bindings_path = manifest_dir / "bindings_vive.json" + with open(bindings_path, 'w', encoding='utf-8') as f: + json.dump(vive_bindings, f, indent=2) + + print(f"✓ Vive控制器绑定已创建: {bindings_path}") + + def _load_action_handles(self): + """加载动作句柄""" + if not self.vr_input: + return + + try: + for action_name, action_path in self.standard_actions.items(): + handle = self.vr_input.getActionHandle(action_path) + self.action_handles[action_name] = handle + print(f"✓ 加载动作: {action_name} -> {handle}") + + except Exception as e: + print(f"⚠️ 加载动作句柄失败: {e}") + + def _setup_action_sets(self): + """设置动作集""" + if not self.vr_input: + return + + try: + # 获取默认动作集句柄 + action_set_handle = self.vr_input.getActionSetHandle(self.default_action_set) + self.action_set_handles = [action_set_handle] + + print(f"✓ 动作集已设置: {self.default_action_set}") + + except Exception as e: + print(f"⚠️ 设置动作集失败: {e}") + + def update_actions(self): + """更新动作状态 - 每帧调用""" + if not self.vr_input or not self.action_set_handles: + return + + try: + # 更新动作状态 + action_sets = (openvr.VRActiveActionSet_t * len(self.action_set_handles))() + for i, action_set_handle in enumerate(self.action_set_handles): + action_sets[i].ulActionSet = action_set_handle + + self.vr_input.updateActionState(action_sets) + + except Exception as e: + # 限制错误输出频率 + if not hasattr(self, '_last_action_error_frame'): + self._last_action_error_frame = 0 + + if hasattr(self.vr_manager, 'frame_count'): + if self.vr_manager.frame_count - self._last_action_error_frame > 300: # 每5秒输出一次 + print(f"⚠️ 更新动作状态失败: {e}") + self._last_action_error_frame = self.vr_manager.frame_count + + def is_digital_action_pressed(self, action_name, device_path=None): + """检查数字动作是否被按下 + + Args: + action_name: 动作名称 + device_path: 设备路径(可选) + + Returns: + tuple: (是否按下, 设备路径) + """ + if not self.vr_input or action_name not in self.action_handles: + return False, None + + try: + action_handle = self.action_handles[action_name] + device_handle = openvr.k_ulInvalidInputValueHandle + + if device_path: + device_handle = self.vr_input.getInputSourceHandle(device_path) + + action_data = self.vr_input.getDigitalActionData(action_handle, device_handle) + + if device_path and action_data.bActive: + origin_info = self.vr_input.getOriginTrackedDeviceInfo(action_data.activeOrigin) + device_path = origin_info.devicePath + + return action_data.bActive and action_data.bState, device_path + + except Exception as e: + return False, None + + def is_digital_action_just_pressed(self, action_name, device_path=None): + """检查数字动作是否刚刚被按下(上升沿)""" + if not self.vr_input or action_name not in self.action_handles: + return False, None + + try: + action_handle = self.action_handles[action_name] + device_handle = openvr.k_ulInvalidInputValueHandle + + if device_path: + device_handle = self.vr_input.getInputSourceHandle(device_path) + + action_data = self.vr_input.getDigitalActionData(action_handle, device_handle) + + if device_path and action_data.bActive: + origin_info = self.vr_input.getOriginTrackedDeviceInfo(action_data.activeOrigin) + device_path = origin_info.devicePath + + return action_data.bActive and action_data.bChanged and action_data.bState, device_path + + except Exception as e: + return False, None + + def is_digital_action_just_released(self, action_name, device_path=None): + """检查数字动作是否刚刚被释放(下降沿)""" + if not self.vr_input or action_name not in self.action_handles: + return False, None + + try: + action_handle = self.action_handles[action_name] + device_handle = openvr.k_ulInvalidInputValueHandle + + if device_path: + device_handle = self.vr_input.getInputSourceHandle(device_path) + + action_data = self.vr_input.getDigitalActionData(action_handle, device_handle) + + if device_path and action_data.bActive: + origin_info = self.vr_input.getOriginTrackedDeviceInfo(action_data.activeOrigin) + device_path = origin_info.devicePath + + return action_data.bActive and action_data.bChanged and not action_data.bState, device_path + + except Exception as e: + return False, None + + def get_analog_action_value(self, action_name, device_path=None): + """获取模拟动作值 + + Args: + action_name: 动作名称 + device_path: 设备路径(可选) + + Returns: + tuple: (值, 设备路径) - 值为Vec2(x,y)对于vector2,float对于vector1 + """ + if not self.vr_input or action_name not in self.action_handles: + return None, None + + try: + action_handle = self.action_handles[action_name] + device_handle = openvr.k_ulInvalidInputValueHandle + + if device_path: + device_handle = self.vr_input.getInputSourceHandle(device_path) + + analog_data = self.vr_input.getAnalogActionData(action_handle, device_handle) + + if device_path and analog_data.bActive: + origin_info = self.vr_input.getOriginTrackedDeviceInfo(analog_data.activeOrigin) + device_path = origin_info.devicePath + + if analog_data.bActive: + # 根据动作类型返回适当的值 + from panda3d.core import Vec2 + if action_name in ['trackpad', 'joystick']: + return Vec2(analog_data.x, analog_data.y), device_path + else: + return analog_data.x, device_path + + return None, device_path + + except Exception as e: + return None, None + + def get_pose_action_data(self, action_name, device_path=None): + """获取姿态动作数据""" + if not self.vr_input or action_name not in self.action_handles: + return None + + try: + action_handle = self.action_handles[action_name] + device_handle = openvr.k_ulInvalidInputValueHandle + + if device_path: + device_handle = self.vr_input.getInputSourceHandle(device_path) + + pose_data = self.vr_input.getPoseActionDataForNextFrame( + action_handle, + openvr.TrackingUniverseStanding, + device_handle + ) + + return pose_data + + except Exception as e: + return None + + def trigger_haptic_pulse(self, action_name, duration=0.001, frequency=1.0, amplitude=1.0, device_path=None): + """触发震动反馈 + + Args: + action_name: 震动动作名称 + duration: 持续时间(秒) + frequency: 频率 + amplitude: 振幅 (0.0-1.0) + device_path: 设备路径(可选) + """ + if not self.vr_input or action_name not in self.action_handles: + return False + + try: + action_handle = self.action_handles[action_name] + device_handle = openvr.k_ulInvalidInputValueHandle + + if device_path: + device_handle = self.vr_input.getInputSourceHandle(device_path) + + # 触发震动 + self.vr_input.triggerHapticVibrationAction( + action_handle, + 0, # 开始时间 + duration, + frequency, + amplitude, + device_handle + ) + + return True + + except Exception as e: + print(f"⚠️ 触发震动反馈失败: {e}") + return False + + def cleanup(self): + """清理资源""" + self.ignoreAll() + + # 清理动作句柄 + self.action_handles.clear() + self.action_set_handles.clear() + + print("🧹 VR动作管理器已清理") \ No newline at end of file diff --git a/core/vr_controller.py b/core/vr_controller.py new file mode 100644 index 00000000..e293d07f --- /dev/null +++ b/core/vr_controller.py @@ -0,0 +1,432 @@ +""" +VR手柄管理模块 + +基于panda3d-openvr参考实现,提供完整的VR手柄追踪和交互功能: +- 手柄位置和姿态追踪 +- 按钮和触摸板输入处理 +- 手柄可视化和射线显示 +- 震动反馈支持 +""" + +from panda3d.core import ( + NodePath, PandaNode, Vec3, Mat4, LVector3, LMatrix4, + GeomNode, LineSegs, CardMaker, Texture, RenderState, + TransparencyAttrib, ColorAttrib, Vec4 +) +from direct.actor.Actor import Actor +from direct.showbase.DirectObject import DirectObject + +try: + import openvr + OPENVR_AVAILABLE = True +except ImportError: + OPENVR_AVAILABLE = False + +# 导入可视化器 +from .vr_visualization import VRControllerVisualizer + + +class VRController(DirectObject): + """VR手柄基类 - 管理单个手柄的追踪和交互""" + + def __init__(self, vr_manager, name, hand_path, device_index=None): + """初始化VR手柄 + + Args: + vr_manager: VR管理器实例 + name: 手柄名称 ('left' 或 'right') + hand_path: OpenVR手部路径 ('/user/hand/left' 或 '/user/hand/right') + device_index: OpenVR设备索引(可选) + """ + super().__init__() + + self.vr_manager = vr_manager + self.name = name + self.hand_path = hand_path + self.device_index = device_index + + # 手柄状态 + self.is_connected = False + self.is_pose_valid = False + self.pose = Mat4.identMat() + self.velocity = Vec3(0, 0, 0) + self.angular_velocity = Vec3(0, 0, 0) + + # 按钮状态 + self.button_states = {} + self.previous_button_states = {} + self.trigger_value = 0.0 + self.grip_value = 0.0 + self.touchpad_pos = Vec3(0, 0, 0) + self.touchpad_touched = False + + # 摇杆状态 - 用于传送和转向交互 + self.joystick_pos = Vec3(0, 0, 0) # 摇杆位置 (x, y, 0) + self.joystick_touched = False # 摇杆是否被触摸 + self.joystick_pressed = False # 摇杆是否被按下 + self.previous_joystick_pos = Vec3(0, 0, 0) # 上一帧摇杆位置 + + # 3D节点和可视化 + self.anchor_node = None + self.visualizer = None + self.ray_length = 10.0 + + # 初始化 + self._create_anchor() + self._create_visualizer() + + print(f"✓ {name}手柄控制器初始化完成") + + def _create_anchor(self): + """创建手柄锚点节点""" + if self.vr_manager.tracking_space: + self.anchor_node = self.vr_manager.tracking_space.attachNewNode(f'{self.name}-controller') + self.anchor_node.hide() # 初始隐藏,直到获得有效姿态 + + def _create_visualizer(self): + """创建手柄可视化器""" + if self.anchor_node and hasattr(self.vr_manager, 'world'): + self.visualizer = VRControllerVisualizer(self, self.vr_manager.world.render) + elif self.anchor_node: + # 如果没有世界对象,使用基础渲染节点 + from panda3d.core import NodePath + render = NodePath('render') + self.visualizer = VRControllerVisualizer(self, render) + + def set_device_index(self, device_index): + """设置OpenVR设备索引""" + self.device_index = device_index + self.is_connected = True + print(f"📱 {self.name}手柄连接 (设备索引: {device_index})") + + def update_pose(self, pose_data): + """更新手柄姿态 + + Args: + pose_data: OpenVR TrackedDevicePose_t数据 + """ + if not pose_data.bPoseIsValid: + self.is_pose_valid = False + if self.anchor_node: + self.anchor_node.hide() + return + + self.is_pose_valid = True + + # 转换OpenVR矩阵到Panda3D + if hasattr(self.vr_manager, 'convert_mat') and hasattr(self.vr_manager, 'coord_mat_inv') and hasattr(self.vr_manager, 'coord_mat'): + modelview = self.vr_manager.convert_mat(pose_data.mDeviceToAbsoluteTracking) + self.pose = self.vr_manager.coord_mat_inv * modelview * self.vr_manager.coord_mat + else: + # 直接使用矩阵数据 + m = pose_data.mDeviceToAbsoluteTracking.m + self.pose = LMatrix4( + m[0][0], m[1][0], m[2][0], m[3][0], + m[0][1], m[1][1], m[2][1], m[3][1], + m[0][2], m[1][2], m[2][2], m[3][2], + m[0][3], m[1][3], m[2][3], m[3][3] + ) + + # 更新锚点变换 + if self.anchor_node: + self.anchor_node.setMat(self.pose) + self.anchor_node.show() + + # 更新可视化 + if self.visualizer: + self.visualizer.update() + + # 更新速度信息 + vel = pose_data.vVelocity + self.velocity = Vec3(vel[0], vel[1], vel[2]) + + ang_vel = pose_data.vAngularVelocity + self.angular_velocity = Vec3(ang_vel[0], ang_vel[1], ang_vel[2]) + + def update_input_state(self, vr_system): + """更新输入状态 + + Args: + vr_system: OpenVR系统实例 + """ + if not self.is_connected or not OPENVR_AVAILABLE or not vr_system: + return + + # 保存上一帧的按钮状态和摇杆位置 + self.previous_button_states = self.button_states.copy() + self.previous_joystick_pos = Vec3(self.joystick_pos) + + # 获取控制器状态 + try: + result, state = vr_system.getControllerState(self.device_index) + if result: + # 更新按钮状态 - 使用正确的OpenVR属性名 + for i in range(openvr.k_EButton_Max): + button_mask = 1 << i + # OpenVR Python绑定中使用ulButtonPressed而不是rButtonPressed + self.button_states[i] = (state.ulButtonPressed & button_mask) != 0 + + # 更新轴状态(扳机、握把、触摸板、摇杆) + # 兼容不同版本的OpenVR Python绑定 + axis_data = None + if hasattr(state, 'rAxis'): + axis_data = state.rAxis # 旧版本使用rAxis + elif hasattr(state, 'vAxis'): + axis_data = state.vAxis # 新版本使用vAxis + + if axis_data is not None and len(axis_data) > 0: + # 调试输出 - 显示所有轴数据(仅当有变化时) + self._debug_axis_data(axis_data) + + # 扳机轴通常在axis[1].x + if len(axis_data) > 1: + self.trigger_value = axis_data[1].x + + # 触摸板/摇杆轴通常在axis[0] + if len(axis_data) > 0: + self.touchpad_pos = Vec3(axis_data[0].x, axis_data[0].y, 0) + # 摇杆和触摸板通常使用同一个轴,但可以区分设备类型 + self.joystick_pos = Vec3(axis_data[0].x, axis_data[0].y, 0) + + # 额外检查其他轴(某些控制器可能将摇杆分配到不同轴) + if len(axis_data) > 2: + # 有些控制器可能在axis[2]有摇杆数据 + axis2_magnitude = abs(axis_data[2].x) + abs(axis_data[2].y) + if axis2_magnitude > 0.1: # 如果有显著输入,使用这个轴作为摇杆 + self.joystick_pos = Vec3(axis_data[2].x, axis_data[2].y, 0) + + # 检查axis[3]和axis[4](Quest控制器可能使用这些轴) + for axis_idx in range(3, min(len(axis_data), 5)): + axis_magnitude = abs(axis_data[axis_idx].x) + abs(axis_data[axis_idx].y) + if axis_magnitude > 0.1: # 如果有显著输入 + # 覆盖之前的摇杆数据,使用最有活动的轴 + self.joystick_pos = Vec3(axis_data[axis_idx].x, axis_data[axis_idx].y, 0) + # 调试输出 + if not hasattr(self, '_last_axis_notify'): + self._last_axis_notify = {} + if self._last_axis_notify.get(axis_idx, 0) == 0: + print(f"🎮 {self.name}手检测到axis[{axis_idx}]活动: ({axis_data[axis_idx].x:.3f}, {axis_data[axis_idx].y:.3f})") + self._last_axis_notify[axis_idx] = 60 # 60帧后再次提醒 + else: + self._last_axis_notify[axis_idx] -= 1 + + # 触摸板和摇杆触摸状态 + self.touchpad_touched = (state.ulButtonTouched & (1 << openvr.k_EButton_SteamVR_Touchpad)) != 0 + + # 摇杆触摸状态(检查多个可能的按钮) + joystick_touch_mask = 0 + if hasattr(openvr, 'k_EButton_Joystick'): + joystick_touch_mask |= (1 << openvr.k_EButton_Joystick) + if hasattr(openvr, 'k_EButton_Thumbstick'): + joystick_touch_mask |= (1 << openvr.k_EButton_Thumbstick) + + self.joystick_touched = (state.ulButtonTouched & joystick_touch_mask) != 0 or self.touchpad_touched + + # 摇杆按下状态 + joystick_press_mask = 0 + if hasattr(openvr, 'k_EButton_Joystick'): + joystick_press_mask |= (1 << openvr.k_EButton_Joystick) + if hasattr(openvr, 'k_EButton_Thumbstick'): + joystick_press_mask |= (1 << openvr.k_EButton_Thumbstick) + + self.joystick_pressed = (state.ulButtonPressed & joystick_press_mask) != 0 + + except Exception as e: + # 减少错误输出频率 + if not hasattr(self, '_last_input_error_frame'): + self._last_input_error_frame = 0 + + # 获取当前帧数(通过VR管理器) + current_frame = getattr(self.vr_manager, 'frame_count', 0) + + # 每5秒最多输出一次错误(300帧@60fps) + if current_frame - self._last_input_error_frame > 300: + print(f"⚠️ 更新{self.name}手柄输入状态失败: {e}") + self._last_input_error_frame = current_frame + + def is_button_pressed(self, button_id): + """检查按钮是否被按下""" + return self.button_states.get(button_id, False) + + def is_button_just_pressed(self, button_id): + """检查按钮是否刚刚被按下(上升沿)""" + current = self.button_states.get(button_id, False) + previous = self.previous_button_states.get(button_id, False) + return current and not previous + + def is_button_just_released(self, button_id): + """检查按钮是否刚刚被释放(下降沿)""" + current = self.button_states.get(button_id, False) + previous = self.previous_button_states.get(button_id, False) + return not current and previous + + def is_trigger_pressed(self, threshold=0.1): + """检查扳机是否被按下""" + return self.trigger_value > threshold + + def is_grip_pressed(self, threshold=0.1): + """检查握把是否被按下""" + return self.grip_value > threshold + + def show_ray(self, show=True): + """显示或隐藏交互射线""" + if self.visualizer: + if show: + self.visualizer.show_ray() + else: + self.visualizer.hide_ray() + + def set_ray_color(self, color): + """设置射线颜色""" + if self.visualizer and len(color) >= 3: + from panda3d.core import Vec4 + color_vec = Vec4(color[0], color[1], color[2], color[3] if len(color) > 3 else 1.0) + self.visualizer.set_ray_color(color_vec) + + def trigger_haptic_feedback(self, duration=0.001, strength=1.0): + """触发震动反馈 + + Args: + duration: 震动持续时间(秒) + strength: 震动强度 (0.0-1.0) + """ + if not self.is_connected or not OPENVR_AVAILABLE: + return + + try: + if hasattr(self.vr_manager, 'vr_system') and self.vr_manager.vr_system: + # OpenVR的震动API + duration_microseconds = int(duration * 1000000) + self.vr_manager.vr_system.triggerHapticPulse( + self.device_index, + 0, # axis ID (通常为0) + int(strength * 3999) # 强度 (0-3999) + ) + except Exception as e: + print(f"⚠️ {self.name}手柄震动反馈失败: {e}") + + def get_world_position(self): + """获取手柄在世界坐标系中的位置""" + if self.anchor_node: + return self.anchor_node.getPos(self.vr_manager.world.render) + return Vec3(0, 0, 0) + + def get_world_rotation(self): + """获取手柄在世界坐标系中的旋转""" + if self.anchor_node: + return self.anchor_node.getHpr(self.vr_manager.world.render) + return Vec3(0, 0, 0) + + def get_forward_direction(self): + """获取手柄指向的方向向量(包含视角转向)""" + if self.anchor_node: + # 获取相对于世界坐标系的方向,包含tracking_space的旋转 + if hasattr(self.vr_manager, 'world') and self.vr_manager.world: + # 使用世界变换,包含所有父节点的旋转 + world_transform = self.anchor_node.getMat(self.vr_manager.world.render) + forward = Vec3(world_transform.getRow3(1)) # Y轴 = 前方 + else: + # 备选:使用局部变换 + forward = Vec3(self.anchor_node.getMat().getRow3(1)) + + if forward.length() > 0: + return forward.normalized() + return Vec3(0, 1, 0) + + def is_joystick_touched(self): + """检查摇杆是否被触摸""" + return self.joystick_touched + + def is_joystick_pressed(self): + """检查摇杆是否被按下""" + return self.joystick_pressed + + def get_joystick_position(self): + """获取摇杆位置 + + Returns: + Vec3: 摇杆位置 (x, y, 0),范围 [-1, 1] + """ + return Vec3(self.joystick_pos) + + def get_joystick_delta(self): + """获取摇杆位置变化 + + Returns: + Vec3: 摇杆位置变化向量 + """ + return self.joystick_pos - self.previous_joystick_pos + + def is_joystick_moved(self, threshold=0.01): + """检查摇杆是否移动 + + Args: + threshold: 移动阈值 + + Returns: + bool: 是否移动 + """ + delta = self.get_joystick_delta() + return delta.length() > threshold + + def _debug_axis_data(self, axis_data): + """调试输出轴数据""" + try: + # 只在有活动时输出调试信息 + has_activity = False + active_axes = [] + + for i, axis in enumerate(axis_data): + magnitude = abs(axis.x) + abs(axis.y) + if magnitude > 0.01: # 检测到活动 + has_activity = True + active_axes.append(f"axis[{i}]: ({axis.x:.3f}, {axis.y:.3f})") + + if has_activity: + # 初始化调试计数器 + if not hasattr(self, '_debug_axis_counter'): + self._debug_axis_counter = 0 + + self._debug_axis_counter += 1 + + # 每30帧输出一次详细信息 + if self._debug_axis_counter % 30 == 1: + print(f"🔍 {self.name}手轴数据调试:") + print(f" 总轴数: {len(axis_data)}") + print(f" 活跃轴: {', '.join(active_axes)}") + + # 显示所有轴的当前值(不管是否活跃) + all_axes = [] + for i, axis in enumerate(axis_data): + all_axes.append(f"[{i}]:({axis.x:.3f},{axis.y:.3f})") + print(f" 所有轴: {' '.join(all_axes)}") + + except Exception as e: + print(f"⚠️ 轴数据调试失败: {e}") + + def cleanup(self): + """清理资源""" + self.ignoreAll() + + if self.visualizer: + self.visualizer.cleanup() + + if self.anchor_node: + self.anchor_node.removeNode() + + self.is_connected = False + print(f"🧹 {self.name}手柄控制器已清理") + + +class LeftController(VRController): + """左手控制器""" + + def __init__(self, vr_manager): + super().__init__(vr_manager, 'left', '/user/hand/left') + + +class RightController(VRController): + """右手控制器""" + + def __init__(self, vr_manager): + super().__init__(vr_manager, 'right', '/user/hand/right') \ No newline at end of file diff --git a/core/vr_input_handler.py b/core/vr_input_handler.py deleted file mode 100644 index 2438e601..00000000 --- a/core/vr_input_handler.py +++ /dev/null @@ -1,430 +0,0 @@ -""" -VR输入处理器 - -处理VR控制器输入、手势识别和VR交互 -支持多种VR控制器和手势输入 -""" - -from direct.showbase.DirectObject import DirectObject -from panda3d.core import Vec3, Point3, CollisionRay, CollisionNode, CollisionHandlerQueue -from direct.task import Task -import time - - -class VRInputHandler(DirectObject): - """VR输入处理器""" - - def __init__(self, world, vr_manager): - super().__init__() - self.world = world - self.vr_manager = vr_manager - - # 控制器状态 - self.controllers = {} - self.controller_nodes = {} - self.controller_rays = {} - - # 手势识别 - self.gesture_enabled = True - self.gesture_history = [] - self.gesture_threshold = 0.1 - - # 交互系统 - self.interaction_enabled = True - self.selected_object = None - self.grab_offset = Vec3(0, 0, 0) - - # 输入映射 - self.input_mappings = { - 'trigger': self._handle_trigger, - 'grip': self._handle_grip, - 'touchpad': self._handle_touchpad, - 'menu': self._handle_menu, - 'system': self._handle_system - } - - print("✓ VR输入处理器初始化完成") - - def start_input_handling(self): - """启动输入处理""" - if not self.vr_manager.is_vr_enabled(): - print("VR未启用,无法启动输入处理") - return False - - # 启动输入更新任务 - self.world.taskMgr.add(self._update_input, "vr_input_update") - - # 设置控制器可视化 - self._setup_controller_visualization() - - print("✓ VR输入处理已启动") - return True - - def stop_input_handling(self): - """停止输入处理""" - self.world.taskMgr.remove("vr_input_update") - self._cleanup_controller_visualization() - print("✓ VR输入处理已停止") - - def _update_input(self, task): - """更新输入处理""" - if not self.vr_manager.is_vr_enabled(): - return Task.cont - - try: - # 更新所有控制器 - self._update_controllers() - - # 处理手势识别 - if self.gesture_enabled: - self._process_gestures() - - # 处理交互 - if self.interaction_enabled: - self._process_interactions() - - except Exception as e: - print(f"VR输入更新错误: {str(e)}") - - return Task.cont - - def _update_controllers(self): - """更新控制器状态""" - # 获取控制器姿态 - controller_poses = self.vr_manager.controller_poses - - for controller_id, pose in controller_poses.items(): - # 获取控制器输入 - input_data = self.vr_manager.get_controller_input(controller_id) - if not input_data: - continue - - # 更新控制器状态 - if controller_id not in self.controllers: - self.controllers[controller_id] = {} - - prev_state = self.controllers[controller_id].copy() - self.controllers[controller_id] = input_data - - # 更新控制器可视化 - self._update_controller_visualization(controller_id, pose) - - # 处理输入事件 - self._process_controller_input(controller_id, input_data, prev_state) - - def _process_controller_input(self, controller_id, current_state, prev_state): - """处理控制器输入""" - # 检查按钮状态变化 - for input_type, handler in self.input_mappings.items(): - if input_type in current_state: - current_value = current_state[input_type] - prev_value = prev_state.get(input_type, 0) - - # 处理按钮按下/释放 - if isinstance(current_value, (int, float)): - if current_value > 0.5 and prev_value <= 0.5: - handler(controller_id, 'press', current_value) - elif current_value <= 0.5 and prev_value > 0.5: - handler(controller_id, 'release', current_value) - elif current_value > 0.5: - handler(controller_id, 'hold', current_value) - - # 处理触摸板 - elif isinstance(current_value, tuple) and len(current_value) == 2: - if current_value != prev_value: - handler(controller_id, 'move', current_value) - - def _handle_trigger(self, controller_id, action, value): - """处理扳机输入""" - if action == 'press': - print(f"控制器 {controller_id} 扳机按下 (强度: {value:.2f})") - self._try_grab_object(controller_id) - elif action == 'release': - print(f"控制器 {controller_id} 扳机释放") - self._try_release_object(controller_id) - - def _handle_grip(self, controller_id, action, value): - """处理握持输入""" - if action == 'press': - print(f"控制器 {controller_id} 握持按下 (强度: {value:.2f})") - self._toggle_interaction_mode(controller_id) - elif action == 'release': - print(f"控制器 {controller_id} 握持释放") - - def _handle_touchpad(self, controller_id, action, value): - """处理触摸板输入""" - if action == 'move': - x, y = value - print(f"控制器 {controller_id} 触摸板: ({x:.2f}, {y:.2f})") - - # 根据触摸板位置执行不同操作 - if abs(x) > 0.7: # 左右滑动 - self._handle_horizontal_swipe(controller_id, x) - elif abs(y) > 0.7: # 上下滑动 - self._handle_vertical_swipe(controller_id, y) - - def _handle_menu(self, controller_id, action, value): - """处理菜单按钮""" - if action == 'press': - print(f"控制器 {controller_id} 菜单按钮按下") - self._show_vr_menu(controller_id) - - def _handle_system(self, controller_id, action, value): - """处理系统按钮""" - if action == 'press': - print(f"控制器 {controller_id} 系统按钮按下") - # 系统按钮通常由VR系统处理 - - def _handle_horizontal_swipe(self, controller_id, direction): - """处理水平滑动""" - if direction > 0: - print(f"控制器 {controller_id} 右滑") - self._switch_tool(controller_id, 'next') - else: - print(f"控制器 {controller_id} 左滑") - self._switch_tool(controller_id, 'prev') - - def _handle_vertical_swipe(self, controller_id, direction): - """处理垂直滑动""" - if direction > 0: - print(f"控制器 {controller_id} 上滑") - self._zoom_in(controller_id) - else: - print(f"控制器 {controller_id} 下滑") - self._zoom_out(controller_id) - - def _try_grab_object(self, controller_id): - """尝试抓取对象""" - if controller_id not in self.controllers: - return - - # 获取控制器射线 - ray = self._get_controller_ray(controller_id) - if not ray: - return - - # 执行射线检测 - hit_object = self._raycast_from_controller(controller_id) - if hit_object: - self.selected_object = hit_object - controller_pose = self.controllers[controller_id].get('pose') - if controller_pose: - # 计算抓取偏移 - object_pos = hit_object.getPos() - controller_pos = controller_pose.getTranslate() - self.grab_offset = object_pos - controller_pos - - print(f"抓取对象: {hit_object.getName()}") - - # 发送抓取事件 - self.world.event_handler.messenger.send('vr-object-grabbed', [hit_object, controller_id]) - - def _try_release_object(self, controller_id): - """尝试释放对象""" - if self.selected_object: - print(f"释放对象: {self.selected_object.getName()}") - - # 发送释放事件 - self.world.event_handler.messenger.send('vr-object-released', [self.selected_object, controller_id]) - - self.selected_object = None - self.grab_offset = Vec3(0, 0, 0) - - def _raycast_from_controller(self, controller_id): - """从控制器发射射线检测""" - if controller_id not in self.controllers: - return None - - controller_pose = self.controllers[controller_id].get('pose') - if not controller_pose: - return None - - # 获取控制器位置和方向 - controller_pos = controller_pose.getTranslate() - controller_forward = controller_pose.getQuat().getForward() - - # 创建射线 - ray = CollisionRay() - ray.setOrigin(controller_pos) - ray.setDirection(controller_forward) - - # 执行碰撞检测 - traverser = self.world.cTrav if hasattr(self.world, 'cTrav') else None - if not traverser: - return None - - handler = CollisionHandlerQueue() - collision_node = CollisionNode('vr_controller_ray') - collision_node.addSolid(ray) - - ray_np = self.world.render.attachNewNode(collision_node) - traverser.addCollider(ray_np, handler) - - # 遍历碰撞 - traverser.traverse(self.world.render) - - # 清理 - ray_np.removeNode() - - # 返回最近的碰撞对象 - if handler.getNumEntries() > 0: - handler.sortEntries() - entry = handler.getEntry(0) - return entry.getIntoNodePath() - - return None - - def _get_controller_ray(self, controller_id): - """获取控制器射线""" - return self.controller_rays.get(controller_id) - - def _setup_controller_visualization(self): - """设置控制器可视化""" - print("设置控制器可视化...") - - # 为每个控制器创建可视化节点 - for controller_id in self.controllers: - self._create_controller_model(controller_id) - - def _create_controller_model(self, controller_id): - """创建控制器模型""" - # 创建简单的控制器模型(立方体) - from panda3d.core import CardMaker - - cm = CardMaker(f"controller_{controller_id}") - cm.setFrame(-0.05, 0.05, -0.05, 0.05) - - controller_node = self.world.render.attachNewNode(cm.generate()) - controller_node.setColor(0.2, 0.8, 1.0, 0.8) - controller_node.setScale(0.1, 0.2, 0.05) - - self.controller_nodes[controller_id] = controller_node - - # 创建控制器射线可视化 - self._create_controller_ray_visual(controller_id) - - def _create_controller_ray_visual(self, controller_id): - """创建控制器射线可视化""" - from panda3d.core import LineSegs - - # 创建射线线段 - lines = LineSegs() - lines.setColor(1, 0, 0, 0.5) - lines.moveTo(0, 0, 0) - lines.drawTo(0, 2, 0) # 2米长的射线 - - ray_node = self.world.render.attachNewNode(lines.create()) - ray_node.setRenderModeWireframe() - ray_node.hide() # 默认隐藏 - - self.controller_rays[controller_id] = ray_node - - def _update_controller_visualization(self, controller_id, pose): - """更新控制器可视化""" - if controller_id in self.controller_nodes: - node = self.controller_nodes[controller_id] - node.setMat(pose) - - if controller_id in self.controller_rays: - ray_node = self.controller_rays[controller_id] - ray_node.setMat(pose) - - def _cleanup_controller_visualization(self): - """清理控制器可视化""" - for node in self.controller_nodes.values(): - node.removeNode() - - for ray in self.controller_rays.values(): - ray.removeNode() - - self.controller_nodes.clear() - self.controller_rays.clear() - - def _process_gestures(self): - """处理手势识别""" - # 简单的手势识别逻辑 - # 这里可以实现更复杂的手势识别算法 - pass - - def _process_interactions(self): - """处理交互逻辑""" - # 如果有选中的对象,更新其位置 - if self.selected_object: - self._update_grabbed_object() - - def _update_grabbed_object(self): - """更新被抓取对象的位置""" - if not self.selected_object: - return - - # 找到抓取该对象的控制器 - grabbing_controller = None - for controller_id, controller_state in self.controllers.items(): - if controller_state.get('trigger', 0) > 0.5: - grabbing_controller = controller_id - break - - if not grabbing_controller: - return - - # 更新对象位置 - controller_pose = self.controllers[grabbing_controller].get('pose') - if controller_pose: - controller_pos = controller_pose.getTranslate() - new_pos = controller_pos + self.grab_offset - self.selected_object.setPos(new_pos) - - def _toggle_interaction_mode(self, controller_id): - """切换交互模式""" - self.interaction_enabled = not self.interaction_enabled - print(f"交互模式: {'启用' if self.interaction_enabled else '禁用'}") - - def _show_vr_menu(self, controller_id): - """显示VR菜单""" - print(f"显示VR菜单 (控制器 {controller_id})") - # 这里可以实现VR菜单显示逻辑 - pass - - def _switch_tool(self, controller_id, direction): - """切换工具""" - print(f"切换工具: {direction} (控制器 {controller_id})") - # 这里可以实现工具切换逻辑 - pass - - def _zoom_in(self, controller_id): - """放大""" - print(f"放大 (控制器 {controller_id})") - # 实现放大逻辑 - pass - - def _zoom_out(self, controller_id): - """缩小""" - print(f"缩小 (控制器 {controller_id})") - # 实现缩小逻辑 - pass - - def show_controller_rays(self, show=True): - """显示/隐藏控制器射线""" - for ray in self.controller_rays.values(): - if show: - ray.show() - else: - ray.hide() - - def get_controller_state(self, controller_id): - """获取控制器状态""" - return self.controllers.get(controller_id, {}) - - def get_all_controllers(self): - """获取所有控制器""" - return list(self.controllers.keys()) - - def set_gesture_enabled(self, enabled): - """设置手势识别启用状态""" - self.gesture_enabled = enabled - print(f"手势识别: {'启用' if enabled else '禁用'}") - - def set_interaction_enabled(self, enabled): - """设置交互启用状态""" - self.interaction_enabled = enabled - print(f"VR交互: {'启用' if enabled else '禁用'}") \ No newline at end of file diff --git a/core/vr_interaction.py b/core/vr_interaction.py new file mode 100644 index 00000000..e5544ecc --- /dev/null +++ b/core/vr_interaction.py @@ -0,0 +1,432 @@ +""" +VR交互系统模块 + +提供VR手柄与3D场景的交互功能: +- 射线投射和碰撞检测 +- 对象选择和高亮 +- 对象抓取和移动 +- UI交互 +- 距离抓取 +""" + +from panda3d.core import ( + Vec3, Vec4, Mat4, Point3, CollisionRay, CollisionTraverser, + CollisionNode, CollisionHandlerQueue, BitMask32, NodePath, + CollisionSphere, CollisionTube, RenderState, TransparencyAttrib, + ColorAttrib +) +from direct.showbase.DirectObject import DirectObject + + +class VRInteractionManager(DirectObject): + """VR交互管理器 - 处理手柄与场景的交互""" + + def __init__(self, vr_manager): + """初始化VR交互管理器 + + Args: + vr_manager: VR管理器实例 + """ + super().__init__() + + self.vr_manager = vr_manager + self.world = vr_manager.world if hasattr(vr_manager, 'world') else None + + # 碰撞检测系统 + self.collision_traverser = CollisionTraverser() + self.collision_queue = CollisionHandlerQueue() + + # 射线投射节点 + self.left_ray_node = None + self.right_ray_node = None + self.ray_collision_nodes = {} + + # 选择和抓取状态 + self.selected_objects = {} # 控制器 -> 选中对象 + self.grabbed_objects = {} # 控制器 -> 抓取对象 + self.grab_offsets = {} # 控制器 -> 抓取偏移 + + # 交互参数 + self.selection_range = 50.0 # 选择距离 + self.grab_threshold = 0.5 # 抓取扳机阈值 + self.selection_color = Vec4(0.9, 0.9, 0.2, 1.0) # 选择高亮颜色 + self.grab_color = Vec4(0.2, 0.9, 0.2, 1.0) # 抓取高亮颜色 + + # 高亮状态 + self.highlighted_objects = set() + self.original_colors = {} # 存储对象原始颜色 + + print("✓ VR交互管理器初始化完成") + + def initialize(self): + """初始化交互系统""" + try: + print("🔧 正在初始化VR交互系统...") + + # 创建射线投射节点 + self._create_ray_casters() + + # 设置碰撞检测 + self._setup_collision_detection() + + print("✅ VR交互系统初始化成功") + return True + + except Exception as e: + print(f"❌ VR交互系统初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def _create_ray_casters(self): + """创建射线投射节点""" + # 为左手控制器创建射线 + if self.vr_manager.left_controller and self.vr_manager.left_controller.anchor_node: + self.left_ray_node = self._create_controller_ray('left', self.vr_manager.left_controller.anchor_node) + + # 为右手控制器创建射线 + if self.vr_manager.right_controller and self.vr_manager.right_controller.anchor_node: + self.right_ray_node = self._create_controller_ray('right', self.vr_manager.right_controller.anchor_node) + + def _create_controller_ray(self, controller_name, anchor_node): + """为控制器创建射线投射节点""" + # 创建射线碰撞体 + ray = CollisionRay() + ray.setOrigin(0, 0, 0) # 从控制器原点开始 + ray.setDirection(0, 1, 0) # 沿Y轴正方向 + + # 创建碰撞节点 + ray_collision_node = CollisionNode(f'{controller_name}_ray') + ray_collision_node.addSolid(ray) + + # 设置碰撞掩码 + ray_collision_node.setFromCollideMask(BitMask32.bit(0)) # 射线掩码 + ray_collision_node.setIntoCollideMask(BitMask32.allOff()) # 不接受碰撞 + + # 附加到控制器锚点 + ray_node = anchor_node.attachNewNode(ray_collision_node) + self.ray_collision_nodes[controller_name] = ray_collision_node + + # 注册到碰撞遍历器 + self.collision_traverser.addCollider(ray_node, self.collision_queue) + + print(f"✓ {controller_name}手控制器射线投射已创建") + return ray_node + + def _setup_collision_detection(self): + """设置碰撞检测系统""" + if self.world: + # 使用世界的碰撞系统 + if hasattr(self.world, 'render'): + # 为所有可交互对象设置碰撞体 + self._setup_scene_collision_objects() + else: + print("⚠️ 无法访问世界对象,跳过场景碰撞设置") + + def _setup_scene_collision_objects(self): + """为场景对象设置碰撞体""" + if not self.world or not hasattr(self.world, 'render'): + return + + try: + # 遍历场景中的所有节点,为它们添加碰撞体 + for node_path in self.world.render.findAllMatches("**/+GeomNode"): + self._add_collision_to_object(node_path) + + except Exception as e: + print(f"⚠️ 设置场景碰撞对象失败: {e}") + + def _add_collision_to_object(self, node_path): + """为对象添加碰撞体""" + try: + # 获取对象的边界框 + bounds = node_path.getBounds() + if bounds.isEmpty(): + return + + # 计算边界球 + center = bounds.getCenter() + radius = bounds.getRadius() + + # 创建球形碰撞体 + collision_sphere = CollisionSphere(center, radius) + + # 创建碰撞节点 + collision_node = CollisionNode(f'{node_path.getName()}_collision') + collision_node.addSolid(collision_sphere) + + # 设置碰撞掩码 + collision_node.setIntoCollideMask(BitMask32.bit(0)) # 接受射线碰撞 + collision_node.setFromCollideMask(BitMask32.allOff()) # 不发射射线 + + # 附加碰撞节点 + collision_node_path = node_path.attachNewNode(collision_node) + + # 标记为可交互对象 + node_path.setTag('interactable', 'true') + node_path.setTag('original_name', node_path.getName()) + + except Exception as e: + print(f"⚠️ 为对象 {node_path.getName()} 添加碰撞体失败: {e}") + + def update(self): + """更新交互系统 - 每帧调用""" + if not self.vr_manager.are_controllers_connected(): + return + + # 执行碰撞检测 + self._perform_collision_detection() + + # 更新选择状态 + self._update_selections() + + # 更新抓取状态 + self._update_grabbing() + + def _perform_collision_detection(self): + """执行碰撞检测""" + if self.world and hasattr(self.world, 'render'): + self.collision_traverser.traverse(self.world.render) + + def _update_selections(self): + """更新对象选择状态""" + # 清除之前的选择高亮 + self._clear_selection_highlights() + + # 检查每个控制器的选择 + for controller in self.vr_manager.get_connected_controllers(): + if not controller: + continue + + # 获取最近的碰撞对象 + hit_object = self._get_closest_hit_object(controller.name) + + if hit_object: + # 高亮选中的对象 + self._highlight_object(hit_object, self.selection_color) + self.selected_objects[controller.name] = hit_object + + # 显示控制器射线 + controller.show_ray(True) + controller.set_ray_color([0.9, 0.9, 0.2, 0.8]) # 黄色 + else: + # 没有选中对象 + if controller.name in self.selected_objects: + del self.selected_objects[controller.name] + + # 隐藏射线(除非正在抓取) + if controller.name not in self.grabbed_objects: + controller.show_ray(False) + + def _get_closest_hit_object(self, controller_name): + """获取指定控制器射线最近的碰撞对象""" + if controller_name not in self.ray_collision_nodes: + return None + + closest_object = None + closest_distance = float('inf') + + # 检查碰撞队列中的条目 + for i in range(self.collision_queue.getNumEntries()): + entry = self.collision_queue.getEntry(i) + + # 检查是否是该控制器的射线 + from_node = entry.getFromNodePath() + if from_node.node() == self.ray_collision_nodes[controller_name]: + # 获取碰撞的对象 + hit_node_path = entry.getIntoNodePath() + + # 获取实际的几何对象(父节点) + geom_object = hit_node_path.getParent() + if geom_object and geom_object.hasTag('interactable'): + distance = entry.getSurfacePoint(geom_object).length() + + if distance < closest_distance and distance <= self.selection_range: + closest_distance = distance + closest_object = geom_object + + return closest_object + + def _update_grabbing(self): + """更新对象抓取状态""" + for controller in self.vr_manager.get_connected_controllers(): + if not controller: + continue + + controller_name = controller.name + + # 检查是否按下抓取按钮 + if controller.is_trigger_pressed(threshold=self.grab_threshold): + # 如果还没有抓取对象 + if controller_name not in self.grabbed_objects: + # 尝试抓取选中的对象 + if controller_name in self.selected_objects: + selected_obj = self.selected_objects[controller_name] + self._start_grab(controller, selected_obj) + + # 如果正在抓取,更新对象位置 + if controller_name in self.grabbed_objects: + self._update_grabbed_object(controller) + + else: + # 释放抓取 + if controller_name in self.grabbed_objects: + self._release_grab(controller) + + def _start_grab(self, controller, obj): + """开始抓取对象""" + controller_name = controller.name + + try: + # 计算抓取偏移(对象相对于控制器的位置) + controller_pos = controller.get_world_position() + object_pos = obj.getPos(self.world.render if self.world else obj.getParent()) + + offset = object_pos - controller_pos + self.grab_offsets[controller_name] = offset + + # 记录抓取状态 + self.grabbed_objects[controller_name] = obj + + # 改变对象颜色表示抓取状态 + self._highlight_object(obj, self.grab_color) + + # 触发震动反馈 + controller.trigger_haptic_feedback(0.01, 0.8) + + # 显示绿色射线表示抓取 + controller.show_ray(True) + controller.set_ray_color([0.2, 0.9, 0.2, 0.8]) + + print(f"🤏 {controller_name}手开始抓取对象: {obj.getName()}") + + except Exception as e: + print(f"⚠️ 开始抓取失败: {e}") + + def _update_grabbed_object(self, controller): + """更新被抓取对象的位置""" + controller_name = controller.name + + if controller_name not in self.grabbed_objects: + return + + try: + grabbed_obj = self.grabbed_objects[controller_name] + grab_offset = self.grab_offsets.get(controller_name, Vec3(0, 0, 0)) + + # 计算新位置 + controller_pos = controller.get_world_position() + new_pos = controller_pos + grab_offset + + # 更新对象位置 + grabbed_obj.setPos(self.world.render if self.world else grabbed_obj.getParent(), new_pos) + + # 可选:同步旋转 + if hasattr(controller, 'get_world_rotation'): + controller_rot = controller.get_world_rotation() + grabbed_obj.setHpr(self.world.render if self.world else grabbed_obj.getParent(), controller_rot) + + except Exception as e: + print(f"⚠️ 更新抓取对象失败: {e}") + + def _release_grab(self, controller): + """释放抓取的对象""" + controller_name = controller.name + + if controller_name not in self.grabbed_objects: + return + + try: + grabbed_obj = self.grabbed_objects[controller_name] + + # 恢复对象原始颜色 + self._restore_object_color(grabbed_obj) + + # 清理抓取状态 + del self.grabbed_objects[controller_name] + if controller_name in self.grab_offsets: + del self.grab_offsets[controller_name] + + # 触发震动反馈 + controller.trigger_haptic_feedback(0.005, 0.4) + + print(f"🫳 {controller_name}手释放对象: {grabbed_obj.getName()}") + + except Exception as e: + print(f"⚠️ 释放抓取失败: {e}") + + def _highlight_object(self, obj, color): + """高亮显示对象""" + if obj in self.highlighted_objects: + return + + try: + # 保存原始颜色 + if obj not in self.original_colors: + self.original_colors[obj] = obj.getColor() + + # 设置高亮颜色 + obj.setColor(color) + self.highlighted_objects.add(obj) + + except Exception as e: + print(f"⚠️ 高亮对象失败: {e}") + + def _restore_object_color(self, obj): + """恢复对象原始颜色""" + if obj not in self.highlighted_objects: + return + + try: + # 恢复原始颜色 + if obj in self.original_colors: + obj.setColor(self.original_colors[obj]) + del self.original_colors[obj] + + self.highlighted_objects.discard(obj) + + except Exception as e: + print(f"⚠️ 恢复对象颜色失败: {e}") + + def _clear_selection_highlights(self): + """清除所有选择高亮""" + for obj in list(self.highlighted_objects): + # 只清除非抓取状态的对象 + is_grabbed = any(obj == grabbed_obj for grabbed_obj in self.grabbed_objects.values()) + if not is_grabbed: + self._restore_object_color(obj) + + def get_selected_object(self, controller_name): + """获取指定控制器选中的对象""" + return self.selected_objects.get(controller_name) + + def get_grabbed_object(self, controller_name): + """获取指定控制器抓取的对象""" + return self.grabbed_objects.get(controller_name) + + def is_grabbing(self, controller_name): + """检查指定控制器是否正在抓取对象""" + return controller_name in self.grabbed_objects + + def force_release_all(self): + """强制释放所有抓取的对象""" + for controller in self.vr_manager.get_connected_controllers(): + if controller and controller.name in self.grabbed_objects: + self._release_grab(controller) + + def cleanup(self): + """清理资源""" + self.ignoreAll() + + # 释放所有抓取 + self.force_release_all() + + # 清理碰撞系统 + self.collision_traverser.clearColliders() + self.ray_collision_nodes.clear() + + # 清理高亮状态 + for obj in list(self.highlighted_objects): + self._restore_object_color(obj) + + print("🧹 VR交互管理器已清理") \ No newline at end of file diff --git a/core/vr_joystick.py b/core/vr_joystick.py new file mode 100644 index 00000000..feaa1cae --- /dev/null +++ b/core/vr_joystick.py @@ -0,0 +1,701 @@ +""" +VR摇杆交互系统模块 + +提供类似SteamVR的摇杆交互功能: +- 摇杆左右转向(旋转视角) +- 摇杆向前传送预览(抛物线轨迹) +- 松开摇杆执行传送 +- 死区处理和平滑控制 +""" + +import math +from panda3d.core import Vec2, Vec3, Vec4 +from direct.showbase.DirectObject import DirectObject +try: + import openvr + OPENVR_AVAILABLE = True +except ImportError: + OPENVR_AVAILABLE = False + + +class VRJoystickManager(DirectObject): + """VR摇杆管理器 - 处理手柄摇杆的转向和传送功能""" + + def __init__(self, vr_manager): + """初始化VR摇杆管理器 + + Args: + vr_manager: VR管理器实例 + """ + super().__init__() + + self.vr_manager = vr_manager + self.teleport_system = None # 传送系统引用,稍后初始化 + + # 摇杆参数配置 + self.deadzone = 0.15 # 摇杆死区 (0-1) + self.turn_threshold = 0.3 # 转向激活阈值 + self.teleport_threshold = 0.5 # 传送激活阈值 + self.turn_sensitivity = 250.0 # 转向灵敏度(度/秒)- 增加速度 + self.smooth_turning = True # 是否平滑转向 + self.snap_turn_angle = 30.0 # 分段转向角度(度) + + # 摇杆状态跟踪 + self.left_joystick_state = JoystickState() + self.right_joystick_state = JoystickState() + + # 转向状态 + self.left_turn_cooldown = 0.0 # 分段转向冷却时间 + self.right_turn_cooldown = 0.0 + self.snap_turn_cooldown = 0.3 # 分段转向间隔(秒) + + # 传送状态 + self.active_teleport_controller = None # 正在传送的控制器 + self.teleport_preview_active = False # 传送预览是否激活 + + # 互斥状态管理 - 防止同时触发多种操作 + self.interaction_mode = 'none' # 当前交互模式: 'none', 'turning', 'teleporting' + self.left_controller_mode = 'none' # 左手控制器状态 + self.right_controller_mode = 'none' # 右手控制器状态 + self.mode_lock_timeout = 0.1 # 模式锁定超时时间(秒) + self.left_mode_timer = 0.0 # 左手模式计时器 + self.right_mode_timer = 0.0 # 右手模式计时器 + + print("✓ VR摇杆管理器初始化完成") + + def initialize(self, teleport_system): + """初始化摇杆系统 + + Args: + teleport_system: VR传送系统实例 + """ + self.teleport_system = teleport_system + print("✅ VR摇杆系统初始化成功") + + def update(self, dt): + """更新摇杆系统 - 每帧调用 + + Args: + dt: 帧间隔时间(秒) + """ + if not self.vr_manager.are_controllers_connected(): + return + + # 调试计数器 + if not hasattr(self, '_debug_frame_count'): + self._debug_frame_count = 0 + print("🎮 VR摇杆系统开始更新") + + self._debug_frame_count += 1 + + # 更新转向冷却时间 + if self.left_turn_cooldown > 0: + self.left_turn_cooldown -= dt + if self.right_turn_cooldown > 0: + self.right_turn_cooldown -= dt + + # 更新互斥状态计时器 + self._update_interaction_modes(dt) + + # 处理左手控制器摇杆 + if self.vr_manager.left_controller: + self._update_controller_joystick( + self.vr_manager.left_controller, + self.left_joystick_state, + 'left', + dt + ) + + # 处理右手控制器摇杆 + if self.vr_manager.right_controller: + self._update_controller_joystick( + self.vr_manager.right_controller, + self.right_joystick_state, + 'right', + dt + ) + + # 每5秒输出一次状态报告 + if self._debug_frame_count % 300 == 1: # 假设60fps + self._print_debug_status() + + def _update_controller_joystick(self, controller, joystick_state, hand, dt): + """更新单个控制器的摇杆状态 + + Args: + controller: VR控制器实例 + joystick_state: 摇杆状态对象 + hand: 'left' 或 'right' + dt: 帧间隔时间 + """ + # 获取摇杆输入 + joystick_input = self._get_joystick_input(controller, hand) + if joystick_input is None: + return + + # 应用死区 + filtered_input = self._apply_deadzone(joystick_input) + + # 更新摇杆状态 + joystick_state.update(filtered_input) + + # 处理转向(左右移动) + self._handle_turning(filtered_input, hand, dt) + + # 处理传送(向前移动) + self._handle_teleport(controller, filtered_input, joystick_state, hand) + + def _update_interaction_modes(self, dt): + """更新互斥交互模式状态""" + # 更新左手模式计时器 + if self.left_mode_timer > 0: + self.left_mode_timer -= dt + if self.left_mode_timer <= 0: + self.left_controller_mode = 'none' + + # 更新右手模式计时器 + if self.right_mode_timer > 0: + self.right_mode_timer -= dt + if self.right_mode_timer <= 0: + self.right_controller_mode = 'none' + + # 更新全局交互模式 + if self.left_controller_mode == 'none' and self.right_controller_mode == 'none': + if self.interaction_mode != 'none': + # 所有操作结束,恢复自由模式 + self.interaction_mode = 'none' + print("🔓 摇杆交互模式解锁,恢复自由操作") + + def _set_controller_mode(self, hand, mode): + """设置控制器交互模式 + + Args: + hand: 'left' 或 'right' + mode: 'turning', 'teleporting', 'none' + """ + if hand == 'left': + if self.left_controller_mode != mode: + old_mode = self.left_controller_mode + self.left_controller_mode = mode + self.left_mode_timer = self.mode_lock_timeout + if mode != 'none': + print(f"🔒 左手控制器: {old_mode} → {mode}模式") + else: + print(f"🔓 左手控制器解锁: {old_mode} → 自由") + else: + if self.right_controller_mode != mode: + old_mode = self.right_controller_mode + self.right_controller_mode = mode + self.right_mode_timer = self.mode_lock_timeout + if mode != 'none': + print(f"🔒 右手控制器: {old_mode} → {mode}模式") + else: + print(f"🔓 右手控制器解锁: {old_mode} → 自由") + + # 更新全局模式 + if mode != 'none' and self.interaction_mode != mode: + self.interaction_mode = mode + + def _can_use_mode(self, hand, requested_mode): + """检查是否可以使用指定的交互模式 + + Args: + hand: 'left' 或 'right' + requested_mode: 'turning' 或 'teleporting' + + Returns: + bool: 是否可以使用该模式 + """ + current_mode = self.left_controller_mode if hand == 'left' else self.right_controller_mode + + # 如果当前控制器已经是该模式,允许继续 + if current_mode == requested_mode: + return True + + # 如果当前控制器是空闲的,且全局模式兼容,允许切换 + if current_mode == 'none': + if self.interaction_mode == 'none' or self.interaction_mode == requested_mode: + return True + + # 其他情况不允许 + return False + + def _get_joystick_input(self, controller, hand): + """获取摇杆输入 + + Args: + controller: VR控制器实例 + hand: 'left' 或 'right' + + Returns: + Vec2: 摇杆位置 (x, y) 或 None + """ + # 直接从控制器读取摇杆输入(绕过动作系统) + joystick_input = Vec2(0, 0) + + # 优先读取joystick_pos + if hasattr(controller, 'joystick_pos') and controller.joystick_pos: + joystick_input = Vec2(controller.joystick_pos.x, controller.joystick_pos.y) + # 检查是否有有效输入 + if joystick_input.length() > 0.01: + # 调试输出 - 仅在有输入时显示 + if not hasattr(self, '_last_debug_time'): + self._last_debug_time = 0 + self._debug_counter = 0 + + self._debug_counter += 1 + # 每30帧输出一次调试信息 + if self._debug_counter % 30 == 1: + print(f"🎮 {hand}手摇杆输入: ({joystick_input.x:.3f}, {joystick_input.y:.3f})") + + return joystick_input + + # 备选方案:读取touchpad_pos(Quest等设备) + if hasattr(controller, 'touchpad_pos') and controller.touchpad_pos: + touchpad_input = Vec2(controller.touchpad_pos.x, controller.touchpad_pos.y) + # 检查是否有有效输入 + if touchpad_input.length() > 0.01: + # 调试输出 - 仅在有输入时显示 + if not hasattr(self, '_last_debug_time'): + self._last_debug_time = 0 + self._debug_counter = 0 + + self._debug_counter += 1 + # 每30帧输出一次调试信息 + if self._debug_counter % 30 == 1: + print(f"🎮 {hand}手触摸板输入: ({touchpad_input.x:.3f}, {touchpad_input.y:.3f})") + + return touchpad_input + + # 可选:尝试从动作系统获取(如果可用) + if (self.vr_manager.action_manager and + hasattr(self.vr_manager.action_manager, 'get_analog_action_value')): + try: + device_path = f'/user/hand/{hand}' + + # 尝试摇杆 + joystick_value, _ = self.vr_manager.action_manager.get_analog_action_value('joystick', device_path) + if joystick_value is not None: + return Vec2(joystick_value.x, joystick_value.y) + + # 尝试触摸板 + trackpad_value, _ = self.vr_manager.action_manager.get_analog_action_value('trackpad', device_path) + if trackpad_value is not None: + return Vec2(trackpad_value.x, trackpad_value.y) + except Exception: + # 静默忽略动作系统错误 + pass + + return Vec2(0, 0) + + def _apply_deadzone(self, input_vec): + """应用摇杆死区 + + Args: + input_vec: 原始摇杆输入 + + Returns: + Vec2: 应用死区后的输入 + """ + magnitude = input_vec.length() + + if magnitude < self.deadzone: + return Vec2(0, 0) + + # 重新映射到 [0, 1] 范围 + normalized_magnitude = (magnitude - self.deadzone) / (1.0 - self.deadzone) + normalized_magnitude = min(normalized_magnitude, 1.0) + + if magnitude > 0: + direction = input_vec / magnitude + return direction * normalized_magnitude + + return Vec2(0, 0) + + def _handle_turning(self, input_vec, hand, dt): + """处理摇杆转向 + + Args: + input_vec: 摇杆输入向量 + hand: 'left' 或 'right' + dt: 帧间隔时间 + """ + # 检查是否超过转向阈值 + if abs(input_vec.x) < self.turn_threshold: + # 没有转向输入,重置该控制器的转向模式 + current_mode = self.left_controller_mode if hand == 'left' else self.right_controller_mode + if current_mode == 'turning': + self._set_controller_mode(hand, 'none') + return + + # 检查是否可以使用转向模式 + if not self._can_use_mode(hand, 'turning'): + # 当前控制器被传送锁定,忽略转向输入 + current_mode = self.left_controller_mode if hand == 'left' else self.right_controller_mode + if current_mode != 'turning': + print(f"⛔ {hand}手转向被阻止 - 当前模式: {current_mode}") + return + + # 激活转向模式 + self._set_controller_mode(hand, 'turning') + + # 检查冷却时间(分段转向) + cooldown = self.left_turn_cooldown if hand == 'left' else self.right_turn_cooldown + + if self.smooth_turning: + # 平滑转向 - 反转方向:摇杆右移(+x)应该向右转(-angle) + turn_amount = -input_vec.x * self.turn_sensitivity * dt + self._apply_rotation(turn_amount) + + # 调试输出转向 + if not hasattr(self, '_turn_debug_counter'): + self._turn_debug_counter = 0 + self._turn_debug_counter += 1 + if self._turn_debug_counter % 60 == 1: # 每秒输出一次 + print(f"🔄 {hand}手转向: 输入={input_vec.x:.3f}, 角度={turn_amount:.1f}°") + else: + # 分段转向 + if cooldown <= 0: + # 反转方向:摇杆右移应该向右转 + turn_amount = -self.snap_turn_angle if input_vec.x > 0 else self.snap_turn_angle + self._apply_rotation(turn_amount) + + print(f"🔄 {hand}手分段转向: 输入={input_vec.x:.3f}, 角度={turn_amount:.1f}°") + + # 设置冷却时间 + if hand == 'left': + self.left_turn_cooldown = self.snap_turn_cooldown + else: + self.right_turn_cooldown = self.snap_turn_cooldown + + def _apply_rotation(self, angle_degrees): + """应用旋转到VR跟踪空间 + + Args: + angle_degrees: 旋转角度(度) + """ + if not self.vr_manager.tracking_space: + return + + # 绕Z轴旋转(垂直轴) + current_h = self.vr_manager.tracking_space.getH() + new_h = current_h + angle_degrees + self.vr_manager.tracking_space.setH(new_h) + + def _handle_teleport(self, controller, input_vec, joystick_state, hand): + """处理摇杆传送 + + Args: + controller: VR控制器实例 + input_vec: 摇杆输入向量 + joystick_state: 摇杆状态对象 + hand: 'left' 或 'right' + """ + # 检查Y轴向前移动是否超过阈值 + forward_input = input_vec.y + + if forward_input > self.teleport_threshold: + # 检查是否可以使用传送模式 + if not self._can_use_mode(hand, 'teleporting'): + # 当前控制器被转向锁定,忽略传送输入 + current_mode = self.left_controller_mode if hand == 'left' else self.right_controller_mode + if current_mode != 'teleporting': + print(f"⛔ {hand}手传送被阻止 - 当前模式: {current_mode}") + return + + # 激活传送模式 + self._set_controller_mode(hand, 'teleporting') + + # 开始或更新传送预览 + if not joystick_state.teleport_active: + joystick_state.teleport_active = True + self.active_teleport_controller = controller + self.teleport_preview_active = True + + # 触发震动反馈 + controller.trigger_haptic_feedback(0.002, 0.3) + + # 计算传送方向 + direction = self._calculate_teleport_direction(controller, input_vec) + + # 更新传送预览 + if self.teleport_system: + if joystick_state.teleport_just_started: + self.teleport_system.start_teleport_preview(controller, direction) + joystick_state.teleport_just_started = False + else: + self.teleport_system.update_teleport_preview(controller, direction) + + else: + # 检查是否需要执行传送 + if joystick_state.teleport_active: + # 松开摇杆,执行传送 + self._execute_teleport(controller) + joystick_state.teleport_active = False + joystick_state.teleport_just_started = True + + # 重置该控制器的传送模式 + self._set_controller_mode(hand, 'none') + else: + # 没有传送输入,检查是否需要重置传送模式 + current_mode = self.left_controller_mode if hand == 'left' else self.right_controller_mode + if current_mode == 'teleporting': + self._set_controller_mode(hand, 'none') + + def _calculate_teleport_direction(self, controller, input_vec): + """计算传送方向向量 - 基于手柄姿态 + + Args: + controller: VR控制器实例 + input_vec: 摇杆输入向量(仅用于激活,不影响方向) + + Returns: + Vec3: 世界坐标中的传送方向 + """ + # 方法1:直接使用手柄指向(推荐) + if controller and hasattr(controller, 'get_forward_direction'): + try: + # 获取手柄的前向方向 + controller_forward = controller.get_forward_direction() + + # 确保方向向量在水平面上 + horizontal_direction = Vec3(controller_forward.x, controller_forward.y, 0) + + if horizontal_direction.length() > 0: + horizontal_direction.normalize() + + # 调试输出(每30帧输出一次) + if not hasattr(self, '_direction_debug_counter'): + self._direction_debug_counter = 0 + + self._direction_debug_counter += 1 + if self._direction_debug_counter % 30 == 1: + # 获取当前tracking_space的旋转角度用于调试 + tracking_rotation = 0 + if self.vr_manager.tracking_space: + tracking_rotation = self.vr_manager.tracking_space.getH() + + print(f"🎯 综合传送方向: 视角旋转({tracking_rotation:.1f}°) + 手柄姿态({controller_forward.x:.2f},{controller_forward.y:.2f},{controller_forward.z:.2f}) → 最终方向({horizontal_direction.x:.2f},{horizontal_direction.y:.2f})") + + return horizontal_direction + + except Exception as e: + print(f"⚠️ 获取手柄方向失败: {e}") + + # 备选方案:使用玩家朝向(保留原逻辑作为备选) + print("🔄 使用备选传送方向计算...") + + # 获取玩家当前朝向的变换矩阵 + player_transform = None + + # 优先使用头显的世界变换 + if self.vr_manager.hmd_anchor and hasattr(self.vr_manager, 'world') and self.vr_manager.world: + player_transform = self.vr_manager.hmd_anchor.getMat(self.vr_manager.world.render) + # 备选:使用tracking_space的世界变换 + elif self.vr_manager.tracking_space and hasattr(self.vr_manager, 'world') and self.vr_manager.world: + player_transform = self.vr_manager.tracking_space.getMat(self.vr_manager.world.render) + + if player_transform: + # 从变换矩阵提取前向向量 + forward = Vec3(player_transform.getRow3(1)) # Y轴 = 前向 + + # 确保向量在水平面上 + forward.z = 0 + if forward.length() > 0: + forward.normalize() + return forward + + # 最终备选方案:默认前向 + print("⚠️ 无法获取任何朝向,使用默认方向") + return Vec3(0, 1, 0) + + def _execute_teleport(self, controller): + """执行传送 + + Args: + controller: VR控制器实例 + """ + if self.teleport_system and self.teleport_preview_active: + success = self.teleport_system.execute_teleport() + + if success: + # 传送成功,触发震动反馈 + controller.trigger_haptic_feedback(0.005, 0.8) + else: + # 传送失败,触发不同的震动反馈 + controller.trigger_haptic_feedback(0.001, 0.2) + + # 停止传送预览 + self.teleport_system.stop_teleport_preview() + self.teleport_preview_active = False + self.active_teleport_controller = None + + def set_turning_mode(self, smooth=True): + """设置转向模式 + + Args: + smooth: True为平滑转向,False为分段转向 + """ + self.smooth_turning = smooth + print(f"✓ 转向模式设置为: {'平滑转向' if smooth else '分段转向'}") + + def set_turn_sensitivity(self, sensitivity): + """设置转向灵敏度 + + Args: + sensitivity: 转向灵敏度(度/秒) + """ + self.turn_sensitivity = max(10.0, min(180.0, sensitivity)) + print(f"✓ 转向灵敏度设置为: {self.turn_sensitivity}度/秒") + + def apply_config(self, config): + """应用配置 + + Args: + config: VRJoystickConfig配置实例 + """ + try: + self.deadzone = config.deadzone + self.turn_threshold = config.turn_threshold + self.teleport_threshold = config.teleport_threshold + self.turn_sensitivity = config.turn_sensitivity + self.smooth_turning = (config.turn_mode.value == 'smooth') + self.snap_turn_angle = config.snap_turn_angle + self.snap_turn_cooldown = config.snap_turn_cooldown + + # 应用传送系统配置 + if self.teleport_system and hasattr(config, 'teleport_range'): + self.teleport_system.teleport_range = config.teleport_range + if hasattr(config, 'teleport_arc_resolution'): + self.teleport_system.arc_resolution = config.teleport_arc_resolution + if hasattr(config, 'teleport_initial_velocity'): + self.teleport_system.initial_velocity = config.teleport_initial_velocity + if hasattr(config, 'min_teleport_distance'): + self.teleport_system.min_teleport_distance = config.min_teleport_distance + + print("✅ 摇杆配置已成功应用") + + except Exception as e: + print(f"⚠️ 应用配置失败: {e}") + + def get_current_config(self): + """获取当前配置 + + Returns: + dict: 当前配置参数 + """ + return { + 'deadzone': self.deadzone, + 'turn_threshold': self.turn_threshold, + 'teleport_threshold': self.teleport_threshold, + 'turn_sensitivity': self.turn_sensitivity, + 'smooth_turning': self.smooth_turning, + 'snap_turn_angle': self.snap_turn_angle, + 'snap_turn_cooldown': self.snap_turn_cooldown + } + + def _print_debug_status(self): + """打印调试状态信息""" + try: + print("🔍 ======= VR摇杆调试状态 =======") + + # 控制器连接状态 + left_connected = self.vr_manager.left_controller is not None + right_connected = self.vr_manager.right_controller is not None + print(f"📱 控制器状态: 左手={left_connected}, 右手={right_connected}") + + # 检查控制器属性 + if left_connected: + left_ctrl = self.vr_manager.left_controller + has_joystick = hasattr(left_ctrl, 'joystick_pos') + has_touchpad = hasattr(left_ctrl, 'touchpad_pos') + print(f"📊 左手控制器: joystick_pos={has_joystick}, touchpad_pos={has_touchpad}") + + if has_joystick and left_ctrl.joystick_pos: + pos = left_ctrl.joystick_pos + print(f" 当前摇杆位置: ({pos.x:.3f}, {pos.y:.3f}, {pos.z:.3f})") + + if has_touchpad and left_ctrl.touchpad_pos: + pos = left_ctrl.touchpad_pos + print(f" 当前触摸板位置: ({pos.x:.3f}, {pos.y:.3f}, {pos.z:.3f})") + + if right_connected: + right_ctrl = self.vr_manager.right_controller + has_joystick = hasattr(right_ctrl, 'joystick_pos') + has_touchpad = hasattr(right_ctrl, 'touchpad_pos') + print(f"📊 右手控制器: joystick_pos={has_joystick}, touchpad_pos={has_touchpad}") + + if has_joystick and right_ctrl.joystick_pos: + pos = right_ctrl.joystick_pos + print(f" 当前摇杆位置: ({pos.x:.3f}, {pos.y:.3f}, {pos.z:.3f})") + + if has_touchpad and right_ctrl.touchpad_pos: + pos = right_ctrl.touchpad_pos + print(f" 当前触摸板位置: ({pos.x:.3f}, {pos.y:.3f}, {pos.z:.3f})") + + # 摇杆配置 + print(f"⚙️ 摇杆配置:") + print(f" 死区: {self.deadzone}") + print(f" 转向阈值: {self.turn_threshold}") + print(f" 传送阈值: {self.teleport_threshold}") + print(f" 转向模式: {'平滑' if self.smooth_turning else '分段'}") + + # 动作系统状态 + action_mgr_available = (self.vr_manager.action_manager is not None) + print(f"🎯 动作系统: {'可用' if action_mgr_available else '不可用'}") + + # 传送系统状态 + teleport_available = (self.teleport_system is not None) + print(f"🚀 传送系统: {'可用' if teleport_available else '不可用'}") + + print("🔍 ==============================") + + except Exception as e: + print(f"⚠️ 调试状态输出失败: {e}") + + def cleanup(self): + """清理摇杆系统资源""" + try: + # 停止任何活跃的传送预览 + if self.teleport_preview_active and self.teleport_system: + self.teleport_system.stop_teleport_preview() + + self.teleport_preview_active = False + self.active_teleport_controller = None + + self.ignoreAll() + print("🧹 VR摇杆系统已清理") + + except Exception as e: + print(f"⚠️ 清理摇杆系统失败: {e}") + + +class JoystickState: + """摇杆状态跟踪类""" + + def __init__(self): + self.current_input = Vec2(0, 0) # 当前摇杆输入 + self.previous_input = Vec2(0, 0) # 上一帧摇杆输入 + self.teleport_active = False # 传送是否激活 + self.teleport_just_started = True # 传送是否刚开始 + + def update(self, new_input): + """更新摇杆状态 + + Args: + new_input: 新的摇杆输入 + """ + self.previous_input = Vec2(self.current_input) + self.current_input = Vec2(new_input) + + def is_input_changed(self, threshold=0.01): + """检查输入是否发生变化 + + Args: + threshold: 变化阈值 + + Returns: + bool: 是否发生变化 + """ + diff = self.current_input - self.previous_input + return diff.length() > threshold \ No newline at end of file diff --git a/core/vr_joystick_config.py b/core/vr_joystick_config.py new file mode 100644 index 00000000..13401e59 --- /dev/null +++ b/core/vr_joystick_config.py @@ -0,0 +1,268 @@ +""" +VR摇杆配置模块 + +提供摇杆交互的配置选项和预设: +- 不同的转向模式和灵敏度设置 +- 传送参数调整 +- 用户体验优化选项 +""" + +from enum import Enum + + +class TurnMode(Enum): + """转向模式枚举""" + SMOOTH = "smooth" # 平滑转向 + SNAP = "snap" # 分段转向 + + +class JoystickProfile(Enum): + """摇杆配置预设""" + COMFORTABLE = "comfortable" # 舒适模式 - 低灵敏度,平滑转向 + STANDARD = "standard" # 标准模式 - 中等灵敏度,分段转向 + GAMING = "gaming" # 游戏模式 - 高灵敏度,快速响应 + ACCESSIBLE = "accessible" # 无障碍模式 - 更大死区,更高阈值 + + +class VRJoystickConfig: + """VR摇杆配置类""" + + def __init__(self): + """初始化默认配置""" + # 基础参数 + self.deadzone = 0.15 # 摇杆死区 (0-1) + self.turn_threshold = 0.3 # 转向激活阈值 + self.teleport_threshold = 0.5 # 传送激活阈值 + + # 转向设置 + self.turn_mode = TurnMode.SNAP # 转向模式 + self.turn_sensitivity = 250.0 # 转向灵敏度(度/秒)- 增加速度 + self.snap_turn_angle = 30.0 # 分段转向角度(度) + self.snap_turn_cooldown = 0.3 # 分段转向间隔(秒) + + # 传送设置 + self.teleport_range = 20.0 # 最大传送距离 + self.teleport_arc_resolution = 50 # 抛物线精度 + self.teleport_initial_velocity = 10.0 # 传送初始速度 + self.min_teleport_distance = 1.0 # 最小传送距离 + + # 反馈设置 + self.haptic_feedback_enabled = True # 是否启用震动反馈 + self.teleport_start_haptic = (0.002, 0.3) # 传送开始震动 (时长, 强度) + self.teleport_success_haptic = (0.005, 0.8) # 传送成功震动 + self.teleport_fail_haptic = (0.001, 0.2) # 传送失败震动 + + # 可视化设置 + self.show_teleport_arc = True # 显示传送抛物线 + self.show_teleport_target = True # 显示传送目标标记 + self.arc_valid_color = (0.2, 0.9, 0.2, 0.8) # 有效抛物线颜色 + self.arc_invalid_color = (0.9, 0.2, 0.2, 0.8) # 无效抛物线颜色 + + def apply_profile(self, profile: JoystickProfile): + """应用预设配置 + + Args: + profile: 配置预设 + """ + if profile == JoystickProfile.COMFORTABLE: + self._apply_comfortable_profile() + elif profile == JoystickProfile.STANDARD: + self._apply_standard_profile() + elif profile == JoystickProfile.GAMING: + self._apply_gaming_profile() + elif profile == JoystickProfile.ACCESSIBLE: + self._apply_accessible_profile() + + def _apply_comfortable_profile(self): + """舒适模式 - 适合长时间使用""" + self.deadzone = 0.2 + self.turn_threshold = 0.4 + self.teleport_threshold = 0.6 + self.turn_mode = TurnMode.SMOOTH + self.turn_sensitivity = 100.0 # 适中的转向速度 + self.snap_turn_cooldown = 0.4 + print("✓ 已应用舒适模式配置") + + def _apply_standard_profile(self): + """标准模式 - 平衡的体验""" + self.deadzone = 0.15 + self.turn_threshold = 0.3 + self.teleport_threshold = 0.5 + self.turn_mode = TurnMode.SNAP + self.turn_sensitivity = 150.0 # 更快的转向速度 + self.snap_turn_angle = 30.0 + self.snap_turn_cooldown = 0.3 + print("✓ 已应用标准模式配置") + + def _apply_gaming_profile(self): + """游戏模式 - 快速响应""" + self.deadzone = 0.1 + self.turn_threshold = 0.2 + self.teleport_threshold = 0.4 + self.turn_mode = TurnMode.SMOOTH + self.turn_sensitivity = 200.0 # 高速转向 + self.snap_turn_cooldown = 0.2 + print("✓ 已应用游戏模式配置") + + def _apply_accessible_profile(self): + """无障碍模式 - 更容易控制""" + self.deadzone = 0.25 + self.turn_threshold = 0.5 + self.teleport_threshold = 0.7 + self.turn_mode = TurnMode.SNAP + self.turn_sensitivity = 80.0 # 较慢但可控的转向 + self.snap_turn_angle = 45.0 + self.snap_turn_cooldown = 0.5 + print("✓ 已应用无障碍模式配置") + + def set_turn_mode(self, mode: TurnMode): + """设置转向模式 + + Args: + mode: 转向模式 + """ + self.turn_mode = mode + print(f"✓ 转向模式设置为: {mode.value}") + + def set_turn_sensitivity(self, sensitivity: float): + """设置转向灵敏度 + + Args: + sensitivity: 转向灵敏度(度/秒) + """ + self.turn_sensitivity = max(10.0, min(180.0, sensitivity)) + print(f"✓ 转向灵敏度设置为: {self.turn_sensitivity}度/秒") + + def set_deadzone(self, deadzone: float): + """设置摇杆死区 + + Args: + deadzone: 死区大小 (0-1) + """ + self.deadzone = max(0.0, min(0.5, deadzone)) + print(f"✓ 摇杆死区设置为: {self.deadzone}") + + def set_teleport_range(self, range_meters: float): + """设置传送范围 + + Args: + range_meters: 传送范围(米) + """ + self.teleport_range = max(5.0, min(50.0, range_meters)) + print(f"✓ 传送范围设置为: {self.teleport_range}米") + + def enable_haptic_feedback(self, enabled: bool): + """启用或禁用震动反馈 + + Args: + enabled: 是否启用 + """ + self.haptic_feedback_enabled = enabled + print(f"✓ 震动反馈: {'启用' if enabled else '禁用'}") + + def get_config_dict(self): + """获取配置字典 + + Returns: + dict: 配置参数字典 + """ + return { + 'deadzone': self.deadzone, + 'turn_threshold': self.turn_threshold, + 'teleport_threshold': self.teleport_threshold, + 'turn_mode': self.turn_mode.value, + 'turn_sensitivity': self.turn_sensitivity, + 'snap_turn_angle': self.snap_turn_angle, + 'snap_turn_cooldown': self.snap_turn_cooldown, + 'teleport_range': self.teleport_range, + 'haptic_feedback_enabled': self.haptic_feedback_enabled + } + + def apply_to_joystick_manager(self, joystick_manager): + """将配置应用到摇杆管理器 + + Args: + joystick_manager: VRJoystickManager实例 + """ + try: + joystick_manager.deadzone = self.deadzone + joystick_manager.turn_threshold = self.turn_threshold + joystick_manager.teleport_threshold = self.teleport_threshold + joystick_manager.turn_sensitivity = self.turn_sensitivity + joystick_manager.smooth_turning = (self.turn_mode == TurnMode.SMOOTH) + joystick_manager.snap_turn_angle = self.snap_turn_angle + joystick_manager.snap_turn_cooldown = self.snap_turn_cooldown + + # 应用传送系统配置 + if hasattr(joystick_manager, 'teleport_system') and joystick_manager.teleport_system: + teleport_sys = joystick_manager.teleport_system + teleport_sys.teleport_range = self.teleport_range + teleport_sys.arc_resolution = self.teleport_arc_resolution + teleport_sys.initial_velocity = self.teleport_initial_velocity + teleport_sys.min_teleport_distance = self.min_teleport_distance + + print("✅ 配置已成功应用到摇杆管理器") + + except Exception as e: + print(f"⚠️ 应用配置失败: {e}") + + +# 预定义的配置实例 +COMFORTABLE_CONFIG = VRJoystickConfig() +COMFORTABLE_CONFIG.apply_profile(JoystickProfile.COMFORTABLE) + +STANDARD_CONFIG = VRJoystickConfig() +STANDARD_CONFIG.apply_profile(JoystickProfile.STANDARD) + +GAMING_CONFIG = VRJoystickConfig() +GAMING_CONFIG.apply_profile(JoystickProfile.GAMING) + +ACCESSIBLE_CONFIG = VRJoystickConfig() +ACCESSIBLE_CONFIG.apply_profile(JoystickProfile.ACCESSIBLE) + + +def create_custom_config(**kwargs): + """创建自定义配置 + + Args: + **kwargs: 配置参数 + + Returns: + VRJoystickConfig: 自定义配置实例 + """ + config = VRJoystickConfig() + + for key, value in kwargs.items(): + if hasattr(config, key): + setattr(config, key, value) + else: + print(f"⚠️ 未知的配置参数: {key}") + + return config + + +def print_usage_guide(): + """打印使用指南""" + print("🎮 ======= VR摇杆交互使用指南 =======") + print() + print("📋 基本操作:") + print(" • 摇杆左右移动 → 转向(旋转视角)") + print(" • 摇杆向前推 → 显示传送抛物线") + print(" • 松开摇杆 → 执行传送到落点") + print() + print("⚙️ 配置选项:") + print(" • 舒适模式 → 低灵敏度,适合长时间使用") + print(" • 标准模式 → 平衡体验,推荐大多数用户") + print(" • 游戏模式 → 高灵敏度,快速响应") + print(" • 无障碍模式 → 更大死区,容易控制") + print() + print("🎯 使用建议:") + print(" • 首次使用建议从标准模式开始") + print(" • 根据个人喜好调整转向模式(平滑/分段)") + print(" • 可以随时调整死区和灵敏度") + print(" • 传送范围可根据场景大小调整") + print() + + +if __name__ == "__main__": + print_usage_guide() \ No newline at end of file diff --git a/core/vr_manager.py b/core/vr_manager.py index c6830b06..ab1bc036 100644 --- a/core/vr_manager.py +++ b/core/vr_manager.py @@ -1,573 +1,4199 @@ -import warnings -warnings.filterwarnings("ignore", category=DeprecationWarning) +""" +VR管理器模块 + +负责VR功能的初始化、渲染和交互: +- OpenVR/SteamVR集成 +- VR头显跟踪和渲染 +- VR控制器交互 +- VR模式切换 +""" -from panda3d.core import * -from direct.showbase.DirectObject import DirectObject -from direct.task import Task import sys +import gc +import numpy as np +from panda3d.core import ( + WindowProperties, GraphicsPipe, FrameBufferProperties, + GraphicsOutput, Texture, Camera, PerspectiveLens, MatrixLens, + Mat4, Vec3, TransformState, RenderState, CardMaker, + BitMask32, PandaNode, NodePath, LMatrix4, LVector3, LVector4, + CS_yup_right, CS_default, PythonCallbackObject +) +from direct.task import Task +from direct.showbase.DirectObject import DirectObject + +try: + import openvr + OPENVR_AVAILABLE = True +except ImportError: + OPENVR_AVAILABLE = False + print("警告: OpenVR未安装,VR功能将不可用") + +# 导入手柄控制器、动作系统、交互系统、摇杆系统和传送系统 +from .vr_controller import LeftController, RightController +from .vr_actions import VRActionManager +from .vr_interaction import VRInteractionManager +from .vr_joystick import VRJoystickManager +from .vr_teleport import VRTeleportSystem + class VRManager(DirectObject): - """VR管理器 - 处理VR系统初始化、追踪和渲染""" - + """VR管理器类 - 处理所有VR相关功能""" + def __init__(self, world): + """初始化VR管理器 + + Args: + world: 主世界对象引用 + """ super().__init__() + self.world = world - self.vr_enabled = False self.vr_system = None + self.vr_enabled = False + self.vr_initialized = False + + # VR渲染相关 + self.vr_left_eye_buffer = None + self.vr_right_eye_buffer = None + self.vr_left_camera = None + self.vr_right_camera = None self.vr_compositor = None - self.render_width = 1920 - self.render_height = 1080 - self.alvr_enabled = False - - # 模拟模式设置 - self.simulation_mode = False - self.simulation_data = { - 'head_pose': {'position': [0, 0, 1.6], 'rotation': [0, 0, 0, 1]}, - 'controller_poses': { - 0: {'position': [-0.3, 0, 1.2], 'rotation': [0, 0, 0, 1], 'connected': True}, - 1: {'position': [0.3, 0, 1.2], 'rotation': [0, 0, 0, 1], 'connected': True} - }, - 'render_size': (1920, 1080) - } - - # 立体渲染缓冲区 - self.left_eye_buffer = None - self.right_eye_buffer = None - self.left_eye_camera = None - self.right_eye_camera = None - - # 控制器相关 - self.controller_nodes = {} + + # VR纹理和ID缓存 - 修复重复准备问题 + self.vr_left_texture = None + self.vr_right_texture = None + self.left_texture_id = None # 缓存左眼纹理的OpenGL ID + self.right_texture_id = None # 缓存右眼纹理的OpenGL ID + self.textures_prepared = False # 标记纹理是否已准备 + + # VR跟踪数据 + self.hmd_pose = Mat4.identMat() self.controller_poses = {} - + self.tracked_device_poses = [] + self.poses = None # OpenVR渲染姿态数组 + self.game_poses = None # OpenVR游戏逻辑姿态数组 + + # 🚀 对象池和缓存系统 - 修复16-19帧周期性GPU峰值 + self._matrix_pool = [] # Mat4对象池 + self._matrix_pool_size = 8 # 池大小,足够处理多个控制器 + self._cached_matrices = {} # 设备ID到矩阵的缓存 + self._controller_poses_cache = {} # 控制器姿态缓存,避免每帧clear() + + # 🚀 OpenVR Texture对象缓存 - 避免每帧创建openvr.Texture_t() + self._left_ovr_texture = None # 左眼纹理对象缓存 + self._right_ovr_texture = None # 右眼纹理对象缓存 + + # Python垃圾回收控制 + self._gc_control_enabled = True # 是否启用GC控制 + self._gc_disabled = False # GC是否被禁用 + self._manual_gc_interval = 900 # 每900帧手动触发一次GC (15秒@60fps) - 减少GC频率 + self._last_manual_gc_frame = 0 + + # 🚀 立即初始化对象池和GC控制(在其他组件之前) + self._initialize_object_pools() + + # VR渲染参数 + self.eye_width = 1080 + self.eye_height = 1200 + self.near_clip = 0.1 + self.far_clip = 1000.0 + + # VR分辨率缩放优化 + self.resolution_scale = 0.75 # 默认0.75倍分辨率,性能和质量平衡 + self.base_eye_width = 1080 # 原始推荐分辨率 + self.base_eye_height = 1200 + self.scaled_eye_width = 1080 # 实际使用的缩放后分辨率 + self.scaled_eye_height = 1200 + + # VR质量预设 + self.quality_presets = { + 'performance': 0.6, # 性能模式 - 约60%分辨率 + 'balanced': 0.75, # 平衡模式 - 约75%分辨率 + 'quality': 1.0 # 质量模式 - 100%分辨率 + } + self.current_quality_preset = 'balanced' # 默认平衡模式 + + # VR任务 + self.vr_task = None + + # VR锚点层级系统 + self.tracking_space = None + self.hmd_anchor = None + self.left_eye_anchor = None + self.right_eye_anchor = None + + # 坐标系转换矩阵 - 使用Panda3D内置方法 + self.coord_mat = LMatrix4.convert_mat(CS_yup_right, CS_default) + self.coord_mat_inv = LMatrix4.convert_mat(CS_default, CS_yup_right) + + # 性能监控 + self.frame_count = 0 + self.last_fps_check = 0 + self.last_fps_time = 0 + self.vr_fps = 0 + self.submit_failures = 0 + self.pose_failures = 0 + + # OpenVR 帧ID跟踪(防止重复提交) + self.openvr_frame_id = 0 + self.left_eye_last_render_frame = -1 + self.right_eye_last_render_frame = -1 + + # 高级性能监控(默认关闭,手动开启) + self.performance_monitoring = False # 是否启用性能监控 + self.debug_output_enabled = False # 是否启用调试输出 + self.debug_mode = 'detailed' # 'brief' 或 'detailed' + self.cpu_usage = 0.0 + self.memory_usage = 0.0 + self.gpu_usage = 0.0 + self.gpu_memory_usage = 0.0 + self.frame_times = [] # 存储最近帧时间 + self.max_frame_time_history = 60 # 保存60帧的历史 + self.last_performance_check = 0 + self.performance_check_interval = 0.5 # 每0.5秒更新一次性能数据 + + # 渲染管线详细监控 + self.enable_pipeline_monitoring = True # 是否启用管线监控 + self.performance_mode_enabled = False # 性能优化模式(禁用监控以减少对象创建) + self.performance_mode_trigger_frame = 600 # 第600帧后启用性能模式 + self.wait_poses_time = 0.0 # waitGetPoses耗时 + self.left_render_time = 0.0 # 左眼渲染耗时 + self.right_render_time = 0.0 # 右眼渲染耗时 + self.submit_time = 0.0 # 纹理提交耗时 + self.left_render_count = 0 # 左眼渲染次数计数 + self.right_render_count = 0 # 右眼渲染次数计数 + self.total_frame_time = 0.0 # 总帧时间 + self.vr_sync_wait_time = 0.0 # VR同步等待时间 + + # 时间监控历史(保存最近30帧) + self.wait_poses_times = [] + self.render_times = [] + self.submit_times = [] + self.sync_wait_times = [] + self.pipeline_history_size = 30 + + # GPU渲染时间监控(OpenVR Frame Timing) + self.enable_gpu_timing = False # 是否启用GPU时间监控(默认关闭) + self.gpu_scene_render_ms = 0.0 # GPU场景渲染时间 + self.gpu_pre_submit_ms = 0.0 # 提交前GPU时间 + self.gpu_post_submit_ms = 0.0 # 提交后GPU时间 + self.gpu_total_render_ms = 0.0 # GPU总渲染时间 + self.gpu_compositor_render_ms = 0.0 # GPU合成器渲染时间 + self.gpu_client_frame_interval_ms = 0.0 # 客户端帧间隔 + self.gpu_timing_history = [] # GPU时间历史记录 + self.gpu_timing_history_size = 30 # GPU时间历史记录大小 + self.gpu_timing_failure_count = 0 # GPU时间获取失败次数 + + # VR系统信息 + self.current_eye_resolution = (self.eye_width, self.eye_height) + self.recommended_eye_resolution = (0, 0) + self.vr_display_frequency = 0.0 + self.vr_vsync_enabled = True + self.vsync_to_photons_ms = 0.0 # VSync到光子的延迟 + self.target_frame_time_ms = 0.0 # 目标帧时间 + self.vsync_window_ms = 0.0 # VSync时间窗口 + self.async_reprojection_enabled = False # 异步重投影状态 + self.motion_smoothing_enabled = False # 运动平滑状态 + + # 🧪 VR测试模式 + self.vr_test_mode = False # 是否启用VR测试模式 + self.test_display_mode = 'stereo' # 'left', 'right', 'stereo' + self.test_display_quad = None # 测试显示的四边形 + self.test_right_quad = None # 右眼显示的四边形(立体模式) + self.stereo_display_created = False # 立体显示是否已创建 + self.test_performance_hud = None # 性能HUD + self.test_performance_text = None # 性能文本节点 + self.test_mode_initialized = False # 测试模式是否已初始化 + self.hud_update_counter = 0 # HUD更新计数器 + self.hud_update_interval = 30 # HUD更新间隔(帧数),30帧约0.5秒@60fps + + # 🔧 VR测试模式调试选项 - 逐步启用普通VR功能 + self.test_mode_submit_texture = False # 是否在测试模式提交纹理到OpenVR + self.test_mode_wait_poses = False # 是否在测试模式调用waitGetPoses + + # waitGetPoses调用策略配置 - 使用高性能的update_task策略 + self.use_prediction_time = 0.011 # 11ms的预测时间 - OpenVR标准值,平衡准确性和延迟 + self.poses_updated_in_task = True # 始终在更新任务中获取姿态(Running Start模式) + + # 尝试导入性能监控库 + self._init_performance_monitoring() + + # VR提交策略 - 基于参考实现 + self.submit_together = True # 是否在right_cb中同时提交左右眼 + + + # Running Start标记 - Valve最佳实践 + self._waitgetposes_called_this_frame = False + + # 姿态缓存 - 修复时序不匹配 + self._cached_render_poses = None # 用于渲染的缓存姿态 + self._first_frame = True # 首帧标记 + + # ATW控制选项 - 备选方案 + self.disable_async_reprojection = False # 是否禁用异步重投影 + + # VR手柄控制器 + self.left_controller = None + self.right_controller = None + self.controllers = {} # 设备索引到控制器的映射 + self.tracked_device_anchors = {} # 跟踪设备锚点 + + # VR动作系统 - 可选择禁用(用于API兼容性问题) + self.disable_action_system = True # 禁用动作系统,使用直接控制器输入 + + if not self.disable_action_system: + try: + self.action_manager = VRActionManager(self) + print("✓ VR动作管理器初始化完成") + except Exception as e: + print(f"⚠️ VR动作管理器初始化失败: {e}") + self.action_manager = None + else: + self.action_manager = None + print("🚫 VR动作系统已禁用,使用直接控制器输入") + + # VR交互系统 - 添加异常保护 + try: + self.interaction_manager = VRInteractionManager(self) + print("✓ VR交互管理器初始化完成") + except Exception as e: + print(f"⚠️ VR交互管理器初始化失败: {e}") + self.interaction_manager = None + + # VR传送系统 - 添加异常保护 + try: + self.teleport_system = VRTeleportSystem(self) + print("✓ VR传送系统初始化完成") + except Exception as e: + print(f"⚠️ VR传送系统初始化失败: {e}") + self.teleport_system = None + + # VR摇杆系统 - 添加异常保护 + try: + self.joystick_manager = VRJoystickManager(self) + print("✓ VR摇杆管理器初始化完成") + except Exception as e: + print(f"⚠️ VR摇杆管理器初始化失败: {e}") + self.joystick_manager = None + print("✓ VR管理器初始化完成") - - def initialize_vr(self, force_simulation=False): + + def _initialize_object_pools(self): + """初始化对象池 - 修复16-19帧周期性GPU峰值""" + try: + # 预填充Mat4对象池 + for _ in range(self._matrix_pool_size): + self._matrix_pool.append(Mat4()) + + print(f"✅ Mat4对象池初始化完成 - 池大小: {self._matrix_pool_size}") + + # 🚀 预创建OpenVR Texture对象 - 消除每帧创建openvr.Texture_t()的开销 + try: + import openvr + self._left_ovr_texture = openvr.Texture_t() + self._right_ovr_texture = openvr.Texture_t() + + # 设置固定属性(这些不变) + self._left_ovr_texture.eType = openvr.TextureType_OpenGL + self._left_ovr_texture.eColorSpace = openvr.ColorSpace_Gamma + self._right_ovr_texture.eType = openvr.TextureType_OpenGL + self._right_ovr_texture.eColorSpace = openvr.ColorSpace_Gamma + + print("✅ OpenVR Texture对象缓存已创建 - 避免每帧创建新对象") + except Exception as texture_error: + print(f"⚠️ OpenVR Texture对象创建失败: {texture_error}") + # 不影响整体初始化,但性能可能不是最优 + + # 启用GC控制 + if self._gc_control_enabled: + # 禁用自动垃圾回收,改为手动控制 + gc.disable() + self._gc_disabled = True + print("✅ Python垃圾回收已禁用,改为手动控制") + + except Exception as e: + print(f"⚠️ 对象池初始化失败: {e}") + + def _get_pooled_matrix(self): + """从对象池获取Mat4对象""" + if self._matrix_pool: + return self._matrix_pool.pop() + else: + # 池为空时创建新对象(不应该发生,但作为备用) + return Mat4() + + def _return_pooled_matrix(self, matrix): + """将Mat4对象返回对象池""" + if len(self._matrix_pool) < self._matrix_pool_size: + # 重置矩阵到单位矩阵 + matrix.identMat() + self._matrix_pool.append(matrix) + + def _manual_gc_control(self): + """手动垃圾回收控制 - 避免VR渲染期间的GC峰值""" + if not self._gc_control_enabled or not self._gc_disabled: + return + + # 🚀 智能GC间隔:性能模式下减少GC频率 + current_interval = self._manual_gc_interval + if self.performance_mode_enabled: + current_interval = self._manual_gc_interval * 2 # 性能模式下间隔翻倍 + + # 每N帧手动触发一次垃圾回收 + if self.frame_count - self._last_manual_gc_frame >= current_interval: + # 在非渲染关键时刻触发GC + collected = gc.collect() + self._last_manual_gc_frame = self.frame_count + + # 仅在收集到对象时输出信息 + if collected > 0: + print(f"🗑️ 手动GC: 清理了 {collected} 个对象 (帧#{self.frame_count})") + + def _update_matrix_from_openvr(self, panda_mat, ovr_matrix): + """直接更新现有Mat4对象的数值,避免创建新对象""" + # 复用_convert_openvr_matrix_to_panda中的转换逻辑 + # X轴行:Panda3D的X轴对应OpenVR的X轴 + panda_mat.setCell(0, 0, ovr_matrix[0][0]) # X_x → X_x + panda_mat.setCell(0, 1, ovr_matrix[0][1]) # X_y → X_y + panda_mat.setCell(0, 2, ovr_matrix[0][2]) # X_z → X_z + panda_mat.setCell(0, 3, ovr_matrix[0][3]) # 位移X分量 + + # Y轴行:Panda3D的Y轴对应OpenVR的-Z轴 + panda_mat.setCell(1, 0, -ovr_matrix[2][0]) # -Z_x → Y_x + panda_mat.setCell(1, 1, -ovr_matrix[2][1]) # -Z_y → Y_y + panda_mat.setCell(1, 2, -ovr_matrix[2][2]) # -Z_z → Y_z + panda_mat.setCell(1, 3, -ovr_matrix[2][3]) # 位移Y分量(-Z位移) + + # Z轴行:Panda3D的Z轴对应OpenVR的Y轴 + panda_mat.setCell(2, 0, ovr_matrix[1][0]) # Y_x → Z_x + panda_mat.setCell(2, 1, ovr_matrix[1][1]) # Y_y → Z_y + panda_mat.setCell(2, 2, ovr_matrix[1][2]) # Y_z → Z_z + panda_mat.setCell(2, 3, ovr_matrix[1][3]) # 位移Z分量(Y位移) + + # 齐次坐标 + panda_mat.setCell(3, 0, 0) + panda_mat.setCell(3, 1, 0) + panda_mat.setCell(3, 2, 0) + panda_mat.setCell(3, 3, 1) + + def convert_mat(self, mat): + """ + 将OpenVR矩阵转换为Panda3D矩阵 - 基于参考实现 + """ + if len(mat.m) == 4: + result = LMatrix4( + mat.m[0][0], mat.m[1][0], mat.m[2][0], mat.m[3][0], + mat.m[0][1], mat.m[1][1], mat.m[2][1], mat.m[3][1], + mat.m[0][2], mat.m[1][2], mat.m[2][2], mat.m[3][2], + mat.m[0][3], mat.m[1][3], mat.m[2][3], mat.m[3][3]) + elif len(mat.m) == 3: + result = LMatrix4( + mat.m[0][0], mat.m[1][0], mat.m[2][0], 0.0, + mat.m[0][1], mat.m[1][1], mat.m[2][1], 0.0, + mat.m[0][2], mat.m[1][2], mat.m[2][2], 0.0, + mat.m[0][3], mat.m[1][3], mat.m[2][3], 1.0) + return result + + def is_vr_available(self): + """检查VR系统是否可用""" + if not OPENVR_AVAILABLE: + return False + + try: + # 检查SteamVR是否运行 + return openvr.isRuntimeInstalled() and openvr.isHmdPresent() + except Exception as e: + print(f"VR检查失败: {e}") + return False + + def initialize_vr(self): """初始化VR系统""" - try: - # 检查是否强制使用模拟模式 - if force_simulation: - print("🔧 强制启用VR模拟模式") - return self._init_simulation_mode() - - # 检查OpenVR支持 - if not self._check_openvr_support(): - print("⚠ OpenVR支持不可用,切换到模拟模式") - return self._init_simulation_mode() - - # 尝试初始化OpenVR - if not self._init_openvr(): - print("⚠ OpenVR初始化失败,切换到模拟模式") - print("提示: 请确保SteamVR正在运行且VR头盔已连接") - return self._init_simulation_mode() - - # 真实VR模式初始化成功 - print("✓ 真实VR模式初始化成功") - return self._init_real_vr_mode() - - except Exception as e: - print(f"VR初始化错误: {str(e)}") - print("⚠ 切换到模拟模式") - return self._init_simulation_mode() - - def _init_simulation_mode(self): - """初始化模拟模式""" - try: - self.simulation_mode = True - - # 使用模拟数据设置渲染尺寸 - self.render_width, self.render_height = self.simulation_data['render_size'] - print(f"🎮 模拟VR渲染尺寸: {self.render_width}x{self.render_height}") - - # 设置模拟立体渲染 - self._setup_stereo_rendering() - - # 启动模拟VR任务 - self._start_simulation_tasks() - - self.vr_enabled = True - print("✓ VR模拟模式初始化完成") - print("ℹ 模拟模式说明:") - print(" - 头盔追踪: 模拟数据") - print(" - 控制器: 模拟两个控制器") - print(" - 渲染: 立体渲染到窗口") - print(" - 交互: 键盘鼠标模拟") - - return True - - except Exception as e: - print(f"模拟模式初始化错误: {str(e)}") - return False - - def _init_real_vr_mode(self): - """初始化真实VR模式""" - try: - self.simulation_mode = False - - # 设置立体渲染 - self._setup_stereo_rendering() - - # 初始化ALVR - if self._init_alvr(): - print("✓ ALVR串流已启用") - self.alvr_enabled = True - - # 启动VR更新任务 - self._start_vr_tasks() - - self.vr_enabled = True - print("✓ 真实VR系统初始化完成") - return True - - except Exception as e: - print(f"真实VR模式初始化错误: {str(e)}") + if not OPENVR_AVAILABLE: + print("❌ OpenVR不可用,无法初始化VR") return False - def _check_openvr_support(self): - """检查OpenVR支持""" - try: - import openvr + if self.vr_initialized: + print("VR系统已经初始化") return True - except ImportError: - print("OpenVR库未安装") - return False - def _init_openvr(self): - """初始化OpenVR""" try: - import openvr - - # 初始化OpenVR + print("🔄 正在初始化VR系统...") + + # 🚀 确保对象池已正确初始化(备用检查) + if not hasattr(self, '_matrix_pool') or len(self._matrix_pool) == 0: + print("⚠️ 对象池未初始化,正在重新初始化...") + self._initialize_object_pools() + + # 初始化OpenVR - 使用Scene应用类型确保正确的焦点管理 self.vr_system = openvr.init(openvr.VRApplication_Scene) if not self.vr_system: + print("❌ 无法初始化OpenVR系统") return False - - # 获取合成器 + + # 获取compositor self.vr_compositor = openvr.VRCompositor() if not self.vr_compositor: + print("❌ 无法获取VR Compositor") return False - - # 获取推荐的渲染尺寸 - self.render_width, self.render_height = self.vr_system.getRecommendedRenderTargetSize() - print(f"VR推荐渲染尺寸: {self.render_width}x{self.render_height}") - - return True - - except Exception as e: - print(f"OpenVR初始化错误: {str(e)}") - return False - def _setup_stereo_rendering(self): - """设置立体渲染""" - try: - # 创建眼部缓冲区 - self._create_eye_buffers() - - # 创建眼部摄像机 - self._create_eye_cameras() - - # 设置渲染目标 - self._setup_render_targets() - - print("✓ 立体渲染设置完成") - - except Exception as e: - print(f"立体渲染设置错误: {str(e)}") + # 获取推荐的渲染目标尺寸 + base_width, base_height = self.vr_system.getRecommendedRenderTargetSize() + self.base_eye_width = base_width + self.base_eye_height = base_height - def _create_eye_buffers(self): - """创建眼部渲染缓冲区""" - try: - # 创建左眼缓冲区 - self.left_eye_buffer = self.world.win.makeTextureBuffer( - "left_eye", self.render_width, self.render_height - ) - self.left_eye_buffer.setSort(-100) - - # 创建右眼缓冲区 - self.right_eye_buffer = self.world.win.makeTextureBuffer( - "right_eye", self.render_width, self.render_height - ) - self.right_eye_buffer.setSort(-99) - - # 获取纹理 - self.left_eye_texture = self.left_eye_buffer.getTexture() - self.right_eye_texture = self.right_eye_buffer.getTexture() - - print("✓ 眼部渲染缓冲区创建完成") - - except Exception as e: - print(f"眼部缓冲区创建错误: {str(e)}") + # 应用分辨率缩放 + self.scaled_eye_width = int(base_width * self.resolution_scale) + self.scaled_eye_height = int(base_height * self.resolution_scale) - def _create_eye_cameras(self): - """创建眼部摄像机""" - try: - # 创建左眼摄像机 - self.left_eye_camera = self.world.makeCamera(self.left_eye_buffer) - self.left_eye_camera.setPos(-0.032, 0, 0) # 瞳距的一半 - - # 创建右眼摄像机 - self.right_eye_camera = self.world.makeCamera(self.right_eye_buffer) - self.right_eye_camera.setPos(0.032, 0, 0) # 瞳距的一半 - - # 设置投影矩阵 - if not self.simulation_mode: - self._update_eye_projection(0, self.left_eye_camera.node().getLens()) - self._update_eye_projection(1, self.right_eye_camera.node().getLens()) + # 使用缩放后的分辨率作为实际渲染分辨率 + self.eye_width = self.scaled_eye_width + self.eye_height = self.scaled_eye_height + + self.current_eye_resolution = (self.eye_width, self.eye_height) + self.recommended_eye_resolution = (base_width, base_height) + + print(f"✓ VR基础分辨率: {base_width}x{base_height}") + print(f"✓ VR缩放系数: {self.resolution_scale}") + print(f"✓ VR实际分辨率: {self.eye_width}x{self.eye_height}") + print(f"📊 分辨率优化: {(1 - self.resolution_scale**2) * 100:.1f}% 像素减少") + + # 获取VR系统信息 + try: + # 获取显示频率 + self.vr_display_frequency = self.vr_system.getFloatTrackedDeviceProperty( + openvr.k_unTrackedDeviceIndex_Hmd, + openvr.Prop_DisplayFrequency_Float + ) + print(f"✓ VR显示频率: {self.vr_display_frequency} Hz") + + # 获取IPD(瞳距) + ipd = self.vr_system.getFloatTrackedDeviceProperty( + openvr.k_unTrackedDeviceIndex_Hmd, + openvr.Prop_UserIpdMeters_Float + ) + print(f"✓ IPD(瞳距): {ipd * 1000:.1f}mm") + + # 获取设备制造商和型号 + manufacturer = self.vr_system.getStringTrackedDeviceProperty( + openvr.k_unTrackedDeviceIndex_Hmd, + openvr.Prop_ManufacturerName_String + ) + model = self.vr_system.getStringTrackedDeviceProperty( + openvr.k_unTrackedDeviceIndex_Hmd, + openvr.Prop_ModelNumber_String + ) + print(f"✓ VR设备: {manufacturer} {model}") + + # 获取更多系统配置信息 + try: + # 获取刷新率相关信息 + seconds_since_last_vsync = self.vr_system.getFloatTrackedDeviceProperty( + openvr.k_unTrackedDeviceIndex_Hmd, + openvr.Prop_SecondsFromVsyncToPhotons_Float + ) + self.vsync_to_photons_ms = seconds_since_last_vsync * 1000 + print(f"✓ VSync到光子延迟: {self.vsync_to_photons_ms:.2f}ms") + + # 获取显示延迟信息 + display_refresh_rate = self.vr_display_frequency + if display_refresh_rate > 0: + self.target_frame_time_ms = 1000.0 / display_refresh_rate + print(f"✓ 目标帧时间: {self.target_frame_time_ms:.2f}ms") + + # 计算VSync时间窗口 + self.vsync_window_ms = self.target_frame_time_ms * 0.1 # 10%的窗口 + print(f"✓ VSync时间窗口: ±{self.vsync_window_ms:.2f}ms") + + except Exception as vsync_error: + print(f"⚠️ 获取VSync信息失败: {vsync_error}") + + # 检查是否启用了异步重投影 + try: + if hasattr(openvr, 'VRSettings'): + settings = openvr.VRSettings() + if settings: + self.async_reprojection_enabled = settings.getBool("steamvr", "enableAsyncReprojection") + print(f"✓ 异步重投影: {'启用' if self.async_reprojection_enabled else '禁用'}") + + self.motion_smoothing_enabled = settings.getBool("steamvr", "motionSmoothing") + print(f"✓ 运动平滑: {'启用' if self.motion_smoothing_enabled else '禁用'}") + + except Exception as settings_error: + print(f"⚠️ 获取VR设置失败: {settings_error}") + + except Exception as e: + print(f"⚠️ 获取VR系统信息失败: {e}") + + # 创建OpenVR姿态数组 - 分离渲染和游戏姿态 + poses_t = openvr.TrackedDevicePose_t * openvr.k_unMaxTrackedDeviceCount + self.poses = poses_t() # 渲染姿态(预测的) + self.game_poses = poses_t() # 游戏逻辑姿态(当前的) + print("✓ VR渲染和游戏姿态数组已创建") + + # 创建VR渲染缓冲区 + if not self._create_vr_buffers(): + print("❌ 创建VR渲染缓冲区失败") + return False + + # 设置VR相机 + if not self._setup_vr_cameras(): + print("❌ 设置VR相机失败") + return False + + # 优化VR渲染管线 + self._optimize_vr_rendering() + + # 初始化手柄控制器 + self._initialize_controllers() + + # 初始化动作系统 - 仅在启用时初始化 + if not self.disable_action_system and self.action_manager: + try: + if not self.action_manager.initialize(): + print("⚠️ VR动作系统初始化失败,但VR系统将继续运行") + except Exception as e: + print(f"⚠️ VR动作系统初始化异常: {e}") else: - # 模拟模式使用标准透视投影 - lens = PerspectiveLens() - lens.setFov(110) # 模拟VR FOV - lens.setNearFar(0.1, 1000) - self.left_eye_camera.node().setLens(lens) - self.right_eye_camera.node().setLens(lens) - - print("✓ 眼部摄像机创建完成") - - except Exception as e: - print(f"眼部摄像机创建错误: {str(e)}") + if self.disable_action_system: + print("🚫 VR动作系统已禁用,跳过初始化") + else: + print("⚠️ VR动作管理器未创建,跳过动作系统初始化") - def _update_eye_projection(self, eye, lens): - """更新眼部投影矩阵""" - try: - if self.simulation_mode: - # 模拟模式使用标准投影 - perspective_lens = PerspectiveLens() - perspective_lens.setFov(110) - perspective_lens.setNearFar(0.1, 1000) - lens.copyFrom(perspective_lens) - return - - import openvr - - # 获取投影矩阵 - projection_matrix = self.vr_system.getProjectionMatrix(eye, 0.1, 1000.0) - - # 转换为Panda3D矩阵 - panda_matrix = self._convert_openvr_matrix(projection_matrix) - - # 设置自定义投影矩阵 - lens.setCustomProjectionMatrix(panda_matrix) - - except Exception as e: - print(f"眼部投影更新错误: {str(e)}") + # 初始化交互系统 - 检查是否存在 + if self.interaction_manager: + try: + if not self.interaction_manager.initialize(): + print("⚠️ VR交互系统初始化失败,但VR系统将继续运行") + except Exception as e: + print(f"⚠️ VR交互系统初始化异常: {e}") + else: + print("⚠️ VR交互管理器未创建,跳过交互系统初始化") - def _setup_render_targets(self): - """设置渲染目标""" - try: - # 在模拟模式下,可以选择将渲染结果显示到主窗口 - if self.simulation_mode: - # 创建并排显示的立体视图 - self._setup_simulation_display() - - print("✓ 渲染目标设置完成") - - except Exception as e: - print(f"渲染目标设置错误: {str(e)}") + # 初始化VR传送系统 + if self.teleport_system: + try: + if not self.teleport_system.initialize(): + print("⚠️ VR传送系统初始化失败,但VR系统将继续运行") + except Exception as e: + print(f"⚠️ VR传送系统初始化异常: {e}") + else: + print("⚠️ VR传送系统未创建,跳过传送系统初始化") - def _setup_simulation_display(self): - """设置模拟显示""" - try: - # 创建卡片来显示眼部纹理 - cm = CardMaker("stereo_display") - - # 左眼显示区域 - cm.setFrame(-1, 0, -1, 1) - left_card = self.world.render2d.attachNewNode(cm.generate()) - left_card.setTexture(self.left_eye_texture) - - # 右眼显示区域 - cm.setFrame(0, 1, -1, 1) - right_card = self.world.render2d.attachNewNode(cm.generate()) - right_card.setTexture(self.right_eye_texture) - - print("✓ 模拟立体显示设置完成") - - except Exception as e: - print(f"模拟显示设置错误: {str(e)}") + # 初始化VR摇杆系统 + if self.joystick_manager: + try: + # 传入传送系统引用给摇杆管理器 + self.joystick_manager.initialize(self.teleport_system) + print("✓ VR摇杆系统初始化成功") + except Exception as e: + print(f"⚠️ VR摇杆系统初始化异常: {e}") + else: + print("⚠️ VR摇杆管理器未创建,跳过摇杆系统初始化") - def _init_alvr(self): - """初始化ALVR(仅在真实VR模式下)""" - if self.simulation_mode: - print("ℹ 模拟模式: ALVR串流已跳过") - return False - - try: - # ALVR初始化逻辑 - # 这里应该连接到ALVR服务器 - print("✓ ALVR初始化完成") + # 可选:禁用异步重投影(备选方案) + if self.disable_async_reprojection: + self._disable_async_reprojection() + + # 启动VR更新任务 + self._start_vr_task() + + self.vr_initialized = True + print("✅ VR系统初始化成功") return True + except Exception as e: - print(f"ALVR初始化错误: {str(e)}") + print(f"❌ VR初始化失败: {e}") + import traceback + traceback.print_exc() return False - def _start_vr_tasks(self): - """启动VR更新任务(真实VR模式)""" - if not self.simulation_mode: - taskMgr.add(self._update_vr_tracking, "vr_tracking") - taskMgr.add(self._update_vr_rendering, "vr_rendering") - - def _start_simulation_tasks(self): - """启动模拟VR任务""" - taskMgr.add(self._update_simulation_tracking, "simulation_tracking") - taskMgr.add(self._update_simulation_rendering, "simulation_rendering") - - def _update_simulation_tracking(self, task): - """更新模拟追踪数据""" + def _create_vr_buffers(self): + """创建VR渲染缓冲区 - 使用分辨率缩放优化""" try: - # 模拟头部追踪(可以添加一些变化) - import math - time_factor = task.time * 0.5 - - # 模拟轻微的头部摆动 - head_pos = self.simulation_data['head_pose']['position'] - head_pos[1] = math.sin(time_factor) * 0.05 # 前后轻微摆动 - - # 更新主摄像机位置 - if hasattr(self.world, 'camera'): - self.world.camera.setPos(head_pos[0], head_pos[1], head_pos[2]) - - # 更新控制器位置(模拟手部动作) - for controller_id, pose in self.simulation_data['controller_poses'].items(): - if pose['connected']: - # 模拟控制器轻微移动 - pose['position'][1] = math.sin(time_factor + controller_id) * 0.1 - - # 更新控制器节点位置 - if controller_id in self.controller_nodes: - node = self.controller_nodes[controller_id] - node.setPos(pose['position'][0], pose['position'][1], pose['position'][2]) - - return task.cont - + print(f"🔧 创建VR缓冲区:") + print(f" 推荐分辨率: {self.base_eye_width}x{self.base_eye_height}") + print(f" 缩放系数: {self.resolution_scale}") + print(f" 实际分辨率: {self.eye_width}x{self.eye_height}") + print(f" 像素减少: {(1 - self.resolution_scale**2) * 100:.1f}%") + + # 创建左眼纹理和缓冲区 + self.vr_left_texture = self._create_vr_texture("VR Left Eye Texture") + self.vr_left_eye_buffer = self._create_vr_buffer( + "VR Left Eye", + self.vr_left_texture, + self.eye_width, + self.eye_height + ) + + if not self.vr_left_eye_buffer: + print("❌ 创建左眼缓冲区失败") + return False + + # 设置左眼缓冲区属性 + self.vr_left_eye_buffer.setSort(-100) + self.vr_left_eye_buffer.setClearColor((0.1, 0.2, 0.4, 1)) # 深蓝色背景便于调试 + self.vr_left_eye_buffer.setActive(True) + + # 创建右眼纹理和缓冲区 + self.vr_right_texture = self._create_vr_texture("VR Right Eye Texture") + self.vr_right_eye_buffer = self._create_vr_buffer( + "VR Right Eye", + self.vr_right_texture, + self.eye_width, + self.eye_height + ) + + if not self.vr_right_eye_buffer: + print("❌ 创建右眼缓冲区失败") + return False + + # 设置右眼缓冲区属性 + self.vr_right_eye_buffer.setSort(-99) + self.vr_right_eye_buffer.setClearColor((0.1, 0.2, 0.4, 1)) # 深蓝色背景便于调试 + self.vr_right_eye_buffer.setActive(True) + + # 🚀 关键优化:立即准备纹理并缓存OpenGL ID,避免后续重复准备 + print("🔧 准备纹理并缓存OpenGL ID...") + if self._prepare_and_cache_textures(): + print("✅ VR渲染缓冲区和纹理缓存创建成功") + return True + else: + print("❌ 纹理准备失败") + return False + except Exception as e: - print(f"模拟追踪更新错误: {str(e)}") - return task.cont + print(f"❌ 创建VR缓冲区失败: {e}") + import traceback + traceback.print_exc() + return False - def _update_simulation_rendering(self, task): - """更新模拟渲染""" + def _create_vr_texture(self, name): + """创建VR纹理对象 - 基于参考实现""" + texture = Texture(name) + texture.setWrapU(Texture.WMClamp) + texture.setWrapV(Texture.WMClamp) + texture.setMinfilter(Texture.FTLinear) + texture.setMagfilter(Texture.FTLinear) + return texture + + def _prepare_and_cache_textures(self): + """准备纹理并缓存OpenGL ID - 解决重复准备问题""" try: - # 在模拟模式下,渲染已经由Panda3D自动处理 - # 这里可以添加任何特殊的渲染逻辑 - return task.cont - + # 获取graphics state guardian和prepared objects + gsg = self.world.win.getGsg() + if not gsg: + print("❌ 无法获取GraphicsStateGuardian") + return False + + prepared_objects = gsg.getPreparedObjects() + if not prepared_objects: + print("❌ 无法获取PreparedObjects") + return False + + # 准备左眼纹理并缓存ID + if self.vr_left_texture: + print(f" 准备左眼纹理: {self.vr_left_texture.getName()}") + texture_context = self.vr_left_texture.prepareNow(0, prepared_objects, gsg) + if texture_context and hasattr(texture_context, 'getNativeId'): + self.left_texture_id = texture_context.getNativeId() + if self.left_texture_id > 0: + print(f" ✅ 左眼纹理ID缓存: {self.left_texture_id}") + else: + print(" ❌ 左眼纹理ID无效") + return False + else: + print(" ❌ 左眼纹理准备失败") + return False + + # 准备右眼纹理并缓存ID + if self.vr_right_texture: + print(f" 准备右眼纹理: {self.vr_right_texture.getName()}") + texture_context = self.vr_right_texture.prepareNow(0, prepared_objects, gsg) + if texture_context and hasattr(texture_context, 'getNativeId'): + self.right_texture_id = texture_context.getNativeId() + if self.right_texture_id > 0: + print(f" ✅ 右眼纹理ID缓存: {self.right_texture_id}") + else: + print(" ❌ 右眼纹理ID无效") + return False + else: + print(" ❌ 右眼纹理准备失败") + return False + + # 标记纹理已准备 + self.textures_prepared = True + print(" ✅ 纹理准备完成,ID已缓存,后续帧将直接使用缓存ID") + return True + except Exception as e: - print(f"模拟渲染更新错误: {str(e)}") + print(f"❌ 纹理准备和缓存失败: {e}") + import traceback + traceback.print_exc() + return False + + def _create_vr_buffer(self, name, texture, width, height): + """创建VR渲染缓冲区 - 基于参考实现 + 性能诊断""" + # 设置帧缓冲属性 + fbprops = FrameBufferProperties() + fbprops.setRgbaBits(1, 1, 1, 1) + # 可以在这里添加多重采样抗锯齿 + # fbprops.setMultisamples(4) + + # 创建缓冲区 + buffer = self.world.win.makeTextureBuffer(name, width, height, to_ram=False, fbp=fbprops) + + if buffer: + # 🔍 性能诊断:检查缓冲区类型和属性 + self._diagnose_buffer_performance(buffer, name, width, height) + + # 清除默认渲染纹理 + buffer.clearRenderTextures() + # 🚀 使用RTMBindOrCopy模式 - 已经是最优选择 + # RTMBindOrCopy会尝试直接绑定到纹理,只有硬件不支持时才回退到复制 + # 这已经是Panda3D中最高效的渲染到纹理模式 + buffer.addRenderTexture(texture, GraphicsOutput.RTMBindOrCopy, GraphicsOutput.RTPColor) + + else: + print(f"⚠️ VR缓冲区创建失败: {name} ({width}x{height})") + + return buffer + + def _diagnose_buffer_performance(self, buffer, name, width, height): + """诊断VR缓冲区性能特性""" + try: + # 检查缓冲区类型 + buffer_type = type(buffer).__name__ + + # 检查是否为parasite buffer(性能较差的类型) + is_parasite = 'Parasite' in buffer_type + + # 获取缓冲区信息 + is_valid = buffer.isValid() + is_hardware = hasattr(buffer, 'getGsg') and buffer.getGsg() is not None + + # 输出诊断信息(仅第一次创建时) + if not hasattr(self, '_buffer_diagnosis_shown'): + print(f"🔍 VR缓冲区诊断 [{name}]:") + print(f" 类型: {buffer_type}") + print(f" 分辨率: {width}x{height} ({width*height/1000000:.1f}M像素)") + print(f" 有效性: {'✓' if is_valid else '✗'}") + print(f" 硬件支持: {'✓' if is_hardware else '✗'}") + + if is_parasite: + print(f" ⚠️ 检测到Parasite Buffer - 性能可能受限") + print(f" 建议: 检查显卡是否支持真正的离屏渲染") + else: + print(f" ✓ 使用硬件FBO - 性能良好") + + # 显示帧缓冲区属性 + if hasattr(buffer, 'getFbProperties'): + fbp = buffer.getFbProperties() + if fbp: + print(f" 颜色位数: R{fbp.getRedBits()}G{fbp.getGreenBits()}B{fbp.getBlueBits()}A{fbp.getAlphaBits()}") + if hasattr(fbp, 'getMultisamples') and fbp.getMultisamples() > 0: + print(f" MSAA: {fbp.getMultisamples()}x") + + self._buffer_diagnosis_shown = True + + except Exception as e: + print(f"缓冲区诊断失败: {e}") + + def _setup_vr_cameras(self): + """设置VR相机 - 使用锚点层级系统""" + try: + # 创建VR追踪空间锚点层级 + self.tracking_space = self.world.render.attachNewNode('tracking-space') + self.hmd_anchor = self.tracking_space.attachNewNode('hmd-anchor') + self.left_eye_anchor = self.hmd_anchor.attachNewNode('left-eye') + self.right_eye_anchor = self.hmd_anchor.attachNewNode('right-eye') + + # 获取投影矩阵 + projection_left = self.coord_mat_inv * self.convert_mat( + self.vr_system.getProjectionMatrix(openvr.Eye_Left, self.near_clip, self.far_clip)) + projection_right = self.coord_mat_inv * self.convert_mat( + self.vr_system.getProjectionMatrix(openvr.Eye_Right, self.near_clip, self.far_clip)) + + # 创建左眼相机节点 + left_cam_node = Camera('left-cam') + left_lens = MatrixLens() + left_lens.setUserMat(projection_left) + left_cam_node.setLens(left_lens) + + # 创建右眼相机节点 + right_cam_node = Camera('right-cam') + right_lens = MatrixLens() + right_lens.setUserMat(projection_right) + right_cam_node.setLens(right_lens) + + # 附加相机到眼睛锚点 + self.vr_left_camera = self.left_eye_anchor.attachNewNode(left_cam_node) + self.vr_right_camera = self.right_eye_anchor.attachNewNode(right_cam_node) + + # 设置显示区域使用标准渲染流程 + left_dr = self.vr_left_eye_buffer.makeDisplayRegion() + left_dr.setCamera(self.vr_left_camera) + left_dr.setActive(True) + # 恢复DrawCallback以精确控制渲染时机 + left_dr.setDrawCallback(PythonCallbackObject(self.simple_left_cb)) + + right_dr = self.vr_right_eye_buffer.makeDisplayRegion() + right_dr.setCamera(self.vr_right_camera) + right_dr.setActive(True) + # 恢复DrawCallback以精确控制渲染时机 + right_dr.setDrawCallback(PythonCallbackObject(self.simple_right_cb)) + + print("✓ VR相机锚点层级系统设置完成") + return True + + except Exception as e: + print(f"❌ 设置VR相机失败: {e}") + import traceback + traceback.print_exc() + return False + + def _get_eye_offset(self, eye): + """获取眼睛相对于头显的偏移""" + try: + if not self.vr_system: + # 使用标准IPD(瞳距)估算值 + ipd = 0.064 # 64mm,平均IPD + if eye == openvr.Eye_Left: + return Vec3(-ipd/2, 0, 0) + else: + return Vec3(ipd/2, 0, 0) + + # 从OpenVR获取眼睛到头显的变换矩阵 + eye_transform = self.vr_system.getEyeToHeadTransform(eye) + + # 提取位移信息 + x = eye_transform[0][3] + y = eye_transform[1][3] + z = eye_transform[2][3] + + return Vec3(x, y, z) + + except Exception as e: + print(f"❌ 获取眼睛偏移失败: {e}") + # 返回默认值 + ipd = 0.064 + if eye == openvr.Eye_Left: + return Vec3(-ipd/2, 0, 0) + else: + return Vec3(ipd/2, 0, 0) + + def _optimize_vr_rendering(self): + """优化VR模式下的渲染管线 - 减少不必要的后处理开销""" + try: + print("🔧 正在优化VR渲染管线...") + + # 检查是否有RenderPipeline + if hasattr(self.world, 'render_pipeline') and self.world.render_pipeline: + print(" 检测到RenderPipeline,正在优化...") + + # 对VR缓冲区应用轻量级渲染设置 + if hasattr(self, 'vr_left_eye_buffer') and self.vr_left_eye_buffer: + self._apply_lightweight_rendering(self.vr_left_eye_buffer, "左眼") + + if hasattr(self, 'vr_right_eye_buffer') and self.vr_right_eye_buffer: + self._apply_lightweight_rendering(self.vr_right_eye_buffer, "右眼") + + print("✅ VR渲染管线优化完成") + else: + print(" 未检测到RenderPipeline,使用默认Panda3D渲染") + + # 禁用VR缓冲区的不必要功能 + self._disable_vr_buffer_extras() + + except Exception as e: + print(f"⚠️ VR渲染优化失败: {e}") + import traceback + traceback.print_exc() + + def _apply_lightweight_rendering(self, buffer, eye_name): + """为VR缓冲区应用轻量级渲染设置""" + try: + # 禁用多重采样抗锯齿(MSAA)以提升性能 + if hasattr(buffer, 'setMultisample'): + buffer.setMultisample(0) + print(f" {eye_name}: 禁用MSAA") + + # 设置更简单的清除颜色操作 + buffer.setClearColorActive(True) + buffer.setClearColor((0.1, 0.2, 0.4, 1.0)) # 深蓝色,便于调试 + + # 禁用深度写入到某些不需要的缓冲区 + # (保留主要深度测试,但减少不必要的写入) + + print(f" {eye_name}: 应用轻量级渲染设置") + + except Exception as e: + print(f"⚠️ {eye_name}缓冲区优化失败: {e}") + + def _disable_vr_buffer_extras(self): + """禁用VR缓冲区的额外功能以提升性能""" + try: + # 禁用VR缓冲区的统计收集 + if hasattr(self, 'vr_left_eye_buffer') and self.vr_left_eye_buffer: + if hasattr(self.vr_left_eye_buffer, 'setOneShot'): + self.vr_left_eye_buffer.setOneShot(False) + + if hasattr(self, 'vr_right_eye_buffer') and self.vr_right_eye_buffer: + if hasattr(self.vr_right_eye_buffer, 'setOneShot'): + self.vr_right_eye_buffer.setOneShot(False) + + print(" VR缓冲区额外功能已优化") + + except Exception as e: + print(f"⚠️ VR缓冲区额外功能优化失败: {e}") + + def _start_vr_task(self): + """启动VR更新任务""" + if self.vr_task: + self.world.taskMgr.remove(self.vr_task) + + # 设置高优先级(sort=-1000),确保在渲染之前执行,参考 panda3d-openvr + self.vr_task = self.world.taskMgr.add(self._update_vr, "update_vr", sort=-1000) + print("✓ VR更新任务已启动(高优先级,sort=-1000)") + + def _update_vr(self, task): + """VR更新任务 - 每帧调用(参考 panda3d-openvr 架构)""" + if not self.vr_enabled or not self.vr_system: return task.cont - def _update_vr_tracking(self, task): - """更新VR追踪数据(真实VR模式)""" try: - if not self.vr_system or self.simulation_mode: - return task.cont - - import openvr - - # 获取设备姿态 - poses, game_poses = self.vr_compositor.waitGetPoses(None, None) - - # 更新头显位置 - if poses[openvr.k_unTrackedDeviceIndex_Hmd].bPoseIsValid: - self._update_main_camera_pose() - - # 更新眼部摄像机 - self._update_eye_cameras() - + # 📌 第一件事:waitGetPoses 阻塞等待 VSync(参考 panda3d-openvr) + # 这会阻塞整个任务,确保每个 VSync 周期只执行一次 + should_call_waitgetposes = ( + not self.vr_test_mode or # 普通VR模式总是需要 + self.test_mode_wait_poses # 测试模式:仅当启用姿态等待时 + ) + + if should_call_waitgetposes: + self._wait_get_poses_immediate() + # 立即更新手柄,使用最新姿态(避免过时数据) + self.update_tracked_devices() + + # 性能监控 + self.frame_count += 1 + + # 🚀 自动启用性能模式 - 减少计时对象创建 + if not self.performance_mode_enabled and self.frame_count >= self.performance_mode_trigger_frame: + self.performance_mode_enabled = True + if self.frame_count <= self.performance_mode_trigger_frame + 5: # 只输出一次 + print(f"🎯 性能模式已启用 (帧#{self.frame_count}) - 禁用详细监控以提升性能") + + # 记录帧时间 + self._track_frame_time() + + # 计算VR FPS + import time + current_time = time.time() + if self.last_fps_time == 0: + self.last_fps_time = current_time + elif current_time - self.last_fps_time >= 1.0: # 每秒更新一次FPS + self.vr_fps = (self.frame_count - self.last_fps_check) / (current_time - self.last_fps_time) + self.last_fps_check = self.frame_count + self.last_fps_time = current_time + + # 📌 使用刚获取的姿态更新相机(而不是旧姿态) + self._update_camera_poses() + + # 输出策略信息(仅第一次) + if not hasattr(self, '_new_architecture_logged'): + print("✅ 新架构已启用 - waitGetPoses 在任务开始阻塞,参考 panda3d-openvr") + print(" 这确保每个 VSync 周期只渲染一次,解决 AlreadySubmitted 错误") + self._new_architecture_logged = True + + # 更新VR动作状态 - 仅在启用时更新 + if not self.disable_action_system and self.action_manager: + self.action_manager.update_actions() + + # 更新VR交互系统 + if self.interaction_manager: + self.interaction_manager.update() + + # 更新VR摇杆系统 + if self.joystick_manager: + # 计算帧间隔时间 + import time + if not hasattr(self, '_last_frame_time'): + self._last_frame_time = time.time() + dt = 0.016 # 默认60fps + else: + current_time = time.time() + dt = current_time - self._last_frame_time + self._last_frame_time = current_time + # 限制dt范围,避免异常情况 + dt = max(0.001, min(0.1, dt)) + + self.joystick_manager.update(dt) + + # 更新系统性能指标(减少频率) + if self.frame_count % 30 == 1: # 每30帧更新一次,减少开销 + self._update_performance_metrics() + + # 更新GPU渲染时间统计(减少频率) + if self.enable_gpu_timing and self.frame_count % 60 == 1: + self._get_gpu_frame_timing() + + # 🚀 手动垃圾回收控制 - 避免16-19帧周期性峰值 + self._manual_gc_control() + + # 定期输出性能报告 - 默认10秒间隔 + report_interval = getattr(self, 'performance_report_interval', 600) + if self.frame_count % report_interval == 1: + self._print_performance_report() + + except Exception as e: + print(f"VR更新错误: {e}") + import traceback + traceback.print_exc() + + return task.cont + + def _sync_gpu_if_needed(self): + """可选的GPU同步 - 仅在需要时使用""" + try: + # 同步等待GPU完成(如果需要) + gsg = self.world.win.getGsg() + if gsg: + gsg.getEngine().syncFrame() + except Exception as e: + print(f"❌ GPU同步异常: {e}") + + def simple_left_cb(self, cbdata): + """简化的左眼渲染回调 - 精确控制渲染和提交""" + try: + # 🔍 精确测量渲染时间 + import time + render_start = time.perf_counter() + + # 触发实际渲染 + cbdata.upcall() + + # 计算真实渲染时间 + self.left_render_time = (time.perf_counter() - render_start) * 1000 # 转换为毫秒 + + # 记录渲染次数 + self.left_render_count += 1 + + # 📌 OpenVR 帧边界检查:防止同一 OpenVR 帧内重复渲染 + if self.left_eye_last_render_frame == self.openvr_frame_id: + return # 已在当前 OpenVR 帧渲染过,跳过 + + # 🔧 OpenVR最佳实践:左眼只渲染,不立即提交 + # 基于官方hellovr示例:两眼都渲染完后再批量提交 + self.left_eye_last_render_frame = self.openvr_frame_id + + # 渐进式VR功能测试:单独启用纹理提交时保留原逻辑 + should_submit = self.vr_test_mode and self.test_mode_submit_texture and not self.test_mode_wait_poses + + if should_submit: + # 测试模式:单独启用纹理提交时的兼容模式 + if self.vr_compositor and self.vr_left_texture: + self.submit_texture(openvr.Eye_Left, self.vr_left_texture) + + if self.vr_test_mode: + # 测试模式:始终触发屏幕显示更新 + self._update_test_display() + + except Exception as e: + print(f"左眼渲染回调错误: {e}") + + def simple_right_cb(self, cbdata): + """简化的右眼渲染回调 - 精确控制渲染和提交""" + try: + # 🔍 精确测量渲染时间 + import time + render_start = time.perf_counter() + + # 触发实际渲染 + cbdata.upcall() + + # 计算真实渲染时间 + self.right_render_time = (time.perf_counter() - render_start) * 1000 # 转换为毫秒 + + # 记录渲染次数 + self.right_render_count += 1 + + # 📌 OpenVR 帧边界检查:防止同一 OpenVR 帧内重复渲染 + if self.right_eye_last_render_frame == self.openvr_frame_id: + return # 已在当前 OpenVR 帧渲染过,跳过 + + # 🔧 OpenVR最佳实践:右眼渲染完成后批量提交两眼纹理 + # 基于官方hellovr示例:避免分散提交导致的VSync阻塞 + self.right_eye_last_render_frame = self.openvr_frame_id + + # 🔧 渐进式VR功能测试:根据调试标志决定启用哪些功能 + should_batch_submit = not self.vr_test_mode or self.test_mode_submit_texture + should_wait_poses = not self.vr_test_mode or self.test_mode_wait_poses + + # 特殊处理:测试模式单独启用功能时的兼容模式 + if self.vr_test_mode: + if self.test_mode_submit_texture and not self.test_mode_wait_poses: + # 单独测试纹理提交:使用原来的分散提交方式 + if self.vr_compositor and self.vr_right_texture: + self.submit_texture(openvr.Eye_Right, self.vr_right_texture) + + # 🚀 测试模式也需要PostPresentHandoff避免36FPS + try: + if hasattr(self.vr_compositor, 'postPresentHandoff'): + self.vr_compositor.postPresentHandoff() + elif hasattr(self.vr_compositor, 'PostPresentHandoff'): + self.vr_compositor.PostPresentHandoff() + except Exception as e: + pass # 测试模式静默忽略错误 + should_batch_submit = False + elif self.test_mode_wait_poses and not self.test_mode_submit_texture: + # 单独测试姿态等待:不提交纹理 + should_batch_submit = False + + if should_batch_submit: + # 🚀 OpenVR最佳实践:批量提交两眼纹理 + # 这是解决36FPS问题的关键修复 + self._batch_submit_textures() + + # 🔧 关键修复:移除Submit后立即WaitGetPoses的错误实现 + # 根据OpenVR官方文档:"Calling WaitGetPoses immediately after Submit is conspicuously wrong" + # WaitGetPoses应该在下一帧开始时通过update_vr_task调用,不是Submit后立即调用 + # + # if should_wait_poses: + # # 错误的实现:Submit后立即获取姿态导致36FPS + # self._wait_get_poses_immediate() # ← 这是36FPS的根本原因! + # + # 正确的实现:让update_vr_task在下一帧开始时调用WaitGetPoses + + if self.vr_test_mode: + # 测试模式:始终更新性能HUD + self._update_test_performance_hud() + + except Exception as e: + print(f"右眼渲染回调错误: {e}") + + def _wait_get_poses(self): + """调用VRCompositor的waitGetPoses来获取焦点和姿态数据""" + try: + if not self.vr_compositor or not self.poses: + return + + # 调用waitGetPoses获取焦点和姿态数据 + # 这个调用可能会阻塞直到下一个VR同步点 + result = self.vr_compositor.waitGetPoses(self.poses, None) + + # 检查姿态数据的有效性 + valid_poses = 0 + + # 更新HMD姿态(设备0通常是头显) + if len(self.poses) > 0 and self.poses[0].bPoseIsValid: + valid_poses += 1 + else: + # 如果HMD姿态无效,不要频繁输出错误信息 + if not hasattr(self, '_hmd_invalid_warning_shown'): + print("⚠️ HMD姿态数据无效") + self._hmd_invalid_warning_shown = True + # 更新控制器姿态 - self._update_controller_poses(poses) - - return task.cont - - except Exception as e: - print(f"VR追踪更新错误: {str(e)}") - return task.cont + self.controller_poses.clear() + for device_id in range(1, min(len(self.poses), openvr.k_unMaxTrackedDeviceCount)): + if self.poses[device_id].bPoseIsValid: + device_class = self.vr_system.getTrackedDeviceClass(device_id) + if device_class == openvr.TrackedDeviceClass_Controller: + controller_matrix = self.poses[device_id].mDeviceToAbsoluteTracking + self.controller_poses[device_id] = self._convert_openvr_matrix_to_panda(controller_matrix) + valid_poses += 1 + + # 性能监控 - 偶尔输出姿态状态 + if self.frame_count % 600 == 1: # 每10秒输出一次@60fps + print(f"📊 VR姿态状态 - 有效姿态数: {valid_poses}, 总帧数: {self.frame_count}") + + except Exception as e: + # 限制错误输出频率 + if not hasattr(self, '_last_error_frame'): + self._last_error_frame = 0 + + if self.frame_count - self._last_error_frame > 300: # 每5秒最多输出一次错误 + print(f"waitGetPoses失败: {e}") + self._last_error_frame = self.frame_count + + # 记录姿态失败次数 + self.pose_failures += 1 + + def _wait_get_poses_immediate(self): + """立即获取VR姿态 - 修复ATW闪烁的关键方法(双姿态版本)""" + # 开始计时waitGetPoses操作 + timing = self._start_timing('wait_poses') - def _update_vr_rendering(self, task): - """更新VR渲染(真实VR模式)""" try: - if not self.vr_compositor or self.simulation_mode: - return task.cont - - # 提交帧到合成器 - self._submit_frames_to_compositor() - - return task.cont - - except Exception as e: - print(f"VR渲染更新错误: {str(e)}") - return task.cont + if not self.vr_compositor or not self.poses or not self.game_poses: + self._end_timing(timing) + return + + # 关键修复:传递渲染姿态和游戏姿态数组 + # 渲染姿态用于绘制,游戏姿态用于逻辑,避免ATW过度补偿 + result = self.vr_compositor.waitGetPoses(self.poses, self.game_poses) + + # 📌 waitGetPoses 成功后立即递增 OpenVR 帧ID(新帧开始) + self.openvr_frame_id += 1 + + # 结束计时 + wait_time = self._end_timing(timing) + + # 检查姿态数据的有效性 + valid_poses = 0 + + # 更新HMD姿态(设备0通常是头显) + if len(self.poses) > 0 and self.poses[0].bPoseIsValid: + valid_poses += 1 + else: + # 如果HMD姿态无效,不要频繁输出错误信息 + if not hasattr(self, '_hmd_invalid_warning_shown'): + print("⚠️ HMD姿态数据无效(立即模式)") + self._hmd_invalid_warning_shown = True + + # 🚀 优化控制器姿态更新:使用缓存,避免每帧clear()和重新创建对象 + for device_id in range(1, min(len(self.poses), openvr.k_unMaxTrackedDeviceCount)): + if self.poses[device_id].bPoseIsValid: + device_class = self.vr_system.getTrackedDeviceClass(device_id) + if device_class == openvr.TrackedDeviceClass_Controller: + controller_matrix = self.poses[device_id].mDeviceToAbsoluteTracking + + # 检查是否已有此设备的缓存矩阵 + if device_id not in self.controller_poses: + # 第一次检测到该控制器,创建新的缓存矩阵 + self.controller_poses[device_id] = self._convert_openvr_matrix_to_panda(controller_matrix) + else: + # 复用现有矩阵,只更新数值,避免创建新对象 + cached_mat = self.controller_poses[device_id] + self._update_matrix_from_openvr(cached_mat, controller_matrix) + + valid_poses += 1 + else: + # 设备姿态无效,从字典中移除(如果存在) + if device_id in self.controller_poses: + # 将矩阵返回对象池 + self._return_pooled_matrix(self.controller_poses[device_id]) + del self.controller_poses[device_id] + + # 🔧 关键修复:立即更新手柄和跟踪设备 + # Running Start模式下必须在WaitGetPoses后立即更新,避免手柄消失 + self.update_tracked_devices() + + # 调试信息 - 仅在第一次成功时输出 + if not hasattr(self, '_dual_pose_mode_logged') and valid_poses > 0: + print(f"✅ 双姿态立即获取模式启用 - 有效姿态数: {valid_poses}") + print(" 渲染姿态用于绘制,游戏姿态用于逻辑,防止ATW过度补偿") + print(" 手柄和跟踪设备在WaitGetPoses后立即更新") + self._dual_pose_mode_logged = True + + except Exception as e: + # 限制错误输出频率 + if not hasattr(self, '_last_immediate_error_frame'): + self._last_immediate_error_frame = 0 + + if self.frame_count - self._last_immediate_error_frame > 300: # 每5秒最多输出一次错误 + print(f"立即姿态获取失败: {e}") + self._last_immediate_error_frame = self.frame_count + + self.pose_failures += 1 + + def _wait_get_poses_with_prediction(self): + """使用预测时间获取VR姿态 - 优化性能的新策略""" + try: + if not self.vr_compositor or not self.poses or not self.game_poses: + return + + # 使用预测时间获取姿态 + # 预测时间通常为11-16ms,对应下一个VR帧的时间 + result = self.vr_compositor.waitGetPoses(self.poses, self.game_poses) + + # 检查姿态数据的有效性 + valid_poses = 0 + + # 更新HMD姿态(设备0通常是头显) + if len(self.poses) > 0 and self.poses[0].bPoseIsValid: + valid_poses += 1 + else: + # 如果HMD姿态无效,不要频繁输出错误信息 + if not hasattr(self, '_hmd_invalid_warning_task'): + print("⚠️ HMD姿态数据无效(更新任务模式)") + self._hmd_invalid_warning_task = True + + # 更新控制器姿态 + self.controller_poses.clear() + for device_id in range(1, min(len(self.poses), openvr.k_unMaxTrackedDeviceCount)): + if self.poses[device_id].bPoseIsValid: + device_class = self.vr_system.getTrackedDeviceClass(device_id) + if device_class == openvr.TrackedDeviceClass_Controller: + controller_matrix = self.poses[device_id].mDeviceToAbsoluteTracking + self.controller_poses[device_id] = self._convert_openvr_matrix_to_panda(controller_matrix) + valid_poses += 1 + + # 调试信息 - 仅在第一次成功时输出 + if not hasattr(self, '_update_task_mode_logged') and valid_poses > 0: + print(f"✅ 更新任务姿态获取模式启用 - 有效姿态数: {valid_poses}") + print(f" 预测时间: {self.use_prediction_time*1000:.1f}ms") + self._update_task_mode_logged = True + + except Exception as e: + # 限制错误输出频率 + if not hasattr(self, '_last_task_error_frame'): + self._last_task_error_frame = 0 + + if self.frame_count - self._last_task_error_frame > 300: # 每5秒最多输出一次错误 + print(f"更新任务姿态获取失败: {e}") + self._last_task_error_frame = self.frame_count + + # 记录姿态失败次数 + self.pose_failures += 1 + + def _cache_poses_for_next_frame(self): + """缓存当前姿态供下一帧渲染使用 - 修复时序不匹配""" + try: + if not self.poses or len(self.poses) == 0: + return + + # 如果是第一帧,直接使用当前姿态 + if self._first_frame: + self._cached_render_poses = self.poses + self._first_frame = False + print("✓ 首帧姿态缓存已设置") + return + + # 复制当前渲染姿态到缓存 + # 下一帧将使用这些姿态进行渲染 + poses_t = openvr.TrackedDevicePose_t * openvr.k_unMaxTrackedDeviceCount + cached_poses = poses_t() + + # 复制姿态数据 + for i in range(len(self.poses)): + cached_poses[i] = self.poses[i] + + self._cached_render_poses = cached_poses + + except Exception as e: + print(f"⚠️ 姿态缓存失败: {e}") + + + def _reset_waitgetposes_flag(self, task): + """重置WaitGetPoses标记 - 确保下一帧可以调用WaitGetPoses""" + self._waitgetposes_called_this_frame = False + return task.done + + def _update_tracking_data(self): + """更新VR追踪数据""" + try: + # 获取设备姿态 + poses = self.vr_system.getDeviceToAbsoluteTrackingPose( + openvr.TrackingUniverseStanding, 0.0, openvr.k_unMaxTrackedDeviceCount + ) + + # 更新HMD姿态(设备0通常是头显) + if poses[0].bPoseIsValid: + hmd_matrix = poses[0].mDeviceToAbsoluteTracking + self.hmd_pose = self._convert_openvr_matrix_to_panda(hmd_matrix) + + # 更新控制器姿态 + for device_id in range(1, openvr.k_unMaxTrackedDeviceCount): + if poses[device_id].bPoseIsValid: + device_class = self.vr_system.getTrackedDeviceClass(device_id) + if device_class == openvr.TrackedDeviceClass_Controller: + controller_matrix = poses[device_id].mDeviceToAbsoluteTracking + self.controller_poses[device_id] = self._convert_openvr_matrix_to_panda(controller_matrix) + + except Exception as e: + print(f"更新追踪数据失败: {e}") + + def _convert_openvr_matrix_to_panda(self, ovr_matrix): + """将OpenVR矩阵转换为Panda3D矩阵 - 使用对象池优化 + + 坐标系转换: + OpenVR: X右, Y上, -Z前(右手坐标系) + Panda3D: X右, Y前, Z上(右手坐标系) + + 转换规则: + OpenVR X → Panda3D X + OpenVR Y → Panda3D Z + OpenVR -Z → Panda3D Y + """ + # 🚀 使用对象池获取预分配的Mat4对象,避免每帧创建新对象 + mat = self._get_pooled_matrix() + + # 修正的坐标转换矩阵 + # OpenVR: X右, Y上, -Z前 → Panda3D: X右, Y前, Z上 + # 转换规则: (ovr_x, ovr_y, ovr_z) → (panda_x, panda_y, panda_z) + # (ovr_x, ovr_y, ovr_z) → (ovr_x, -ovr_z, ovr_y) + + # X轴行:Panda3D的X轴对应OpenVR的X轴 + mat.setCell(0, 0, ovr_matrix[0][0]) # X_x → X_x + mat.setCell(0, 1, ovr_matrix[0][1]) # X_y → X_y + mat.setCell(0, 2, ovr_matrix[0][2]) # X_z → X_z + mat.setCell(0, 3, ovr_matrix[0][3]) # 位移X分量 + + # Y轴行:Panda3D的Y轴对应OpenVR的-Z轴 + mat.setCell(1, 0, -ovr_matrix[2][0]) # -Z_x → Y_x + mat.setCell(1, 1, -ovr_matrix[2][1]) # -Z_y → Y_y + mat.setCell(1, 2, -ovr_matrix[2][2]) # -Z_z → Y_z + mat.setCell(1, 3, -ovr_matrix[2][3]) # 位移Y分量(-Z位移) + + # Z轴行:Panda3D的Z轴对应OpenVR的Y轴 + mat.setCell(2, 0, ovr_matrix[1][0]) # Y_x → Z_x + mat.setCell(2, 1, ovr_matrix[1][1]) # Y_y → Z_y + mat.setCell(2, 2, ovr_matrix[1][2]) # Y_z → Z_z + mat.setCell(2, 3, ovr_matrix[1][3]) # 位移Z分量(Y位移) + + # 齐次坐标 + mat.setCell(3, 0, 0) + mat.setCell(3, 1, 0) + mat.setCell(3, 2, 0) + mat.setCell(3, 3, 1) + + # 🚀 优化调试信息 - 避免创建Vec3对象,减少GC压力 + if not hasattr(self, '_coord_debug_counter'): + self._coord_debug_counter = 0 + self._coord_debug_counter += 1 + + if self._coord_debug_counter % 600 == 1: # 每10秒输出一次@60fps + print(f"🔄 坐标系转换调试 (第{self._coord_debug_counter}帧)") + + # 直接输出数值,避免创建Vec3对象 + ovr_x, ovr_y, ovr_z = ovr_matrix[0][3], ovr_matrix[1][3], ovr_matrix[2][3] + print(f" OpenVR原始位置: ({ovr_x:.3f}, {ovr_y:.3f}, {ovr_z:.3f})") + + # 直接从矩阵读取数值,避免创建Vec3对象 + panda_x = mat.getCell(0, 3) + panda_y = mat.getCell(1, 3) + panda_z = mat.getCell(2, 3) + print(f" Panda3D转换位置: ({panda_x:.3f}, {panda_y:.3f}, {panda_z:.3f})") + + # 手动验证转换:OpenVR (x,y,z) → Panda3D (x,-z,y) + expected_x = ovr_x + expected_y = -ovr_z + expected_z = ovr_y + print(f" 预期转换结果: ({expected_x:.3f}, {expected_y:.3f}, {expected_z:.3f})") + + # 计算误差,避免创建Vec3对象 + diff_x = panda_x - expected_x + diff_y = panda_y - expected_y + diff_z = panda_z - expected_z + diff_magnitude = (diff_x*diff_x + diff_y*diff_y + diff_z*diff_z)**0.5 + + if diff_magnitude < 0.001: + print(f" ✅ 坐标转换正确 (误差: {diff_magnitude:.6f})") + else: + print(f" ⚠️ 坐标转换可能有误 (误差: {diff_magnitude:.6f})") + print(f" 差异: ({diff_x:.6f}, {diff_y:.6f}, {diff_z:.6f})") - def _convert_openvr_matrix(self, openvr_matrix): - """转换OpenVR矩阵为Panda3D矩阵""" - # 实现矩阵转换逻辑 - mat = Mat4() - # 这里需要实现具体的矩阵转换 return mat - def _update_main_camera_pose(self): - """更新主摄像机姿态""" + def update_hmd(self, pose): + """ + 更新HMD锚点 - 基于参考实现 + """ try: - if self.simulation_mode: - return - - # 从VR系统获取头显姿态并应用到主摄像机 - pass - - except Exception as e: - print(f"主摄像机姿态更新错误: {str(e)}") + # 将OpenVR姿态转换为Panda3D矩阵 + modelview = self.convert_mat(pose.mDeviceToAbsoluteTracking) - def _update_eye_cameras(self): - """更新眼部摄像机""" - try: - if self.simulation_mode: - return - - # 更新左右眼摄像机位置 - pass - - except Exception as e: - print(f"眼部摄像机更新错误: {str(e)}") + # 应用坐标系转换并设置HMD锚点 + self.hmd_anchor.setMat(self.coord_mat_inv * modelview * self.coord_mat) - def _update_controller_poses(self, poses): - """更新控制器姿态""" - try: - if self.simulation_mode: - return - - # 更新控制器位置和姿态 - pass - - except Exception as e: - print(f"控制器姿态更新错误: {str(e)}") + # 获取眼睛到头部的变换 + view_left = self.convert_mat(self.vr_system.getEyeToHeadTransform(openvr.Eye_Left)) + view_right = self.convert_mat(self.vr_system.getEyeToHeadTransform(openvr.Eye_Right)) - def _submit_frames_to_compositor(self): - """提交帧到合成器""" - try: - if not self.vr_compositor or self.simulation_mode: - return - - import openvr - - # 提交左眼纹理 - left_eye_texture = openvr.Texture_t() - left_eye_texture.handle = self.left_eye_texture.getTextureId() - left_eye_texture.eType = openvr.TextureType_OpenGL - left_eye_texture.eColorSpace = openvr.ColorSpace_Gamma - self.vr_compositor.submit(openvr.Eye_Left, left_eye_texture) - - # 提交右眼纹理 - right_eye_texture = openvr.Texture_t() - right_eye_texture.handle = self.right_eye_texture.getTextureId() - right_eye_texture.eType = openvr.TextureType_OpenGL - right_eye_texture.eColorSpace = openvr.ColorSpace_Gamma - self.vr_compositor.submit(openvr.Eye_Right, right_eye_texture) - - except Exception as e: - print(f"帧提交错误: {str(e)}") + # 设置眼睛锚点 + self.left_eye_anchor.setMat(self.coord_mat_inv * view_left * self.coord_mat) + self.right_eye_anchor.setMat(self.coord_mat_inv * view_right * self.coord_mat) - def get_controller_input(self, controller_id): - """获取控制器输入""" - try: - if self.simulation_mode: - # 返回模拟的控制器输入 - return { - 'trigger': 0.0, - 'grip': 0.0, - 'touchpad': {'x': 0.0, 'y': 0.0, 'pressed': False}, - 'menu': False, - 'connected': controller_id in self.simulation_data['controller_poses'] - } - - if not self.vr_system: - return None - - import openvr - - # 获取控制器状态 - result, controller_state = self.vr_system.getControllerState(controller_id) - if not result: - return None - - # 解析控制器输入 - return { - 'trigger': controller_state.rAxis[1].x, - 'grip': controller_state.rAxis[2].x, - 'touchpad': { - 'x': controller_state.rAxis[0].x, - 'y': controller_state.rAxis[0].y, - 'pressed': controller_state.rAxis[0].x != 0 or controller_state.rAxis[0].y != 0 - }, - 'menu': controller_state.ulButtonPressed & (1 << openvr.k_EButton_ApplicationMenu) != 0, - 'connected': True - } - except Exception as e: - print(f"控制器输入获取错误: {str(e)}") + print(f"更新HMD姿态失败: {e}") + + def _update_camera_poses(self): + """更新相机姿态 - 使用锚点系统简化处理""" + try: + # 使用锚点系统后,相机位置自动跟随锚点 + # 只需要获取HMD姿态并更新锚点即可 + + # 从poses数组中获取HMD姿态 + if hasattr(self, 'poses') and len(self.poses) > 0: + hmd_pose = self.poses[openvr.k_unTrackedDeviceIndex_Hmd] + if hmd_pose.bPoseIsValid: + self.update_hmd(hmd_pose) + else: + print("⚠️ HMD姿态数据无效") + + except Exception as e: + print(f"更新相机姿态失败: {e}") + import traceback + traceback.print_exc() + + def _update_camera_poses_with_cache(self): + """使用缓存姿态更新相机 - 符合OpenVR时序假设""" + try: + # 使用缓存的姿态,符合OpenVR的时序假设 + # OpenVR假设你用上一次WaitGetPoses的姿态渲染当前提交的帧 + + if not self._cached_render_poses: + # 如果没有缓存姿态,回退到当前姿态(首帧情况) + print("⚠️ 没有缓存姿态,使用当前姿态") + return self._update_camera_poses() + + # 从缓存姿态数组中获取HMD姿态 + if len(self._cached_render_poses) > 0: + hmd_pose = self._cached_render_poses[openvr.k_unTrackedDeviceIndex_Hmd] + if hmd_pose.bPoseIsValid: + self.update_hmd(hmd_pose) + + # 调试信息 - 验证缓存姿态使用 + if not hasattr(self, '_cached_pose_logged'): + print("✅ 使用缓存姿态更新相机 - 符合OpenVR时序假设") + self._cached_pose_logged = True + else: + print("⚠️ 缓存的HMD姿态数据无效") + + except Exception as e: + print(f"使用缓存姿态更新相机失败: {e}") + # 回退到正常更新方式 + self._update_camera_poses() + import traceback + traceback.print_exc() + + + def enable_vr(self): + """启用VR模式""" + if not self.is_vr_available(): + print("❌ VR系统不可用") + return False + + if not self.vr_initialized: + if not self.initialize_vr(): + return False + + self.vr_enabled = True + + # 禁用主相机避免干扰VR渲染 + self._disable_main_cam() + + # VR性能优化:使用Running Start模式(Valve最佳实践) + print("🚀 VR性能优化:Running Start模式已启用") + print(" 优势:在帧开始时获取姿态,提供VSync前3ms的渲染时间") + print(" 注意:Submit后立即调用WaitGetPoses是错误实现") + self.set_prediction_time(11.0) # 11ms预测时间 - OpenVR标准值,平衡准确性和延迟 + + # 🚀 动态调整Qt Timer频率以支持VR + if hasattr(self.world, 'qtWidget') and self.world.qtWidget: + if hasattr(self.world.qtWidget, 'synchronizer'): + # 设置为144Hz,让OpenVR控制实际渲染节奏 + self.world.qtWidget.synchronizer.setInterval(int(1000/144)) + print("✓ Qt Timer调整为144Hz,让OpenVR控制VR渲染节奏") + + print("✅ VR模式已启用") + return True + + def disable_vr(self): + """禁用VR模式""" + self.vr_enabled = False + + # 清理手柄可视化(但保留控制器对象以便重新启用) + if hasattr(self, 'left_controller') and self.left_controller: + if hasattr(self.left_controller, 'visualizer') and self.left_controller.visualizer: + try: + self.left_controller.visualizer.cleanup() + self.left_controller.visualizer = None + print("✓ 左手控制器可视化已清理") + except Exception as e: + print(f"⚠️ 清理左手控制器可视化失败: {e}") + + if hasattr(self, 'right_controller') and self.right_controller: + if hasattr(self.right_controller, 'visualizer') and self.right_controller.visualizer: + try: + self.right_controller.visualizer.cleanup() + self.right_controller.visualizer = None + print("✓ 右手控制器可视化已清理") + except Exception as e: + print(f"⚠️ 清理右手控制器可视化失败: {e}") + + # 隐藏手柄锚点节点 + if hasattr(self, 'left_controller') and self.left_controller and self.left_controller.anchor_node: + self.left_controller.anchor_node.hide() + + if hasattr(self, 'right_controller') and self.right_controller and self.right_controller.anchor_node: + self.right_controller.anchor_node.hide() + + # 恢复主相机 + self._enable_main_cam() + + # 恢复Qt Timer到60FPS + if hasattr(self.world, 'qtWidget') and self.world.qtWidget: + if hasattr(self.world.qtWidget, 'synchronizer'): + self.world.qtWidget.synchronizer.setInterval(int(1000/60)) + print("✓ Qt Timer恢复为60Hz") + + print("✅ VR模式已禁用,手柄模型已隐藏") + + def cleanup(self): + """清理VR资源""" + try: + print("🔄 正在清理VR资源...") + + # 停止VR任务 + if self.vr_task: + self.world.taskMgr.remove(self.vr_task) + self.vr_task = None + + # 🚀 恢复Python垃圾回收并清理对象池 + if self._gc_disabled: + # 最后一次手动垃圾回收 + collected = gc.collect() + print(f"🗑️ 最终GC清理: {collected} 个对象") + + # 恢复自动垃圾回收 + gc.enable() + self._gc_disabled = False + print("✅ Python垃圾回收已恢复为自动模式") + + # 清理对象池 + if hasattr(self, '_matrix_pool'): + pool_size = len(self._matrix_pool) + self._matrix_pool.clear() + print(f"🧹 Mat4对象池已清理: {pool_size} 个对象") + + # 清理缓存 + if hasattr(self, '_cached_matrices'): + self._cached_matrices.clear() + if hasattr(self, '_controller_poses_cache'): + self._controller_poses_cache.clear() + + # 清理OpenVR Texture对象缓存 + if hasattr(self, '_left_ovr_texture'): + self._left_ovr_texture = None + if hasattr(self, '_right_ovr_texture'): + self._right_ovr_texture = None + print("🧹 OpenVR Texture对象缓存已清理") + + # 清理渲染缓冲区 + if self.vr_left_eye_buffer: + self.vr_left_eye_buffer.removeAllDisplayRegions() + self.world.graphicsEngine.removeWindow(self.vr_left_eye_buffer) + self.vr_left_eye_buffer = None + + if self.vr_right_eye_buffer: + self.vr_right_eye_buffer.removeAllDisplayRegions() + self.world.graphicsEngine.removeWindow(self.vr_right_eye_buffer) + self.vr_right_eye_buffer = None + + # 清理相机 + if self.vr_left_camera: + self.vr_left_camera.removeNode() + self.vr_left_camera = None + + if self.vr_right_camera: + self.vr_right_camera.removeNode() + self.vr_right_camera = None + + # 清理控制器 + if hasattr(self, 'left_controller') and self.left_controller: + try: + self.left_controller.cleanup() + self.left_controller = None + print("✓ 左手控制器已清理") + except Exception as e: + print(f"⚠️ 清理左手控制器失败: {e}") + + if hasattr(self, 'right_controller') and self.right_controller: + try: + self.right_controller.cleanup() + self.right_controller = None + print("✓ 右手控制器已清理") + except Exception as e: + print(f"⚠️ 清理右手控制器失败: {e}") + + # 清理控制器字典 + if hasattr(self, 'controllers'): + self.controllers.clear() + print("✓ 控制器字典已清理") + + # 清理VR子系统 + if hasattr(self, 'joystick_manager') and self.joystick_manager: + try: + self.joystick_manager.cleanup() + self.joystick_manager = None + print("✓ VR摇杆系统已清理") + except Exception as e: + print(f"⚠️ 清理VR摇杆系统失败: {e}") + + if hasattr(self, 'teleport_system') and self.teleport_system: + try: + self.teleport_system.cleanup() + self.teleport_system = None + print("✓ VR传送系统已清理") + except Exception as e: + print(f"⚠️ 清理VR传送系统失败: {e}") + + if hasattr(self, 'interaction_manager') and self.interaction_manager: + try: + self.interaction_manager.cleanup() + self.interaction_manager = None + print("✓ VR交互系统已清理") + except Exception as e: + print(f"⚠️ 清理VR交互系统失败: {e}") + + if hasattr(self, 'action_manager') and self.action_manager: + try: + self.action_manager.cleanup() + self.action_manager = None + print("✓ VR动作系统已清理") + except Exception as e: + print(f"⚠️ 清理VR动作系统失败: {e}") + + # 关闭OpenVR + if self.vr_system and OPENVR_AVAILABLE: + try: + openvr.shutdown() + except: + pass + self.vr_system = None + + self.vr_enabled = False + self.vr_initialized = False + + print("✅ VR资源清理完成") + + except Exception as e: + print(f"⚠️ VR清理过程中出错: {e}") + + def get_vr_status(self): + """获取VR状态信息""" + return { + 'available': self.is_vr_available(), + 'initialized': self.vr_initialized, + 'enabled': self.vr_enabled, + 'eye_resolution': (self.eye_width, self.eye_height), + 'device_count': len(self.controller_poses) + (1 if self.vr_enabled else 0), + 'vr_fps': self.vr_fps, + 'frame_count': self.frame_count, + 'submit_failures': self.submit_failures, + 'pose_failures': self.pose_failures + } + + def _print_performance_report(self): + """输出VR性能报告""" + if not self.performance_monitoring or not self.debug_output_enabled: + return + + stats = self.get_performance_stats() + + # 简短模式输出 + if self.debug_mode == 'brief': + self._print_brief_performance_report(stats) + return + + print("📊 ======= VR性能监控报告 =======") + + # 帧率和帧时间信息 + print(f"🎯 渲染性能:") + print(f" VR帧率: {stats['vr_fps']:.1f} FPS") + print(f" 平均帧时间: {stats['frame_time_avg']:.2f} ms") + print(f" 最小帧时间: {stats['frame_time_min']:.2f} ms") + print(f" 最大帧时间: {stats['frame_time_max']:.2f} ms") + print(f" 95%帧时间: {stats['frame_time_95th']:.2f} ms") + + # 系统性能 + print(f"💻 系统性能:") + print(f" CPU使用率: {stats['cpu_usage']:.1f}%") + print(f" 内存使用率: {stats['memory_usage']:.1f}%") + + # GPU性能 + print(f"🎮 GPU性能:") + if self.gputil_available or self.nvidia_ml_available: + print(f" GPU使用率: {stats['gpu_usage']:.1f}%") + print(f" 显存使用率: {stats['gpu_memory_usage']:.1f}%") + else: + print(f" GPU监控: 不可用 (需要安装 GPUtil 或 pynvml)") + print(f" 安装命令: pip install GPUtil nvidia-ml-py") + + # GPU渲染时间(OpenVR Frame Timing) + if self.enable_gpu_timing: + print(f"⚡ GPU渲染时间:") + pipeline_stats = self._get_pipeline_stats() + gpu_stats = pipeline_stats.get('gpu_timing', {}) + gpu_current = pipeline_stats.get('current', {}) + + # 检查是否有可用的GPU时间数据 + has_gpu_data = any( + gpu_current.get(field, 0) > 0 + for field in ['gpu_scene_render', 'gpu_total_render', 'gpu_pre_submit', 'gpu_post_submit', 'gpu_compositor_render'] + ) + + if has_gpu_data: + # 显示GPU时间统计(最近30帧平均) + scene_render = gpu_stats.get('scene_render', {'avg': 0}) + total_render = gpu_stats.get('total_render', {'avg': 0}) + pre_submit = gpu_stats.get('pre_submit', {'avg': 0}) + post_submit = gpu_stats.get('post_submit', {'avg': 0}) + compositor = gpu_stats.get('compositor_render', {'avg': 0}) + + if scene_render['avg'] > 0: + print(f" GPU场景渲染: {scene_render['avg']:.2f}ms (min:{scene_render['min']:.1f}, max:{scene_render['max']:.1f})") + if total_render['avg'] > 0: + print(f" GPU总渲染时间: {total_render['avg']:.2f}ms (min:{total_render['min']:.1f}, max:{total_render['max']:.1f})") + if pre_submit['avg'] > 0: + print(f" GPU提交前时间: {pre_submit['avg']:.2f}ms (min:{pre_submit['min']:.1f}, max:{pre_submit['max']:.1f})") + if post_submit['avg'] > 0: + print(f" GPU提交后时间: {post_submit['avg']:.2f}ms (min:{post_submit['min']:.1f}, max:{post_submit['max']:.1f})") + if compositor['avg'] > 0: + print(f" GPU合成器时间: {compositor['avg']:.2f}ms (min:{compositor['min']:.1f}, max:{compositor['max']:.1f})") + + # 显示当前帧GPU时间 + print(f"🔍 当前帧GPU时间:") + if gpu_current.get('gpu_scene_render', 0) > 0: + print(f" 场景渲染: {gpu_current['gpu_scene_render']:.2f}ms") + if gpu_current.get('gpu_total_render', 0) > 0: + print(f" 总渲染: {gpu_current['gpu_total_render']:.2f}ms") + if gpu_current.get('gpu_compositor_render', 0) > 0: + print(f" 合成器: {gpu_current['gpu_compositor_render']:.2f}ms") + + # GPU时间瓶颈分析 + current_total = gpu_current.get('gpu_total_render', 0) + current_scene = gpu_current.get('gpu_scene_render', 0) + if current_total > 12.0: # 假设72fps目标,留出一些余量 + print(f" ⚠️ GPU总渲染时间过长: {current_total:.1f}ms") + elif current_scene > 8.0: + print(f" ⚠️ GPU场景渲染时间偏高: {current_scene:.1f}ms") + + else: + print(f" GPU渲染时间: 暂无数据") + if self.gpu_timing_failure_count > 0: + print(f" 获取失败次数: {self.gpu_timing_failure_count}") + else: + print(f" 等待OpenVR Frame Timing数据...") + else: + print(f"⚡ GPU渲染时间: 已禁用") + + # VR特定指标 + print(f"🥽 VR指标:") + print(f" 总帧数: {stats['frame_count']}") + print(f" 提交失败: {stats['submit_failures']}") + print(f" 姿态失败: {stats['pose_failures']}") + + # 计算失败率 + if stats['frame_count'] > 0: + submit_fail_rate = (stats['submit_failures'] / stats['frame_count']) * 100 + pose_fail_rate = (stats['pose_failures'] / stats['frame_count']) * 100 + print(f" 提交失败率: {submit_fail_rate:.2f}%") + print(f" 姿态失败率: {pose_fail_rate:.2f}%") + + # 渲染管线监控 + if self.enable_pipeline_monitoring: + pipeline_stats = self._get_pipeline_stats() + print(f"⚙️ 渲染管线分析:") + print(f" 当前分辨率: {pipeline_stats['vr_info']['eye_resolution'][0]}x{pipeline_stats['vr_info']['eye_resolution'][1]}") + print(f" 推荐分辨率: {pipeline_stats['vr_info']['recommended_resolution'][0]}x{pipeline_stats['vr_info']['recommended_resolution'][1]}") + print(f" 显示频率: {pipeline_stats['vr_info']['display_frequency']:.1f} Hz") + + # VSync和时序信息 + if pipeline_stats['vr_info']['target_frame_time_ms'] > 0: + print(f"🎯 VSync时序:") + print(f" 目标帧时间: {pipeline_stats['vr_info']['target_frame_time_ms']:.2f}ms") + print(f" VSync到光子: {pipeline_stats['vr_info']['vsync_to_photons_ms']:.2f}ms") + print(f" VSync窗口: ±{pipeline_stats['vr_info']['vsync_window_ms']:.2f}ms") + print(f" 异步重投影: {'启用' if pipeline_stats['vr_info']['async_reprojection'] else '禁用'}") + print(f" 运动平滑: {'启用' if pipeline_stats['vr_info']['motion_smoothing'] else '禁用'}") + + # 分析帧时间是否在目标范围内 + current_frame_time = stats['frame_time_avg'] + target_frame_time = pipeline_stats['vr_info']['target_frame_time_ms'] + if current_frame_time > 0 and target_frame_time > 0: + frame_time_ratio = current_frame_time / target_frame_time + if frame_time_ratio > 1.1: + print(f" ⚠️ 帧时间超标: {current_frame_time:.1f}ms (目标:{target_frame_time:.1f}ms)") + elif frame_time_ratio < 0.9: + print(f" ✅ 帧时间充裕: {current_frame_time:.1f}ms (目标:{target_frame_time:.1f}ms)") + else: + print(f" ✓ 帧时间正常: {current_frame_time:.1f}ms (目标:{target_frame_time:.1f}ms)") + + # 🔧 性能优化诊断 + print(f"🔧 优化诊断:") + current = pipeline_stats.get('current', {}) if self.enable_pipeline_monitoring else {} + + # 🔍 渲染回调诊断 - 新增 + self._print_render_callback_diagnostics() + + # waitGetPoses时序分析 + wait_poses_time = current.get('wait_poses', 0) + if wait_poses_time > 10: + print(f" ⚠️ waitGetPoses时间过长: {wait_poses_time:.1f}ms") + print(f" 可能原因: 错过VSync窗口,建议调整预测时间") + elif wait_poses_time > 5: + print(f" ⚠️ waitGetPoses时间偏高: {wait_poses_time:.1f}ms") + else: + print(f" ✓ waitGetPoses时间正常: {wait_poses_time:.1f}ms") + + # CPU-GPU并行度分析 - 增强诊断 + gpu_total = current.get('gpu_total_render', 0) + cpu_render_left = getattr(self, 'left_render_time', 0) + cpu_render_right = getattr(self, 'right_render_time', 0) + cpu_render_total = cpu_render_left + cpu_render_right + + print(f" 真实渲染时间对比:") + print(f" CPU左眼: {cpu_render_left:.2f}ms") + print(f" CPU右眼: {cpu_render_right:.2f}ms") + print(f" CPU总计: {cpu_render_total:.2f}ms") + print(f" GPU总计: {gpu_total:.2f}ms") + + if cpu_render_total < 1.0: + print(f" ⚠️ CPU渲染时间异常短: {cpu_render_total:.2f}ms") + print(f" 可能原因: 渲染回调未正确执行或渲染被跳过") + elif gpu_total > 0 and cpu_render_total > 0: + ratio = gpu_total / cpu_render_total + if ratio > 10: + print(f" ⚠️ GPU严重等待CPU: 比例{ratio:.1f}:1") + print(f" 建议: 检查OpenGL命令提交时机和渲染状态") + elif ratio > 3: + print(f" ⚠️ GPU等待CPU: 比例{ratio:.1f}:1") + print(f" 建议: 检查OpenGL命令提交是否及时") + else: + print(f" ✓ CPU-GPU时间匹配: 比例{ratio:.1f}:1") + + # 预测时间诊断 + current_prediction = self.use_prediction_time * 1000 + print(f" 预测时间设置: {current_prediction:.1f}ms") + if current_prediction > 15: + print(f" 建议: 预测时间较高,可能增加waitGetPoses延迟") + elif current_prediction < 8: + print(f" 注意: 预测时间较低,可能影响姿态准确性") + else: + print(f" ✓ 预测时间在合理范围内") + + # 优化状态总结 + optimization_score = 0 + if wait_poses_time < 8: + optimization_score += 1 + if stats['vr_fps'] > 60: + optimization_score += 1 + if stats.get('frame_time_avg', 0) < target_frame_time * 1.1: + optimization_score += 1 + + if optimization_score >= 2: + print(f" ✅ 优化效果良好 ({optimization_score}/3)") + else: + print(f" ⚠️ 仍有优化空间 ({optimization_score}/3)") + + print(f"🕐 各阶段耗时 (最近{self.pipeline_history_size}帧平均):") + print(f" waitGetPoses: {pipeline_stats['wait_poses']['avg']:.2f}ms (min:{pipeline_stats['wait_poses']['min']:.1f}, max:{pipeline_stats['wait_poses']['max']:.1f})") + print(f" 渲染总计: {pipeline_stats['render']['avg']:.2f}ms (min:{pipeline_stats['render']['min']:.1f}, max:{pipeline_stats['render']['max']:.1f})") + print(f" 纹理提交: {pipeline_stats['submit']['avg']:.2f}ms (min:{pipeline_stats['submit']['min']:.1f}, max:{pipeline_stats['submit']['max']:.1f})") + if pipeline_stats['sync_wait']['avg'] > 0: + print(f" 同步等待: {pipeline_stats['sync_wait']['avg']:.2f}ms (min:{pipeline_stats['sync_wait']['min']:.1f}, max:{pipeline_stats['sync_wait']['max']:.1f})") + + print(f"🔍 当前帧详情:") + print(f" 左眼渲染: {pipeline_stats['current']['left_render']:.2f}ms") + print(f" 右眼渲染: {pipeline_stats['current']['right_render']:.2f}ms") + print(f" 姿态获取: {pipeline_stats['current']['wait_poses']:.2f}ms") + + # 显示Running Start模式信息 + print(f"🎯 Running Start模式:") + print(f" 模式: Valve Running Start - 帧开始时获取姿态") + print(f" 优势: VSync前3ms获取姿态,提供充足渲染时间") + print(f" 预测时间: {self.use_prediction_time * 1000:.1f}ms") + + # 分析最大瓶颈 + current = pipeline_stats['current'] + bottleneck_analysis = [] + if current['wait_poses'] > 5.0: + bottleneck_analysis.append(f"姿态获取耗时过长({current['wait_poses']:.1f}ms)") + if current['total_render'] > 8.0: + bottleneck_analysis.append(f"渲染耗时过长({current['total_render']:.1f}ms)") + if current['submit'] > 2.0: + bottleneck_analysis.append(f"纹理提交耗时过长({current['submit']:.1f}ms)") + + if bottleneck_analysis: + print(f"🚨 瓶颈分析:") + for analysis in bottleneck_analysis: + print(f" ⚠️ {analysis}") + + # 性能建议 + self._print_performance_recommendations(stats) + + print("===============================") + + def _print_performance_recommendations(self, stats): + """根据性能数据输出优化建议""" + print(f"💡 性能建议:") + + recommendations = [] + + # FPS相关建议 + if stats['vr_fps'] < 60: + recommendations.append(" ⚠️ VR帧率过低,可能影响体验") + + # 帧时间相关建议 + if stats['frame_time_avg'] > 16.7: # 60fps = 16.7ms + recommendations.append(" ⚠️ 平均帧时间过高,建议降低渲染质量") + + if stats['frame_time_max'] > 50: + recommendations.append(" ⚠️ 检测到严重卡顿,检查CPU/GPU负载") + + # CPU建议 + if stats['cpu_usage'] > 80: + recommendations.append(" 🔴 CPU使用率过高,可能存在CPU瓶颈") + elif stats['cpu_usage'] > 60: + recommendations.append(" 🟡 CPU使用率偏高,注意监控") + + # 内存建议 + if stats['memory_usage'] > 85: + recommendations.append(" 🔴 内存使用率过高,可能影响性能") + + # GPU建议 + if self.gputil_available or self.nvidia_ml_available: + if stats['gpu_usage'] > 95: + recommendations.append(" 🔴 GPU使用率接近满载,存在GPU瓶颈") + if stats['gpu_memory_usage'] > 90: + recommendations.append(" 🔴 显存使用率过高,可能需要降低纹理质量") + + # GPU渲染时间建议 + if self.enable_gpu_timing: + if self.gpu_total_render_ms > 12.0: + recommendations.append(" ⚠️ GPU总渲染时间过长,建议优化场景复杂度") + if self.gpu_scene_render_ms > 8.0: + recommendations.append(" ⚠️ GPU场景渲染时间偏高,考虑降低渲染质量") + if self.gpu_compositor_render_ms > 3.0: + recommendations.append(" ⚠️ GPU合成器时间过长,检查VR设置或叠加层") + if self.gpu_timing_failure_count > 100: + recommendations.append(" ⚠️ GPU时间统计频繁失败,可能需要更新OpenVR") + + # 失败率建议 + submit_fail_rate = (stats['submit_failures'] / max(stats['frame_count'], 1)) * 100 + if submit_fail_rate > 1: + recommendations.append(" ⚠️ VR帧提交失败率较高,检查VR系统状态") + + if not recommendations: + recommendations.append(" ✅ 性能表现良好,无明显问题") + + for rec in recommendations: + print(rec) + + def _print_brief_performance_report(self, stats): + """输出简短的VR性能报告""" + # 创建一行简短摘要 + summary = f"🥽 VR性能: {stats['vr_fps']:.1f}fps" + + if stats['frame_time_avg'] > 0: + summary += f" | 帧时间: {stats['frame_time_avg']:.1f}ms" + + if self.psutil_available: + summary += f" | CPU: {stats['cpu_usage']:.0f}%" + summary += f" | 内存: {stats['memory_usage']:.0f}%" + + # 显示GPU信息,如果库不可用则显示提示 + if self.gputil_available or self.nvidia_ml_available: + summary += f" | GPU: {stats['gpu_usage']:.0f}%" + else: + summary += " | GPU: N/A" + + # 添加失败率指示 + if stats['frame_count'] > 0: + submit_fail_rate = (stats['submit_failures'] / stats['frame_count']) * 100 + if submit_fail_rate > 0.1: + summary += f" | 提交失败: {submit_fail_rate:.1f}%" + + # 添加管线关键信息 + if self.enable_pipeline_monitoring: + pipeline_stats = self._get_pipeline_stats() + current = pipeline_stats['current'] + + # 显示关键瓶颈 + if current['wait_poses'] > 5.0: + summary += f" | 姿态: {current['wait_poses']:.1f}ms⚠️" + elif current['wait_poses'] > 0: + summary += f" | 姿态: {current['wait_poses']:.1f}ms" + + if current['total_render'] > 8.0: + summary += f" | 渲染: {current['total_render']:.1f}ms⚠️" + elif current['total_render'] > 0: + summary += f" | 渲染: {current['total_render']:.1f}ms" + + # 添加GPU渲染时间信息 + if self.enable_gpu_timing: + gpu_total = current.get('gpu_total_render', 0) + gpu_scene = current.get('gpu_scene_render', 0) + if gpu_total > 12.0: + summary += f" | GPU: {gpu_total:.1f}ms⚠️" + elif gpu_total > 0: + summary += f" | GPU: {gpu_total:.1f}ms" + elif gpu_scene > 0: + summary += f" | GPU场景: {gpu_scene:.1f}ms" + + # 显示目标帧时间对比 + vr_info = pipeline_stats['vr_info'] + if vr_info['target_frame_time_ms'] > 0: + target = vr_info['target_frame_time_ms'] + current_avg = stats['frame_time_avg'] + if current_avg > target * 1.1: + summary += f" | 目标:{target:.0f}ms⚠️" + else: + summary += f" | 目标:{target:.0f}ms" + + # 性能状态指示 + if stats['vr_fps'] < 72: + summary += " ⚠️" + elif stats['vr_fps'] > 85: + summary += " ✅" + + print(summary) + + # 注意:原来的left_cb和right_cb函数已被删除 + # 它们的功能已集成到_handle_vr_rendering_and_submit方法中 + + # 注意:_safe_submit_texture方法已删除 + # VR纹理提交现在完全由Panda3D的renderFrame()自动处理 + + def submit_texture(self, eye, texture): + """优化的VR纹理提交 - 使用缓存的纹理ID,避免重复prepareNow""" + try: + if not self.vr_compositor: + print("❌ VR compositor不可用") + self.submit_failures += 1 + return + + # 🚀 关键修复:防止同一帧重复提交 + current_frame = getattr(self, 'frame_count', 0) + if not hasattr(self, '_last_submit_frame'): + self._last_submit_frame = {} + + # 检查当前帧是否已经提交过此眼睛的纹理 + if eye in self._last_submit_frame and self._last_submit_frame[eye] == current_frame: + # 静默跳过,避免spam输出 + return + + # 记录当前帧提交 + self._last_submit_frame[eye] = current_frame + + # 🚀 关键优化:直接使用缓存的纹理ID,避免重复prepareNow + if eye == openvr.Eye_Left: + handle = self.left_texture_id + eye_name = "左眼" + elif eye == openvr.Eye_Right: + handle = self.right_texture_id + eye_name = "右眼" + else: + print(f"❌ 未知的眼睛类型: {eye}") + self.submit_failures += 1 + return + + # 检查缓存的纹理ID是否有效 + if not handle or handle <= 0: + print(f"❌ {eye_name}纹理ID缓存无效: {handle}") + print(" 这可能表示纹理准备失败,需要检查_prepare_and_cache_textures()") + self.submit_failures += 1 + return + + # ❌ 移除gsg.flush()调用 - 基于OpenVR官方实践 + # gsg.flush()等同于OpenGL的glFlush(),导致强制CPU-GPU同步 + # 每帧调用2次(左右眼)是GPU周期性峰值的主要原因 + # 参考:OpenVR社区经验表明同步调用是性能杀手 + # + # gsg = self.world.win.getGsg() + # if gsg and hasattr(gsg, 'flush'): + # try: + # gsg.flush() + # except Exception as flush_error: + # if not hasattr(self, '_gsg_flush_error_logged'): + # print(f"⚠️ GSG刷新失败: {flush_error}") + # self._gsg_flush_error_logged = True + + # 🚀 关键优化:使用缓存的OpenVR Texture对象,避免每帧创建 + if eye == openvr.Eye_Left: + ovr_texture = self._left_ovr_texture + else: + ovr_texture = self._right_ovr_texture + + # 检查缓存对象是否存在(向后兼容) + if ovr_texture is None: + # 备用方案:如果缓存对象不存在,创建新的(性能较差) + ovr_texture = openvr.Texture_t() + ovr_texture.eType = openvr.TextureType_OpenGL + ovr_texture.eColorSpace = openvr.ColorSpace_Gamma + if not hasattr(self, '_texture_fallback_warned'): + print("⚠️ 使用Texture对象备用方案(性能次优)") + self._texture_fallback_warned = True + + # 只更新handle,其他属性已预设置 + ovr_texture.handle = handle + + # 提交到VR系统 + error = self.vr_compositor.submit(eye, ovr_texture) + + # 检查错误 + if error and error != openvr.VRCompositorError_None: + print(f"⚠️ VR{eye_name}纹理提交错误: {error}") + self.submit_failures += 1 + else: + # 只在第一次成功时输出 + if not hasattr(self, '_optimized_submit_success_logged'): + print(f"✅ 优化版VR纹理提交成功! 使用缓存ID,避免重复prepareNow") + print(f" {eye_name}纹理ID: {handle}") + self._optimized_submit_success_logged = True + + # 🔧 智能GPU同步策略 - 减少不必要的flush调用 + # 只有在检测到性能问题或每N帧时才强制flush + if eye == openvr.Eye_Right: + self._smart_gpu_sync() + + except Exception as e: + print(f"❌ VR纹理提交异常: {e}") + import traceback + traceback.print_exc() + self.submit_failures += 1 + + def _smart_gpu_sync(self): + """智能GPU同步策略 - 只在必要时进行同步""" + try: + # 初始化同步控制变量 + if not hasattr(self, '_last_gpu_sync_frame'): + self._last_gpu_sync_frame = 0 + self._gpu_sync_interval = 60 # 每60帧强制同步一次(1秒@60FPS) + self._performance_based_sync = True + + current_frame = getattr(self, 'frame_count', 0) + + # 策略1: 性能自适应同步 + if self._performance_based_sync: + # 如果渲染时间过长,增加同步频率 + total_render_time = getattr(self, 'left_render_time', 0) + getattr(self, 'right_render_time', 0) + + if total_render_time > 15.0: # 如果总渲染时间超过15ms + # 高负载时更频繁同步 + sync_interval = 30 # 每30帧同步 + elif total_render_time > 10.0: + sync_interval = 45 # 中等负载 + else: + sync_interval = self._gpu_sync_interval # 正常负载 + + should_sync = (current_frame - self._last_gpu_sync_frame) >= sync_interval + else: + # 策略2: 固定间隔同步 + should_sync = (current_frame - self._last_gpu_sync_frame) >= self._gpu_sync_interval + + # 执行同步 + if should_sync: + gsg = self.world.win.getGsg() + if gsg and hasattr(gsg, 'flush'): + try: + gsg.flush() + self._last_gpu_sync_frame = current_frame + + # 只在首次或Debug时输出 + if not hasattr(self, '_smart_sync_logged') or self.debug_output_enabled: + if not hasattr(self, '_smart_sync_logged'): + print(f"🔧 智能GPU同步已启用 - 间隔:{sync_interval}帧") + self._smart_sync_logged = True + elif self.debug_output_enabled and current_frame % 600 == 1: + print(f"🔧 智能同步触发 (帧#{current_frame}, 间隔:{sync_interval})") + + except Exception as flush_error: + if not hasattr(self, '_smart_sync_error_logged'): + print(f"⚠️ 智能GPU同步失败: {flush_error}") + self._smart_sync_error_logged = True + + except Exception as e: + print(f"⚠️ 智能GPU同步策略异常: {e}") + + def _disable_main_cam(self): + """禁用主相机 - 基于参考实现""" + try: + # 保存原始相机状态 + if not hasattr(self, '_original_camera_parent'): + self._original_camera_parent = self.world.camera.getParent() + + # 创建空节点并将主相机重新附加到它 + self._empty_world = NodePath("empty_world") + self.world.camera.reparentTo(self._empty_world) + + print("✓ 主相机已禁用") + except Exception as e: + print(f"⚠️ 禁用主相机失败: {e}") + + def _enable_main_cam(self): + """恢复主相机 - 基于参考实现""" + try: + # 恢复原始相机状态 + if hasattr(self, '_original_camera_parent') and self._original_camera_parent: + self.world.camera.reparentTo(self._original_camera_parent) + else: + # 如果没有保存的父节点,重新附加到render + self.world.camera.reparentTo(self.world.render) + + # 清理空世界节点 + if hasattr(self, '_empty_world'): + self._empty_world.removeNode() + delattr(self, '_empty_world') + + print("✓ 主相机已恢复") + except Exception as e: + print(f"⚠️ 恢复主相机失败: {e}") + + def _initialize_controllers(self): + """初始化VR手柄控制器""" + try: + print("🎮 正在初始化VR手柄控制器...") + + # 创建左右手柄控制器实例 + self.left_controller = LeftController(self) + self.right_controller = RightController(self) + + # 检测现有连接的控制器 + self._detect_controllers() + + print("✓ VR手柄控制器初始化完成") + + except Exception as e: + print(f"⚠️ VR手柄初始化失败: {e}") + import traceback + traceback.print_exc() + + def _detect_controllers(self): + """检测并连接VR控制器""" + if not self.vr_system: + return + + try: + for device_index in range(openvr.k_unMaxTrackedDeviceCount): + # 检查设备是否已连接 + if not self.vr_system.isTrackedDeviceConnected(device_index): + continue + + # 获取设备类型 + device_class = self.vr_system.getTrackedDeviceClass(device_index) + + if device_class == openvr.TrackedDeviceClass_Controller: + # 获取控制器角色 + role = self.vr_system.getControllerRoleForTrackedDeviceIndex(device_index) + + if role == openvr.TrackedControllerRole_LeftHand and self.left_controller: + self.left_controller.set_device_index(device_index) + self.controllers[device_index] = self.left_controller + # 为设备创建锚点 + self._create_tracked_device_anchor(device_index, 'left_controller') + + elif role == openvr.TrackedControllerRole_RightHand and self.right_controller: + self.right_controller.set_device_index(device_index) + self.controllers[device_index] = self.right_controller + # 为设备创建锚点 + self._create_tracked_device_anchor(device_index, 'right_controller') + + print(f"🎮 检测到 {len(self.controllers)} 个控制器") + + except Exception as e: + print(f"⚠️ 控制器检测失败: {e}") + + def _create_tracked_device_anchor(self, device_index, name): + """为跟踪设备创建锚点节点""" + if not self.tracking_space: + print(f"⚠️ 无法为设备 {device_index} 创建锚点 - tracking_space未初始化") + return + + try: + # 获取设备模型名称 + if self.vr_system: + model_name = self.vr_system.getStringTrackedDeviceProperty( + device_index, + openvr.Prop_RenderModelName_String + ) + anchor_name = f"{device_index}:{model_name}:{name}" + else: + anchor_name = f"{device_index}:{name}" + + # 创建锚点节点 + device_anchor = self.tracking_space.attachNewNode(anchor_name) + self.tracked_device_anchors[device_index] = device_anchor + + print(f"✓ 为设备 {device_index} 创建锚点: {anchor_name}") + + except Exception as e: + print(f"⚠️ 创建设备锚点失败: {e}") + + def update_tracked_devices(self): + """更新所有跟踪设备的姿态 - 基于参考实现""" + if not self.poses or not self.vr_system: + return + + try: + # 更新每个已连接的控制器 + for device_index, controller in self.controllers.items(): + if device_index < len(self.poses): + pose_data = self.poses[device_index] + + # 更新控制器姿态 + controller.update_pose(pose_data) + + # 更新控制器输入状态 + controller.update_input_state(self.vr_system) + + # 更新其他跟踪设备的锚点 + for device_index in range(1, min(len(self.poses), openvr.k_unMaxTrackedDeviceCount)): + if device_index in self.tracked_device_anchors: + pose_data = self.poses[device_index] + if pose_data.bPoseIsValid: + # 转换姿态矩阵 + modelview = self.convert_mat(pose_data.mDeviceToAbsoluteTracking) + final_matrix = self.coord_mat_inv * modelview * self.coord_mat + + # 更新锚点变换 + anchor = self.tracked_device_anchors[device_index] + anchor.setMat(final_matrix) + anchor.show() + else: + # 姿态无效,隐藏锚点 + self.tracked_device_anchors[device_index].hide() + + except Exception as e: + if self.frame_count % 300 == 0: # 每5秒输出一次错误 + print(f"⚠️ 更新跟踪设备失败: {e}") + + def get_controller_by_role(self, role): + """根据角色获取控制器 + + Args: + role: 'left' 或 'right' + + Returns: + VRController实例或None + """ + if role == 'left': + return self.left_controller + elif role == 'right': + return self.right_controller + return None + + def are_controllers_connected(self): + """检查是否有控制器连接""" + return len(self.controllers) > 0 + + def get_connected_controllers(self): + """获取所有连接的控制器列表""" + return list(self.controllers.values()) + + def trigger_controller_haptic(self, role, duration=0.001, strength=1.0): + """触发控制器震动反馈 + + Args: + role: 'left', 'right' 或 'both' + duration: 震动持续时间(秒) + strength: 震动强度 (0.0-1.0) + """ + if role in ['left', 'both'] and self.left_controller: + self.left_controller.trigger_haptic_feedback(duration, strength) + + if role in ['right', 'both'] and self.right_controller: + self.right_controller.trigger_haptic_feedback(duration, strength) + + # VR动作系统便捷方法 + def is_trigger_pressed(self, hand='any'): + """检查扳机是否被按下 + + Args: + hand: 'left', 'right', 'any' + """ + device_path = None + if hand == 'left': + device_path = '/user/hand/left' + elif hand == 'right': + device_path = '/user/hand/right' + + pressed, _ = self.action_manager.is_digital_action_pressed('trigger', device_path) + return pressed + + def is_trigger_just_pressed(self, hand='any'): + """检查扳机是否刚刚被按下""" + device_path = None + if hand == 'left': + device_path = '/user/hand/left' + elif hand == 'right': + device_path = '/user/hand/right' + + pressed, _ = self.action_manager.is_digital_action_just_pressed('trigger', device_path) + return pressed + + def is_grip_pressed(self, hand='any'): + """检查握把是否被按下""" + device_path = None + if hand == 'left': + device_path = '/user/hand/left' + elif hand == 'right': + device_path = '/user/hand/right' + + pressed, _ = self.action_manager.is_digital_action_pressed('grip', device_path) + return pressed + + def is_grip_just_pressed(self, hand='any'): + """检查握把是否刚刚被按下""" + device_path = None + if hand == 'left': + device_path = '/user/hand/left' + elif hand == 'right': + device_path = '/user/hand/right' + + pressed, _ = self.action_manager.is_digital_action_just_pressed('grip', device_path) + return pressed + + def is_menu_pressed(self, hand='any'): + """检查菜单按钮是否被按下""" + device_path = None + if hand == 'left': + device_path = '/user/hand/left' + elif hand == 'right': + device_path = '/user/hand/right' + + pressed, _ = self.action_manager.is_digital_action_pressed('menu', device_path) + return pressed + + def is_trackpad_touched(self, hand='any'): + """检查触摸板是否被触摸""" + device_path = None + if hand == 'left': + device_path = '/user/hand/left' + elif hand == 'right': + device_path = '/user/hand/right' + + touched, _ = self.action_manager.is_digital_action_pressed('trackpad_touch', device_path) + return touched + + def get_trackpad_position(self, hand='any'): + """获取触摸板位置 + + Returns: + Vec2或None: 触摸板位置 (-1到1的范围) + """ + device_path = None + if hand == 'left': + device_path = '/user/hand/left' + elif hand == 'right': + device_path = '/user/hand/right' + + value, _ = self.action_manager.get_analog_action_value('trackpad', device_path) + return value + + # VR交互系统便捷方法 + def get_selected_object(self, hand='any'): + """获取指定手选中的对象 + + Args: + hand: 'left', 'right', 'any' + + Returns: + 选中的对象节点或None + """ + if hand == 'any': + # 返回任意手选中的对象 + for controller in self.get_connected_controllers(): + selected = self.interaction_manager.get_selected_object(controller.name) + if selected: + return selected + return None + else: + return self.interaction_manager.get_selected_object(hand) + + def get_grabbed_object(self, hand='any'): + """获取指定手抓取的对象 + + Args: + hand: 'left', 'right', 'any' + + Returns: + 抓取的对象节点或None + """ + if hand == 'any': + # 返回任意手抓取的对象 + for controller in self.get_connected_controllers(): + grabbed = self.interaction_manager.get_grabbed_object(controller.name) + if grabbed: + return grabbed + return None + else: + return self.interaction_manager.get_grabbed_object(hand) + + def is_grabbing_object(self, hand='any'): + """检查是否正在抓取对象 + + Args: + hand: 'left', 'right', 'any' + + Returns: + bool: 是否正在抓取 + """ + if hand == 'any': + # 检查任意手是否正在抓取 + for controller in self.get_connected_controllers(): + if self.interaction_manager.is_grabbing(controller.name): + return True + return False + else: + return self.interaction_manager.is_grabbing(hand) + + def force_release_all_grabs(self): + """强制释放所有抓取的对象""" + self.interaction_manager.force_release_all() + + def add_interactable_object(self, object_node): + """将对象标记为可交互 + + Args: + object_node: 要标记的对象节点 + """ + self.interaction_manager._add_collision_to_object(object_node) + + def _disable_async_reprojection(self): + """禁用异步重投影 - 备选修复方案""" + try: + # 尝试通过OpenVR设置禁用异步重投影 + if hasattr(openvr, 'VRSettings'): + settings = openvr.VRSettings() + if settings: + # 禁用异步重投影 + error = settings.setBool("steamvr", "enableAsyncReprojection", False) + if error == openvr.VRSettingsError_None: + print("✅ 异步重投影已禁用") + else: + print(f"⚠️ 禁用异步重投影失败: 设置错误 {error}") + else: + print("⚠️ 无法获取VR设置接口") + else: + print("⚠️ OpenVR设置接口不可用") + + except Exception as e: + print(f"⚠️ 禁用异步重投影失败: {e}") + + def enable_async_reprojection_disable(self): + """启用异步重投影禁用选项 - 用户可调用的方法""" + self.disable_async_reprojection = True + print("📝 异步重投影禁用选项已启用,将在下次VR初始化时生效") + + def disable_async_reprojection_disable(self): + """禁用异步重投影禁用选项 - 恢复默认行为""" + self.disable_async_reprojection = False + print("📝 异步重投影禁用选项已关闭,将使用默认ATW行为") + + def _start_timing(self, operation_name): + """开始计时操作 - 性能优化版本""" + if not self.enable_pipeline_monitoring or self.performance_mode_enabled: return None - def shutdown_vr(self): - """关闭VR系统""" - try: - # 停止任务 - if self.simulation_mode: - taskMgr.remove("simulation_tracking") - taskMgr.remove("simulation_rendering") - else: - taskMgr.remove("vr_tracking") - taskMgr.remove("vr_rendering") - - # 关闭OpenVR - if self.vr_system and not self.simulation_mode: - import openvr - openvr.shutdown() - - # 清理资源 - self.left_eye_buffer = None - self.right_eye_buffer = None - self.left_eye_camera = None - self.right_eye_camera = None - - self.vr_enabled = False - self.simulation_mode = False - - print("✓ VR系统已关闭") - - except Exception as e: - print(f"VR关闭错误: {str(e)}") - - def is_vr_enabled(self): - """检查VR是否启用""" - return self.vr_enabled - - def get_vr_info(self): - """获取VR系统信息""" - info = { - 'enabled': self.vr_enabled, - 'simulation_mode': self.simulation_mode, - 'render_size': (self.render_width, self.render_height), - 'alvr_enabled': self.alvr_enabled + import time + # 🚀 性能优化:减少字典创建,只在必要时创建 + return { + 'operation': operation_name, + 'start_time': time.perf_counter() } - - if self.simulation_mode: - info['mode'] = 'simulation' - info['controllers'] = len(self.simulation_data['controller_poses']) + + def _end_timing(self, timing_data): + """结束计时并记录结果 - 性能优化版本""" + if not self.enable_pipeline_monitoring or self.performance_mode_enabled or not timing_data: + return 0.0 + + import time + elapsed = (time.perf_counter() - timing_data['start_time']) * 1000 # 转换为毫秒 + + # 保存到相应的历史记录 + operation = timing_data['operation'] + if operation == 'wait_poses': + self.wait_poses_time = elapsed + self.wait_poses_times.append(elapsed) + if len(self.wait_poses_times) > self.pipeline_history_size: + self.wait_poses_times.pop(0) + elif operation == 'left_render': + self.left_render_time = elapsed + elif operation == 'right_render': + self.right_render_time = elapsed + elif operation == 'submit': + self.submit_time = elapsed + self.submit_times.append(elapsed) + if len(self.submit_times) > self.pipeline_history_size: + self.submit_times.pop(0) + elif operation == 'sync_wait': + self.vr_sync_wait_time = elapsed + self.sync_wait_times.append(elapsed) + if len(self.sync_wait_times) > self.pipeline_history_size: + self.sync_wait_times.pop(0) + + # 计算总渲染时间 + total_render = self.left_render_time + self.right_render_time + self.render_times.append(total_render) + if len(self.render_times) > self.pipeline_history_size: + self.render_times.pop(0) + + return elapsed + + def _get_pipeline_stats(self): + """获取渲染管线统计信息""" + def get_stats(times_list): + if not times_list: + return {'avg': 0.0, 'min': 0.0, 'max': 0.0} + return { + 'avg': sum(times_list) / len(times_list), + 'min': min(times_list), + 'max': max(times_list) + } + + # 计算GPU时间统计 + def get_gpu_field_stats(field_name): + """从GPU时间历史记录中提取特定字段的统计信息""" + values = [] + for gpu_data in self.gpu_timing_history: + if field_name in gpu_data: + values.append(gpu_data[field_name]) + return get_stats(values) + + return { + 'wait_poses': get_stats(self.wait_poses_times), + 'render': get_stats(self.render_times), + 'submit': get_stats(self.submit_times), + 'sync_wait': get_stats(self.sync_wait_times), + 'gpu_timing': { + 'scene_render': get_gpu_field_stats('scene_render'), + 'pre_submit': get_gpu_field_stats('pre_submit'), + 'post_submit': get_gpu_field_stats('post_submit'), + 'total_render': get_gpu_field_stats('total_render'), + 'compositor_render': get_gpu_field_stats('compositor_render'), + 'frame_interval': get_gpu_field_stats('frame_interval') + }, + 'current': { + 'wait_poses': self.wait_poses_time, + 'left_render': self.left_render_time, + 'right_render': self.right_render_time, + 'submit': self.submit_time, + 'sync_wait': self.vr_sync_wait_time, + 'total_render': self.left_render_time + self.right_render_time, + 'gpu_scene_render': self.gpu_scene_render_ms, + 'gpu_pre_submit': self.gpu_pre_submit_ms, + 'gpu_post_submit': self.gpu_post_submit_ms, + 'gpu_total_render': self.gpu_total_render_ms, + 'gpu_compositor_render': self.gpu_compositor_render_ms, + 'gpu_frame_interval': self.gpu_client_frame_interval_ms + }, + 'vr_info': { + 'eye_resolution': self.current_eye_resolution, + 'recommended_resolution': self.recommended_eye_resolution, + 'display_frequency': self.vr_display_frequency, + 'vsync_enabled': self.vr_vsync_enabled, + 'target_frame_time_ms': self.target_frame_time_ms, + 'vsync_to_photons_ms': self.vsync_to_photons_ms, + 'vsync_window_ms': self.vsync_window_ms, + 'async_reprojection': self.async_reprojection_enabled, + 'motion_smoothing': self.motion_smoothing_enabled, + 'gpu_timing_enabled': self.enable_gpu_timing, + 'gpu_timing_failures': self.gpu_timing_failure_count + } + } + + + def set_prediction_time(self, prediction_time_ms): + """设置预测时间(仅用于update_task策略) + + Args: + prediction_time_ms: 预测时间,单位毫秒(通常8-16ms) + """ + prediction_time_s = prediction_time_ms / 1000.0 + if prediction_time_s < 0.005 or prediction_time_s > 0.020: + print(f"⚠️ 预测时间超出推荐范围(5-20ms): {prediction_time_ms}ms") + + old_time = self.use_prediction_time * 1000 + self.use_prediction_time = prediction_time_s + print(f"✓ VR预测时间已设置: {old_time:.1f}ms → {prediction_time_ms:.1f}ms") + + + def test_pipeline_monitoring(self): + """测试管线监控功能 - 用于调试和验证""" + print("🔧 正在测试VR管线监控功能...") + + try: + # 测试基本信息获取 + print("📊 基本信息:") + print(f" 当前分辨率: {self.current_eye_resolution}") + print(f" 显示频率: {self.vr_display_frequency} Hz") + print(f" 目标帧时间: {self.target_frame_time_ms:.2f}ms") + + # Running Start模式信息 + print("🎯 Running Start模式:") + print(f" 预测时间: {self.use_prediction_time * 1000:.1f}ms") + print(f" 模式: Valve Running Start - 帧开始时获取姿态") + + # 测试管线统计 + if self.enable_pipeline_monitoring: + pipeline_stats = self._get_pipeline_stats() + print("⚙️ 管线统计:") + print(f" waitGetPoses历史: {len(self.wait_poses_times)}帧") + print(f" 渲染历史: {len(self.render_times)}帧") + print(f" 提交历史: {len(self.submit_times)}帧") + + # 测试性能报告 + print("📋 生成详细性能报告:") + self._print_performance_report() + + print("✅ 管线监控功能测试完成") + else: + print("⚠️ 管线监控已禁用,无法测试统计功能") + + except Exception as e: + print(f"❌ 测试管线监控功能时发生错误: {e}") + import traceback + traceback.print_exc() + + def _init_performance_monitoring(self): + """初始化性能监控库""" + self.psutil_available = False + self.gputil_available = False + self.nvidia_ml_available = False + + try: + import psutil + self.psutil = psutil + self.psutil_available = True + print("✓ psutil性能监控库已加载") + except ImportError: + print("⚠️ psutil库未安装,CPU和内存监控将不可用") + + try: + import GPUtil + self.gputil = GPUtil + self.gputil_available = True + print("✓ GPUtil GPU监控库已加载") + except ImportError: + print("⚠️ GPUtil库未安装,GPU监控将不可用") + + try: + import pynvml + self.pynvml = pynvml + pynvml.nvmlInit() + self.nvidia_ml_available = True + print("✓ NVIDIA-ML GPU监控库已加载") + except ImportError: + print("⚠️ pynvml库未安装,NVIDIA GPU详细监控将不可用") + except Exception as e: + print(f"⚠️ NVIDIA-ML初始化失败: {e}") + + def _get_gpu_frame_timing(self, frames_ago=0): + """获取GPU渲染时间统计 + + Args: + frames_ago: 获取多少帧之前的数据,0表示当前帧 + + Returns: + dict: GPU时间数据,如果获取失败返回None + """ + if not self.enable_gpu_timing or not self.vr_compositor: + return None + + try: + # 调用OpenVR的getFrameTiming API + result, timing = self.vr_compositor.getFrameTiming(framesAgo=frames_ago) + + if not result: + self.gpu_timing_failure_count += 1 + if self.gpu_timing_failure_count % 300 == 1: # 每5秒输出一次错误 + print("⚠️ OpenVR getFrameTiming调用失败") + return None + + # 提取GPU时间数据(单位:毫秒) + gpu_data = {} + + # 检查timing对象是否有GPU时间相关的属性 + if hasattr(timing, 'm_flSceneRenderGpuMs'): + gpu_data['scene_render'] = timing.m_flSceneRenderGpuMs + self.gpu_scene_render_ms = timing.m_flSceneRenderGpuMs + + if hasattr(timing, 'm_flPreSubmitGpuMs'): + gpu_data['pre_submit'] = timing.m_flPreSubmitGpuMs + self.gpu_pre_submit_ms = timing.m_flPreSubmitGpuMs + + if hasattr(timing, 'm_flPostSubmitGpuMs'): + gpu_data['post_submit'] = timing.m_flPostSubmitGpuMs + self.gpu_post_submit_ms = timing.m_flPostSubmitGpuMs + + if hasattr(timing, 'm_flTotalRenderGpuMs'): + gpu_data['total_render'] = timing.m_flTotalRenderGpuMs + self.gpu_total_render_ms = timing.m_flTotalRenderGpuMs + + if hasattr(timing, 'm_flCompositorRenderGpuMs'): + gpu_data['compositor_render'] = timing.m_flCompositorRenderGpuMs + self.gpu_compositor_render_ms = timing.m_flCompositorRenderGpuMs + + if hasattr(timing, 'm_flClientFrameIntervalMs'): + gpu_data['frame_interval'] = timing.m_flClientFrameIntervalMs + self.gpu_client_frame_interval_ms = timing.m_flClientFrameIntervalMs + + # 将GPU时间数据添加到历史记录 + if gpu_data: + self.gpu_timing_history.append(gpu_data) + if len(self.gpu_timing_history) > self.gpu_timing_history_size: + self.gpu_timing_history.pop(0) + + # 调试信息 - 仅在第一次成功时输出 + if not hasattr(self, '_gpu_timing_success_logged'): + available_fields = list(gpu_data.keys()) + print(f"✅ GPU时间统计已启用 - 可用字段: {available_fields}") + self._gpu_timing_success_logged = True + + return gpu_data + + except AttributeError as e: + # OpenVR Python绑定可能不包含某些字段 + if self.gpu_timing_failure_count == 0: + print(f"⚠️ GPU时间统计部分功能不可用: {e}") + print(" 这可能是由于OpenVR Python绑定版本问题") + self.gpu_timing_failure_count += 1 + return None + + except Exception as e: + self.gpu_timing_failure_count += 1 + if self.gpu_timing_failure_count % 300 == 1: # 每5秒输出一次错误 + print(f"⚠️ 获取GPU时间统计失败: {e}") + return None + + def _update_performance_metrics(self): + """更新系统性能指标""" + if not self.performance_monitoring: + return + + import time + current_time = time.time() + + # 限制更新频率 + if current_time - self.last_performance_check < self.performance_check_interval: + return + + self.last_performance_check = current_time + + try: + # 更新CPU和内存使用率 + if self.psutil_available: + self.cpu_usage = self.psutil.cpu_percent(interval=None) + memory = self.psutil.virtual_memory() + self.memory_usage = memory.percent + + # 更新GPU使用率 + self._update_gpu_metrics() + + except Exception as e: + if self.frame_count % 600 == 0: # 每10秒输出一次错误 + print(f"⚠️ 性能监控更新失败: {e}") + + def _update_gpu_metrics(self): + """更新GPU相关指标""" + try: + # 方法1: 使用GPUtil + if self.gputil_available: + gpus = self.gputil.getGPUs() + if gpus: + gpu = gpus[0] # 使用第一个GPU + self.gpu_usage = gpu.load * 100 + self.gpu_memory_usage = gpu.memoryUtil * 100 + + # 方法2: 使用NVIDIA-ML (更精确) + elif self.nvidia_ml_available: + try: + handle = self.pynvml.nvmlDeviceGetHandleByIndex(0) + + # GPU使用率 + utilization = self.pynvml.nvmlDeviceGetUtilizationRates(handle) + self.gpu_usage = utilization.gpu + + # GPU内存使用率 + memory_info = self.pynvml.nvmlDeviceGetMemoryInfo(handle) + self.gpu_memory_usage = (memory_info.used / memory_info.total) * 100 + + except Exception as e: + # NVIDIA-ML可能无法在某些系统上工作 + pass + + except Exception as e: + # GPU监控失败,但不影响VR功能 + pass + + def _track_frame_time(self): + """记录帧时间 - 性能优化版本""" + import time + current_time = time.time() + + if hasattr(self, '_last_frame_time'): + frame_time = (current_time - self._last_frame_time) * 1000 # 转换为毫秒 + + # 🚀 性能优化:性能模式下跳过列表操作以减少内存分配 + if not self.performance_mode_enabled: + # 添加到帧时间历史 + self.frame_times.append(frame_time) + + # 限制历史长度 + if len(self.frame_times) > self.max_frame_time_history: + self.frame_times.pop(0) + + self._last_frame_time = current_time + + def get_performance_stats(self): + """获取详细的性能统计信息""" + stats = { + 'vr_fps': self.vr_fps, + 'frame_count': self.frame_count, + 'submit_failures': self.submit_failures, + 'pose_failures': self.pose_failures, + 'cpu_usage': self.cpu_usage, + 'memory_usage': self.memory_usage, + 'gpu_usage': self.gpu_usage, + 'gpu_memory_usage': self.gpu_memory_usage, + } + + # 计算帧时间统计 + if self.frame_times: + stats['frame_time_avg'] = sum(self.frame_times) / len(self.frame_times) + stats['frame_time_min'] = min(self.frame_times) + stats['frame_time_max'] = max(self.frame_times) + stats['frame_time_95th'] = sorted(self.frame_times)[int(len(self.frame_times) * 0.95)] else: - info['mode'] = 'real_vr' - info['openvr_connected'] = self.vr_system is not None - - return info + stats['frame_time_avg'] = 0 + stats['frame_time_min'] = 0 + stats['frame_time_max'] = 0 + stats['frame_time_95th'] = 0 - # 便捷方法 - def enable_simulation_mode(self): - """启用模拟模式(调试用)""" - return self.initialize_vr(force_simulation=True) + return stats - def get_simulation_data(self): - """获取模拟数据""" - return self.simulation_data if self.simulation_mode else None + def enable_performance_monitoring(self): + """启用性能监控""" + self.performance_monitoring = True + print("✓ VR性能监控已启用") + + def disable_performance_monitoring(self): + """禁用性能监控""" + self.performance_monitoring = False + print("✓ VR性能监控已禁用") + + def enable_gpu_timing_monitoring(self): + """启用GPU时间监控""" + self.enable_gpu_timing = True + print("✓ VR GPU时间监控已启用") + + def disable_gpu_timing_monitoring(self): + """禁用GPU时间监控""" + self.enable_gpu_timing = False + print("✓ VR GPU时间监控已禁用") + + def set_performance_check_interval(self, interval): + """设置性能检查间隔 + + Args: + interval: 检查间隔(秒),建议0.1-2.0之间 + """ + if 0.1 <= interval <= 5.0: + self.performance_check_interval = interval + print(f"✓ 性能监控间隔设置为 {interval:.1f} 秒") + else: + print("⚠️ 性能监控间隔应在0.1-5.0秒之间") + + def set_frame_time_history_size(self, size): + """设置帧时间历史记录大小 + + Args: + size: 历史记录大小(帧数),建议30-300之间 + """ + if 10 <= size <= 1000: + self.max_frame_time_history = size + # 清理超出的历史记录 + if len(self.frame_times) > size: + self.frame_times = self.frame_times[-size:] + print(f"✓ 帧时间历史记录大小设置为 {size} 帧") + else: + print("⚠️ 帧时间历史记录大小应在10-1000帧之间") + + def set_performance_report_interval(self, frames): + """设置性能报告输出间隔 + + Args: + frames: 帧数间隔,建议300-7200之间(5秒-2分钟@60fps) + """ + if 300 <= frames <= 7200: + # 修改_update_vr中的报告间隔 + print(f"✓ 性能报告间隔设置为每 {frames} 帧(约 {frames/60:.1f} 秒@60fps)") + # 这里可以添加一个实例变量来控制 + self.performance_report_interval = frames + else: + print("⚠️ 性能报告间隔应在300-7200帧之间") + + def get_performance_monitoring_config(self): + """获取当前性能监控配置""" + return { + 'enabled': self.performance_monitoring, + 'check_interval': self.performance_check_interval, + 'frame_history_size': self.max_frame_time_history, + 'report_interval': getattr(self, 'performance_report_interval', 1800), + 'psutil_available': self.psutil_available, + 'gputil_available': self.gputil_available, + 'nvidia_ml_available': self.nvidia_ml_available + } + + def print_performance_monitoring_status(self): + """输出性能监控状态""" + config = self.get_performance_monitoring_config() + + print("🔧 ===== VR性能监控配置 =====") + print(f" 监控状态: {'✅ 启用' if config['enabled'] else '❌ 禁用'}") + print(f" 检查间隔: {config['check_interval']:.1f} 秒") + print(f" 帧时间历史: {config['frame_history_size']} 帧") + print(f" 报告间隔: {config['report_interval']} 帧") + + print(f" 可用库:") + print(f" psutil (CPU/内存): {'✅' if config['psutil_available'] else '❌'}") + print(f" GPUtil (GPU): {'✅' if config['gputil_available'] else '❌'}") + print(f" NVIDIA-ML (GPU): {'✅' if config['nvidia_ml_available'] else '❌'}") + print("=============================") + + def force_performance_report(self): + """强制输出一次性能报告""" + print("🔄 手动触发性能报告...") + self._print_performance_report() + + def reset_performance_counters(self): + """重置性能计数器""" + self.frame_count = 0 + self.last_fps_check = 0 + self.last_fps_time = 0 + self.vr_fps = 0 + self.submit_failures = 0 + self.pose_failures = 0 + self.frame_times.clear() + print("✅ 性能计数器已重置") + + def get_current_performance_summary(self): + """获取当前性能摘要(简短版本)""" + stats = self.get_performance_stats() + + summary = f"VR性能: {stats['vr_fps']:.1f}fps" + + if stats['frame_time_avg'] > 0: + summary += f" | 帧时间: {stats['frame_time_avg']:.1f}ms" + + if self.psutil_available: + summary += f" | CPU: {stats['cpu_usage']:.0f}%" + summary += f" | 内存: {stats['memory_usage']:.0f}%" + + # 显示GPU信息,如果库不可用则显示提示 + if self.gputil_available or self.nvidia_ml_available: + summary += f" | GPU: {stats['gpu_usage']:.0f}%" + else: + summary += " | GPU: N/A" + + return summary + + def _print_render_callback_diagnostics(self): + """输出渲染回调诊断信息 - 包含优化效果分析""" + try: + print(f"🔍 DrawCallback渲染诊断:") + + # 回调次数统计 + left_count = self.left_render_count + right_count = self.right_render_count + print(f" 渲染次数: 左眼={left_count}, 右眼={right_count}") + + if left_count == 0 and right_count == 0: + print(f" ❌ VR缓冲区未被渲染 - 这是严重问题!") + return + + # 渲染次数平衡性检查 + if abs(left_count - right_count) > 5: + print(f" ⚠️ 左右眼渲染次数不平衡: 差异={abs(left_count - right_count)}") + else: + print(f" ✓ 左右眼渲染次数平衡") + + # 🔧 真实渲染时间分析 - 显示优化效果 + left_render_time = getattr(self, 'left_render_time', 0) + right_render_time = getattr(self, 'right_render_time', 0) + total_render_time = left_render_time + right_render_time + + print(f" ⏱️ 精确渲染时间测量:") + print(f" 左眼cbdata.upcall(): {left_render_time:.2f}ms") + print(f" 右眼cbdata.upcall(): {right_render_time:.2f}ms") + print(f" 总计: {total_render_time:.2f}ms") + + # 渲染性能评估 + if total_render_time > 16.0: # 超过60FPS时间 + print(f" 🔴 渲染时间过长: {total_render_time:.1f}ms (目标<13.9ms@72Hz)") + print(f" 建议: 检查RenderPipeline优化是否生效") + elif total_render_time > 10.0: + print(f" 🟡 渲染时间偏高: {total_render_time:.1f}ms (可接受)") + else: + print(f" 🟢 渲染性能良好: {total_render_time:.1f}ms") + + # 🔧 渲染管线优化状态检查 + self._check_rendering_optimizations() + + # OpenGL状态诊断 + self._diagnose_opengl_state() + + except Exception as e: + print(f" 渲染回调诊断失败: {e}") + + def _check_rendering_optimizations(self): + """检查渲染优化状态""" + try: + print(f" 🔧 渲染优化状态:") + + # 检查RenderPipeline优化 + if hasattr(self.world, 'render_pipeline') and self.world.render_pipeline: + print(f" RenderPipeline: 已检测并优化") + else: + print(f" RenderPipeline: 未检测到(使用基础Panda3D)") + + # 检查GPU同步优化 + if hasattr(self, '_smart_sync_logged'): + last_sync_frame = getattr(self, '_last_gpu_sync_frame', 0) + current_frame = getattr(self, 'frame_count', 0) + frames_since_sync = current_frame - last_sync_frame + print(f" 智能GPU同步: 已启用 (距离上次同步: {frames_since_sync}帧)") + else: + print(f" 智能GPU同步: 未初始化") + + # 检查对象池状态 + matrix_pool_status = self.get_object_pool_status() + print(f" 对象池: Mat4={matrix_pool_status['matrix_pool_size']}/{matrix_pool_status['matrix_pool_capacity']}") + + # 检查垃圾回收控制 + gc_status = self.get_debug_status() + if gc_status['gc_disabled']: + print(f" GC控制: 已启用手动模式 (间隔:{gc_status['manual_gc_interval']}帧)") + else: + print(f" GC控制: 自动模式") + + except Exception as e: + print(f" 优化状态检查失败: {e}") + + def _diagnose_opengl_state(self): + """诊断OpenGL渲染状态""" + try: + # 检查VR缓冲区状态 + if hasattr(self, 'vr_left_eye_buffer') and self.vr_left_eye_buffer: + left_gsg = self.vr_left_eye_buffer.getGsg() + left_valid = self.vr_left_eye_buffer.isValid() + print(f" 左眼缓冲区: {'有效' if left_valid else '无效'}") + + if hasattr(self, 'vr_right_eye_buffer') and self.vr_right_eye_buffer: + right_gsg = self.vr_right_eye_buffer.getGsg() + right_valid = self.vr_right_eye_buffer.isValid() + print(f" 右眼缓冲区: {'有效' if right_valid else '无效'}") + + # 检查纹理准备状态 + if hasattr(self, 'textures_prepared'): + print(f" 纹理准备状态: {'已准备' if self.textures_prepared else '未准备'}") + + # 检查纹理ID缓存 + if hasattr(self, 'left_texture_id') and hasattr(self, 'right_texture_id'): + left_id = self.left_texture_id or 0 + right_id = self.right_texture_id or 0 + print(f" 纹理ID缓存: 左眼={left_id}, 右眼={right_id}") + + if left_id == 0 or right_id == 0: + print(f" ⚠️ 检测到无效的纹理ID,这可能导致提交失败") + else: + print(f" ✓ 纹理ID缓存正常") + + # 检查场景渲染状态 + if hasattr(self.world, 'render') and self.world.render: + render_children = len(self.world.render.getChildren()) + print(f" 场景节点数: {render_children}") + if render_children == 0: + print(f" ⚠️ 场景为空,可能导致渲染时间异常短") + + except Exception as e: + print(f" OpenGL状态诊断失败: {e}") + + def enable_debug_output(self): + """启用调试输出""" + self.debug_output_enabled = True + print("✓ VR调试输出已启用") + + def disable_debug_output(self): + """禁用调试输出""" + self.debug_output_enabled = False + print("✓ VR调试输出已禁用") + + def set_debug_mode(self, mode): + """设置调试模式 + + Args: + mode: 'brief' 或 'detailed' + """ + if mode in ['brief', 'detailed']: + self.debug_mode = mode + print(f"✓ VR调试模式设置为: {mode}") + else: + print("⚠️ 调试模式只能是 'brief' 或 'detailed'") + + def toggle_debug_output(self): + """切换调试输出状态""" + self.debug_output_enabled = not self.debug_output_enabled + status = "启用" if self.debug_output_enabled else "禁用" + print(f"✓ VR调试输出已{status}") + return self.debug_output_enabled + + def get_debug_status(self): + """获取调试状态""" + return { + 'debug_enabled': self.debug_output_enabled, + 'debug_mode': self.debug_mode, + 'performance_monitoring': self.performance_monitoring, + 'report_interval_frames': getattr(self, 'performance_report_interval', 600), + 'report_interval_seconds': getattr(self, 'performance_report_interval', 600) / 60, # 假设60fps + 'gc_control_enabled': self._gc_control_enabled, + 'gc_disabled': self._gc_disabled, + 'manual_gc_interval': self._manual_gc_interval, + } + + # ====== Python垃圾回收控制方法 ====== + + def enable_gc_control(self): + """启用垃圾回收控制 - 减少VR渲染期间的GC峰值""" + if not self._gc_control_enabled: + self._gc_control_enabled = True + if not self._gc_disabled: + gc.disable() + self._gc_disabled = True + print("✅ VR垃圾回收控制已启用") + else: + print("ℹ️ VR垃圾回收控制已经启用") + + def disable_gc_control(self): + """禁用垃圾回收控制 - 恢复自动垃圾回收""" + if self._gc_control_enabled: + self._gc_control_enabled = False + if self._gc_disabled: + gc.enable() + self._gc_disabled = False + print("✅ VR垃圾回收控制已禁用,恢复自动垃圾回收") + else: + print("ℹ️ VR垃圾回收控制已经禁用") + + def set_manual_gc_interval(self, frames): + """设置手动垃圾回收间隔 + + Args: + frames: 帧数间隔 (建议100-600) + """ + if 50 <= frames <= 1800: + old_interval = self._manual_gc_interval + self._manual_gc_interval = frames + print(f"✅ 手动GC间隔: {old_interval} → {frames} 帧") + else: + print("⚠️ GC间隔应在50-1800帧之间") + + def force_manual_gc(self): + """强制执行一次垃圾回收""" + collected = gc.collect() + print(f"🗑️ 强制GC: 清理了 {collected} 个对象") + return collected + + def get_object_pool_status(self): + """获取对象池状态""" + return { + 'matrix_pool_size': len(self._matrix_pool) if hasattr(self, '_matrix_pool') else 0, + 'matrix_pool_capacity': getattr(self, '_matrix_pool_size', 0), + 'cached_controllers': len(self.controller_poses), + 'cached_matrices': len(getattr(self, '_cached_matrices', {})), + } + + # ====== VR分辨率缩放和质量预设系统 ====== + + def set_resolution_scale(self, scale): + """设置VR分辨率缩放系数 + + Args: + scale: 缩放系数 (0.3-1.0),0.75表示75%分辨率 + """ + if not (0.3 <= scale <= 1.0): + print(f"⚠️ 分辨率缩放系数应在0.3-1.0之间,当前: {scale}") + return False + + old_scale = self.resolution_scale + self.resolution_scale = scale + + # 如果VR已初始化,重新创建缓冲区 + if self.vr_initialized: + self._apply_resolution_scale() + + print(f"✓ VR分辨率缩放: {old_scale} → {scale}") + pixel_reduction = (1 - scale**2) * 100 + print(f"📊 像素减少: {pixel_reduction:.1f}%") + + return True + + def set_quality_preset(self, preset_name): + """设置VR质量预设 + + Args: + preset_name: 'performance', 'balanced', 'quality' + """ + if preset_name not in self.quality_presets: + print(f"⚠️ 未知的质量预设: {preset_name}") + print(f" 可用预设: {list(self.quality_presets.keys())}") + return False + + old_preset = self.current_quality_preset + self.current_quality_preset = preset_name + scale = self.quality_presets[preset_name] + + print(f"🎯 切换VR质量预设: {old_preset} → {preset_name}") + + return self.set_resolution_scale(scale) + + def cycle_quality_preset(self): + """循环切换质量预设""" + presets = list(self.quality_presets.keys()) + current_index = presets.index(self.current_quality_preset) + next_index = (current_index + 1) % len(presets) + next_preset = presets[next_index] + + return self.set_quality_preset(next_preset) + + def _apply_resolution_scale(self): + """应用分辨率缩放,重新创建VR缓冲区""" + try: + # 计算新的分辨率 + self.scaled_eye_width = int(self.base_eye_width * self.resolution_scale) + self.scaled_eye_height = int(self.base_eye_height * self.resolution_scale) + + # 更新当前分辨率 + self.eye_width = self.scaled_eye_width + self.eye_height = self.scaled_eye_height + self.current_eye_resolution = (self.eye_width, self.eye_height) + + print(f"🔄 重新创建VR缓冲区...") + print(f" 新分辨率: {self.eye_width}x{self.eye_height}") + + # 清理旧的缓冲区 + self._cleanup_vr_buffers() + + # 重新创建缓冲区 + if self._create_vr_buffers(): + # 重新设置相机 + self._setup_vr_cameras() + print("✅ VR缓冲区重新创建成功") + return True + else: + print("❌ VR缓冲区重新创建失败") + return False + + except Exception as e: + print(f"❌ 应用分辨率缩放失败: {e}") + import traceback + traceback.print_exc() + return False + + def _cleanup_vr_buffers(self): + """清理旧的VR缓冲区""" + try: + # 清理左眼缓冲区 + if hasattr(self, 'vr_left_eye_buffer') and self.vr_left_eye_buffer: + self.vr_left_eye_buffer.removeAllDisplayRegions() + self.world.graphicsEngine.removeWindow(self.vr_left_eye_buffer) + self.vr_left_eye_buffer = None + + # 清理右眼缓冲区 + if hasattr(self, 'vr_right_eye_buffer') and self.vr_right_eye_buffer: + self.vr_right_eye_buffer.removeAllDisplayRegions() + self.world.graphicsEngine.removeWindow(self.vr_right_eye_buffer) + self.vr_right_eye_buffer = None + + # 清理相机 + if hasattr(self, 'vr_left_camera') and self.vr_left_camera: + self.vr_left_camera.removeNode() + self.vr_left_camera = None + + if hasattr(self, 'vr_right_camera') and self.vr_right_camera: + self.vr_right_camera.removeNode() + self.vr_right_camera = None + + except Exception as e: + print(f"⚠️ 清理VR缓冲区时出错: {e}") + + def get_resolution_info(self): + """获取分辨率相关信息""" + return { + 'base_resolution': (self.base_eye_width, self.base_eye_height), + 'current_resolution': (self.eye_width, self.eye_height), + 'resolution_scale': self.resolution_scale, + 'current_preset': self.current_quality_preset, + 'available_presets': self.quality_presets, + 'pixel_reduction_percent': (1 - self.resolution_scale**2) * 100 + } + + def print_resolution_info(self): + """输出分辨率信息""" + info = self.get_resolution_info() + print("🔧 ===== VR分辨率信息 =====") + print(f" 推荐分辨率: {info['base_resolution'][0]}x{info['base_resolution'][1]}") + print(f" 当前分辨率: {info['current_resolution'][0]}x{info['current_resolution'][1]}") + print(f" 缩放系数: {info['resolution_scale']}") + print(f" 当前预设: {info['current_preset']}") + print(f" 像素减少: {info['pixel_reduction_percent']:.1f}%") + print(" 可用预设:") + for name, scale in info['available_presets'].items(): + marker = "✓" if name == info['current_preset'] else " " + print(f" {marker} {name}: {scale} ({scale*100:.0f}%)") + print("==========================") + + # ====== 性能模式控制方法 ====== + + def enable_performance_mode(self): + """手动启用性能模式 - 立即禁用详细监控以提升性能""" + if not self.performance_mode_enabled: + self.performance_mode_enabled = True + print("🎯 性能模式已手动启用 - 禁用详细监控以提升性能") + print(" 现在将减少每帧对象创建,显著提升VR性能稳定性") + else: + print("ℹ️ 性能模式已经启用") + + def disable_performance_mode(self): + """禁用性能模式 - 重新启用详细监控(用于调试)""" + if self.performance_mode_enabled: + self.performance_mode_enabled = False + print("🔍 性能模式已禁用 - 重新启用详细监控") + print(" 注意:这将增加每帧对象创建,可能影响VR性能") + else: + print("ℹ️ 性能模式已经禁用") + + def set_performance_mode_trigger_frame(self, frame_count): + """设置性能模式自动触发的帧数 + + Args: + frame_count: 触发帧数 (建议300-1200) + """ + if 100 <= frame_count <= 3600: + old_trigger = self.performance_mode_trigger_frame + self.performance_mode_trigger_frame = frame_count + print(f"✅ 性能模式触发帧数: {old_trigger} → {frame_count}") + else: + print("⚠️ 触发帧数应在100-3600之间") + + def get_performance_mode_status(self): + """获取性能模式状态""" + return { + 'performance_mode_enabled': self.performance_mode_enabled, + 'trigger_frame': self.performance_mode_trigger_frame, + 'current_frame': self.frame_count, + 'will_trigger_at_frame': self.performance_mode_trigger_frame if not self.performance_mode_enabled else None, + 'gc_interval_normal': self._manual_gc_interval, + 'gc_interval_performance': self._manual_gc_interval * 2, + } + + # ====== VR测试模式 ====== + + def enable_vr_test_mode(self, display_mode='stereo'): + """启用VR测试模式 - 将VR渲染直接显示在屏幕上 + + Args: + display_mode: 显示模式 + - 'stereo': 左右眼并排显示 + - 'left': 只显示左眼 + - 'right': 只显示右眼 + """ + if not self.is_vr_available(): + print("❌ VR系统不可用,无法启动测试模式") + return False + + try: + print(f"🧪 启动VR测试模式 - 显示模式: {display_mode}") + + # 初始化VR系统(如果还没初始化) + if not self.vr_initialized: + if not self.initialize_vr(): + print("❌ VR初始化失败") + return False + + # 🔧 关键修复:确保测试模式的纹理资源已初始化 + # 这解决了测试模式纹理提交失败导致的36FPS问题 + print("🔧 检查VR测试模式纹理资源...") + if not self._ensure_test_mode_textures(): + print("❌ VR测试模式纹理资源初始化失败") + return False + + # 设置测试模式 + self.vr_test_mode = True + self.test_display_mode = display_mode + + # 启用VR渲染但不提交给OpenVR + if not self.vr_enabled: + # 启用VR渲染流程(但会在回调中跳过提交) + self.vr_enabled = True + self._disable_main_cam() + + # 设置高帧率用于测试 + if hasattr(self.world, 'qtWidget') and self.world.qtWidget: + if hasattr(self.world.qtWidget, 'synchronizer'): + self.world.qtWidget.synchronizer.setInterval(int(1000/144)) + print("✓ 测试模式:Qt Timer设置为144Hz") + + # 初始化测试显示系统 + if not self._initialize_test_display(): + print("❌ 测试显示系统初始化失败") + return False + + # 创建性能HUD + if not self._initialize_test_performance_hud(): + print("❌ 性能HUD初始化失败") + return False + + # 恢复主相机以查看测试内容 + self._enable_main_cam() + + # 重置HUD更新计数器 + self.hud_update_counter = 0 + + print("✅ VR测试模式已启用") + print(" - VR内容将显示在屏幕上") + print(" - 不会向OpenVR提交纹理") + print(" - 可以准确测量纯渲染性能") + print(f" - 当前显示模式: {display_mode}") - def update_simulation_data(self, key, value): - """更新模拟数据""" - if self.simulation_mode and key in self.simulation_data: - self.simulation_data[key] = value return True - return False \ No newline at end of file + + except Exception as e: + print(f"❌ 启动VR测试模式失败: {e}") + import traceback + traceback.print_exc() + return False + + def _ensure_test_mode_textures(self): + """确保VR测试模式的纹理资源已正确初始化 + + 这解决了测试模式启用纹理提交时的36FPS问题: + - VR测试模式可能跳过了VR渲染缓冲区的初始化 + - submit_texture()需要有效的texture ID和OpenVR Texture对象 + - 如果未初始化会导致提交失败,造成帧率减半 + """ + try: + print(" 检查VR渲染缓冲区...") + + # 检查VR渲染缓冲区是否存在 + buffers_exist = ( + hasattr(self, 'vr_left_eye_buffer') and self.vr_left_eye_buffer and + hasattr(self, 'vr_right_eye_buffer') and self.vr_right_eye_buffer + ) + + if not buffers_exist: + print(" ⚠️ VR渲染缓冲区不存在,正在创建...") + if not self._setup_vr_render_buffers(): + print(" ❌ VR渲染缓冲区创建失败") + return False + print(" ✅ VR渲染缓冲区创建成功") + else: + print(" ✅ VR渲染缓冲区已存在") + + # 检查纹理ID是否已缓存 + print(" 检查纹理ID缓存...") + texture_ids_cached = ( + hasattr(self, 'left_texture_id') and self.left_texture_id and self.left_texture_id > 0 and + hasattr(self, 'right_texture_id') and self.right_texture_id and self.right_texture_id > 0 + ) + + if not texture_ids_cached: + print(" ⚠️ 纹理ID未缓存,正在准备纹理...") + if not self._prepare_and_cache_textures(): + print(" ❌ 纹理准备和缓存失败") + return False + print(f" ✅ 纹理ID缓存成功 - 左眼:{self.left_texture_id}, 右眼:{self.right_texture_id}") + else: + print(f" ✅ 纹理ID已缓存 - 左眼:{self.left_texture_id}, 右眼:{self.right_texture_id}") + + # 检查OpenVR Texture对象是否已创建 + print(" 检查OpenVR Texture对象...") + ovr_textures_exist = ( + hasattr(self, '_left_ovr_texture') and self._left_ovr_texture and + hasattr(self, '_right_ovr_texture') and self._right_ovr_texture + ) + + if not ovr_textures_exist: + print(" ⚠️ OpenVR Texture对象未创建,正在创建...") + self._create_cached_ovr_textures() + print(" ✅ OpenVR Texture对象创建成功") + else: + print(" ✅ OpenVR Texture对象已存在") + + print(" ✅ VR测试模式纹理资源检查完成,可安全启用纹理提交") + return True + + except Exception as e: + print(f" ❌ VR测试模式纹理资源检查失败: {e}") + import traceback + traceback.print_exc() + return False + + def _create_cached_ovr_textures(self): + """创建缓存的OpenVR Texture对象 - 避免每帧创建新对象""" + try: + import openvr + self._left_ovr_texture = openvr.Texture_t() + self._right_ovr_texture = openvr.Texture_t() + + # 设置固定属性(这些不变) + self._left_ovr_texture.eType = openvr.TextureType_OpenGL + self._left_ovr_texture.eColorSpace = openvr.ColorSpace_Gamma + self._right_ovr_texture.eType = openvr.TextureType_OpenGL + self._right_ovr_texture.eColorSpace = openvr.ColorSpace_Gamma + + print("✅ OpenVR Texture对象缓存已创建") + except Exception as e: + print(f"⚠️ OpenVR Texture对象创建失败: {e}") + # 不抛出异常,使用备用方案 + + def _batch_submit_textures(self): + """批量提交两眼纹理 - OpenVR最佳实践 + + 基于官方hellovr示例的实现: + - 两眼都渲染完成后,快速连续提交 + - 减少submit阻塞时间,避免错过VSync窗口 + - 这是解决36FPS问题的关键 + """ + try: + if not self.vr_compositor: + return False + + # 检查纹理是否准备好 + if not (self.vr_left_texture and self.vr_right_texture): + return False + + # 🚀 关键:快速连续提交两眼,最小化阻塞时间 + # 这符合OpenVR官方示例的做法 + success_left = False + success_right = False + + # 提交左眼纹理 + try: + self.submit_texture(openvr.Eye_Left, self.vr_left_texture) + success_left = True + except Exception as e: + print(f"❌ 批量提交左眼失败: {e}") + + # 立即提交右眼纹理(不等待) + try: + self.submit_texture(openvr.Eye_Right, self.vr_right_texture) + success_right = True + except Exception as e: + print(f"❌ 批量提交右眼失败: {e}") + + # 🚀 关键修复:调用PostPresentHandoff解除compositor阻塞 + # 这是解决36FPS问题的核心 - 确保compositor不会等待VSync + if success_left and success_right: + try: + # PostPresentHandoff告诉compositor我们已完成帧处理 + # 防止compositor等待下一个VSync周期 + if hasattr(self.vr_compositor, 'postPresentHandoff'): + self.vr_compositor.postPresentHandoff() + elif hasattr(self.vr_compositor, 'PostPresentHandoff'): + self.vr_compositor.PostPresentHandoff() + else: + # 备用方案:如果没有PostPresentHandoff,记录警告 + if not hasattr(self, '_post_present_warning_logged'): + print("⚠️ PostPresentHandoff方法未找到,可能影响时序") + self._post_present_warning_logged = True + except Exception as handoff_error: + if not hasattr(self, '_handoff_error_logged'): + print(f"⚠️ PostPresentHandoff调用失败: {handoff_error}") + self._handoff_error_logged = True + + # 记录批量提交状态(仅首次成功时) + if not hasattr(self, '_batch_submit_success_logged'): + print("✅ OpenVR批量提交模式+PostPresentHandoff已启用") + print(" 这应该解决36FPS → 72FPS的问题") + self._batch_submit_success_logged = True + return True + else: + return False + + except Exception as e: + print(f"❌ 批量提交纹理失败: {e}") + return False + + def disable_vr_test_mode(self): + """禁用VR测试模式""" + try: + print("🧪 禁用VR测试模式...") + + # 清理测试显示 + self._cleanup_test_display() + + # 清理性能HUD + self._cleanup_test_performance_hud() + + # 关闭测试模式 + self.vr_test_mode = False + self.test_mode_initialized = False + + # 重置HUD更新计数器 + self.hud_update_counter = 0 + + # 恢复正常VR模式或禁用VR + if self.vr_enabled: + print(" 选择: [1] 恢复正常VR模式 [2] 完全禁用VR") + # 这里可以让用户选择,现在默认禁用VR + self.disable_vr() + + print("✅ VR测试模式已禁用") + return True + + except Exception as e: + print(f"❌ 禁用VR测试模式失败: {e}") + return False + + def switch_test_display_mode(self, display_mode): + """切换测试显示模式 + + Args: + display_mode: 'stereo', 'left', 'right' + """ + if not self.vr_test_mode: + print("⚠️ 请先启用VR测试模式") + return False + + if display_mode not in ['stereo', 'left', 'right']: + print(f"⚠️ 无效的显示模式: {display_mode}") + return False + + print(f"🧪 切换显示模式: {self.test_display_mode} → {display_mode}") + + # 如果切换模式,需要清理旧的显示并重置标志位 + if self.test_display_mode != display_mode: + self._cleanup_test_display() + + self.test_display_mode = display_mode + + # 更新显示 + self._update_test_display() + return True + + def _initialize_test_display(self): + """初始化测试显示系统""" + try: + print("🔧 正在初始化测试显示系统...") + + # 导入必要的Panda3D组件 + from panda3d.core import CardMaker, PandaNode + + # 创建显示四边形 + cm = CardMaker("vr_test_display") + + if self.test_display_mode == 'stereo': + # 并排显示:左眼在左半屏,右眼在右半屏 + cm.setFrame(-1, 1, -0.5, 0.5) # 全屏 + else: + # 单眼显示:占据整个屏幕 + cm.setFrame(-1, 1, -1, 1) + + # 创建节点 + self.test_display_quad = self.world.render2d.attachNewNode(cm.generate()) + + # 设置初始纹理 + self._update_test_display() + + # 设置渲染状态 + self.test_display_quad.setTransparency(0) # 不透明 + self.test_display_quad.setBin("background", 0) # 背景层 + + self.test_mode_initialized = True + print("✅ 测试显示系统初始化完成") + return True + + except Exception as e: + print(f"❌ 测试显示系统初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def _update_test_display(self): + """更新测试显示内容""" + try: + if not self.test_mode_initialized: + return + + # 根据显示模式设置纹理 + if self.test_display_mode == 'left': + if not self.test_display_quad: + return + if self.vr_left_texture: + self.test_display_quad.setTexture(self.vr_left_texture) + elif self.test_display_mode == 'right': + if not self.test_display_quad: + return + if self.vr_right_texture: + self.test_display_quad.setTexture(self.vr_right_texture) + elif self.test_display_mode == 'stereo': + # 立体显示:只在第一次创建,后续只更新纹理 + if not self.stereo_display_created: + self._create_stereo_display() + else: + # 只更新纹理,不重新创建 + if self.test_display_quad and self.vr_left_texture: + self.test_display_quad.setTexture(self.vr_left_texture) + if self.test_right_quad and self.vr_right_texture: + self.test_right_quad.setTexture(self.vr_right_texture) + + except Exception as e: + print(f"⚠️ 更新测试显示失败: {e}") + + def _create_stereo_display(self): + """创建左右眼并排显示""" + try: + # 如果已经创建,直接返回 + if self.stereo_display_created and self.test_display_quad and self.test_right_quad: + return + + # 移除旧的显示 + if self.test_display_quad: + self.test_display_quad.removeNode() + if self.test_right_quad: + self.test_right_quad.removeNode() + + from panda3d.core import CardMaker + + # 创建左眼显示 + left_cm = CardMaker("vr_test_left") + left_cm.setFrame(-1, 0, -1, 1) # 左半屏 + left_quad = self.world.render2d.attachNewNode(left_cm.generate()) + if self.vr_left_texture: + left_quad.setTexture(self.vr_left_texture) + + # 创建右眼显示 + right_cm = CardMaker("vr_test_right") + right_cm.setFrame(0, 1, -1, 1) # 右半屏 + right_quad = self.world.render2d.attachNewNode(right_cm.generate()) + if self.vr_right_texture: + right_quad.setTexture(self.vr_right_texture) + + # 保存引用 + self.test_display_quad = left_quad # 保存左眼引用 + self.test_right_quad = right_quad # 额外保存右眼引用 + self.stereo_display_created = True # 标记已创建 + + print("✓ 立体显示已创建") + + except Exception as e: + print(f"⚠️ 创建立体显示失败: {e}") + self.stereo_display_created = False + + def _cleanup_test_display(self): + """清理测试显示""" + try: + if self.test_display_quad: + self.test_display_quad.removeNode() + self.test_display_quad = None + + if hasattr(self, 'test_right_quad') and self.test_right_quad: + self.test_right_quad.removeNode() + self.test_right_quad = None + + # 重置立体显示标志位 + self.stereo_display_created = False + + print("✓ 测试显示已清理") + + except Exception as e: + print(f"⚠️ 清理测试显示失败: {e}") + + def _initialize_test_performance_hud(self): + """初始化性能HUD""" + try: + print("🔧 正在初始化性能HUD...") + + from direct.gui.OnscreenText import OnscreenText + from panda3d.core import TextNode + + # 创建性能文本显示 + self.test_performance_text = OnscreenText( + text="初始化中...", + pos=(-0.95, 0.9), # 左上角,调整到屏幕内 + scale=0.05, + fg=(1, 1, 0, 1), # 黄色 + align=TextNode.ALeft, + shadow=(0, 0, 0, 0.5), # 黑色阴影 + parent=self.world.render2d + ) + + print("✅ 性能HUD初始化完成") + return True + + except Exception as e: + print(f"❌ 性能HUD初始化失败: {e}") + return False + + def _update_test_performance_hud(self): + """更新性能HUD显示(限制更新频率避免重影)""" + try: + if not self.test_performance_text or not self.vr_test_mode: + return + + # 增加计数器 + self.hud_update_counter += 1 + + # 只在指定间隔更新文本,避免重影 + if self.hud_update_counter < self.hud_update_interval: + return + + # 重置计数器 + self.hud_update_counter = 0 + + # 收集性能数据 + left_render_time = getattr(self, 'left_render_time', 0) + right_render_time = getattr(self, 'right_render_time', 0) + total_render_time = left_render_time + right_render_time + + # 计算FPS + current_fps = self.vr_fps if hasattr(self, 'vr_fps') else 0 + + # 获取系统性能 + cpu_usage = getattr(self, 'cpu_usage', 0) + memory_usage = getattr(self, 'memory_usage', 0) + gpu_usage = getattr(self, 'gpu_usage', 0) + + # 构建显示文本 + hud_text = f"""VR TEST MODE - {self.test_display_mode.upper()} + +Render Performance: + FPS: {current_fps:.1f} + Left Eye: {left_render_time:.2f}ms + Right Eye: {right_render_time:.2f}ms + Total: {total_render_time:.2f}ms + +System Performance: + CPU: {cpu_usage:.1f}% + Memory: {memory_usage:.1f}% + GPU: {gpu_usage:.1f}% + +Render Count: + Left Eye: {getattr(self, 'left_render_count', 0)} + Right Eye: {getattr(self, 'right_render_count', 0)} + +Target: <13.9ms@72Hz +Status: {'GOOD' if total_render_time < 10 else 'HIGH' if total_render_time < 16 else 'SLOW'} + +Hotkeys: +F1=Left F2=Right F3=SideBySide +ESC=Exit Test Mode""" + + # 更新文本 + self.test_performance_text.setText(hud_text) + + except Exception as e: + print(f"⚠️ 更新性能HUD失败: {e}") + + def _cleanup_test_performance_hud(self): + """清理性能HUD""" + try: + if self.test_performance_text: + self.test_performance_text.destroy() + self.test_performance_text = None + + print("✓ 性能HUD已清理") + + except Exception as e: + print(f"⚠️ 清理性能HUD失败: {e}") + + def get_test_mode_status(self): + """获取测试模式状态""" + if not self.vr_test_mode: + return None + + left_render_time = getattr(self, 'left_render_time', 0) + right_render_time = getattr(self, 'right_render_time', 0) + total_render_time = left_render_time + right_render_time + + return { + 'test_mode_enabled': self.vr_test_mode, + 'display_mode': self.test_display_mode, + 'vr_fps': getattr(self, 'vr_fps', 0), + 'left_render_time': left_render_time, + 'right_render_time': right_render_time, + 'total_render_time': total_render_time, + 'left_render_count': getattr(self, 'left_render_count', 0), + 'right_render_count': getattr(self, 'right_render_count', 0), + 'performance_rating': ('excellent' if total_render_time < 10 else + 'good' if total_render_time < 16 else 'poor') + } + + def set_test_mode_features(self, submit_texture=None, wait_poses=None): + """设置测试模式功能开关 - 用于渐进式调试 + + Args: + submit_texture: 是否在测试模式启用纹理提交(True/False/None=保持当前) + wait_poses: 是否在测试模式启用waitGetPoses(True/False/None=保持当前) + """ + if submit_texture is not None: + self.test_mode_submit_texture = submit_texture + if wait_poses is not None: + self.test_mode_wait_poses = wait_poses + + print(f"🔧 VR测试模式功能设置:") + print(f" 纹理提交: {'启用' if self.test_mode_submit_texture else '禁用'}") + print(f" 姿态等待: {'启用' if self.test_mode_wait_poses else '禁用'}") + + # 如果当前在测试模式,输出预期效果 + if self.vr_test_mode: + print(f" 当前测试模式已启用,设置将立即生效") + else: + print(f" 设置已保存,将在下次启用测试模式时生效") + + def get_test_mode_features(self): + """获取当前测试模式功能设置""" + return { + 'submit_texture': self.test_mode_submit_texture, + 'wait_poses': self.test_mode_wait_poses, + 'test_mode_active': self.vr_test_mode + } + + def run_vr_performance_test(self, duration_seconds=30, display_mode='stereo'): + """运行VR性能测试 - 简单的测试入口方法 + + Args: + duration_seconds: 测试持续时间(秒) + display_mode: 显示模式 ('stereo', 'left', 'right') + + Returns: + dict: 测试结果统计 + """ + print("🧪 ======= VR性能测试开始 =======") + print(f" 测试模式: {display_mode}") + print(f" 测试时长: {duration_seconds}秒") + print(" 按ESC键提前退出测试") + print(" F1=左眼 F2=右眼 F3=并排显示") + print("=================================") + + # 启动测试模式 + if not self.enable_vr_test_mode(display_mode): + print("❌ VR测试模式启动失败") + return None + + # 记录测试开始状态 + import time + test_start_time = time.time() + start_frame_count = getattr(self, 'frame_count', 0) + + print("✅ VR测试模式已启动") + print(" - VR内容直接显示在屏幕上") + print(" - 无OpenVR纹理提交开销") + print(" - 实时性能监控已启用") + print(f" - 当前显示: {display_mode.upper()}模式") + print("\n开始性能测试...") + + # 等待测试完成(用户手动停止或超时) + print(f"\n📊 测试将运行 {duration_seconds} 秒") + print(" 实时性能数据显示在屏幕左上角") + print(" 观察帧率和渲染时间变化") + + # 这里可以添加自动停止逻辑,但现在让用户手动控制 + print(f"\n⏰ 请观察 {duration_seconds} 秒后手动调用 disable_vr_test_mode() 停止测试") + print(" 或者随时调用 get_test_mode_status() 查看当前状态") + + return { + 'status': 'running', + 'start_time': test_start_time, + 'start_frame': start_frame_count, + 'display_mode': display_mode, + 'instructions': { + 'stop_test': 'vr_manager.disable_vr_test_mode()', + 'check_status': 'vr_manager.get_test_mode_status()', + 'switch_mode': 'vr_manager.switch_test_display_mode("left/right/stereo")' + } + } \ No newline at end of file diff --git a/core/vr_teleport.py b/core/vr_teleport.py new file mode 100644 index 00000000..25dd7b53 --- /dev/null +++ b/core/vr_teleport.py @@ -0,0 +1,411 @@ +""" +VR传送系统模块 + +提供VR传送功能: +- 抛物线轨迹计算和可视化 +- 传送点有效性检测 +- 传送执行 +- 可视化反馈(抛物线、落点标记) +""" + +import math +from panda3d.core import ( + Vec3, Vec4, Mat4, Point3, CollisionRay, CollisionTraverser, + CollisionNode, CollisionHandlerQueue, BitMask32, NodePath, + CollisionSphere, CollisionPlane, LineSegs, GeomNode, Material, + RenderState, TransparencyAttrib, ColorAttrib +) +from direct.showbase.DirectObject import DirectObject + + +class VRTeleportSystem(DirectObject): + """VR传送系统 - 处理传送功能和可视化""" + + def __init__(self, vr_manager): + """初始化VR传送系统 + + Args: + vr_manager: VR管理器实例 + """ + super().__init__() + + self.vr_manager = vr_manager + self.world = vr_manager.world if hasattr(vr_manager, 'world') else None + + # 传送参数 + self.teleport_range = 20.0 # 最大传送距离 + self.arc_resolution = 50 # 抛物线精度 + self.gravity = -9.8 # 重力系数 + self.initial_velocity = 10.0 # 初始速度 + self.min_teleport_distance = 1.0 # 最小传送距离 + + # 可视化元素 + self.teleport_arc_node = None # 抛物线节点 + self.teleport_target_node = None # 落点标记节点 + self.teleport_invalid_node = None # 无效位置标记 + + # 传送状态 + self.is_teleport_active = False # 是否正在预览传送 + self.teleport_target_pos = None # 传送目标位置 + self.teleport_valid = False # 传送位置是否有效 + self.active_controller = None # 正在使用传送的控制器 + + # 碰撞检测 + self.teleport_collision_traverser = CollisionTraverser() + self.teleport_collision_queue = CollisionHandlerQueue() + + # 可视化颜色 + self.valid_arc_color = Vec4(0.2, 0.9, 0.2, 0.8) # 绿色抛物线 + self.invalid_arc_color = Vec4(0.9, 0.2, 0.2, 0.8) # 红色抛物线 + self.target_color = Vec4(0.2, 0.7, 1.0, 0.9) # 蓝色落点 + + print("✓ VR传送系统初始化完成") + + def initialize(self): + """初始化传送系统""" + try: + print("🔧 正在初始化VR传送系统...") + + # 创建可视化元素 + self._create_teleport_visuals() + + # 设置地面检测 + self._setup_ground_detection() + + print("✅ VR传送系统初始化成功") + return True + + except Exception as e: + print(f"❌ VR传送系统初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def _create_teleport_visuals(self): + """创建传送可视化元素""" + if not self.world or not hasattr(self.world, 'render'): + print("⚠️ 无法创建传送可视化 - 缺少世界渲染节点") + return + + # 创建抛物线节点 + self.teleport_arc_node = self.world.render.attachNewNode("teleport_arc") + self.teleport_arc_node.hide() # 初始隐藏 + + # 创建落点标记 + self._create_target_marker() + + # 创建无效位置标记 + self._create_invalid_marker() + + print("✓ 传送可视化元素已创建") + + def _create_target_marker(self): + """创建传送目标标记""" + try: + # 创建一个圆形平台作为落点标记 + from panda3d.core import CardMaker + + cm = CardMaker("teleport_target") + cm.setFrame(-1, 1, -1, 1) # 2x2的平面 + + self.teleport_target_node = self.world.render.attachNewNode(cm.generate()) + self.teleport_target_node.setScale(1.0) # 2米直径的圆形 + self.teleport_target_node.setP(-90) # 平放在地面 + self.teleport_target_node.setColor(self.target_color) + self.teleport_target_node.setTransparency(TransparencyAttrib.MAlpha) + self.teleport_target_node.hide() + + # 添加PBR材质以兼容RenderPipeline + material = Material() + material.setBaseColor(self.target_color) + material.setRoughness(0.8) + material.setMetallic(0.0) + material.setRefractiveIndex(1.5) + self.teleport_target_node.setMaterial(material, 1) + + except Exception as e: + print(f"⚠️ 创建传送目标标记失败: {e}") + + def _create_invalid_marker(self): + """创建无效位置标记""" + try: + from panda3d.core import CardMaker + + cm = CardMaker("teleport_invalid") + cm.setFrame(-0.8, 0.8, -0.8, 0.8) # 稍小的红色叉号 + + self.teleport_invalid_node = self.world.render.attachNewNode(cm.generate()) + self.teleport_invalid_node.setScale(1.0) + self.teleport_invalid_node.setP(-90) + self.teleport_invalid_node.setColor(self.invalid_arc_color) + self.teleport_invalid_node.setTransparency(TransparencyAttrib.MAlpha) + self.teleport_invalid_node.hide() + + # 添加PBR材质 + material = Material() + material.setBaseColor(self.invalid_arc_color) + material.setRoughness(0.8) + material.setMetallic(0.0) + material.setRefractiveIndex(1.5) + self.teleport_invalid_node.setMaterial(material, 1) + + except Exception as e: + print(f"⚠️ 创建无效位置标记失败: {e}") + + def _setup_ground_detection(self): + """设置地面检测""" + try: + # 假设地面在Z=0平面,使用正确的CollisionPlane API + from panda3d.core import Plane + ground_plane = CollisionPlane(Plane(Vec3(0, 0, 1), Point3(0, 0, 0))) + + ground_node = CollisionNode("ground_detection") + ground_node.addSolid(ground_plane) + ground_node.setIntoCollideMask(BitMask32.bit(1)) # 地面碰撞掩码 + + # 附加到渲染节点 + if self.world and hasattr(self.world, 'render'): + ground_node_path = self.world.render.attachNewNode(ground_node) + print("✓ 地面检测设置成功") + + except Exception as e: + print(f"⚠️ 设置地面检测失败: {e}") + # 继续运行,不让这个错误阻止传送系统 + + def start_teleport_preview(self, controller, direction): + """开始传送预览 + + Args: + controller: VR控制器实例 + direction: 传送方向向量(摇杆输入转换的世界方向) + """ + if not controller or not controller.anchor_node: + return False + + self.is_teleport_active = True + self.active_controller = controller + + # 计算抛物线轨迹 + start_pos = controller.get_world_position() + self._calculate_teleport_trajectory(start_pos, direction) + + # 显示可视化 + self._show_teleport_visuals() + + return True + + def _calculate_teleport_trajectory(self, start_pos, direction): + """计算传送抛物线轨迹 + + Args: + start_pos: 起始位置 + direction: 方向向量 + """ + try: + # 确保方向向量已标准化 + direction = direction.normalized() + + # 计算抛物线点 + trajectory_points = [] + hit_ground = False + self.teleport_valid = False + + # 初始速度向量 + velocity = direction * self.initial_velocity + velocity.z += 2.0 # 给一些向上的初始速度 + + dt = 0.05 # 时间步长 + current_pos = Vec3(start_pos) + current_velocity = Vec3(velocity) + + for i in range(self.arc_resolution): + trajectory_points.append(Point3(current_pos)) + + # 更新位置和速度 + current_pos += current_velocity * dt + current_velocity.z += self.gravity * dt # 应用重力 + + # 检查是否碰撞地面 + if current_pos.z <= 0.1: # 假设地面在z=0 + # 精确计算落地点 + ground_pos = self._calculate_ground_intersection( + trajectory_points[-2] if len(trajectory_points) > 1 else start_pos, + current_pos + ) + trajectory_points.append(ground_pos) + + # 检查传送有效性 + distance = (ground_pos - start_pos).length() + if (distance >= self.min_teleport_distance and + distance <= self.teleport_range): + self.teleport_target_pos = ground_pos + self.teleport_valid = True + else: + self.teleport_target_pos = ground_pos + self.teleport_valid = False + + hit_ground = True + break + + # 超出范围 + if (current_pos - start_pos).length() > self.teleport_range: + break + + # 如果没有碰撞地面,传送无效 + if not hit_ground: + if trajectory_points: + self.teleport_target_pos = trajectory_points[-1] + self.teleport_valid = False + + # 创建抛物线几何体 + self._create_arc_geometry(trajectory_points) + + except Exception as e: + print(f"⚠️ 计算传送轨迹失败: {e}") + self.teleport_valid = False + + def _calculate_ground_intersection(self, p1, p2): + """计算与地面的精确交点""" + if p1.z == p2.z: + return Point3(p2) + + # 线性插值找到z=0的点 + t = -p1.z / (p2.z - p1.z) + t = max(0, min(1, t)) # 限制在0-1范围内 + + intersection = p1 + (p2 - p1) * t + intersection.z = 0.1 # 稍微高于地面 + + return Point3(intersection) + + def _create_arc_geometry(self, points): + """创建抛物线几何体""" + if not points or len(points) < 2: + return + + try: + # 创建线段 + line_segs = LineSegs() + line_segs.setThickness(3) + + # 根据有效性设置颜色 + color = self.valid_arc_color if self.teleport_valid else self.invalid_arc_color + line_segs.setColor(color) + + # 添加线段点 + line_segs.moveTo(points[0]) + for point in points[1:]: + line_segs.drawTo(point) + + # 清除旧的几何体 + self.teleport_arc_node.removeNode() + self.teleport_arc_node = self.world.render.attachNewNode("teleport_arc") + + # 创建新的几何体 + geom_node = line_segs.create() + arc_node_path = self.teleport_arc_node.attachNewNode(geom_node) + + # 设置材质 + material = Material() + material.setBaseColor(color) + material.setRoughness(0.1) + material.setMetallic(0.0) + material.setRefractiveIndex(1.5) + arc_node_path.setMaterial(material, 1) + + # 设置透明度 + arc_node_path.setTransparency(TransparencyAttrib.MAlpha) + + except Exception as e: + print(f"⚠️ 创建抛物线几何体失败: {e}") + + def _show_teleport_visuals(self): + """显示传送可视化""" + if self.teleport_arc_node: + self.teleport_arc_node.show() + + if self.teleport_target_pos: + if self.teleport_valid and self.teleport_target_node: + # 显示有效的传送目标 + self.teleport_target_node.setPos(self.teleport_target_pos) + self.teleport_target_node.show() + self.teleport_invalid_node.hide() + elif self.teleport_invalid_node: + # 显示无效的传送位置 + self.teleport_invalid_node.setPos(self.teleport_target_pos) + self.teleport_invalid_node.show() + self.teleport_target_node.hide() + + def stop_teleport_preview(self): + """停止传送预览""" + self.is_teleport_active = False + self.active_controller = None + + # 隐藏可视化 + if self.teleport_arc_node: + self.teleport_arc_node.hide() + if self.teleport_target_node: + self.teleport_target_node.hide() + if self.teleport_invalid_node: + self.teleport_invalid_node.hide() + + def execute_teleport(self): + """执行传送""" + if not self.teleport_valid or not self.teleport_target_pos: + print("⚠️ 传送位置无效,无法执行传送") + return False + + try: + # 计算传送偏移 + if self.vr_manager.tracking_space: + current_pos = self.vr_manager.tracking_space.getPos() + target_offset = self.teleport_target_pos - self.active_controller.get_world_position() + new_pos = current_pos + target_offset + + # 执行传送 + self.vr_manager.tracking_space.setPos(new_pos) + + print(f"✅ 传送成功: {current_pos} → {new_pos}") + + # 停止预览 + self.stop_teleport_preview() + + return True + else: + print("⚠️ 无法获取VR跟踪空间,传送失败") + return False + + except Exception as e: + print(f"❌ 执行传送失败: {e}") + return False + + def update_teleport_preview(self, controller, direction): + """更新传送预览(摇杆移动时调用)""" + if self.is_teleport_active and controller == self.active_controller: + start_pos = controller.get_world_position() + self._calculate_teleport_trajectory(start_pos, direction) + self._show_teleport_visuals() + + def cleanup(self): + """清理传送系统资源""" + try: + self.stop_teleport_preview() + + if self.teleport_arc_node: + self.teleport_arc_node.removeNode() + self.teleport_arc_node = None + + if self.teleport_target_node: + self.teleport_target_node.removeNode() + self.teleport_target_node = None + + if self.teleport_invalid_node: + self.teleport_invalid_node.removeNode() + self.teleport_invalid_node = None + + self.ignoreAll() + + print("🧹 VR传送系统已清理") + + except Exception as e: + print(f"⚠️ 清理传送系统失败: {e}") \ No newline at end of file diff --git a/core/vr_visualization.py b/core/vr_visualization.py new file mode 100644 index 00000000..c331a299 --- /dev/null +++ b/core/vr_visualization.py @@ -0,0 +1,729 @@ +""" +VR可视化模块 + +提供VR手柄和交互元素的高级可视化功能: +- 手柄3D模型渲染 +- 交互射线显示 +- 按钮状态可视化 +- 触摸板和扳机反馈 +""" + +from panda3d.core import ( + NodePath, GeomNode, LineSegs, CardMaker, Geom, GeomVertexData, + GeomVertexFormat, GeomVertexWriter, GeomTriangles, GeomPoints, + Vec3, Vec4, Mat4, TransparencyAttrib, RenderState, ColorAttrib, + InternalName, loadPrcFileData +) +from panda3d.core import Texture, Material, TextureStage + +# 启用Assimp支持OBJ文件加载 +loadPrcFileData("", "load-file-type p3assimp") + + +class VRControllerVisualizer: + """VR手柄可视化器""" + + def __init__(self, controller, render_node): + """初始化手柄可视化器 + + Args: + controller: VRController实例 + render_node: 渲染节点 + """ + self.controller = controller + self.render = render_node + + # 可视化节点 + self.visual_node = None + self.model_node = None + self.ray_node = None + self.button_indicator_node = None + + # 射线参数 + self.ray_length = 10.0 + self.ray_color = Vec4(0.9, 0.9, 0.2, 0.8) + self.ray_hit_color = Vec4(0.2, 0.9, 0.2, 0.8) + + # 按钮指示器参数 + self.button_colors = { + 'normal': Vec4(0.3, 0.3, 0.8, 1.0), + 'left': Vec4(0.2, 0.6, 0.9, 1.0), + 'right': Vec4(0.9, 0.3, 0.3, 1.0), + 'trigger': Vec4(0.9, 0.6, 0.2, 1.0), + 'grip': Vec4(0.6, 0.9, 0.3, 1.0), + 'menu': Vec4(0.9, 0.2, 0.9, 1.0), + 'trackpad': Vec4(0.2, 0.9, 0.9, 1.0) + } + + self._create_visual_components() + + def _create_visual_components(self): + """创建可视化组件""" + if not self.controller.anchor_node: + return + + # 创建主可视化节点 + self.visual_node = self.controller.anchor_node.attachNewNode(f'{self.controller.name}_visual') + + # 创建手柄模型 + self._create_controller_model() + + # 创建交互射线 + self._create_interaction_ray() + + # 暂时注释按钮指示器功能,避免额外几何体造成悬空零件 + # self._create_button_indicators() + + def _create_controller_model(self): + """创建手柄3D模型""" + if not self.visual_node: + return + + # 创建模型节点 + self.model_node = self.visual_node.attachNewNode(f'{self.controller.name}_model') + + # 尝试加载SteamVR官方模型 + steamvr_model = self._load_steamvr_model() + + if steamvr_model: + # 使用SteamVR官方模型 + steamvr_model.reparentTo(self.model_node) + + # 应用SteamVR配置中的正确旋转值,绕Y轴(俯仰轴)旋转90度 + # body组件的rotate_xyz: [5.037,0.0,0.0],再加上绕Y轴旋转90度 + # 右手正确,左手需要反向 + if self.controller.name == 'left': + # 左手控制器:绕Y轴俯仰+90度(修正反向) + steamvr_model.setHpr(0, 5.037 + 90, 0) + else: + # 右手控制器:绕Y轴俯仰+90度(保持不变) + steamvr_model.setHpr(0, 5.037 + 90, 0) + + # 设置合适的缩放值 + steamvr_model.setScale(1.0) + + # 打印实际应用的旋转值 + if self.controller.name == 'left': + print(f"🔧 {self.controller.name}手柄:缩放: 1.0,旋转: (0, {5.037 + 90}, 0) [Y轴俯仰+90度]") + else: + print(f"🔧 {self.controller.name}手柄:缩放: 1.0,旋转: (0, {5.037 + 90}, 0) [Y轴俯仰+90度]") + + # 修复纯黑色问题:重新设置材质属性 + self._fix_model_material(steamvr_model) + + # 暂时注释身份标记功能,避免额外几何体造成悬空零件 + # self._apply_controller_identity_marker(steamvr_model) + + # 设置手柄始终显示在上层 + self._set_always_on_top(steamvr_model) + + print(f"✅ {self.controller.name}手柄已加载SteamVR官方模型(缩放: 1.0,实体渲染模式)") + else: + # 降级到改进的程序化模型 + self._create_fallback_model() + print(f"⚠️ {self.controller.name}手柄使用程序化模型(未找到SteamVR模型)") + + def _load_steamvr_model(self): + """加载SteamVR官方手柄模型""" + import os + from panda3d.core import Filename, Texture, TextureStage + + # SteamVR模型基础路径 + steamvr_base_paths = [ + "/home/hello/.local/share/Steam/steamapps/common/SteamVR/resources/rendermodels/vr_controller_vive_1_5", + "/home/hello/.steam/steam/steamapps/common/SteamVR/resources/rendermodels/vr_controller_vive_1_5", + "~/.local/share/Steam/steamapps/common/SteamVR/resources/rendermodels/vr_controller_vive_1_5" + ] + + for base_path in steamvr_base_paths: + expanded_base_path = os.path.expanduser(base_path) + if os.path.exists(expanded_base_path): + print(f"🔍 找到SteamVR模型目录: {expanded_base_path}") + + # 不再添加目录到搜索路径,避免自动加载多余组件 + # from panda3d.core import getModelPath + # getModelPath().appendDirectory(expanded_base_path) + + # 尝试加载不同的模型文件,按优先级排序 + model_files = [ + ("body.obj", "手柄主体"), # 最重要的部分 + ("vr_controller_vive_1_5.obj", "完整手柄模型"), # 组合模型 + ] + + for model_file, description in model_files: + model_path = os.path.join(expanded_base_path, model_file) + if os.path.exists(model_path): + try: + print(f"🎮 尝试加载{description}: {model_file}") + + # 加载主模型 + model = loader.loadModel(Filename.fromOsSpecific(model_path)) + if model: + # 先应用纹理,再修复材质(保持纹理效果) + self._apply_steamvr_textures(model, expanded_base_path) + print(f"✅ 成功加载{description}") + return model + else: + print(f"⚠️ 模型文件存在但加载失败: {model_file}") + + except Exception as e: + print(f"❌ 加载{description}失败: {e}") + continue + + # 不再尝试组合加载多个部件,避免悬空零件问题 + print("⚠️ 单个模型文件加载失败,跳过组合加载以避免悬空零件") + break + + print("❌ 未找到任何SteamVR模型目录") + return None + + def _fix_model_material(self, model): + """修复模型材质,使用RenderPipeline兼容的新Material API""" + from panda3d.core import Material, Vec4 + + # 检查模型是否有纹理 + has_texture = model.hasTexture() + + # 创建新的材质,使用RenderPipeline兼容的API + material = Material() + + if has_texture: + # 有纹理时,设置白色基础颜色让纹理完全显示 + material.setBaseColor(Vec4(1.0, 1.0, 1.0, 1.0)) # 白色让纹理完全显示 + material.setRoughness(0.4) # 中等粗糙度 + material.setMetallic(0.3) # 轻度金属感 + material.setRefractiveIndex(1.5) # 标准折射率 + print(f"🎨 {self.controller.name}手柄:已设置材质(纹理+PBR)") + else: + # 无纹理时,设置手柄颜色 + material.setBaseColor(Vec4(0.7, 0.7, 0.8, 1.0)) # 略偏蓝的灰色 + material.setRoughness(0.5) # 中等粗糙度 + material.setMetallic(0.2) # 轻度金属感 + material.setRefractiveIndex(1.5) # 标准折射率 + print(f"🎨 {self.controller.name}手柄:已设置材质(颜色+PBR)") + + # 应用材质到模型,使用优先级1确保覆盖默认材质 + model.setMaterial(material, 1) + + # 确保模型能正确渲染(双面渲染是NodePath的方法) + model.setTwoSided(False) + + print(f"🔧 {self.controller.name}手柄:已使用RenderPipeline兼容的Material API") + + def _apply_controller_identity_marker(self, model): + """为控制器添加身份标记,区分左右手""" + from panda3d.core import RenderModeAttrib + + # 创建一个小的标识几何体 + marker_geom = self._create_box_geometry(0.005, 0.005, 0.02) + marker_node = model.attachNewNode(marker_geom) + + # 根据左右手设置不同位置和颜色(现在都是Y轴+90度俯仰) + if self.controller.name == 'left': + # 左手控制器:左侧标记 + marker_node.setPos(-0.03, 0.05, 0.02) + marker_node.setColor(0.2, 0.4, 1.0, 1.0) # 蓝色 + print(f"🔵 {self.controller.name}手柄已添加蓝色身份标记") + else: + # 右手控制器:右侧标记 + marker_node.setPos(0.03, 0.05, 0.02) + marker_node.setColor(1.0, 0.2, 0.2, 1.0) # 红色 + print(f"🔴 {self.controller.name}手柄已添加红色身份标记") + + # 让标记发光以便更容易看到 + marker_node.setLightOff() # 不受光照影响,保持明亮 + + # 添加轻微的色彩调整(非常微弱,不影响主要纹理) + if self.controller.name == 'left': + model.setColorScale(0.98, 0.98, 1.02, 1.0) # 极轻微的蓝色调 + else: + model.setColorScale(1.02, 0.98, 0.98, 1.0) # 极轻微的红色调 + + + def _apply_steamvr_textures(self, model, base_path): + """为SteamVR模型应用纹理""" + import os + from panda3d.core import Texture, TextureStage + + # SteamVR纹理文件 + texture_files = { + 'diffuse': 'onepointfive_texture.png', + 'specular': 'onepointfive_spec.png' + } + + textures_applied = 0 + + for texture_type, texture_file in texture_files.items(): + texture_path = os.path.join(base_path, texture_file) + if os.path.exists(texture_path): + try: + texture = loader.loadTexture(texture_path) + if texture: + # 确保纹理能正确加载 + texture.setWrapU(Texture.WMClamp) + texture.setWrapV(Texture.WMClamp) + texture.setMinfilter(Texture.FTLinearMipmapLinear) + texture.setMagfilter(Texture.FTLinear) + + if texture_type == 'diffuse': + # 应用主要漫反射纹理 + model.setTexture(texture) + print(f"✅ 应用了主纹理: {texture_file}") + textures_applied += 1 + elif texture_type == 'specular': + # 应用高光纹理 + ts = TextureStage('specular') + ts.setMode(TextureStage.MModulate) + model.setTexture(ts, texture) + print(f"✅ 应用了高光纹理: {texture_file}") + textures_applied += 1 + except Exception as e: + print(f"⚠️ 应用纹理失败 {texture_file}: {e}") + + if textures_applied == 0: + print(f"⚠️ {self.controller.name}手柄未能加载任何纹理,将使用材质颜色") + else: + print(f"🎨 {self.controller.name}手柄成功应用了 {textures_applied} 个纹理") + + def _load_combined_steamvr_model(self, base_path): + """尝试组合加载多个SteamVR模型部件""" + import os + from panda3d.core import NodePath + + # 重要的模型部件 + important_components = [ + "body.obj", + "trigger.obj", + "trackpad.obj", + "l_grip.obj" if self.controller.name == 'left' else "r_grip.obj" + ] + + combined_model = NodePath("combined_controller") + has_components = False + + for component in important_components: + component_path = os.path.join(base_path, component) + if os.path.exists(component_path): + try: + part = loader.loadModel(component_path) + if part: + part.reparentTo(combined_model) + has_components = True + print(f"✅ 加载了部件: {component}") + except Exception as e: + print(f"⚠️ 加载部件失败 {component}: {e}") + + if has_components: + self._apply_steamvr_textures(combined_model, base_path) + return combined_model + + return None + + def _create_fallback_model(self): + """创建改进的程序化手柄模型作为后备方案""" + # 主体(长条形状) + main_body = self._create_box_geometry(0.025, 0.15, 0.04) + main_node = self.model_node.attachNewNode(main_body) + + # 根据左右手设置不同颜色 + if self.controller.name == 'left': + color = self.button_colors['left'] + else: + color = self.button_colors['right'] + + main_node.setColor(color) + + # 启用光照响应 + from panda3d.core import RenderState, MaterialAttrib, Material + material = Material() + material.setShininess(32) + material.setAmbient((0.2, 0.2, 0.2, 1)) + material.setDiffuse(color) + material.setSpecular((0.5, 0.5, 0.5, 1)) + main_node.setMaterial(material) + + # 扳机区域(小突起) + trigger = self._create_box_geometry(0.015, 0.03, 0.02) + trigger_node = self.model_node.attachNewNode(trigger) + trigger_node.setPos(0, -0.08, 0.03) + trigger_node.setColor(self.button_colors['trigger']) + trigger_node.setMaterial(material) + + # 握把区域 + grip = self._create_box_geometry(0.02, 0.06, 0.03) + grip_node = self.model_node.attachNewNode(grip) + grip_node.setPos(0, 0.05, -0.03) + grip_node.setColor(self.button_colors['grip']) + grip_node.setMaterial(material) + + # 触摸板区域(圆盘) + trackpad = self._create_disc_geometry(0.015, 0.005) + trackpad_node = self.model_node.attachNewNode(trackpad) + trackpad_node.setPos(0, -0.02, 0.04) + trackpad_node.setColor(self.button_colors['trackpad']) + trackpad_node.setMaterial(material) + + def _create_box_geometry(self, width, length, height): + """创建立方体几何体""" + # 创建顶点格式 + format = GeomVertexFormat.getV3n3() + vdata = GeomVertexData('box', format, Geom.UHStatic) + + vertex = GeomVertexWriter(vdata, 'vertex') + normal = GeomVertexWriter(vdata, 'normal') + + # 定义立方体的8个顶点 + vertices = [ + Vec3(-width/2, -length/2, -height/2), + Vec3( width/2, -length/2, -height/2), + Vec3( width/2, length/2, -height/2), + Vec3(-width/2, length/2, -height/2), + Vec3(-width/2, -length/2, height/2), + Vec3( width/2, -length/2, height/2), + Vec3( width/2, length/2, height/2), + Vec3(-width/2, length/2, height/2) + ] + + # 立方体的6个面,每个面4个顶点 + faces = [ + # 底面 (z = -height/2) + [0, 1, 2, 3, Vec3(0, 0, -1)], + # 顶面 (z = height/2) + [7, 6, 5, 4, Vec3(0, 0, 1)], + # 前面 (y = -length/2) + [4, 5, 1, 0, Vec3(0, -1, 0)], + # 后面 (y = length/2) + [3, 2, 6, 7, Vec3(0, 1, 0)], + # 左面 (x = -width/2) + [0, 3, 7, 4, Vec3(-1, 0, 0)], + # 右面 (x = width/2) + [5, 6, 2, 1, Vec3(1, 0, 0)] + ] + + # 添加顶点和法线 + for face in faces: + for i in range(4): + vertex.addData3(vertices[face[i]]) + normal.addData3(face[4]) # 法线向量 + + # 创建几何体 + geom = Geom(vdata) + + # 为每个面创建三角形 + for face_idx in range(6): + base_idx = face_idx * 4 + prim = GeomTriangles(Geom.UHStatic) + # 第一个三角形 + prim.addVertices(base_idx, base_idx + 1, base_idx + 2) + # 第二个三角形 + prim.addVertices(base_idx, base_idx + 2, base_idx + 3) + geom.addPrimitive(prim) + + # 创建几何体节点 + geom_node = GeomNode('box') + geom_node.addGeom(geom) + + return geom_node + + def _create_disc_geometry(self, radius, thickness): + """创建圆盘几何体(用于触摸板)""" + format = GeomVertexFormat.getV3n3() + vdata = GeomVertexData('disc', format, Geom.UHStatic) + + vertex = GeomVertexWriter(vdata, 'vertex') + normal = GeomVertexWriter(vdata, 'normal') + + # 创建圆盘顶点 + segments = 16 + import math + + # 中心点 + vertex.addData3(0, 0, thickness/2) + normal.addData3(0, 0, 1) + + # 圆周点 + for i in range(segments): + angle = 2 * math.pi * i / segments + x = radius * math.cos(angle) + y = radius * math.sin(angle) + + vertex.addData3(x, y, thickness/2) + normal.addData3(0, 0, 1) + + # 创建几何体 + geom = Geom(vdata) + prim = GeomTriangles(Geom.UHStatic) + + # 创建扇形三角形 + for i in range(segments): + next_i = (i + 1) % segments + prim.addVertices(0, i + 1, next_i + 1) + + geom.addPrimitive(prim) + + # 创建几何体节点 + geom_node = GeomNode('disc') + geom_node.addGeom(geom) + + return geom_node + + def _create_interaction_ray(self): + """创建交互射线""" + if not self.visual_node: + return + + # 创建射线几何 + line_segs = LineSegs() + line_segs.setThickness(3) + line_segs.setColor(self.ray_color) + + # 射线主体 + line_segs.moveTo(0, 0, 0) + line_segs.drawTo(0, self.ray_length, 0) + + # 射线端点(小球) + end_point = self._create_sphere_geometry(0.02) + + # 创建射线节点 + geom_node = line_segs.create() + self.ray_node = self.visual_node.attachNewNode(geom_node) + + # 添加端点球 + end_node = self.ray_node.attachNewNode(end_point) + end_node.setPos(0, self.ray_length, 0) + end_node.setColor(self.ray_color) + + # 设置透明度 + self.ray_node.setTransparency(TransparencyAttrib.MAlpha) + + # 使用RenderPipeline兼容的Material API设置射线材质 + from panda3d.core import Material, Vec4 + ray_material = Material() + ray_material.setBaseColor(Vec4(self.ray_color.x, self.ray_color.y, self.ray_color.z, self.ray_color.w)) + ray_material.setRoughness(0.1) # 光滑射线 + ray_material.setMetallic(0.0) # 非金属 + ray_material.setRefractiveIndex(1.5) # 标准折射率 + + # 为射线应用材质 + self.ray_node.setMaterial(ray_material, 1) + self.ray_node.setTwoSided(False) + + # 为端点球设置相同的材质 + end_material = Material() + end_material.setBaseColor(Vec4(self.ray_color.x, self.ray_color.y, self.ray_color.z, self.ray_color.w)) + end_material.setRoughness(0.2) # 略粗糙 + end_material.setMetallic(0.0) # 非金属 + end_material.setRefractiveIndex(1.5) # 标准折射率 + + # 为端点球应用材质 + end_node.setMaterial(end_material, 1) + end_node.setTwoSided(False) + + # 默认隐藏射线 + self.ray_node.hide() + + print(f"✓ {self.controller.name}手柄交互射线已创建(含PBR支持)") + + def _create_sphere_geometry(self, radius): + """创建球体几何体""" + # 简单的立方体作为球体替代 + return self._create_box_geometry(radius, radius, radius) + + def _create_button_indicators(self): + """创建按钮状态指示器""" + if not self.visual_node: + return + + # 创建按钮指示器容器 + self.button_indicator_node = self.visual_node.attachNewNode(f'{self.controller.name}_indicators') + + # 扳机指示器 + trigger_indicator = self._create_box_geometry(0.005, 0.01, 0.005) + self.trigger_indicator = self.button_indicator_node.attachNewNode(trigger_indicator) + self.trigger_indicator.setPos(0.02, -0.08, 0.03) + self.trigger_indicator.setColor(0.2, 0.2, 0.2, 1.0) + + # 握把指示器 + grip_indicator = self._create_box_geometry(0.005, 0.02, 0.005) + self.grip_indicator = self.button_indicator_node.attachNewNode(grip_indicator) + self.grip_indicator.setPos(-0.02, 0.05, -0.03) + self.grip_indicator.setColor(0.2, 0.2, 0.2, 1.0) + + # 触摸板指示器 + trackpad_indicator = self._create_disc_geometry(0.003, 0.002) + self.trackpad_indicator = self.button_indicator_node.attachNewNode(trackpad_indicator) + self.trackpad_indicator.setPos(0, -0.02, 0.045) + self.trackpad_indicator.setColor(0.2, 0.2, 0.2, 1.0) + + # 为所有按钮指示器设置RenderPipeline兼容的材质 + from panda3d.core import Material, Vec4 + indicator_material = Material() + indicator_material.setBaseColor(Vec4(0.2, 0.2, 0.2, 1.0)) # 深灰色 + indicator_material.setRoughness(0.8) # 比较粗糙的表面 + indicator_material.setMetallic(0.1) # 轻微金属感 + indicator_material.setRefractiveIndex(1.5) # 标准折射率 + + # 为所有指示器应用材质 + for indicator in [self.trigger_indicator, self.grip_indicator, self.trackpad_indicator]: + indicator.setMaterial(indicator_material, 1) + indicator.setTwoSided(False) + + print(f"✓ {self.controller.name}手柄按钮指示器已创建") + + def update(self): + """更新可视化状态""" + if not self.controller.is_connected: + self.hide() + return + + self.show() + + # 更新按钮指示器状态 + self._update_button_indicators() + + # 更新射线显示状态 + self._update_ray_display() + + def _update_button_indicators(self): + """更新按钮指示器状态""" + if not hasattr(self, 'trigger_indicator'): + return + + # 扳机指示器 + if self.controller.is_trigger_pressed(): + self.trigger_indicator.setColor(self.button_colors['trigger']) + # 根据扳机值调整位置 + trigger_offset = self.controller.trigger_value * 0.01 + self.trigger_indicator.setPos(0.02, -0.08 + trigger_offset, 0.03) + else: + self.trigger_indicator.setColor(0.2, 0.2, 0.2, 1.0) + self.trigger_indicator.setPos(0.02, -0.08, 0.03) + + # 握把指示器 + if self.controller.is_grip_pressed(): + self.grip_indicator.setColor(self.button_colors['grip']) + else: + self.grip_indicator.setColor(0.2, 0.2, 0.2, 1.0) + + # 触摸板指示器 + if self.controller.touchpad_touched: + self.trackpad_indicator.setColor(self.button_colors['trackpad']) + # 根据触摸位置调整指示器位置 + if hasattr(self.controller, 'touchpad_pos'): + offset_x = self.controller.touchpad_pos.x * 0.01 + offset_y = self.controller.touchpad_pos.y * 0.01 + self.trackpad_indicator.setPos(offset_x, -0.02 + offset_y, 0.045) + else: + self.trackpad_indicator.setColor(0.2, 0.2, 0.2, 1.0) + self.trackpad_indicator.setPos(0, -0.02, 0.045) + + def _update_ray_display(self): + """更新射线显示""" + if not self.ray_node: + return + + # 根据交互状态显示/隐藏射线 + # 这里可以添加更复杂的逻辑,比如只在指向对象时显示 + show_ray = (self.controller.is_trigger_pressed(threshold=0.1) or + self.controller.touchpad_touched) + + if show_ray: + self.show_ray() + else: + self.hide_ray() + + def show(self): + """显示手柄可视化""" + if self.visual_node: + self.visual_node.show() + + def hide(self): + """隐藏手柄可视化""" + if self.visual_node: + self.visual_node.hide() + + def show_ray(self): + """显示交互射线""" + if self.ray_node: + self.ray_node.show() + + def hide_ray(self): + """隐藏交互射线""" + if self.ray_node: + self.ray_node.hide() + + def set_ray_color(self, color): + """设置射线颜色""" + if self.ray_node: + self.ray_node.setColor(color) + + # 更新射线材质的baseColor + from panda3d.core import Material, Vec4 + if isinstance(color, (list, tuple)) and len(color) >= 3: + alpha = color[3] if len(color) > 3 else 0.8 + ray_color = Vec4(color[0], color[1], color[2], alpha) + elif isinstance(color, Vec4): + ray_color = color + else: + ray_color = Vec4(0.9, 0.9, 0.2, 0.8) # 默认黄色 + + # 更新射线材质 + ray_material = Material() + ray_material.setBaseColor(ray_color) + ray_material.setRoughness(0.1) + ray_material.setMetallic(0.0) + ray_material.setRefractiveIndex(1.5) + self.ray_node.setMaterial(ray_material, 1) + + def set_ray_length(self, length): + """设置射线长度""" + self.ray_length = length + # 重新创建射线(简单的实现) + if self.ray_node: + self.ray_node.removeNode() + self._create_interaction_ray() + + def _set_always_on_top(self, model_node): + """设置手柄模型始终显示在上层,不被其他物体遮挡""" + if not model_node: + return + + from panda3d.core import RenderState + + # 设置为固定渲染bin,优先级设为较高值(1000) + # fixed bin中的对象按sort值从小到大渲染,越大越后渲染(越在上层) + model_node.setBin("fixed", 1000) + + # 禁用深度测试和深度写入,确保始终可见 + model_node.setDepthTest(False) + model_node.setDepthWrite(False) + + # 递归设置所有子节点的渲染属性 + for child in model_node.findAllMatches("**"): + child.setBin("fixed", 1000) + child.setDepthTest(False) + child.setDepthWrite(False) + + print(f"🔝 {self.controller.name}手柄已设置为始终显示在上层") + + def cleanup(self): + """清理资源""" + # 清理射线节点 + if hasattr(self, 'ray_node') and self.ray_node: + self.ray_node.removeNode() + self.ray_node = None + print(f"🧹 {self.controller.name}手柄射线已清理") + + # 清理模型节点 + if hasattr(self, 'model_node') and self.model_node: + self.model_node.removeNode() + self.model_node = None + print(f"🧹 {self.controller.name}手柄模型已清理") + + # 清理主可视化节点 + if self.visual_node: + self.visual_node.removeNode() + self.visual_node = None + print(f"🧹 {self.controller.name}手柄主节点已清理") + + print(f"✅ {self.controller.name}手柄可视化已彻底清理") \ No newline at end of file diff --git a/core/world.py b/core/world.py index 0f4ec28a..696161c2 100644 --- a/core/world.py +++ b/core/world.py @@ -323,8 +323,8 @@ class CoreWorld(Panda3DWorld): self.ground.setP(-90) self.ground.setZ(-1.0) self.ground.setColor(0.8, 0.8, 0.8, 1) - # self.ground.setTag("is_scene_element", "1") - # self.ground.setTag("tree_item_type", "SCENE_NODE") + self.ground.setTag("is_scene_element", "1") + self.ground.setTag("tree_item_type", "SCENE_NODE") # 创建支持贴图的材质 mat = Material() @@ -335,46 +335,56 @@ class CoreWorld(Panda3DWorld): mat.set_metallic(0.5) # 设置较低的初始金属性 self.ground.set_material(mat) - #创建第二个相同的地面,位置稍有偏移 - self.ground2 = self.render.attachNewNode(cm.generate()) - self.ground2.setH(-90) - self.ground2.setZ(-1.0) - self.ground2.setX(50) # 在X轴方向偏移 - self.ground2.setZ(49) # 在X轴方向偏移 - self.ground2.setColor(0.8, 0.8, 0.8, 1) - self.ground2.set_material(mat) - - # 创建第三个相同的地面,位置在另一个方向 - self.ground3 = self.render.attachNewNode(cm.generate()) - self.ground3.setH(90) - self.ground3.setZ(-1.0) - self.ground3.setX(-50) # 在X轴负方向偏移 - self.ground3.setZ(49) # 在X轴负方向偏移 - self.ground3.setColor(0.8, 0.8, 0.8, 1) - self.ground3.set_material(mat) - - self.ground4 = self.render.attachNewNode(cm.generate()) - # self.ground3.setR(90) - self.ground4.setZ(-1.0) - self.ground4.setY(50) # 在X轴负方向偏移 - self.ground4.setZ(49) # 在X轴负方向偏移 - self.ground4.setColor(0.8, 0.8, 0.8, 1) - self.ground4.set_material(mat) - - self.ground5 = self.render.attachNewNode(cm.generate()) - self.ground5.setP(180) - self.ground5.setZ(-1) - self.ground5.setY(-50) # 在X轴负方向偏移 - self.ground5.setZ(49) # 在X轴负方向偏移 - self.ground5.setColor(0.8, 0.8, 0.8, 1) - self.ground5.set_material(mat) - - self.ground6 = self.render.attachNewNode(cm.generate()) - self.ground6.setP(90) - self.ground6.setZ(-1) - self.ground6.setZ(99) # 在X轴负方向偏移 - self.ground6.setColor(0.8, 0.8, 0.8, 1) - self.ground6.set_material(mat) + # #创建第二个相同的地面,位置稍有偏移 + # self.ground2 = self.render.attachNewNode(cm.generate()) + # self.ground2.setH(-90) + # self.ground2.setZ(-1.0) + # self.ground2.setX(50) # 在X轴方向偏移 + # self.ground2.setZ(49) # 在X轴方向偏移 + # self.ground2.setColor(0.8, 0.8, 0.8, 1) + # self.ground2.set_material(mat) + # self.ground2.setTag("is_scene_element", "1") + # self.ground2.setTag("tree_item_type", "SCENE_NODE") + # + # # 创建第三个相同的地面,位置在另一个方向 + # self.ground3 = self.render.attachNewNode(cm.generate()) + # self.ground3.setH(90) + # self.ground3.setZ(-1.0) + # self.ground3.setX(-50) # 在X轴负方向偏移 + # self.ground3.setZ(49) # 在X轴负方向偏移 + # self.ground3.setColor(0.8, 0.8, 0.8, 1) + # self.ground3.set_material(mat) + # self.ground3.setTag("is_scene_element", "1") + # self.ground3.setTag("tree_item_type", "SCENE_NODE") + # + # self.ground4 = self.render.attachNewNode(cm.generate()) + # # self.ground3.setR(90) + # self.ground4.setZ(-1.0) + # self.ground4.setY(50) # 在X轴负方向偏移 + # self.ground4.setZ(49) # 在X轴负方向偏移 + # self.ground4.setColor(0.8, 0.8, 0.8, 1) + # self.ground4.set_material(mat) + # self.ground4.setTag("is_scene_element", "1") + # self.ground4.setTag("tree_item_type", "SCENE_NODE") + # + # self.ground5 = self.render.attachNewNode(cm.generate()) + # self.ground5.setP(180) + # self.ground5.setZ(-1) + # self.ground5.setY(-50) # 在X轴负方向偏移 + # self.ground5.setZ(49) # 在X轴负方向偏移 + # self.ground5.setColor(0.8, 0.8, 0.8, 1) + # self.ground5.set_material(mat) + # self.ground5.setTag("is_scene_element", "1") + # self.ground5.setTag("tree_item_type", "SCENE_NODE") + # + # self.ground6 = self.render.attachNewNode(cm.generate()) + # self.ground6.setP(90) + # self.ground6.setZ(-1) + # self.ground6.setZ(99) # 在X轴负方向偏移 + # self.ground6.setColor(0.8, 0.8, 0.8, 1) + # self.ground6.set_material(mat) + # self.ground6.setTag("is_scene_element", "1") + # self.ground6.setTag("tree_item_type", "SCENE_NODE") # 应用默认PBR效果,确保支持贴图 try: @@ -392,73 +402,73 @@ class CoreWorld(Panda3DWorld): }, 50 ) - # 为其他两个地面也应用相同的效果 - self.render_pipeline.set_effect( - self.ground2, - "effects/default.yaml", - { - "normal_mapping": True, - "render_gbuffer": True, - "alpha_testing": False, - "parallax_mapping": False, - "render_shadow": True, - "render_envmap": True - }, - 50 - ) - self.render_pipeline.set_effect( - self.ground3, - "effects/default.yaml", - { - "normal_mapping": True, - "render_gbuffer": True, - "alpha_testing": False, - "parallax_mapping": False, - "render_shadow": True, - "render_envmap": True - }, - 50 - ) - self.render_pipeline.set_effect( - self.ground4, - "effects/default.yaml", - { - "normal_mapping": True, - "render_gbuffer": True, - "alpha_testing": False, - "parallax_mapping": False, - "render_shadow": True, - "render_envmap": True - }, - 50 - ) - self.render_pipeline.set_effect( - self.ground5, - "effects/default.yaml", - { - "normal_mapping": True, - "render_gbuffer": True, - "alpha_testing": False, - "parallax_mapping": False, - "render_shadow": True, - "render_envmap": True - }, - 50 - ) - self.render_pipeline.set_effect( - self.ground6, - "effects/default.yaml", - { - "normal_mapping": True, - "render_gbuffer": True, - "alpha_testing": False, - "parallax_mapping": False, - "render_shadow": True, - "render_envmap": True - }, - 50 - ) - print("✓ 地板PBR效果已应用") + # # 为其他两个地面也应用相同的效果 + # self.render_pipeline.set_effect( + # self.ground2, + # "effects/default.yaml", + # { + # "normal_mapping": True, + # "render_gbuffer": True, + # "alpha_testing": False, + # "parallax_mapping": False, + # "render_shadow": True, + # "render_envmap": True + # }, + # 50 + # ) + # self.render_pipeline.set_effect( + # self.ground3, + # "effects/default.yaml", + # { + # "normal_mapping": True, + # "render_gbuffer": True, + # "alpha_testing": False, + # "parallax_mapping": False, + # "render_shadow": True, + # "render_envmap": True + # }, + # 50 + # ) + # self.render_pipeline.set_effect( + # self.ground4, + # "effects/default.yaml", + # { + # "normal_mapping": True, + # "render_gbuffer": True, + # "alpha_testing": False, + # "parallax_mapping": False, + # "render_shadow": True, + # "render_envmap": True + # }, + # 50 + # ) + # self.render_pipeline.set_effect( + # self.ground5, + # "effects/default.yaml", + # { + # "normal_mapping": True, + # "render_gbuffer": True, + # "alpha_testing": False, + # "parallax_mapping": False, + # "render_shadow": True, + # "render_envmap": True + # }, + # 50 + # ) + # self.render_pipeline.set_effect( + # self.ground6, + # "effects/default.yaml", + # { + # "normal_mapping": True, + # "render_gbuffer": True, + # "alpha_testing": False, + # "parallax_mapping": False, + # "render_shadow": True, + # "render_envmap": True + # }, + # 50 + # ) + # print("✓ 地板PBR效果已应用") else: print("⚠️ RenderPipeline未初始化,地板将使用基础渲染") except Exception as e: @@ -547,12 +557,32 @@ class CoreWorld(Panda3DWorld): self.mouseRightPressed = True self.lastMouseX = evt['x'] self.lastMouseY = evt['y'] + # + # # 通过 Qt 窗口隐藏光标并捕获鼠标 + # try: + # if hasattr(self, 'qtWidget') and self.qtWidget: + # from PyQt5.QtCore import Qt + # self.qtWidget.setCursor(Qt.BlankCursor) + # # 捕获鼠标,使其无法离开窗口 + # self.qtWidget.grabMouse() + # except Exception as e: + # print(f"通过 Qt 隐藏光标时出错: {e}") def mouseReleaseEventRight(self, evt): """处理鼠标右键释放事件""" #print("右键释放") self.mouseRightPressed = False + # # 恢复 Qt 窗口光标并释放鼠标捕获 + # try: + # if hasattr(self, 'qtWidget') and self.qtWidget: + # from PyQt5.QtCore import Qt + # self.qtWidget.unsetCursor() # 恢复默认光标 + # # 释放鼠标捕获 + # self.qtWidget.releaseMouse() + # except Exception as e: + # print(f"恢复 Qt 光标时出错: {e}") + def mouseMoveEvent(self, evt): """处理鼠标移动事件 - 只处理相机旋转""" if not evt: diff --git a/demo/VR_ALVR_实现指南.md b/demo/VR_ALVR_实现指南.md deleted file mode 100644 index b788f31e..00000000 --- a/demo/VR_ALVR_实现指南.md +++ /dev/null @@ -1,374 +0,0 @@ -# VR + ALVR 串流实现指南 - -## 🎯 概述 - -本指南详细介绍了如何在Panda3D引擎中实现VR支持,并通过ALVR串流到Quest等VR头显设备。 - -## 🏗️ 系统架构 - -### 核心组件 - -1. **VRManager** (`core/vr_manager.py`) - - 负责VR系统初始化 - - 管理立体渲染(左右眼) - - 处理VR设备跟踪 - - 集成OpenVR接口 - -2. **VRInputHandler** (`core/vr_input_handler.py`) - - 处理VR控制器输入 - - 手势识别系统 - - 交互逻辑处理 - -3. **ALVRStreamer** (`core/alvr_streamer.py`) - - ALVR服务器通信 - - 视频流编码和传输 - - 性能监控 - -4. **VRControlPanel** (`ui/vr_control_panel.py`) - - VR系统GUI控制界面 - - 实时状态监控 - - 参数调整面板 - -## 🔧 安装和配置 - -### 1. 系统依赖 - -```bash -# 安装Python依赖 -pip install -r requirements/vr-requirements.txt - -# 安装OpenVR运行时 -# Windows: 下载并安装SteamVR -# Linux: -sudo apt-get install steam -# 然后在Steam中安装SteamVR -``` - -### 2. ALVR服务器设置 - -```bash -# 下载ALVR服务器 -# 从 https://github.com/alvr-org/ALVR 下载最新版本 - -# Windows -# 1. 解压到 C:\ALVR\ -# 2. 运行 alvr_server.exe - -# Linux -# 1. 解压到 /usr/local/bin/ -# 2. 运行 alvr_server -``` - -### 3. Quest设备配置 - -```bash -# 1. 在Quest上安装ALVR客户端 -# 2. 启用开发者模式 -# 3. 连接到同一WiFi网络 -# 4. 配置防火墙允许ALVR通信(端口9943-9944) -``` - -## 🚀 使用方法 - -### 快速启动 - -```python -# 在主程序中 -world = MyWorld() - -# 一键启用VR模式 -if world.enableVRMode(): - print("VR模式已启用") - - # 检查VR状态 - status = world.getVRStatus() - print(f"VR设备: {status['vr_info']}") - print(f"ALVR连接: {status['alvr_connected']}") - print(f"串流状态: {status['alvr_streaming']}") -``` - -### 详细控制 - -```python -# 分步骤启动VR -world = MyWorld() - -# 1. 初始化VR系统 -if world.initializeVR(): - print("VR系统初始化成功") - - # 2. 启动VR输入处理 - world.startVRInput() - - # 3. 显示控制器射线 - world.showControllerRays(True) - - # 4. 初始化ALVR串流 - if world.initializeALVR(): - print("ALVR初始化成功") - - # 5. 设置串流质量 - world.setALVRStreamQuality( - width=2880, - height=1700, - fps=72, - bitrate=150 - ) - - # 6. 开始串流 - world.startALVRStreaming() -``` - -### GUI控制界面 - -```python -# 创建VR控制面板 -from ui.vr_control_panel import VRControlPanel - -vr_panel = VRControlPanel(world) -vr_panel.show() - -# 面板功能: -# - 一键启用/禁用VR -# - ALVR串流控制 -# - 质量参数调整 -# - 实时性能监控 -# - 控制器状态显示 -``` - -## 🎮 VR交互功能 - -### 控制器输入处理 - -```python -# 获取控制器状态 -controllers = world.getAllControllers() -for controller_id in controllers: - state = world.getControllerState(controller_id) - - # 检查扳机按下 - if state['trigger'] > 0.5: - print(f"控制器 {controller_id} 扳机按下") - - # 检查触摸板输入 - touchpad_x, touchpad_y = state['touchpad'] - if abs(touchpad_x) > 0.5: - print(f"触摸板水平滑动: {touchpad_x}") -``` - -### 对象抓取和操作 - -```python -# VR输入处理器自动处理对象抓取 -# 扳机按下时自动检测和抓取对象 -# 扳机释放时自动释放对象 - -# 监听VR事件 -world.event_handler.accept('vr-object-grabbed', onObjectGrabbed) -world.event_handler.accept('vr-object-released', onObjectReleased) - -def onObjectGrabbed(object_node, controller_id): - print(f"抓取对象: {object_node.getName()}") - -def onObjectReleased(object_node, controller_id): - print(f"释放对象: {object_node.getName()}") -``` - -### 触觉反馈 - -```python -# 发送触觉反馈 -world.sendHapticFeedback( - controller_id=0, - duration=0.5, # 持续时间(秒) - intensity=0.8 # 强度(0-1) -) -``` - -## 📊 性能优化 - -### 渲染优化 - -```python -# 调整VR渲染质量 -vr_manager = world.vr_manager -vr_manager.render_scale = 1.0 # 渲染缩放 (0.5-2.0) - -# 设置多重采样抗锯齿 -fb_props = FrameBufferProperties() -fb_props.setMultisamples(4) # 4x MSAA -``` - -### 网络优化 - -```python -# 优化ALVR串流参数 -world.setALVRStreamQuality( - width=2160, # 降低分辨率提高性能 - height=1200, - fps=60, # 降低帧率减少延迟 - bitrate=100 # 降低比特率适应网络带宽 -) -``` - -### 系统资源监控 - -```python -# 获取性能状态 -streaming_status = world.getALVRStreamingStatus() -print(f"串流FPS: {streaming_status['fps']}") -print(f"延迟: {streaming_status['latency']} ms") -print(f"分辨率: {streaming_status['resolution']}") -``` - -## 🔍 故障排除 - -### 常见问题 - -1. **VR系统初始化失败** - ``` - 错误: OpenVR初始化失败 - 解决: 确保SteamVR已安装并运行 - ``` - -2. **ALVR连接失败** - ``` - 错误: 无法连接到ALVR服务器 - 解决: - - 检查ALVR服务器是否运行 - - 确认防火墙设置 - - 检查网络连接 - ``` - -3. **串流质量问题** - ``` - 问题: 画面卡顿或延迟高 - 解决: - - 降低分辨率和帧率 - - 检查WiFi信号强度 - - 调整比特率设置 - ``` - -### 调试命令 - -```python -# 启用调试模式 -world.vr_manager.debug_mode = True - -# 获取详细状态信息 -vr_info = world.getVRInfo() -print(f"VR设备信息: {vr_info}") - -# 检查网络连接 -if world.isALVRConnected(): - print("ALVR连接正常") -else: - print("ALVR连接失败") -``` - -## 🎯 高级功能 - -### 自定义VR交互 - -```python -# 创建自定义VR交互逻辑 -class CustomVRInteraction: - def __init__(self, world): - self.world = world - - def handleCustomGesture(self, gesture_data): - # 处理自定义手势 - pass - - def createVRMenu(self, controller_id): - # 在VR中创建3D菜单 - pass -``` - -### 多用户VR支持 - -```python -# 支持多个VR用户 -class MultiUserVRManager: - def __init__(self): - self.vr_users = {} - - def addVRUser(self, user_id, vr_manager): - self.vr_users[user_id] = vr_manager - - def syncVRUsers(self): - # 同步多用户VR状态 - pass -``` - -## 📋 配置文件 - -### VR配置 (`config/vr_config.yaml`) - -```yaml -vr: - enabled: true - render_scale: 1.0 - tracking_space: "standing" - -alvr: - server_ip: "127.0.0.1" - server_port: 9943 - streaming_port: 9944 - - video: - width: 2880 - height: 1700 - fps: 72 - bitrate: 150 - codec: "h264" - - audio: - enabled: true - sample_rate: 48000 - channels: 2 -``` - -## 🚀 部署建议 - -### 开发环境 - -```bash -# 创建VR开发环境 -python -m venv vr_env -source vr_env/bin/activate -pip install -r requirements/vr-requirements.txt - -# 启动开发服务器 -python main.py --vr-mode -``` - -### 生产环境 - -```bash -# 优化生产部署 -# 1. 使用专用VR计算机 -# 2. 配置高性能网络 -# 3. 启用GPU加速 -# 4. 监控系统性能 -``` - -## 📚 参考资料 - -- [OpenVR API文档](https://github.com/ValveSoftware/openvr/wiki/API-Documentation) -- [ALVR项目主页](https://github.com/alvr-org/ALVR) -- [Panda3D VR指南](https://docs.panda3d.org/1.10/python/programming/render-to-texture/index) -- [Quest开发者文档](https://developer.oculus.com/documentation/quest/) - -## 🤝 贡献指南 - -欢迎贡献代码和改进建议!请遵循以下步骤: - -1. Fork此仓库 -2. 创建功能分支 -3. 提交代码修改 -4. 创建Pull Request - -## 📄 许可证 - -本项目采用MIT许可证。详见LICENSE文件。 \ No newline at end of file diff --git a/demo/VR测试说明.md b/demo/VR测试说明.md deleted file mode 100644 index 7303f308..00000000 --- a/demo/VR测试说明.md +++ /dev/null @@ -1,253 +0,0 @@ -# VR测试说明 - -## 📋 测试条件说明 - -### 🎮 VR测试的两种模式 - -#### 1. 模拟模式(推荐用于开发和调试) -- **无需VR硬件** -- **无需SteamVR** -- **无需ALVR服务器** -- 自动启用,当真实VR不可用时 -- 模拟头盔和控制器追踪数据 -- 立体渲染显示到窗口 -- 适用于功能测试和开发 - -#### 2. 真实VR模式(需要完整VR设备) -- **需要VR头盔** -- **需要SteamVR运行** -- **需要ALVR服务器(Quest无线)** -- 真实的6DOF追踪 -- 立体渲染到VR头盔 -- 完整的VR交互体验 - -## 🔧 软件要求 - -### 必需依赖 -```bash -# 安装VR相关依赖 -pip install -r requirements/vr-requirements.txt - -# 主要包含: -# - openvr>=1.26.7 -# - psutil>=5.9.0 -# - numpy>=1.21.0 -# - Pillow>=9.0.1 -``` - -### 系统要求 -- Python 3.8+ -- Panda3D -- PyQt5 -- OpenGL支持的显卡 - -## 🎮 硬件要求(完整VR模式) - -### 最低硬件要求 -- **显卡**: GTX 1060 / RX 580 或更好 -- **内存**: 8GB RAM -- **处理器**: Intel i5-4590 / AMD FX 8350 或更好 -- **USB**: 至少1个USB 3.0端口 - -### 支持的VR头盔 -- **Quest 2/3**: 有线(USB-C)或无线(ALVR) -- **Valve Index**: DisplayPort + USB 3.0 -- **HTC Vive**: HDMI + USB 3.0 -- **其他OpenVR兼容设备** - -## 🌐 连接方式 - -### 有线连接 -1. **Quest 2/3**: USB-C数据线连接PC -2. **Valve Index**: DisplayPort + USB 3.0 -3. **HTC Vive**: HDMI + USB 3.0 - -### 无线连接(推荐Quest) -1. **下载ALVR服务器**: [GitHub](https://github.com/alvr-org/ALVR) -2. **在PC上运行ALVR服务器** -3. **在Quest上安装ALVR客户端** -4. **连接到同一Wi-Fi网络(5GHz推荐)** - -## 🔄 启动顺序 - -### 完整VR模式启动顺序 -1. **连接VR头盔**到PC -2. **启动SteamVR** -3. **(可选)启动ALVR服务器**(Quest无线) -4. **运行VR测试脚本** - -### 模拟模式启动顺序 -1. **直接运行VR测试脚本** -2. **系统会自动切换到模拟模式** - -## 📊 测试项目说明 - -### 1. 基本VR功能测试 -- VR系统初始化 -- 控制器检测 -- 追踪数据获取 -- 立体渲染测试 - -### 2. VR模拟模式测试 -- 强制启用模拟模式 -- 模拟数据更新 -- 控制器输入模拟 -- 立体渲染显示 - -### 3. ALVR串流测试 -- ALVR服务器连接 -- 串流质量设置 -- 触觉反馈测试 -- 串流开关控制 - -### 4. VR GUI控制面板 -- 图形界面控制 -- 实时状态监控 -- 参数调整 -- 系统开关 - -### 5. VR交互功能测试 -- 控制器射线显示 -- 手势识别 -- 对象交互 -- 输入处理 - -## 🚀 快速开始 - -### 开发和调试(推荐) -```bash -# 直接运行,会自动使用模拟模式 -python vr_test.py - -# 选择 "2. VR模拟模式测试" -``` - -### 完整VR测试 -```bash -# 确保VR设备已连接并启动SteamVR -# 然后运行 -python vr_test.py - -# 选择 "1. 基本VR功能测试" -``` - -## 💡 故障排除 - -### 常见问题 - -#### 1. OpenVR初始化失败 -**现象**: 显示"OpenVR初始化失败" -**解决方案**: -- 检查SteamVR是否运行 -- 确认VR头盔被系统识别 -- 重启SteamVR和头盔 -- 系统会自动切换到模拟模式 - -#### 2. 控制器未检测到 -**现象**: 控制器显示"未连接" -**解决方案**: -- 确保控制器已配对 -- 检查控制器电量 -- 在SteamVR中重新配对 -- 模拟模式下会显示模拟控制器 - -#### 3. ALVR连接失败 -**现象**: ALVR串流测试失败 -**解决方案**: -- 确保ALVR服务器正在运行 -- 检查Quest上的ALVR客户端 -- 确认PC和Quest在同一网络 -- 使用5GHz Wi-Fi网络 - -#### 4. 性能问题 -**现象**: 帧率低或卡顿 -**解决方案**: -- 降低渲染分辨率 -- 关闭不必要的后台程序 -- 检查显卡驱动更新 -- 使用模拟模式进行开发 - -### 日志分析 -测试过程中的详细日志可以帮助诊断问题: -- `✓` 表示成功 -- `⚠` 表示警告(非致命) -- `✗` 表示失败 -- `🔧` 表示配置或调试信息 - -## 📈 性能优化建议 - -### 开发阶段 -1. **使用模拟模式**进行功能开发 -2. **降低渲染分辨率**提高帧率 -3. **关闭不必要的特效** -4. **使用性能分析工具** - -### 生产部署 -1. **使用真实VR模式** -2. **根据硬件配置调整质量** -3. **启用异步时间扭曲** -4. **优化渲染管线** - -## 🔍 调试技巧 - -### 1. 检查VR系统状态 -```python -# 在代码中添加调试信息 -vr_info = world.getVRInfo() -print(f"VR模式: {vr_info['mode']}") -print(f"模拟模式: {vr_info['simulation_mode']}") -``` - -### 2. 监控性能 -```python -# 获取VR系统状态 -status = world.getVRStatus() -print(f"VR启用: {status['vr_enabled']}") -print(f"控制器数量: {len(status['controllers'])}") -``` - -### 3. 测试特定功能 -```python -# 强制启用模拟模式 -world.vr_manager.enable_simulation_mode() - -# 更新模拟数据 -world.vr_manager.update_simulation_data('head_pose', new_data) -``` - -## 📚 API参考 - -### VR系统控制 -- `world.initializeVR()` - 初始化VR系统 -- `world.shutdownVR()` - 关闭VR系统 -- `world.isVREnabled()` - 检查VR状态 -- `world.getVRInfo()` - 获取VR信息 - -### 模拟模式 -- `world.vr_manager.enable_simulation_mode()` - 启用模拟模式 -- `world.vr_manager.get_simulation_data()` - 获取模拟数据 -- `world.vr_manager.update_simulation_data()` - 更新模拟数据 - -### ALVR串流 -- `world.initializeALVR()` - 初始化ALVR -- `world.startALVRStreaming()` - 开始串流 -- `world.stopALVRStreaming()` - 停止串流 -- `world.setALVRStreamQuality()` - 设置质量 - -### VR输入 -- `world.startVRInput()` - 启动输入处理 -- `world.showControllerRays()` - 显示控制器射线 -- `world.getAllControllers()` - 获取所有控制器 -- `world.getControllerState()` - 获取控制器状态 - -## 🎯 总结 - -现在的VR测试系统具有以下优势: - -1. **自动适应**: 自动在真实VR和模拟模式之间切换 -2. **开发友好**: 无需VR硬件即可开发和测试 -3. **完整功能**: 支持所有VR功能的测试 -4. **详细反馈**: 提供清晰的状态信息和错误提示 -5. **灵活配置**: 可根据需要调整各种参数 - -无论您是否拥有VR设备,都可以使用这个测试系统来验证VR功能的正确性。 \ No newline at end of file diff --git a/gui/gui_manager.py b/gui/gui_manager.py index cfd10728..fc26e86c 100644 --- a/gui/gui_manager.py +++ b/gui/gui_manager.py @@ -423,7 +423,7 @@ class GUIManager: entry = DirectEntry( text="", - pos=gui_pos, + pos=(pos[0],pos[1],pos[2]), scale=size, command=self.onGUIEntrySubmit, extraArgs=[f"entry_{len(self.gui_elements)}"], @@ -431,6 +431,12 @@ class GUIManager: numLines=1, width=12, focus=0, + frameColor = (0,0,0,0), + text_fg=(1,1,1,1), + text_align=TextNode.ACenter, + text_wordwrap=None, + rolloverSound=None, + clickSound=None, parent=parent_gui_node, # 设置GUI父节点 text_font = font, frameSize=(-0.1,0.1,-0.05,0.05) @@ -496,7 +502,7 @@ class GUIManager: traceback.print_exc() return None - def createGUI2DImage(self, pos=(0, 0, 0), image_path=None, size=0.2): + def createGUI2DImage(self, pos=(0, 0, 0), image_path=None, size=(1,1,1)): """创建2D GUI图片""" try: from direct.gui.DirectGui import DirectButton @@ -536,9 +542,17 @@ class GUIManager: parent_gui_node = None # 使用默认的aspect2d print(f"📎 挂载到3D父节点: {parent_item.text(0)}") - # 使用CardMaker创建一个更可靠的图片框架 - cm = CardMaker('gui-2d-image') - cm.setFrame(-size, size, -size, size) + if isinstance(size, (list, tuple)) and len(size) >= 2: + # 分别处理宽度和高度的缩放 + width_scale = size[0] * 0.25 + height_scale = size[2] * 0.25 + else: + # 如果只提供了一个缩放值,则使用相同值 + width_scale = size * 0.1 if isinstance(size, (int, float)) else 0.2 + height_scale = width_scale + + cm = CardMaker("gui-2d-image") + cm.setFrame(-width_scale, width_scale, -height_scale, height_scale) # image_node = self.world.aspect2d.attachNewNode(cm.generate()) if parent_gui_node: @@ -1017,51 +1031,97 @@ class GUIManager: # 如果提供了视频路径,则加载视频纹理 movie_texture = None - if video_path and os.path.exists(video_path): - try: - print(f"🔍 尝试加载视频纹理: {video_path}") - # 加载视频纹理 - movie_texture = self._loadMovieTexture(video_path) - if movie_texture: - # 创建纹理阶段 - texture_stage = TextureStage("video") - texture_stage.setSort(0) - texture_stage.setMode(TextureStage.MModulate) - video_screen.setTexture(texture_stage, movie_texture) - print(f"✅ 视频纹理加载成功: {video_path}") + if video_path: + if video_path.startswith(("http://","https://")): + try: + if hasattr(self.world,'property_panel'): + success = self.world.property_panel._loadVideoFromURLWithOpenCV_3D(video_screen,video_path) + if success: + print(f"✅ 视频流URL加载成功: {video_path}") + else: + print(f"⚠️ 视频流URL加载失败: {video_path}") + else: + print("⚠️ property_manager不可用,无法加载视频流") + except Exception as e: + print(f"❌ 加载视频流URL失败: {e}") + import traceback + traceback.print_exc() + elif os.path.exists(video_path): + # 对于本地视频文件,使用原有方法 + try: + print(f"🔍 尝试加载2D视频纹理: {video_path}") + # 加载视频纹理 + movie_texture = self._loadMovieTexture(video_path) + if movie_texture: + # 应用纹理到视频屏幕(替换占位符) + video_screen["frameTexture"] = movie_texture + print(f"✅ 2D视频纹理加载成功: {video_path}") - # 保存视频纹理引用以便后续控制 - video_screen.setPythonTag("movie_texture", movie_texture) + # 保存视频纹理引用以便后续控制 + video_screen.setPythonTag("movie_texture", movie_texture) - # 尝试自动播放视频(如果支持) - try: - if hasattr(movie_texture, 'play'): - movie_texture.play() - print("▶️ 视频已开始播放") - except Exception as play_error: - print(f"⚠️ 视频自动播放失败: {play_error}") - else: - print(f"⚠️ 无法加载视频纹理: {video_path}") - # 使用默认颜色作为占位符 - video_screen.setColor(0.1, 0.1, 0.3, 0.8) - except Exception as e: - print(f"❌ 加载视频纹理失败: {e}") - import traceback - traceback.print_exc() - # 使用默认颜色作为占位符 - video_screen.setColor(0.1, 0.1, 0.3, 0.8) - else: - # 没有视频文件时显示默认颜色 - video_screen.setColor(0.1, 0.1, 0.3, 0.8) - if video_path: - print(f"⚠️ 视频文件不存在: {video_path}") + # 尝试自动播放视频(如果支持) + try: + if hasattr(movie_texture, 'play'): + movie_texture.play() + print("▶️ 2D视频已开始播放") + except Exception as play_error: + print(f"⚠️ 2D视频自动播放失败: {play_error}") + else: + print(f"⚠️ 无法加载2D视频纹理: {video_path}") + except Exception as e: + print(f"❌ 加载2D视频纹理失败: {e}") + import traceback + traceback.print_exc() else: - print("ℹ️ 未提供视频文件,显示默认占位符") + print(f"⚠️ 2D视频文件不存在: {video_path}") - # 保存视频纹理引用以便后续控制 - if movie_texture: - video_screen.setPythonTag("movie_texture", movie_texture) + # if video_path and os.path.exists(video_path): + # try: + # print(f"🔍 尝试加载视频纹理: {video_path}") + # # 加载视频纹理 + # movie_texture = self._loadMovieTexture(video_path) + # if movie_texture: + # # 创建纹理阶段 + # texture_stage = TextureStage("video") + # texture_stage.setSort(0) + # texture_stage.setMode(TextureStage.MModulate) + # video_screen.setTexture(texture_stage, movie_texture) + # + # print(f"✅ 视频纹理加载成功: {video_path}") + # + # # 保存视频纹理引用以便后续控制 + # video_screen.setPythonTag("movie_texture", movie_texture) + # + # # 尝试自动播放视频(如果支持) + # try: + # if hasattr(movie_texture, 'play'): + # movie_texture.play() + # print("▶️ 视频已开始播放") + # except Exception as play_error: + # print(f"⚠️ 视频自动播放失败: {play_error}") + # else: + # print(f"⚠️ 无法加载视频纹理: {video_path}") + # # 使用默认颜色作为占位符 + # video_screen.setColor(0.1, 0.1, 0.3, 0.8) + # except Exception as e: + # print(f"❌ 加载视频纹理失败: {e}") + # import traceback + # traceback.print_exc() + # # 使用默认颜色作为占位符 + # video_screen.setColor(0.1, 0.1, 0.3, 0.8) + # else: + # # 没有视频文件时显示默认颜色 + # video_screen.setColor(0.1, 0.1, 0.3, 0.8) + # if video_path: + # print(f"⚠️ 视频文件不存在: {video_path}") + # else: + # print("ℹ️ 未提供视频文件,显示默认占位符") + # + # # 保存视频纹理引用以便后续控制 + # if movie_texture: + # video_screen.setPythonTag("movie_texture", movie_texture) # 添加到GUI元素列表 self.gui_elements.append(video_screen) @@ -1509,9 +1569,7 @@ class GUIManager: video_screen.setTag("tree_item_type", "GUI_2D_VIDEO_SCREEN") video_screen.setTag("created_by_user", "1") video_screen.setTag("name",screen_name) - - video_screen.setTag("video_path", video_path if video_path else "") - print(f"🔧 设置2D视频屏幕标签 - video_path: {video_path if video_path else '空'}") + video_screen.setTag("video_path",video_path or "") # 关键修改:预先创建一个占位符纹理,为后续视频播放做准备 placeholder_texture = Texture(f"placeholder_video_texture_{len(self.gui_elements)}") @@ -1529,36 +1587,81 @@ class GUIManager: # 如果提供了视频路径,则加载视频纹理 movie_texture = None - if video_path and os.path.exists(video_path): - try: - print(f"🔍 尝试加载2D视频纹理: {video_path}") - # 加载视频纹理 - movie_texture = self._loadMovieTexture(video_path) - if movie_texture: - # 应用纹理到视频屏幕(替换占位符) - video_screen["frameTexture"] = movie_texture - print(f"✅ 2D视频纹理加载成功: {video_path}") - # 保存视频纹理引用以便后续控制 - video_screen.setPythonTag("movie_texture", movie_texture) + if video_path: + if video_path.startswith(("http://","https://")): + try: + if hasattr(self.world,'property_panel'): + success = self.world.property_panel._loadVideoFromURLWithOpenCV(video_screen,video_path) + if success: + print(f"✅ 视频流URL加载成功: {video_path}") + else: + print(f"⚠️ 视频流URL加载失败: {video_path}") + else: + print("⚠️ property_manager不可用,无法加载视频流") + except Exception as e: + print(f"❌ 加载视频流URL失败: {e}") + import traceback + traceback.print_exc() + elif os.path.exists(video_path): + try: + print(f"🔍 尝试加载2D视频纹理: {video_path}") + # 加载视频纹理 + movie_texture = self._loadMovieTexture(video_path) + if movie_texture: + # 应用纹理到视频屏幕(替换占位符) + video_screen["frameTexture"] = movie_texture + print(f"✅ 2D视频纹理加载成功: {video_path}") - # 尝试自动播放视频(如果支持) - try: - if hasattr(movie_texture, 'play'): - movie_texture.play() - print("▶️ 2D视频已开始播放") - except Exception as play_error: - print(f"⚠️ 2D视频自动播放失败: {play_error}") - else: - print(f"⚠️ 无法加载2D视频纹理: {video_path}") - except Exception as e: - print(f"❌ 加载2D视频纹理失败: {e}") - import traceback - traceback.print_exc() - else: - if video_path: + # 保存视频纹理引用以便后续控制 + video_screen.setPythonTag("movie_texture", movie_texture) + + # 尝试自动播放视频(如果支持) + try: + if hasattr(movie_texture, 'play'): + movie_texture.play() + print("▶️ 2D视频已开始播放") + except Exception as play_error: + print(f"⚠️ 2D视频自动播放失败: {play_error}") + else: + print(f"⚠️ 无法加载2D视频纹理: {video_path}") + except Exception as e: + print(f"❌ 加载2D视频纹理失败: {e}") + import traceback + traceback.print_exc() + else: print(f"⚠️ 2D视频文件不存在: {video_path}") + # if video_path and os.path.exists(video_path): + # try: + # print(f"🔍 尝试加载2D视频纹理: {video_path}") + # # 加载视频纹理 + # movie_texture = self._loadMovieTexture(video_path) + # if movie_texture: + # # 应用纹理到视频屏幕(替换占位符) + # video_screen["frameTexture"] = movie_texture + # print(f"✅ 2D视频纹理加载成功: {video_path}") + # + # # 保存视频纹理引用以便后续控制 + # video_screen.setPythonTag("movie_texture", movie_texture) + # + # # 尝试自动播放视频(如果支持) + # try: + # if hasattr(movie_texture, 'play'): + # movie_texture.play() + # print("▶️ 2D视频已开始播放") + # except Exception as play_error: + # print(f"⚠️ 2D视频自动播放失败: {play_error}") + # else: + # print(f"⚠️ 无法加载2D视频纹理: {video_path}") + # except Exception as e: + # print(f"❌ 加载2D视频纹理失败: {e}") + # import traceback + # traceback.print_exc() + # else: + # if not video_path: + # print(f"⚠️ 2D视频文件不存在: {video_path}") + # 添加到GUI元素列表 self.gui_elements.append(video_screen) @@ -1612,6 +1715,7 @@ class GUIManager: video_screen.setTag("video_path",video_path) path = video_screen.getTag("video_path") + print({video_screen.getTag("gui_type")}) print(f"🔧 更新2D视频屏幕标签 - video_path: {path}") if not video_path or not os.path.exists(video_path): @@ -2186,9 +2290,17 @@ class GUIManager: print(f"开始编辑GUI元素: 类型={gui_type}, 属性={property_name}, 值={value}") if property_name == "text": + original_frame_size = None + if hasattr(gui_element,'getFrameSize'): + try: + original_frame_size = gui_element.getFrameSize() + except: + pass if gui_type in ["button", "label"]: gui_element['text'] = value print(f"成功更新2D GUI文本: {value}") + # if gui_type == "button": + # self._resizeButtonToText(gui_element,value,original_frame_size) elif gui_type == "entry": gui_element.set(value) print(f"成功更新输入框文本: {value}") @@ -2267,6 +2379,64 @@ class GUIManager: traceback.print_exc() return False + def _resizeButtonToText(self, button, text, original_frame_size=None): + """ + 根据文本内容调整按钮大小 + + Args: + button: DirectButton 对象 + text: 新的文本内容 + original_frame_size: 原始frameSize,用于计算合适的padding + """ + try: + # 获取按钮当前的文本缩放 + text_scale = 0.03 # 默认文本缩放 + padding = 0.2 # 默认边距 + + # 尝试从按钮获取实际的文本缩放值 + if hasattr(button, 'getTextScale'): + text_scale = button.getTextScale() + elif hasattr(button, 'textScale'): + text_scale = button.textScale + + # 根据原始frameSize计算合适的padding + if original_frame_size and len(original_frame_size) >= 4: + # 基于原始按钮宽度计算合适的padding + padding = max((original_frame_size[1] - original_frame_size[0]) * 0.15, 0.1) + + # 更精确的文本尺寸估算 + # 考虑中文字符和英文字符的不同宽度 + chinese_chars = len([c for c in text if ord(c) > 127]) + english_chars = len(text) - chinese_chars + + # 中文字符通常比英文字符宽 + char_width = text_scale * 0.6 + text_width = (chinese_chars * char_width * 1.5) + (english_chars * char_width) + text_height = text_scale * 1.2 # 文本高度 + + # 计算新的frameSize,确保有足够的边距 + half_width = max(text_width * 0.5 + padding, 0.3) # 最小宽度0.3 + half_height = max(text_height * 0.5 + padding * 0.5, 0.15) # 最小高度0.15 + + # 正确设置frameSize - 使用字典方式设置 + new_frame = (-half_width, half_width, -half_height, half_height) + button['frameSize'] = new_frame # 使用字典方式设置 + + print(f"按钮大小已调整: {new_frame} (文本: {text})") + + except Exception as e: + print(f"调整按钮大小失败: {e}") + # 如果自动调整失败,保持原有大小或设置一个合理的默认大小 + try: + if original_frame_size: + # 保持原有大小 + button['frameSize'] = original_frame_size + else: + # 设置一个合理的默认大小 + button['frameSize'] = (-0.5, 0.5, -0.15, 0.15) + except: + pass + def duplicateGUIElement(self, gui_element): """复制GUI元素""" try: diff --git a/icons/close_icon.png b/icons/close_icon.png new file mode 100644 index 00000000..b5c923a1 Binary files /dev/null and b/icons/close_icon.png differ diff --git a/icons/delete_fail_icon.png b/icons/delete_fail_icon.png new file mode 100644 index 00000000..90ba204d Binary files /dev/null and b/icons/delete_fail_icon.png differ diff --git a/icons/down_arrows.png b/icons/down_arrows.png new file mode 100644 index 00000000..86bdb1ee Binary files /dev/null and b/icons/down_arrows.png differ diff --git a/icons/left_arrows.png b/icons/left_arrows.png new file mode 100644 index 00000000..7e782f69 Binary files /dev/null and b/icons/left_arrows.png differ diff --git a/icons/minimize_icon.png b/icons/minimize_icon.png new file mode 100644 index 00000000..5c8155ec Binary files /dev/null and b/icons/minimize_icon.png differ diff --git a/icons/property_select_image.png b/icons/property_select_image.png new file mode 100644 index 00000000..04abe10a Binary files /dev/null and b/icons/property_select_image.png differ diff --git a/icons/right_arrows.png b/icons/right_arrows.png new file mode 100644 index 00000000..2c29b1c2 Binary files /dev/null and b/icons/right_arrows.png differ diff --git a/icons/solid_down_arrows.png b/icons/solid_down_arrows.png new file mode 100644 index 00000000..6fc740f5 Binary files /dev/null and b/icons/solid_down_arrows.png differ diff --git a/icons/solid_right_arrows.png b/icons/solid_right_arrows.png new file mode 100644 index 00000000..22b41456 Binary files /dev/null and b/icons/solid_right_arrows.png differ diff --git a/icons/success_icon.png b/icons/success_icon.png new file mode 100644 index 00000000..4dda99f9 Binary files /dev/null and b/icons/success_icon.png differ diff --git a/icons/up_arrows.png b/icons/up_arrows.png new file mode 100644 index 00000000..72504376 Binary files /dev/null and b/icons/up_arrows.png differ diff --git a/icons/warning_icon.png b/icons/warning_icon.png new file mode 100644 index 00000000..540f03b4 Binary files /dev/null and b/icons/warning_icon.png differ diff --git a/icons/windowing_icon.png b/icons/windowing_icon.png new file mode 100644 index 00000000..b128b61e Binary files /dev/null and b/icons/windowing_icon.png differ diff --git a/main.py b/main.py index bd204899..daa599e1 100644 --- a/main.py +++ b/main.py @@ -1,12 +1,14 @@ import warnings +from core.Command_System import CommandManager from core.InfoPanelManager import InfoPanelManager +from core.patrol_system import PatrolSystem from demo.video_integration import VideoManager warnings.filterwarnings("ignore", category=DeprecationWarning) import sys -import builtins # 添加这一行 +import builtins from PyQt5.QtWidgets import (QApplication, QMainWindow, QMenuBar, QMenu, QAction, QDockWidget, QTreeWidget, QListWidget, QWidget, QVBoxLayout, QTreeWidgetItem, QLabel, QLineEdit, QFormLayout, QDoubleSpinBox, QScrollArea, QTreeView, QInputDialog, QFileDialog, QMessageBox, QDialog, QGroupBox, QHBoxLayout, QPushButton, QDialogButtonBox) @@ -14,15 +16,13 @@ from PyQt5.QtCore import Qt, QDir, QUrl from PyQt5.QtGui import QDrag, QPainter, QPixmap from PyQt5.QtWidgets import QFileSystemModel from QPanda3D.QPanda3DWidget import QPanda3DWidget +from panda3d.core import loadPrcFileData +loadPrcFileData("", "assertions 0") from core.world import CoreWorld from core.selection import SelectionSystem from core.event_handler import EventHandler from core.tool_manager import ToolManager from core.script_system import ScriptManager -from core.vr_manager import VRManager -from core.vr_input_handler import VRInputHandler -from core.alvr_streamer import ALVRStreamer -from core.patrol_system import PatrolSystem from gui.gui_manager import GUIManager from core.terrain_manager import TerrainManager from scene.scene_manager import SceneManager @@ -49,7 +49,6 @@ from direct.actor.Actor import Actor from PyQt5.sip import wrapinstance #from RenderPipelineFile.toolkit.material_editor.main import MaterialEditor - class MyWorld(CoreWorld): def __init__(self): super().__init__() @@ -93,11 +92,7 @@ class MyWorld(CoreWorld): # 初始化界面管理系统 self.interface_manager = InterfaceManager(self) - # 初始化VR系统 - self.vr_manager = VRManager(self) - self.vr_input_handler = VRInputHandler(self, self.vr_manager) - self.alvr_streamer = ALVRStreamer(self, self.vr_manager) - + # 启动脚本系统 self.script_manager.start_system() @@ -110,10 +105,21 @@ class MyWorld(CoreWorld): self.info_panel_manager = InfoPanelManager(self) + self.command_manager = CommandManager() + # 初始化碰撞管理器 from core.collision_manager import CollisionManager self.collision_manager = CollisionManager(self) + # 初始化VR管理器 + try: + from core.vr_manager import VRManager + self.vr_manager = VRManager(self) + print("✓ VR管理器初始化完成") + except Exception as e: + print(f"⚠ VR管理器初始化失败: {e}") + self.vr_manager = None + # 调试选项 self.debug_collision = True # 是否显示碰撞体 @@ -239,13 +245,13 @@ class MyWorld(CoreWorld): """创建3D空间文本""" return self.gui_manager.createGUI3DText(pos, text, size) - def createGUI3DImage(self,pos=(0,0,0),text="3D图片",size=(2,2)): + def createGUI3DImage(self,pos=(0,0,0),text="3D图片",size=(1,1,1)): """创建3D图片""" return self.gui_manager.createGUI3DImage(pos,text,size) - def createGUI2DImage(self, pos=(0, 0, 0), image_path=None, size=1): + def createGUI2DImage(self, pos=(0, 0, 0), image_path=None, size=2): """创建2D GUI图片""" - return self.gui_manager.createGUI2DImage(pos, image_path, size*0.2) + return self.gui_manager.createGUI2DImage(pos, image_path, size) def createVideoScreen(self,pos=(0,0,0),size=1,video_path=None): """创建视频屏幕""" @@ -476,26 +482,12 @@ class MyWorld(CoreWorld): # 模型导入和处理方法 - 代理到scene_manager def importModel(self, filepath): """导入模型到场景""" - # 检查是否是FBX文件,如果是则询问用户是否要转换为GLB - if filepath.lower().endswith('.fbx'): - try: - from PyQt5.QtWidgets import QMessageBox - reply = QMessageBox.question( - None, - '格式转换选择', - 'FBX文件检测到!\n\n是否要尝试转换为GLB格式以获得更好的动画支持?\n\n点击"是":尝试转换为GLB格式(需要安装转换工具)\n点击"否":直接使用原始FBX格式导入', - QMessageBox.Yes | QMessageBox.No, - QMessageBox.Yes - ) - auto_convert = (reply == QMessageBox.Yes) - except ImportError: - # 如果没有PyQt5,默认不转换 - print("检测到FBX文件,由于GUI不可用,将直接使用原始格式导入") - auto_convert = False - else: - # 非FBX文件,保持原有逻辑 - auto_convert = True - + # 自动转换FBX等格式为GLB以获得更好的动画支持(不再弹窗询问) + # 如果你不想自动转换,可以将 auto_convert 改为 False + auto_convert = False + + print(f"[模型导入] 文件: {filepath}, 自动转换GLB: {auto_convert}") + return self.scene_manager.importModel(filepath, auto_convert_to_glb=auto_convert) def importModelAsync(self, filepath): @@ -616,143 +608,6 @@ class MyWorld(CoreWorld): def listAllScripts(self): """列出所有脚本信息""" return self.script_manager.list_all_scripts() - # ==================== VR系统功能代理 ==================== - - # VR系统控制方法 - 代理到vr_manager - def initializeVR(self): - """初始化VR系统""" - return self.vr_manager.initialize_vr() - - def shutdownVR(self): - """关闭VR系统""" - return self.vr_manager.shutdown_vr() - - def isVREnabled(self): - """检查VR是否启用""" - return self.vr_manager.is_vr_enabled() - - def getVRInfo(self): - """获取VR系统信息""" - return self.vr_manager.get_vr_info() - - # VR输入处理方法 - 代理到vr_input_handler - def startVRInput(self): - """启动VR输入处理""" - return self.vr_input_handler.start_input_handling() - - def stopVRInput(self): - """停止VR输入处理""" - return self.vr_input_handler.stop_input_handling() - - def showControllerRays(self, show=True): - """显示/隐藏控制器射线""" - return self.vr_input_handler.show_controller_rays(show) - - def getControllerState(self, controller_id): - """获取控制器状态""" - return self.vr_input_handler.get_controller_state(controller_id) - - def getAllControllers(self): - """获取所有控制器""" - return self.vr_input_handler.get_all_controllers() - - def setVRGestureEnabled(self, enabled): - """设置VR手势识别启用状态""" - return self.vr_input_handler.set_gesture_enabled(enabled) - - def setVRInteractionEnabled(self, enabled): - """设置VR交互启用状态""" - return self.vr_input_handler.set_interaction_enabled(enabled) - - # ALVR串流方法 - 代理到alvr_streamer - def initializeALVR(self): - """初始化ALVR串流""" - return self.alvr_streamer.initialize() - - def startALVRStreaming(self): - """开始ALVR串流""" - return self.alvr_streamer.start_streaming() - - def stopALVRStreaming(self): - """停止ALVR串流""" - return self.alvr_streamer.stop_streaming() - - def getALVRStreamingStatus(self): - """获取ALVR串流状态""" - return self.alvr_streamer.get_streaming_status() - - def setALVRStreamQuality(self, width, height, fps, bitrate): - """设置ALVR串流质量""" - return self.alvr_streamer.set_stream_quality(width, height, fps, bitrate) - - def sendHapticFeedback(self, controller_id, duration, intensity): - """发送触觉反馈""" - return self.alvr_streamer.send_haptic_feedback(controller_id, duration, intensity) - - def shutdownALVR(self): - """关闭ALVR串流""" - return self.alvr_streamer.shutdown() - - def isALVRConnected(self): - """检查ALVR是否连接""" - return self.alvr_streamer.is_connected() - - def isALVRStreaming(self): - """检查ALVR是否在串流""" - return self.alvr_streamer.is_streaming() - - # 便捷方法 - def enableVRMode(self): - """启用VR模式(一键启动)""" - print("启用VR模式...") - - # 1. 初始化VR系统 - if not self.initializeVR(): - print("VR系统初始化失败") - return False - - # 2. 启动VR输入处理 - if not self.startVRInput(): - print("VR输入处理启动失败") - return False - - # 3. 初始化ALVR串流 - if self.initializeALVR(): - print("✓ ALVR串流已启用") - # 自动开始串流 - self.startALVRStreaming() - else: - print("⚠ ALVR串流启用失败,但VR系统仍可用") - - print("✓ VR模式已启用") - return True - - def disableVRMode(self): - """禁用VR模式(一键关闭)""" - print("禁用VR模式...") - - # 1. 停止ALVR串流 - self.stopALVRStreaming() - self.shutdownALVR() - - # 2. 停止VR输入处理 - self.stopVRInput() - - # 3. 关闭VR系统 - self.shutdownVR() - - print("✓ VR模式已禁用") - - def getVRStatus(self): - """获取VR系统总体状态""" - return { - "vr_enabled": self.isVREnabled(), - "vr_info": self.getVRInfo(), - "controllers": self.getAllControllers(), - "alvr_connected": self.isALVRConnected(), - "alvr_streaming": self.isALVRStreaming(), - "streaming_status": self.getALVRStreamingStatus() if self.isALVRConnected() else None - } def loadCesiumTileset(self,tileset_url,position=(0,0,0)): return self.scene_manager.load_cesium_tileset(tileset_url,position) diff --git a/new/project.json b/new/project.json new file mode 100644 index 00000000..5d10691e --- /dev/null +++ b/new/project.json @@ -0,0 +1,8 @@ +{ + "name": "new", + "path": "D:\\PythonProject\\CH_EG\\EG\\new", + "created_at": "2025-09-09 16:47:27", + "last_modified": "2025-09-11 09:28:24", + "version": "1.0.0", + "engine_version": "1.0.0" +} \ No newline at end of file diff --git a/project/project_manager.py b/project/project_manager.py index 31b2df70..71d18e87 100644 --- a/project/project_manager.py +++ b/project/project_manager.py @@ -19,7 +19,7 @@ from PyQt5.QtWidgets import ( from PyQt5.QtCore import Qt # 导入自定义对话框 -from ui.widgets import NewProjectDialog +from ui.widgets import NewProjectDialog, UniversalMessageDialog class ProjectManager: """项目管理器 - 统一管理项目的生命周期""" @@ -103,11 +103,23 @@ class ProjectManager: parent_window.current_project_path = full_project_path # 显示成功消息 - QMessageBox.information(parent_window, "成功", f"项目 '{project_name}' 创建成功!") + UniversalMessageDialog.show_success( + parent_window, + "成功", + f"项目 '{project_name}' 创建成功!", + show_cancel=False, + confirm_text="确认" + ) return True except Exception as e: - QMessageBox.critical(parent_window, "错误", f"创建项目失败: {str(e)}") + UniversalMessageDialog.show_error( + parent_window, + "错误", + f"创建项目失败: {str(e)}", + show_cancel=False, + confirm_text="确认" + ) return False def openProject(self, parent_window): @@ -127,7 +139,13 @@ class ProjectManager: # 检查是否是有效的项目文件夹 config_file = os.path.join(project_path, "project.json") if not os.path.exists(config_file): - QMessageBox.warning(parent_window, "警告", "选择的不是有效的项目文件夹!") + UniversalMessageDialog.show_warning( + parent_window, + "警告", + "选择的不是有效的项目文件夹!", + show_cancel=False, + confirm_text="确认" + ) return False # 读取项目配置 @@ -161,7 +179,13 @@ class ProjectManager: if hasattr(parent_window, 'fileView') and hasattr(parent_window, 'fileModel'): parent_window.fileView.setRootIndex(parent_window.fileModel.index(project_path)) - QMessageBox.information(parent_window, "成功", "项目加载成功!") + UniversalMessageDialog.show_success( + parent_window, + "成功", + "项目加载成功!", + show_cancel=False, + confirm_text="确认" + ) return True # 检查场景文件 # scene_file = os.path.join(project_path, "scenes", "scene.bam") @@ -200,7 +224,13 @@ class ProjectManager: # return False except Exception as e: - QMessageBox.critical(parent_window, "错误", f"加载项目时发生错误:{str(e)}") + UniversalMessageDialog.show_error( + parent_window, + "错误", + f"加载项目时发生错误:{str(e)}", + show_cancel=False, + confirm_text="确认" + ) return False def openProjectForPath(self, project_path, parent_window=None): @@ -217,7 +247,13 @@ class ProjectManager: config_file = os.path.join(project_path, "project.json") if not os.path.exists(config_file): if parent_window: - QMessageBox.warning(parent_window, "警告", f"选择的不是有效的项目文件夹!{project_path}") + UniversalMessageDialog.show_warning( + parent_window, + "警告", + f"选择的不是有效的项目文件夹!{project_path}", + show_cancel=False, + confirm_text="确认" + ) else: print("警告: 选择的不是有效的项目文件夹!") return False @@ -255,13 +291,25 @@ class ProjectManager: if hasattr(parent_window, 'fileView') and hasattr(parent_window, 'fileModel'): parent_window.fileView.setRootIndex(parent_window.fileModel.index(project_path)) - QMessageBox.information(parent_window, "成功", "项目加载成功!") + UniversalMessageDialog.show_success( + parent_window, + "成功", + "项目加载成功!", + show_cancel=False, + confirm_text="确认" + ) print(f"项目 '{project_path}' 加载成功!") return True else: if parent_window: - QMessageBox.warning(parent_window, "错误", "加载场景失败!") + UniversalMessageDialog.show_warning( + parent_window, + "错误", + "加载场景失败!", + show_cancel=False, + confirm_text="确认" + ) else: print("错误: 加载场景失败!") return False @@ -269,7 +317,13 @@ class ProjectManager: except Exception as e: error_msg = f"加载项目时发生错误:{str(e)}" if parent_window: - QMessageBox.critical(parent_window, "错误", error_msg) + UniversalMessageDialog.show_error( + parent_window, + "错误", + error_msg, + show_cancel=False, + confirm_text="确认" + ) else: print(error_msg) return False @@ -283,7 +337,13 @@ class ProjectManager: if hasattr(parent_window, 'current_project_path'): self.current_project_path = parent_window.current_project_path else: - QMessageBox.warning(parent_window, "警告", "请先创建或打开一个项目!") + UniversalMessageDialog.show_warning( + parent_window, + "警告", + "请先创建或打开一个项目!", + show_cancel=False, + confirm_text="确认" + ) return False project_path = self.current_project_path @@ -324,14 +384,32 @@ class ProjectManager: project_name = os.path.basename(project_path) self.updateWindowTitle(parent_window, project_name) - QMessageBox.information(parent_window, "成功", "项目保存成功!") + UniversalMessageDialog.show_success( + parent_window, + "成功", + "项目保存成功!", + show_cancel=False, + confirm_text="确认" + ) return True else: - QMessageBox.warning(parent_window, "错误", "保存场景失败!") + UniversalMessageDialog.show_warning( + parent_window, + "错误", + "保存场景失败!", + show_cancel=False, + confirm_text="确认" + ) return False except Exception as e: - QMessageBox.critical(parent_window, "错误", f"保存项目时发生错误:{str(e)}") + UniversalMessageDialog.show_error( + parent_window, + "错误", + f"保存项目时发生错误:{str(e)}", + show_cancel=False, + confirm_text="确认" + ) return False # ==================== 项目打包功能 ==================== @@ -355,9 +433,45 @@ class ProjectManager: if not os.path.exists(scene_file): QMessageBox.warning(parent_window, "警告", "请先保存场景!") return False + + if hasattr(self.world,'selection') and self.world.selection: + self.world.selection.clearSelection() + print("已取消场景中的物体选中状态") + + if self.world.scene_manager.saveScene(scene_file,project_path): + print("场景保存成功") + config_file = os.path.join(project_path,"project.json") + if os.path.exists(config_file): + with open(config_file,"r",encoding="utf-8") as f: + project_config = json.load(f) + + project_config["last_modified"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + project_config["scene_file"] = os.path.relpath(scene_file,project_path) + + with open(config_file,"w",encoding = "utf-8") as f: + json.dump(project_config,f,ensure_ascii = False,indent=4) + + self.project_config = project_config + + project_name = os.path.basename(project_path) + self.updateWindowTitle(parent_window,project_name) + else: + QMessageBox.warning(parent_window,"错误","保存场景失败!") + return False + + build_dir = QFileDialog.getExistingDirectory( + parent_window, + "选择打包输出目录", + project_path, + QFileDialog.ShowDirsOnly + ) + + if not build_dir: + return False # 创建构建目录 - build_dir = os.path.join(project_path, "build") + build_dir = os.path.join(build_dir, "build") + if not os.path.exists(build_dir): os.makedirs(build_dir) @@ -368,7 +482,7 @@ class ProjectManager: success = self._executeStandardBuild(build_dir, parent_window) if success: - QMessageBox.information(parent_window, "成功", + QMessageBox.information(parent_window, "成功", "打包完成!\n可执行文件在 build/dist/ 目录中。\n" "支持的格式:\n" "- Windows: .exe 安装程序\n" @@ -377,7 +491,7 @@ class ProjectManager: return True else: return False - + except Exception as e: QMessageBox.critical(parent_window, "错误", f"打包过程出错:{str(e)}") return False @@ -392,374 +506,963 @@ class ProjectManager: # 复制场景文件到构建目录 shutil.copy2(scene_file, os.path.join(build_dir, "scene.bam")) - + + # 复制Resources文件夹到build目录 + source_resources = os.path.join(project_path, "scenes", "resources") + self.copy_folder(source_resources, build_dir) + + self._saveGUIElementsToJSON(build_dir, project_path) + + self._copyScriptsToBuild(build_dir, project_path) + + self._copyScriptSystemToBuild(build_dir) + + source_render_pipeline = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),"RenderPipelineFile") + dest_render_pipeline = os.path.join(build_dir,"RenderPipelineFile") + + if os.path.exists(source_render_pipeline): + if os.path.exists(dest_render_pipeline): + shutil.rmtree(dest_render_pipeline) + + shutil.copytree( + source_render_pipeline, + dest_render_pipeline, + ignore=shutil.ignore_patterns('__pycache__','*.pyc','.git','.vscode','*.log','samples') + ) + print("✓ RenderPipelineFile文件夹已复制到build目录") + else: + print("⚠️ RenderPipelineFile文件夹未找到") + # 创建标准的应用程序入口文件 self._createAppFile(build_dir, project_name) # 创建标准的setup.py文件 self._createStandardSetupFile(build_dir, project_name) - - def _createAppFile(self, build_dir, project_name): - """创建应用程序主文件""" - app_code = f'''#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -{project_name} - Panda3D应用程序 -使用Panda3D引擎编辑器创建 -""" + #创建requirements.txt文件 + self._createRequirementsFile(build_dir) -import sys -import os -from direct.showbase.ShowBase import ShowBase -from panda3d.core import (loadPrcFileData, WindowProperties, AmbientLight, - DirectionalLight, Point3, Vec3) - -# 配置Panda3D -loadPrcFileData("", """ - win-size 1280 720 - window-title {project_name} - show-frame-rate-meter 1 - sync-video 1 - want-directtools #f - want-tk #f - audio-library-name p3openal_audio -""") - -class {project_name.replace(' ', '').replace('-', '')}App(ShowBase): - """应用程序主类""" - - def __init__(self): - ShowBase.__init__(self) - - print(f"启动 {project_name}...") - - # 设置窗口属性 - self.setupWindow() - - # 设置光照 - self.setupLighting() - - # 加载场景 - self.loadScene() - - # 设置相机控制 - self.setupControls() - - print("✓ 应用程序初始化完成") - - def setupWindow(self): - """设置窗口""" - # 设置背景色 - self.setBackgroundColor(0.2, 0.2, 0.2) - - # 设置窗口属性 - props = WindowProperties() - props.setTitle("{project_name}") - self.win.requestProperties(props) - - def setupLighting(self): - """设置光照系统""" - # 环境光 - alight = AmbientLight('alight') - alight.setColor((0.3, 0.3, 0.3, 1)) - alnp = self.render.attachNewNode(alight) - self.render.setLight(alnp) - - # 定向光(模拟太阳光) - dlight = DirectionalLight('dlight') - dlight.setColor((0.8, 0.8, 0.8, 1)) - dlight.setDirection(Vec3(-1, -1, -1)) - dlnp = self.render.attachNewNode(dlight) - self.render.setLight(dlnp) - - def loadScene(self): - """加载场景""" + def _copyScriptsToBuild(self, build_dir, project_path): + """复制脚本文件到构建目录的scripts文件夹""" try: - # 查找场景文件 - scene_file = "scene.bam" - if not os.path.exists(scene_file): - print("警告: 没有找到场景文件,创建默认场景") - self.createDefaultScene() - return - - # 加载场景 - scene = self.loader.loadModel(scene_file) - if scene: - scene.reparentTo(self.render) - print("✓ 场景加载成功") - - # 自动调整相机位置 - self.adjustCamera() + # 创建目标scripts目录 + scripts_dest = os.path.join(build_dir, "scripts") + + # 正确的源scripts目录路径 + scripts_src = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "scripts") + + # 如果上面的路径不存在,尝试项目路径下的scripts目录 + if not os.path.exists(scripts_src): + scripts_src = os.path.join(project_path, "scripts") + + if os.path.exists(scripts_src): + # 直接复制整个scripts目录 + if os.path.exists(scripts_dest): + shutil.rmtree(scripts_dest) + + shutil.copytree( + scripts_src, + scripts_dest, + ignore=shutil.ignore_patterns('__pycache__', '*.pyc', '.git', '.vscode', '*.log') + ) + print("✓ Scripts目录已复制到build/scripts") else: - print("警告: 场景加载失败,创建默认场景") - self.createDefaultScene() - + # 创建空的scripts目录 + if not os.path.exists(scripts_dest): + os.makedirs(scripts_dest) + print("⚠️ 项目中没有scripts目录") + except Exception as e: - print(f"加载场景时出错: {{str(e)}}") - self.createDefaultScene() - - def createDefaultScene(self): - """创建默认场景""" - # 加载默认的环境模型 - env = self.loader.loadModel("models/environment") - if env: - env.reparentTo(self.render) - env.setScale(0.25) - env.setPos(-8, 42, 0) - - # 创建一个简单的立方体作为示例 - from panda3d.core import CardMaker - cm = CardMaker("ground") - cm.setFrame(-10, 10, -10, 10) - ground = self.render.attachNewNode(cm.generate()) - ground.setP(-90) - ground.setColor(0.5, 0.8, 0.5, 1) - - def adjustCamera(self): - """调整相机位置以查看场景""" - # 计算场景边界 - bounds = self.render.getBounds() - if bounds and not bounds.isEmpty(): - center = bounds.getCenter() - radius = bounds.getRadius() - - # 设置相机位置 - distance = radius * 3 - self.cam.setPos(center.x, center.y - distance, center.z + radius) - self.cam.lookAt(center) - else: - # 默认相机位置 - self.cam.setPos(0, -20, 5) - self.cam.lookAt(0, 0, 0) - - def setupControls(self): - """设置相机控制""" - # 启用鼠标控制 - self.accept("wheel_up", self.zoomIn) - self.accept("wheel_down", self.zoomOut) - - # 键盘控制说明 - print("\\n=== 控制说明 ===") - print("鼠标滚轮: 缩放") - print("ESC: 退出") - print("================\\n") - - # ESC键退出 - self.accept("escape", sys.exit) - - def zoomIn(self): - """放大""" - pos = self.cam.getPos() - lookAt = Point3(0, 0, 0) # 假设看向原点 - direction = (lookAt - pos).normalized() - newPos = pos + direction * 2 - self.cam.setPos(newPos) - - def zoomOut(self): - """缩小""" - pos = self.cam.getPos() - lookAt = Point3(0, 0, 0) # 假设看向原点 - direction = (lookAt - pos).normalized() - newPos = pos - direction * 2 - self.cam.setPos(newPos) + print(f"⚠️ 复制脚本文件时出错: {str(e)}") -def main(): - """主函数""" - try: - app = {project_name.replace(' ', '').replace('-', '')}App() - app.run() - except Exception as e: - print(f"应用程序启动失败: {{str(e)}}") - import traceback - traceback.print_exc() - input("按Enter键退出...") + def _copyScriptSystemToBuild(self,build_dir): + core_files = [ + "script_system.py", + "InfoPanelManager.py", + "CustomMouseController.py" + ] -if __name__ == "__main__": - main() -''' - + source_core_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),"core") + + core_dest = os.path.join(build_dir,"core") + + if not os.path.exists(core_dest): + os.makedirs(core_dest) + + for file_name in core_files: + source_file = os.path.join(source_core_dir,file_name) + + if os.path.exists(source_file): + shutil.copy2(source_file,os.path.join(core_dest,file_name)) + + def _saveGUIElementsToJSON(self, build_dir, project_path): + """保存GUI元素到JSON文件,内容与_collectGUIElementInfo保持一致""" + try: + # 创建目标gui目录 + gui_dest = os.path.join(build_dir, "gui") + if not os.path.exists(gui_dest): + os.makedirs(gui_dest) + + # 收集所有GUI元素信息 + gui_data = [] + + # 获取当前场景中的GUI元素 + if hasattr(self.world, 'gui_elements'): + for gui_node in self.world.gui_elements: + if gui_node and not gui_node.isEmpty(): + # 使用_collectGUIElementInfo方法收集信息 + gui_info = self.world.scene_manager._collectGUIElementInfo(gui_node) + if gui_info: + self._updateResourcePaths(gui_info,project_path,build_dir) + gui_data.append(gui_info) + print(f"收集GUI元素信息: {gui_info['name']}") + + # 保存GUI信息到JSON文件 + gui_file_path = os.path.join(gui_dest, "gui_elements.json") + with open(gui_file_path, "w", encoding="utf-8") as f: + json.dump(gui_data, f, ensure_ascii=False, indent=4) + + print(f"✓ GUI元素数据已保存到 {gui_file_path},共 {len(gui_data)} 个元素") + return True + + except Exception as e: + print(f"⚠️ 保存GUI元素时出错: {str(e)}") + import traceback + traceback.print_exc() + return False + + def _updateResourcePaths(self,gui_info,project_path,build_dir): + def normalize_resource_path(old_path): + if not old_path: + return old_path + + if old_path.startswith(("http://","https://")): + return old_path + + if os.path.isabs(old_path): + try: + rel_path = os.path.relpath(old_path,project_path) + return os.path.join("resources",os.path.basename(rel_path)).replace("\\","/") + except Exception: + return os.path.join("resources",os.path.basename(old_path)).replace("\\","/") + else: + return os.path.join("resources",os.path.basename(old_path)).replace("\\","/") + if "image_path" in gui_info and gui_info["image_path"]: + gui_info["image_path"] = normalize_resource_path(gui_info["image_path"]) + if "video_path" in gui_info and gui_info["video_path"]: + gui_info["video_path"] = normalize_resource_path(gui_info["video_path"]) + if "bg_image_path" in gui_info and gui_info["bg_image_path"]: + gui_info["bg_image_path"] = normalize_resource_path(gui_info["bg_image_path"]) + + def _createRequirementsFile(self,build_dir): + requirements_content = """panda3d>=1.10.13""" + + requirements_path = os.path.join(build_dir,"requirements.txt") + with open(requirements_path,"w",encoding="utf-8") as f: + f.write(requirements_content) + + def copy_folder(self, source_folder, destination_folder): + """将一个文件夹从源路径复制到目标路径下的resources文件夹中 + + Args: + source_folder (str): 源文件夹路径 + destination_folder (str): 目标文件夹路径 + """ + try: + # 创建resources文件夹作为目标 + resources_dest = os.path.join(destination_folder, "resources") + + # 确保目标目录存在 + if not os.path.exists(destination_folder): + os.makedirs(destination_folder) + + # 如果目标resources文件夹已存在,先删除 + if os.path.exists(resources_dest): + shutil.rmtree(resources_dest) + + # 检查源文件夹是否存在 + if os.path.exists(source_folder): + # 复制整个文件夹到resources目录下 + shutil.copytree( + source_folder, + resources_dest, + ignore=shutil.ignore_patterns('__pycache__', '*.pyc', '.git', '.vscode', '*.log') + ) + print(f"✓ 文件夹已从 {source_folder} 复制到 {resources_dest}") + return True + else: + print(f"⚠️ 源文件夹不存在: {source_folder}") + # 即使源文件夹不存在,也创建空的resources目录 + if not os.path.exists(resources_dest): + os.makedirs(resources_dest) + return False + + except Exception as e: + print(f"⚠️ 复制文件夹时出错: {str(e)}") + return False + + def _copyResourcesToBuild(self, build_dir, project_path): + """复制GUI资源到构建目录的resources文件夹""" + try: + # 创建目标resources目录 + resources_dest = os.path.join(build_dir, "resources") + + # 源Resources目录 + resources_src = os.path.join(project_path, "Resources") + + if os.path.exists(resources_src): + # 直接复制整个Resources目录 + if os.path.exists(resources_dest): + shutil.rmtree(resources_dest) + + shutil.copytree( + resources_src, + resources_dest, + ignore=shutil.ignore_patterns('__pycache__', '*.pyc', '.git', '.vscode', '*.log') + ) + print("✓ Resources目录已复制到build/resources") + + # 统计复制的文件数量 + file_count = 0 + for root, dirs, files in os.walk(resources_dest): + file_count += len(files) + print(f"✓ 共复制了 {file_count} 个资源文件") + else: + # 创建空的resources目录 + if not os.path.exists(resources_dest): + os.makedirs(resources_dest) + print("⚠️ 项目中没有Resources目录") + + except Exception as e: + print(f"⚠️ 复制资源文件时出错: {str(e)}") + + def _collectResourceFiles(self, project_path): + """收集项目中GUI使用的资源文件""" + resource_files = set() + + try: + # 收集Resources目录中的所有文件(这是最主要的资源来源) + resources_dir = os.path.join(project_path, "Resources") + if os.path.exists(resources_dir): + for root, dirs, files in os.walk(resources_dir): + for file in files: + file_path = os.path.join(root, file) + # 收集所有文件,不仅仅是媒体文件 + resource_files.add(file_path) + + # 同时收集场景中引用的特定资源 + scene_file = os.path.join(project_path, "scenes", "scene.bam") + if os.path.exists(scene_file): + # 从场景文件中提取资源引用 + referenced_files = self._extractResourcesFromScene(scene_file, project_path) + for file_path in referenced_files: + if os.path.isabs(file_path): + if os.path.exists(file_path): + resource_files.add(file_path) + else: + # 相对路径 + full_path = os.path.join(project_path, file_path) + if os.path.exists(full_path): + resource_files.add(full_path) + + except Exception as e: + print(f"收集资源文件时出错: {str(e)}") + + return list(resource_files) + + def _extractResourcesFromScene(self, scene_file, project_path): + """从场景文件中提取资源引用""" + referenced_files = [] + + try: + # 这里应该实现从BAM文件中提取贴图、视频等资源引用的逻辑 + # 由于直接解析BAM文件比较复杂,我们采用间接方式 + + # 检查项目配置文件或其他元数据文件中可能包含的资源引用 + config_file = os.path.join(project_path, "project.json") + if os.path.exists(config_file): + try: + with open(config_file, "r", encoding="utf-8") as f: + project_config = json.load(f) + + # 如果配置中有资源列表信息,可以在这里处理 + # 这里暂时保持简单实现 + except Exception as e: + print(f"读取项目配置时出错: {str(e)}") + + except Exception as e: + print(f"从场景提取资源引用时出错: {str(e)}") + + return referenced_files + + def _isMediaFile(self, file_path): + """判断是否为媒体文件(图片或视频)""" + media_extensions = { + # 图片格式 + '.png', '.jpg', '.jpeg', '.bmp', '.gif', '.tga', '.tiff', + # 视频格式 + '.mp4', '.avi', '.mov', '.wmv', '.mkv', '.webm', '.flv' + } + + _, ext = os.path.splitext(file_path.lower()) + return ext in media_extensions + + def _createAppFile(self, build_dir, project_name): + """创建应用程序主文件 - 通过复制模板文件""" + # 获取模板文件路径(假设模板文件在项目根目录下的templates文件夹中) + template_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), + "templates", "main_template.py") + + # 目标文件路径 app_path = os.path.join(build_dir, "main.py") - with open(app_path, "w", encoding="utf-8") as f: - f.write(app_code) - + + # 检查模板文件是否存在 + if os.path.exists(template_path): + # 直接复制模板文件 + shutil.copy2(template_path, app_path) + print(f"✓ 应用程序主文件已从模板创建: {app_path}") + + # def _createAppFile(self, build_dir, project_name): +# """创建应用程序主文件""" +# app_code = f'''#!/usr/bin/env python3 +# # -*- coding: utf-8 -*- +# +# """ +# {project_name} - Panda3D应用程序 +# 使用Panda3D引擎编辑器创建 +# """ +# +# from __future__ import print_function +# +# import json +# +# from direct.actor.Actor import Actor +# from panda3d.core import TextNode, CardMaker, TextureStage, NodePath +# #获取渲染管线路径 +# import sys +# import os +# +# render_pipeline_path = 'RenderPipelineFile' +# project_root = os.path.dirname(os.path.abspath(__file__)) +# sys.path.insert(0,project_root) +# sys.path.insert(0,render_pipeline_path) +# +# import math +# from random import random,randint,seed +# from panda3d.core import Vec3,load_prc_file_data,Filename +# from direct.showbase.ShowBase import ShowBase +# +# os.chdir(os.path.dirname(os.path.realpath(__file__))) +# +# class MainApp(ShowBase): +# def __init__(self): +# load_prc_file_data("",""" +# win-size 1200 720 +# window-title Render +# """) +# +# pipeline_path = "../../" +# +# if not os.path.isfile(os.path.join(pipeline_path,"setup.py")): +# pipeline_path = "../../RenderPipeline" +# +# sys.path.insert(0,pipeline_path) +# +# from rpcore import RenderPipeline,SpotLight +# self.render_pipeline = RenderPipeline() +# self.render_pipeline.create(self) +# +# from rpcore.util.movement_controller import MovementController +# +# self.render_pipeline.daytime_mgr.time = "12:00" +# self._loadFont() +# +# self.loadFullScene() +# self.loadGUIFromJSON() +# +# self.controller = MovementController(self) +# self.controller.set_initial_position( +# Vec3(-7.5,-5.3,1.8),Vec3(-5.9,-4.0,1.6)) +# self.controller.setup() +# +# base.accept("l",self.tour) +# +# def _loadFont(self): +# """加载中文字体""" +# self.chinese_font = None +# try: +# self.chinese_font = self.loader.loadFont('/usr/share/fonts/truetype/wqy/wqy-microhei.ttc') +# if not self.chinese_font: +# print("警告: 无法加载中文字体,将使用默认字体") +# else: +# print("✓ 中文字体加载成功") +# except: +# print("警告: 无法加载中文字体,将使用默认字体") +# self.chinese_font = None +# +# def getChineseFont(self): +# """获取中文字体""" +# return self.chinese_font +# +# def loadFullScene(self): +# """加载完整场景,包括所有元素""" +# try: +# scene_file = "scene.bam" +# if os.path.exists(scene_file): +# # 使用readBamFile加载完整场景 +# from panda3d.core import BamCache +# BamCache.getGlobalPtr().setActive(False) # 禁用缓存以避免问题 +# +# scene = self.loader.loadModel(Filename.fromOsSpecific(scene_file)) +# if scene: +# scene.reparentTo(self.render) +# self.render_pipeline.prepare_scene(scene) +# print("✓ 完整场景加载成功") +# +# # 处理场景中的各种元素 +# self.processSceneElements(scene) +# else: +# print("⚠️ 场景文件加载失败") +# else: +# print("⚠️ 未找到场景文件") +# except Exception as e: +# print(f"加载完整场景时出错: {{str(e)}}") +# import traceback +# traceback.print_exc() +# +# def processSceneElements(self, scene): +# """处理场景中的各种元素""" +# try: +# # 处理光源 +# self.processLights(scene) +# +# # 处理GUI元素 +# self.processGUIElements(scene) +# +# except Exception as e: +# print(f"处理场景元素时出错: {{str(e)}}") +# +# def processLights(self, scene): +# """处理场景中的光源""" +# try: +# # 查找并处理点光源 +# point_lights = scene.findAllMatches("**/=element_type=point_light") +# for light_node in point_lights: +# try: +# from RenderPipelineFile.rpcore import PointLight +# light = PointLight() +# +# # 恢复光源属性 +# if light_node.hasTag("light_energy"): +# light.energy = float(light_node.getTag("light_energy")) +# else: +# 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 +# +# light.setPos(light_node.getPos()) +# self.render_pipeline.add_light(light) +# print(f"✓ 点光源 {{light_node.getName()}} 恢复成功") +# except Exception as e: +# print(f"恢复点光源 {{light_node.getName()}} 失败: {{str(e)}}") +# +# # 查找并处理聚光灯 +# spot_lights = scene.findAllMatches("**/=element_type=spot_light") +# for light_node in spot_lights: +# try: +# from RenderPipelineFile.rpcore import SpotLight +# light = SpotLight() +# +# # 恢复光源属性 +# if light_node.hasTag("light_energy"): +# light.energy = float(light_node.getTag("light_energy")) +# else: +# 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 +# +# light.setPos(light_node.getPos()) +# self.render_pipeline.add_light(light) +# print(f"✓ 聚光灯 {{light_node.getName()}} 恢复成功") +# except Exception as e: +# print(f"恢复聚光灯 {{light_node.getName()}} 失败: {{str(e)}}") +# +# except Exception as e: +# print(f"处理光源时出错: {{str(e)}}") +# +# def processGUIElements(self, scene): +# """处理场景中的GUI元素""" +# try: +# # 查找并处理2D图像 +# images_2d = scene.findAllMatches("**/=gui_type=image_2d") +# for img_node in images_2d: +# try: +# # GUI元素通常在场景加载时自动处理 +# print(f"✓ 2D图像 {{img_node.getName()}} 已加载") +# except Exception as e: +# print(f"处理2D图像 {{img_node.getName()}} 失败: {{str(e)}}") +# +# except Exception as e: +# print(f"处理GUI元素时出错: {{str(e)}}") +# +# def tour(self): +# mopath = ( +# (Vec3(-10.8645000458, 9.76458263397, 2.13306283951), Vec3(-133.556228638, -4.23447799683, 0.0)), +# (Vec3(-10.6538448334, -5.98406457901, 1.68028640747), Vec3(-59.3999938965, -3.32706642151, 0.0)), +# (Vec3(9.58458328247, -5.63625621796, 2.63269257545), Vec3(58.7906494141, -9.40668964386, 0.0)), +# (Vec3(6.8135137558, 11.0153560638, 2.25509500504), Vec3(148.762527466, -6.41223621368, 0.0)), +# (Vec3(-9.07093334198, 3.65908527374, 1.42396306992), Vec3(245.362503052, -3.59927511215, 0.0)), +# (Vec3(-8.75390911102, -3.82727789879, 0.990055501461), Vec3(296.090484619, -0.604830980301, 0.0)), +# ) +# self.controller.play_motion_path(mopath,3.0) +# +# def loadGUIFromJSON(self): +# gui_json_path = "gui/gui_elements.json" +# +# try: +# if os.path.exists(gui_json_path): +# with open(gui_json_path, "r", encoding="utf-8") as f: +# content = f.read().strip() +# if content: +# gui_data = json.loads(content) +# self.createGUIElement(gui_data) +# except Exception as e: +# print(f"加载GUI元素失败: {{str(e)}}") +# import traceback +# traceback.print_exc() +# +# def createGUIElement(self,element_data): +# try: +# processed_names = set() +# element_original_data={{}} +# for i, gui_info in enumerate(element_data): +# name = gui_info.get("name", f"gui_element_{{i}}") +# element_original_data[name] = { +# "scale": gui_info.get("scale", [1, 1, 1]), +# "position": gui_info.get("position", [0, 0, 0]), +# "parent_name": gui_info.get("parent_name") +# } +# valid_parents = set() +# for gui_info in element_data: +# name = gui_info.get("name", f"gui_element_{{gui_info.get('index', 0)}}") +# valid_parents.add(name) +# +# for i ,gui_info in enumerate(element_data): +# try: +# gui_type = gui_info.get("type","unknown") +# name = gui_info.get("name",f"gui_element_{{i}}") +# position = gui_info.get("position",[0,0,0]) +# scale = gui_info.get("scale",[1,1,1]) +# tags = gui_info.get("tags",{{}}) +# text = gui_info.get("text","") +# image_path = gui_info.get("image_path","") +# video_path = gui_info.get("video_path","") +# bg_image_path = gui_info.get("bg_image_path","") +# parent_name = gui_info.get("parent_name") +# +# if name in processed_names: +# continue +# +# processed_names.add(name) +# +# absolute_position = list(position) +# absolute_scale = list(scale) +# +# if parent_name and parent_name in element_original_data: +# parent_data = element_original_data[parent_name] +# parent_scale = parent_data["scale"] +# +# if gui_type in ["3d_text", "3d_image", "button", "label", "entry", "2d_image", +# "2d_video_screen"]: +# # 位置需要乘以父级缩放来得到绝对位置 +# for j in range(min(len(absolute_position), len(parent_scale))): +# absolute_position[j] *= parent_scale[j] if len(parent_scale) > j else parent_scale[0] +# +# # 缩放需要乘以父级缩放来得到绝对缩放 +# for j in range(min(len(absolute_scale), len(parent_scale))): +# absolute_scale[j] *= parent_scale[j] if len(parent_scale) > j else parent_scale[0] +# +# new_element = None +# +# if gui_type =="3d_text": +# size = absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 0.5 +# new_element = self.createGUI3DText( +# pos = tuple(absolute_position), +# text = text, +# size = size +# ) +# elif gui_type == "button": +# # 确保传入正确的参数类型 +# new_element = self.createGUIButton( +# pos=tuple(absolute_position), +# text=text, +# size=absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 1.0, +# ) +# except Exception as e: +# print(f"重建GUI元素失败 {{name}}: {{e}}") +# import traceback +# traceback.print_exc() +# continue +# except Exception as e: +# print(f"重建GUI元素失败: {{str(e)}}") +# +# def createGUIButton(self, pos=(0, 0, 0), text="按钮", size=0.1,command=None): +# from direct.gui.DirectGui import DirectButton +# +# button = DirectButton( +# text=text, +# pos=(pos[0], pos[1], pos[2]), # 保持正确的坐标格式 +# scale=size, # size 应该是数值而不是元组 +# frameColor=(0.2, 0.6, 0.8, 1), +# text_font=self.getChineseFont() if self.getChineseFont() else None, +# rolloverSound=None, +# clickSound=None, +# parent=None, +# command=command +# ) +# +# def createGUI3DText(self, pos=(0, 0, 0), text="3D文本", size=0.5): +# """创建3D文本GUI元素""" +# try: +# # 创建文本节点 +# text_node = TextNode("gui_3d_text") +# text_node.setText(text) +# text_node.setAlign(TextNode.ACenter) +# +# # 设置字体(如果可用) +# if self.getChineseFont(): +# text_node.setFont(self.getChineseFont()) +# +# # 创建节点路径并添加到场景 +# text_np = self.render.attachNewNode(text_node) +# +# # 设置位置和大小 +# text_np.setPos(Vec3(pos[0], pos[1], pos[2])) +# text_np.setScale(size) +# +# # 设置面向摄像机 +# #text_np.setBillboardPointEye() +# +# # 设置渲染属性 +# text_np.setBin("fixed", 40) +# text_np.setDepthWrite(False) +# +# return text_np +# except Exception as e: +# print(f"❌ 创建3D文本失败: {{str(e)}}") +# import traceback +# traceback.print_exc() +# return None +# +# MainApp().run() +# ''' +# +# app_path = os.path.join(build_dir, "main.py") +# with open(app_path, "w", encoding="utf-8") as f: +# f.write(app_code) + def _createStandardSetupFile(self, build_dir, project_name): - """创建标准的setup.py文件 - 按照Panda3D官方文档""" + """创建优化的标准setup.py文件""" setup_code = f'''#!/usr/bin/env python3 -# -*- coding: utf-8 -*- + # -*- coding: utf-8 -*- -""" -{project_name} 打包配置文件 -使用 Panda3D 标准打包工具 -""" + """ + {project_name} 打包配置文件 + 使用 Panda3D 标准打包工具 + """ -from setuptools import setup + from setuptools import setup -# 应用程序配置 -APP_NAME = "{project_name}" -APP_VERSION = "1.0.0" -MAIN_SCRIPT = "main.py" + # 应用程序配置 + APP_NAME = "{project_name}" + APP_VERSION = "1.0.0" + MAIN_SCRIPT = "main.py" -setup( - name=APP_NAME, - version=APP_VERSION, - - # Panda3D 打包选项 - options={{ - 'build_apps': {{ - # GUI应用程序 - 'gui_apps': {{ - APP_NAME: MAIN_SCRIPT, - }}, - - # 包含的文件模式 - 'include_patterns': [ - '*.bam', # 场景文件 - '*.egg', # 模型文件 - '*.jpg', '*.png', # 纹理文件 - '*.wav', '*.ogg', # 音频文件 - '*.ttf', '*.otf', # 字体文件 - ], - - # 排除的文件模式 - 'exclude_patterns': [ - '*.pyc', - '__pycache__/**', - '.git/**', - '.vscode/**', - '*.log', - ], - - # Panda3D 插件 - 'plugins': [ - 'pandagl', # OpenGL渲染器 - 'pandaegg', # Egg文件支持 - 'p3openal_audio', # OpenAL音频 - ], - - # 包含的Python模块 - 'include_modules': {{ - '*': [ - 'direct.showbase.ShowBase', - 'direct.task', - 'direct.actor', - 'direct.interval', - 'panda3d.core', - 'panda3d.direct', + setup( + name=APP_NAME, + version=APP_VERSION, + + # Panda3D 打包选项 + options={{ + 'build_apps': {{ + # GUI应用程序 + 'gui_apps': {{ + APP_NAME: MAIN_SCRIPT, + }}, + + # 包含的文件模式 + 'include_patterns': [ + '*.bam', # 场景文件 + '*.egg', # 模型文件 + '*.jpg', '*.png', '*.tga', '*.dds', # 纹理文件 + '*.wav', '*.ogg', '*.mp3', # 音频文件 + '*.ttf', '*.otf', # 字体文件 + 'config/*.yaml', # 配置文件 + 'shaders/**/*.glsl', # 着色器文件 ], - }}, - - # 排除的Python模块(减小体积) - 'exclude_modules': {{ - '*': [ - 'tkinter', # Tkinter GUI - 'matplotlib', # 绘图库 - 'numpy', # 数值计算(如果不需要) - 'scipy', # 科学计算(如果不需要) - 'PIL', # 图像处理(如果不需要) - 'wx', # wxPython - 'PyQt5', # Qt界面库 - 'setuptools', # 安装工具 - 'distutils', # 分发工具 + + # 排除的文件模式 + 'exclude_patterns': [ + '*.pyc', + '__pycache__/**', + '.git/**', + '.vscode/**', + '*.log', + '**/*.tmp', + '**/temp/**', + 'samples/**', # 示例文件 + '**/.DS_Store', + '**/Thumbs.db', ], + + # Panda3D 插件 + 'plugins': [ + 'pandagl', # OpenGL渲染器 + 'pandaegl', # EGL支持 + 'p3assimp', # Assimp模型导入 + 'p3ffmpeg', # FFmpeg视频支持 + 'p3openal_audio', # OpenAL音频 + ], + + # 包含的Python模块 + 'include_modules': {{ + '*': [ + # 核心模块 + 'direct.showbase.ShowBase', + 'direct.task', + 'direct.actor', + 'direct.interval', + 'direct.gui', + 'direct.stdpy.file', + 'direct.stdpy.pickle', + 'panda3d.core', + 'panda3d.direct', + + # 渲染管线相关 + 'rpcore', + 'rpcore.util.movement_controller', + 'rpcore.native', + 'rpcore.render_pipeline', + 'rplibs', + 'rpplugins', + + # 标准库 + 'json', + 'os', + 'sys', + 'math', + 'random', + 'collections', + 'copy', + 'itertools', + 'importlib', + ], + }}, + + # 排除的Python模块(减小体积) + 'exclude_modules': {{ + '*': [ + # GUI框架 + 'tkinter', + 'matplotlib', + 'numpy', + 'scipy', + 'PIL', + 'wx', + 'PyQt5', + 'PySide2', + 'kivy', + + # 开发工具 + 'setuptools', + 'pip', + 'wheel', + 'twine', + 'pytest', + 'unittest', + + # 其他不必要的模块 + 'sqlite3', + 'xmlrpc', + 'ftplib', + 'telnetlib', + ], + }}, + + # 平台设置 + 'platforms': [ + 'win_amd64', # Windows 64位 + 'linux_x86_64', # Linux 64位 + ], + + # 优化设置 + 'strip_docstrings': True, # 移除文档字符串 + 'optimize': 1, # 字节码优化级别 }}, - - # 平台设置 - 'platforms': [ - 'win_amd64', # Windows 64位 - 'linux_x86_64', # Linux 64位 - # 'macosx_10_9_x86_64', # macOS(如果需要) - ], - - # 优化设置 - 'strip_docstrings': True, # 移除文档字符串 }}, - }}, - - # 标准setuptools选项 - author="Panda3D 引擎编辑器", - author_email="user@example.com", - description=f"{{APP_NAME}} - 使用Panda3D创建的3D应用程序", - long_description="这是一个使用Panda3D引擎编辑器创建的3D应用程序。", - - # 依赖项 - install_requires=[ - 'panda3d>=1.10.13', - ], - - # Python版本要求 - python_requires='>=3.7', - - # 分类信息 - classifiers=[ - 'Development Status :: 4 - Beta', - 'Intended Audience :: End Users/Desktop', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Topic :: Games/Entertainment', - 'Topic :: Multimedia :: Graphics :: 3D Rendering', - ], - - # 使用现代的许可证表达式 - license='MIT', -) -''' - + + # 标准setuptools选项 + author="Panda3D Engine Editor User", + author_email="user@example.com", + description=f"{{APP_NAME}} - 使用Panda3D创建的3D应用程序", + long_description="这是一个使用Panda3D引擎编辑器创建的3D应用程序。", + + # 依赖项 + install_requires=[ + 'panda3d>=1.10.13', + ], + + # Python版本要求 + python_requires='>=3.7', + + # 分类信息 + classifiers=[ + 'Development Status :: 4 - Beta', + 'Intended Audience :: End Users/Desktop', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Topic :: Games/Entertainment', + 'Topic :: Multimedia :: Graphics :: 3D Rendering', + ], + + license='MIT', + ) + ''' + setup_path = os.path.join(build_dir, "setup.py") with open(setup_path, "w", encoding="utf-8") as f: f.write(setup_code) - + def _executeStandardBuild(self, build_dir, parent_window): - """执行标准的Panda3D打包命令""" + """执行标准的Panda3D打包命令 - 增强版""" try: print(f"开始打包,工作目录: {build_dir}") - - # 首先尝试 bdist_apps(推荐方式) - print("执行标准打包命令: python setup.py bdist_apps") - + + # 显示打包进度提示 + QMessageBox.information(parent_window, "打包进行中", + "正在打包项目,请稍候...\n" + "此过程可能需要几分钟时间。\n" + "请勿关闭程序。") + + # 使用线程执行打包以避免界面冻结 + import threading + from PyQt5.QtCore import pyqtSignal, QObject + + class BuildSignals(QObject): + finished = pyqtSignal(bool, str) + progress = pyqtSignal(str) + + signals = BuildSignals() + + def build_process(): + try: + print("执行标准打包命令: python setup.py bdist_apps") + signals.progress.emit("正在初始化打包过程...") + + process = subprocess.Popen( + [sys.executable, "setup.py", "bdist_apps"], + cwd=build_dir, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + encoding='utf-8' + ) + + while True: + output = process.stdout.readline() + if output == '' and process.poll() is not None: + break + if output: + print(output.strip()) + if "copying" in output.lower(): + signals.progress.emit(f"正在处理文件: {output.strip()[-50:]}") + elif "building" in output.lower(): + signals.progress.emit(output.strip()) + + # 获取错误输出 + stderr = process.stderr.read() + if stderr: + print("错误输出:", stderr) + + except Exception as e: + print(f"执行打包命令时出错: {str(e)}") + signals.finished.emit(False, f"打包失败:{str(e)}") + + # 连接信号 + def on_finished(success, message): + if success: + QMessageBox.information(parent_window, "成功", message) + else: + QMessageBox.critical(parent_window, "错误", message) + + def on_progress(message): + print(f"[打包进度] {message}") + + signals.finished.connect(on_finished) + signals.progress.connect(on_progress) + + # 启动打包线程 + build_thread = threading.Thread(target=build_process) + build_thread.daemon = True + build_thread.start() + + return True # 线程已启动 + + except Exception as e: + print(f"启动打包过程时出错: {str(e)}") + QMessageBox.critical(parent_window, "错误", f"启动打包失败:{str(e)}") + return False + + def _tryBuildAppsThreaded(self, build_dir, signals): + """尝试使用 build_apps 命令(线程安全版本)""" + try: + print("执行备用打包命令: python setup.py build_apps") + signals.progress.emit("正在执行备用打包方法...") + process = subprocess.Popen( - [sys.executable, "setup.py", "bdist_apps"], + [sys.executable, "setup.py", "build_apps"], cwd=build_dir, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, encoding='utf-8' ) - + # 实时显示输出 - stdout_lines = [] - stderr_lines = [] - while True: output = process.stdout.readline() if output == '' and process.poll() is not None: break if output: print(output.strip()) - stdout_lines.append(output.strip()) - - # 获取错误输出 + signals.progress.emit(output.strip()) + stderr = process.stderr.read() if stderr: print("错误输出:", stderr) - stderr_lines.append(stderr) - - # 检查返回码 + if process.returncode == 0: - print("✓ 打包成功完成") - return True + print("✓ build_apps 成功完成") + signals.finished.emit(True, "打包成功完成!\n可执行文件在 dist/ 目录中。") else: - # 如果bdist_apps失败,尝试build_apps - print(f"bdist_apps 失败 (返回码: {process.returncode}),尝试 build_apps...") - return self._tryBuildApps(build_dir, parent_window) - + error_msg = f"打包失败,返回码:{process.returncode}" + if stderr: + error_msg += f"\n错误信息:{stderr}" + signals.finished.emit(False, error_msg) + except Exception as e: - print(f"执行打包命令时出错: {str(e)}") - QMessageBox.critical(parent_window, "错误", f"打包失败:{str(e)}") - return False + signals.finished.emit(False, f"执行 build_apps 失败:{str(e)}") def _tryBuildApps(self, build_dir, parent_window): """尝试使用 build_apps 命令""" @@ -867,4 +1570,4 @@ def updateWindowTitle(window, project_name=None): if project_name: window.setWindowTitle(f"{base_title} - {project_name}") else: - window.setWindowTitle(base_title) \ No newline at end of file + window.setWindowTitle(base_title) diff --git a/quick_menu_fix.py b/quick_menu_fix.py new file mode 100644 index 00000000..a0f822d6 --- /dev/null +++ b/quick_menu_fix.py @@ -0,0 +1,277 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +快速菜单栏修复脚本 +用于诊断和修复菜单栏显示问题 +""" + +import sys +import os +from PyQt5.QtWidgets import QApplication, QMainWindow, QLabel, QVBoxLayout, QWidget, QPushButton +from PyQt5.QtCore import Qt, QTimer + +# 设置环境变量避免DPI问题 +os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1" +os.environ["QT_SCALE_FACTOR"] = "1" + +class QuickMenuFix(QMainWindow): + def __init__(self): + super().__init__() + self.setupUI() + + def setupUI(self): + """设置UI""" + self.setWindowTitle("菜单栏快速修复工具") + self.setGeometry(300, 300, 900, 700) + + # 强制窗口标志 + self.setWindowFlags(Qt.Window) + + # 设置主窗口样式 + self.setStyleSheet(""" + QMainWindow { + background-color: #1E1E1E; + color: #FFFFFF; + } + QPushButton { + background-color: #0078D4; + color: white; + border: none; + padding: 10px 20px; + margin: 5px; + border-radius: 5px; + font-size: 12px; + } + QPushButton:hover { + background-color: #106EBE; + } + QPushButton:pressed { + background-color: #005A9E; + } + """) + + # 创建中央部件 + central_widget = QWidget() + self.setCentralWidget(central_widget) + layout = QVBoxLayout(central_widget) + + # 标题 + title = QLabel("🔧 菜单栏修复工具") + title.setStyleSheet("font-size: 24px; font-weight: bold; color: #0078D4; margin: 20px;") + title.setAlignment(Qt.AlignCenter) + layout.addWidget(title) + + # 状态显示 + self.status_label = QLabel("准备就绪...") + self.status_label.setStyleSheet("font-size: 14px; color: white; margin: 10px; padding: 10px; background-color: #2D2D30; border-radius: 5px;") + self.status_label.setAlignment(Qt.AlignCenter) + layout.addWidget(self.status_label) + + # 按钮区域 + button_layout = QVBoxLayout() + + # 测试按钮 + test_btn = QPushButton("🔍 测试菜单栏显示") + test_btn.clicked.connect(self.testMenuBar) + button_layout.addWidget(test_btn) + + # 修复按钮 + fix_btn = QPushButton("🛠️ 强制修复菜单栏") + fix_btn.clicked.connect(self.forceFixMenuBar) + button_layout.addWidget(fix_btn) + + # 重置按钮 + reset_btn = QPushButton("🔄 重置菜单栏样式") + reset_btn.clicked.connect(self.resetMenuBarStyle) + button_layout.addWidget(reset_btn) + + # 信息按钮 + info_btn = QPushButton("📊 显示系统信息") + info_btn.clicked.connect(self.showSystemInfo) + button_layout.addWidget(info_btn) + + layout.addLayout(button_layout) + + # 创建初始菜单栏 + self.createInitialMenuBar() + + def createInitialMenuBar(self): + """创建初始菜单栏""" + menubar = self.menuBar() + menubar.setNativeMenuBar(False) + + # 添加基本菜单 + file_menu = menubar.addMenu('文件') + file_menu.addAction('新建') + file_menu.addAction('打开') + + edit_menu = menubar.addMenu('编辑') + edit_menu.addAction('复制') + edit_menu.addAction('粘贴') + + help_menu = menubar.addMenu('帮助') + help_menu.addAction('关于') + + print("✅ 初始菜单栏已创建") + + def testMenuBar(self): + """测试菜单栏显示""" + menubar = self.menuBar() + + # 收集信息 + info = [] + info.append(f"可见性: {menubar.isVisible()}") + info.append(f"启用状态: {menubar.isEnabled()}") + info.append(f"高度: {menubar.height()}px") + info.append(f"宽度: {menubar.width()}px") + info.append(f"位置: {menubar.pos()}") + info.append(f"菜单数量: {len(menubar.actions())}") + info.append(f"原生菜单栏: {menubar.isNativeMenuBar()}") + + # 显示结果 + status_text = "📊 菜单栏状态:\n" + "\n".join(info) + self.status_label.setText(status_text) + + # 控制台输出 + print("\n🔍 菜单栏测试结果:") + for item in info: + print(f" - {item}") + + # 判断是否正常 + if menubar.isVisible() and menubar.height() > 0: + self.status_label.setStyleSheet("font-size: 14px; color: white; margin: 10px; padding: 10px; background-color: #0F7B0F; border-radius: 5px;") + print("✅ 菜单栏显示正常") + else: + self.status_label.setStyleSheet("font-size: 14px; color: white; margin: 10px; padding: 10px; background-color: #A80000; border-radius: 5px;") + print("❌ 菜单栏显示异常") + + def forceFixMenuBar(self): + """强制修复菜单栏""" + menubar = self.menuBar() + + print("🛠️ 开始强制修复菜单栏...") + + # 重置基本属性 + menubar.setNativeMenuBar(False) + menubar.setVisible(True) + menubar.setEnabled(True) + + # 设置尺寸 + menubar.setMinimumHeight(50) + menubar.setMaximumHeight(50) + + # 应用强制样式 + menubar.setStyleSheet(""" + QMenuBar { + background-color: #FF4444 !important; + color: #FFFFFF !important; + border: 4px solid #00FF00 !important; + min-height: 50px !important; + max-height: 50px !important; + font-size: 18px !important; + font-weight: bold !important; + padding: 8px !important; + } + QMenuBar::item { + background-color: #FFFF00 !important; + color: #000000 !important; + padding: 12px 20px !important; + margin: 4px !important; + font-size: 18px !important; + font-weight: bold !important; + border: 2px solid #FF0000 !important; + border-radius: 3px !important; + } + QMenuBar::item:selected { + background-color: #0066FF !important; + color: #FFFFFF !important; + } + """) + + # 强制显示和刷新 + menubar.show() + menubar.raise_() + menubar.update() + menubar.repaint() + self.update() + self.repaint() + + self.status_label.setText("🛠️ 强制修复已应用!\n如果看到红色菜单栏说明修复成功") + self.status_label.setStyleSheet("font-size: 14px; color: white; margin: 10px; padding: 10px; background-color: #FF6600; border-radius: 5px;") + + print("🛠️ 强制修复完成") + + # 延迟测试结果 + QTimer.singleShot(1000, self.testMenuBar) + + def resetMenuBarStyle(self): + """重置菜单栏样式""" + menubar = self.menuBar() + + # 清除样式 + menubar.setStyleSheet("") + + # 重新设置基本样式 + menubar.setStyleSheet(""" + QMenuBar { + background-color: #2D2D30; + color: #FFFFFF; + border-bottom: 1px solid #3E3E42; + min-height: 30px; + font-size: 14px; + } + QMenuBar::item { + background-color: transparent; + color: #FFFFFF; + padding: 8px 12px; + margin: 0px 2px; + } + QMenuBar::item:selected { + background-color: #0078D4; + } + """) + + menubar.update() + self.status_label.setText("🔄 菜单栏样式已重置为正常样式") + self.status_label.setStyleSheet("font-size: 14px; color: white; margin: 10px; padding: 10px; background-color: #2D2D30; border-radius: 5px;") + + print("🔄 菜单栏样式已重置") + + def showSystemInfo(self): + """显示系统信息""" + import platform + from PyQt5.QtCore import QT_VERSION_STR, PYQT_VERSION_STR + + info = [] + info.append(f"操作系统: {platform.system()} {platform.release()}") + info.append(f"Python版本: {sys.version.split()[0]}") + info.append(f"PyQt5版本: {PYQT_VERSION_STR}") + info.append(f"Qt版本: {QT_VERSION_STR}") + info.append(f"屏幕分辨率: {QApplication.desktop().screenGeometry().width()}x{QApplication.desktop().screenGeometry().height()}") + + status_text = "📊 系统信息:\n" + "\n".join(info) + self.status_label.setText(status_text) + self.status_label.setStyleSheet("font-size: 12px; color: white; margin: 10px; padding: 10px; background-color: #0078D4; border-radius: 5px;") + + print("\n📊 系统信息:") + for item in info: + print(f" - {item}") + +def main(): + """主函数""" + app = QApplication(sys.argv) + + # 设置应用样式 + app.setStyle('Fusion') + + print("🚀 启动菜单栏修复工具") + print("📋 这个工具可以帮助诊断和修复菜单栏显示问题") + print("🎯 使用明显的颜色来测试菜单栏是否正常显示") + + window = QuickMenuFix() + window.show() + + return app.exec_() + +if __name__ == "__main__": + sys.exit(main()) \ No newline at end of file diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 8ac360a5..a4b7d40c 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -98,3 +98,4 @@ webencodings==0.5.1 xdg==5 xkit==0.0.0 zipp==1.0.0 +openvr==2.2.0 diff --git a/requirements/vr-requirements.txt b/requirements/vr-requirements.txt deleted file mode 100644 index 57cbb876..00000000 --- a/requirements/vr-requirements.txt +++ /dev/null @@ -1,18 +0,0 @@ -# VR功能依赖包 -# 用于支持VR显示和ALVR串流功能 - -# OpenVR Python库 - VR系统核心 -openvr>=1.26.7 - -# 网络和通信 -psutil>=5.9.0 - -# 数学和科学计算 -numpy>=1.21.0 - -# 图像处理 -Pillow>=9.0.1 - -# 现有核心依赖 -Panda3D>=1.10.15 -PyQt5>=5.15.9 \ No newline at end of file diff --git a/run_vr_test.sh b/run_vr_test.sh new file mode 100755 index 00000000..e6202e9a --- /dev/null +++ b/run_vr_test.sh @@ -0,0 +1,123 @@ +#!/bin/bash + +# VR性能测试启动脚本 +# 使用方法: ./run_vr_test.sh + +echo "🚀 VR性能测试启动脚本" +echo "======================" + +# 检查是否在正确的目录 +if [ ! -f "vr_performance_test.py" ]; then + echo "❌ 错误:找不到vr_performance_test.py文件" + echo "请在EG项目根目录运行此脚本" + exit 1 +fi + +# 检查Python是否可用 +if ! command -v python3 &> /dev/null; then + if ! command -v python &> /dev/null; then + echo "❌ 错误:找不到Python解释器" + exit 1 + else + PYTHON_CMD="python" + fi +else + PYTHON_CMD="python3" +fi + +echo "✓ 使用Python解释器: $PYTHON_CMD" + +# 检查核心模块是否存在 +if [ ! -d "core" ]; then + echo "❌ 错误:找不到core目录" + echo "请确保在EG项目根目录中运行" + exit 1 +fi + +if [ ! -f "core/vr_manager.py" ]; then + echo "❌ 错误:找不到core/vr_manager.py文件" + echo "VR管理器模块不存在" + exit 1 +fi + +echo "✓ 核心模块检查通过" + +# 检查VR相关依赖 +echo "🔍 检查VR依赖..." + +$PYTHON_CMD -c "import openvr" 2>/dev/null +if [ $? -eq 0 ]; then + echo "✓ OpenVR Python绑定可用" +else + echo "⚠️ 警告:OpenVR Python绑定不可用" + echo " 如果需要VR功能,请安装: pip install openvr" +fi + +$PYTHON_CMD -c "from panda3d.core import Vec3" 2>/dev/null +if [ $? -eq 0 ]; then + echo "✓ Panda3D可用" +else + echo "❌ 错误:Panda3D不可用" + echo "请安装Panda3D: pip install panda3d" + exit 1 +fi + +# 检查可选的性能监控依赖 +echo "🔍 检查性能监控依赖..." + +$PYTHON_CMD -c "import psutil" 2>/dev/null +if [ $? -eq 0 ]; then + echo "✓ psutil可用(CPU/内存监控)" +else + echo "⚠️ 建议安装psutil以获得完整性能监控: pip install psutil" +fi + +$PYTHON_CMD -c "import GPUtil" 2>/dev/null +if [ $? -eq 0 ]; then + echo "✓ GPUtil可用(GPU监控)" +else + echo "⚠️ 建议安装GPUtil以获得GPU监控: pip install GPUtil" +fi + +echo "" +echo "🎮 启动前检查清单:" +echo "□ VR头显已连接并开机" +echo "□ SteamVR正在运行" +echo "□ VR头显已完成初始设置" +echo "□ VR跟踪系统正常工作" +echo "" + +# 询问用户是否继续 +echo "是否继续启动VR性能测试?" +echo "按Enter继续,或按Ctrl+C取消..." +read -r + +echo "" +echo "🚀 正在启动VR性能测试..." +echo "控制说明:" +echo " ESC - 退出测试" +echo " 1-9 - 设置场景复杂度" +echo " R - 重置性能计数器" +echo " P - 手动输出性能报告" +echo " D - 切换调试模式" +echo "VR分辨率控制:" +echo " Q - 切换质量预设 (性能/平衡/质量)" +echo " [ - 降低分辨率缩放" +echo " ] - 提高分辨率缩放" +echo " I - 显示分辨率信息" +echo "" + +# 启动测试 +$PYTHON_CMD vr_performance_test.py + +# 检查退出状态 +if [ $? -eq 0 ]; then + echo "" + echo "✅ VR性能测试正常结束" +else + echo "" + echo "❌ VR性能测试异常退出(错误代码:$?)" +fi + +echo "📋 检查当前目录中的CSV文件获取详细性能数据" +echo "📖 查看VR_PERFORMANCE_TEST_README.md获取详细使用说明" \ No newline at end of file diff --git a/scene/scene_manager.py b/scene/scene_manager.py index 1ad2d9ab..ebac4a6b 100644 --- a/scene/scene_manager.py +++ b/scene/scene_manager.py @@ -7,6 +7,7 @@ """ import os +import shutil import time from PyQt5.QtCore import Qt @@ -14,7 +15,7 @@ from PyQt5.QtWidgets import QTreeWidgetItem from panda3d.core import ( ModelPool, ModelRoot, Filename, NodePath, GeomNode, Material, Vec4, Vec3, MaterialAttrib, ColorAttrib, Point3, CollisionNode, CollisionSphere, - BitMask32, TransparencyAttrib, LColor, TransformState + BitMask32, TransparencyAttrib, LColor, TransformState, RenderModeAttrib ) import json import aiohttp @@ -93,52 +94,33 @@ class SceneManager: # ==================== 模型导入和处理 ==================== def importModel(self, filepath, apply_unit_conversion=False, normalize_scales=True, auto_convert_to_glb=True): - """导入模型到场景 - - Args: - filepath: 模型文件路径 - apply_unit_conversion: 是否应用单位转换(主要针对FBX文件) - normalize_scales: 是否标准化子节点缩放(推荐开启) - auto_convert_to_glb: 是否自动将非GLB格式转换为GLB以获得更好的动画支持 - """ - try: - print(f"\n=== 开始导入模型: {filepath} ===") - #print(f"单位转换: {'开启' if apply_unit_conversion else '关闭'}") - #print(f"自动转换GLB: {'开启' if auto_convert_to_glb else '关闭'}") + if not os.path.exists(filepath): + print("文件不存在") + return None filepath = util.normalize_model_path(filepath) original_filepath = filepath - # 在加载前设置忽略未知属性 - from panda3d.core import ConfigVariableBool - ConfigVariableBool("model-cache-ignore-unknown-properties").setValue(True) + # # 在加载前设置忽略未知属性 + # from panda3d.core import ConfigVariableBool + # ConfigVariableBool("model-cache-ignore-unknown-properties").setValue(True) + # + # # 清除可能存在的模型缓存 + # from panda3d.core import ModelPool + # ModelPool.releaseAllModels() + # + # # 检查是否需要转换为GLB以获得更好的动画支持 + # if auto_convert_to_glb and self._shouldConvertToGLB(filepath): + # print(f"🔄 检测到需要转换的格式,尝试转换为GLB...") + # converted_path = self._convertToGLBWithProgress(filepath) + # if converted_path: + # print(f"✅ 转换成功: {converted_path}") + # filepath = converted_path + # # 转换成功的消息已在控制台显示,不再弹窗提示 + # else: + # print(f"⚠️ 转换失败,使用原始文件") - # 清除可能存在的模型缓存 - from panda3d.core import ModelPool - ModelPool.releaseAllModels() - - # 检查是否需要转换为GLB以获得更好的动画支持 - if auto_convert_to_glb and self._shouldConvertToGLB(filepath): - #print(f"🔄 检测到需要转换的格式,尝试转换为GLB...") - converted_path = self._convertToGLBWithProgress(filepath) - if converted_path: - #print(f"✅ 转换成功: {converted_path}") - filepath = converted_path - # 显示成功消息 - try: - from PyQt5.QtWidgets import QMessageBox - original_ext = os.path.splitext(original_filepath)[1].upper() - QMessageBox.information(None, "转换成功", - f"已将 {original_ext} 格式自动转换为 GLB 格式\n以获得更好的动画支持!") - except: - pass - else: - print(f"⚠️ 转换失败,使用原始文件") - - # 总是重新加载模型以确保材质信息完整 - # 不使用ModelPool缓存,避免材质信息丢失问题 - #print("直接从文件加载模型...") model = self.world.loader.loadModel(filepath) if not model: print("加载模型失败") @@ -165,27 +147,17 @@ class SceneManager: model.setTag("converted_from", os.path.splitext(original_filepath)[1]) model.setTag("converted_to_glb", "true") - # 特殊处理FBX模型 - # if filepath.lower().endswith('.fbx'): - # print("检测到FBX模型,应用特殊处理...") - # - # # 将模型缩放设置为原来的1/100 - # model.setScale(0.01) - # print("设置模型缩放为 0.01 (原始大小的1/100)") - # - # # 设置模型旋转为 (0, 90, 0) - # model.setHpr(0, 90, 0) - # print("设置模型旋转为 (0, 90, 0)") + #特殊处理FBX模型 + if filepath.lower().endswith('.fbx'): + print("检测到FBX模型,应用特殊处理...") - # # 可选的单位转换(主要针对FBX - # if apply_unit_conversion and filepath.lower().endswith('.fbx'): - # #print("应用FBX单位转换(厘米到米)...") - # self._applyUnitConversion(model, 0.01) - # - # # 智能缩放标准化(处理FBX子节点的大缩放值) - # if normalize_scales and filepath.lower().endswith('.fbx'): - # #print("标准化FBX模型缩放层级...") - # self._normalizeModelScales(model) + # 将模型缩放设置为原来的1/100 + model.setScale(0.01) + print("设置模型缩放为 0.01 (原始大小的1/100)") + + # 设置模型旋转为 (0, 90, 0) + model.setHpr(0, 90, 0) + print("设置模型旋转为 (0, 90, 0)") # 调整模型位置到地面 model.setPos(0,0,0) @@ -193,7 +165,7 @@ class SceneManager: # 创建并设置基础材质 print("\n=== 开始设置材质 ===") - self._applyMaterialsToModel(model) + #self._applyMaterialsToModel(model) # 设置碰撞检测(重要!用于选择功能) print("\n=== 设置碰撞检测 ===") @@ -546,54 +518,6 @@ class SceneManager: # 失败时使用默认位置 model.setPos(0, 0, 0) - def _applyUnitConversion(self, model, scale_factor): - """应用单位转换缩放 - - Args: - model: 要转换的模型 - scale_factor: 缩放因子(如0.01表示从厘米转换到米) - """ - try: - print(f"应用单位转换缩放: {scale_factor}") - - # 检查模型是否已经应用过单位转换 - if model.hasTag("unit_conversion_applied"): - print("模型已应用过单位转换,跳过") - return - - - - # 获取当前边界用于后续位置调整 - original_bounds = model.getBounds() - - # 应用缩放 - model.setScale(scale_factor) - - # 应用缩放(添加异常处理) - try: - model.setScale(scale_factor) - except Exception as e: - print(f"直接设置缩放失败: {e},尝试使用变换状态") - try: - model.setTransform(TransformState.makeScale(scale_factor)) - except Exception as e2: - print(f"使用变换状态设置缩放也失败: {e2}") - - # 应用缩放后验证变换 - self._validateAndFixTransform(model) - # 重新调整位置(因为缩放会影响边界) - if original_bounds and not original_bounds.isEmpty(): - new_bounds = model.getBounds() - min_point = new_bounds.getMin() - ground_offset = -min_point.getZ() - model.setZ(ground_offset) - print(f"缩放后重新调整位置: Z偏移 = {ground_offset}") - - print(f"单位转换完成,缩放因子: {scale_factor}") - - except Exception as e: - print(f"应用单位转换失败: {str(e)}") - def _normalizeModelScales(self, model): """智能标准化模型缩放层级 @@ -685,7 +609,9 @@ class SceneManager: return None def _applyScaleNormalization(self, node, normalize_factor, depth=0): - """递归应用缩放标准化,同时调整位置以保持视觉一致性""" + """ + 安全地应用缩放标准化 + """ try: indent = " " * depth current_scale = node.getScale() @@ -695,8 +621,19 @@ class SceneManager: max_scale_component = max(abs(current_scale.x), abs(current_scale.y), abs(current_scale.z)) if max_scale_component > 10: # 只标准化明显的大缩放 + # 确保标准化因子有效 + if normalize_factor <= 0 or normalize_factor > 1000: + print(f"{indent}无效的标准化因子: {normalize_factor},跳过") + return + # 应用新的缩放 new_scale = current_scale * normalize_factor + + # 检查新缩放是否有效 + if any(s <= 0 for s in [new_scale.x, new_scale.y, new_scale.z]): + print(f"{indent}标准化后产生无效缩放,跳过") + return + node.setScale(new_scale) # 同时调整位置:当缩放变小时,位置也应该相应变小以保持视觉相对位置 @@ -708,11 +645,6 @@ class SceneManager: print(f"{indent} 缩放: {current_scale} -> {new_scale}") print(f"{indent} 位置: {current_pos} -> {new_pos}") - # 递归处理子节点 - for i in range(node.getNumChildren()): - child = node.getChild(i) - self._applyScaleNormalization(child, normalize_factor, depth + 1) - except Exception as e: print(f"应用缩放标准化失败 ({node.getName()}): {str(e)}") @@ -899,7 +831,9 @@ class SceneManager: "rotation": list(gui_node.getHpr()), "scale": list(gui_node.getScale()), "tags": {}, - "parent_name":None + "parent_name":None, + "video_path":gui_node.getTag("video_path") if gui_node.hasTag("video_path") else None, + "panel_id":gui_node.getTag("panel_id") if gui_node.hasTag("panel_id") else None, } parent = gui_node.getParent() @@ -948,10 +882,12 @@ class SceneManager: elif gui_type == "3d_image": if hasattr(gui_node,'hasTag') and gui_node.hasTag("gui_image_path"): gui_info["image_path"] = gui_node.getTag("gui_image_path") - elif gui_type in ["video_screen", "2d_video_screen"]: + elif gui_type == "video_screen": + if hasattr(gui_node, 'hasTag') and gui_node.hasTag("video_path"): + gui_info["video_path"] = gui_node.getTag("video_path") + elif gui_type == "2d_video_screen": if hasattr(gui_node, 'hasTag') and gui_node.hasTag("video_path"): gui_info["video_path"] = gui_node.getTag("video_path") - gui_info["video_path"] = gui_node.getTag("video_path") elif gui_type == "virtual_screen": if hasattr(gui_node, 'hasTag') and gui_node.hasTag("gui_text"): gui_info["text"] = gui_node.getTag("gui_text") @@ -961,39 +897,37 @@ class SceneManager: gui_info["panel_id"] = gui_node.getTag("panel_id") # 收集背景图片信息 - if hasattr(gui_node, 'hasTag') and gui_node.hasTag("bg_image_path"): - gui_info["bg_image_path"] = gui_node.getTag("bg_image_path") + if hasattr(gui_node, 'hasTag') and gui_node.hasTag("image_path"): + gui_info["image_path"] = gui_node.getTag("image_path") - # 如果是信息面板,收集面板数据 - if hasattr(gui_node, 'hasTag') and gui_node.hasTag("info_panel_data"): - gui_info["panel_data"] = gui_node.getTag("info_panel_data") + # 收集挂载的脚本信息 if hasattr(self.world, 'script_manager') and self.world.script_manager: try: script_manager = self.world.script_manager - scripts = script_manager.get_scripts_on_object(gui_node) + scripts = script_manager.get_scripts_on_object(gui_node) # 修复:使用 gui_node 而不是 node if scripts: gui_info["scripts"] = [] for script_component in scripts: try: - script_name = script_manager.script_name - #获取脚本路径 + script_name = script_component.script_name + # 获取脚本路径 script_class = script_component.script_instance.__class__ - script_file = self._get_script_file_path(script_class,script_name) - #只有当脚本文件存在时才保存 + script_file = self._get_script_file_path(script_class, script_name) + # 只有当脚本文件存在时才保存 if script_file and os.path.exists(script_file): - gui_info["scirpts"].append({ - "name":script_name, - "file":script_file + gui_info["scripts"].append({ + "name": script_name, + "file": script_file }) - print(f"收集脚本信息{script_name}from {script_file}") + print(f"收集脚本信息: {script_name} from {script_file}") else: print(f"警告: 脚本文件不存在: {script_file}") except Exception as e: - print(f"收集单个脚本信息失败{script_name},错误{e}") + print(f"收集单个脚本信息失败 {script_name}, 错误: {e}") continue except Exception as e: - print(f"收集脚本信息失败{e}") + print(f"收集脚本信息失败: {e}") print(f"成功收集GUI元素信息: {gui_info}") return gui_info @@ -1084,6 +1018,10 @@ class SceneManager: if directory and not os.path.exists(directory): os.makedirs(directory) + resources_dir = os.path.join(directory,"resources") + if not os.path.exists(resources_dir): + os.makedirs(resources_dir) + # 存储需要临时隐藏的节点,以便保存后恢复 nodes_to_restore = [] @@ -1134,11 +1072,76 @@ class SceneManager: print(self.world.gui_elements) # 收集GUI元素信息(排除3D文本和3D图像) gui_data = [] + copied_resources = {} for gui_node in gui_elements: gui_info = self._collectGUIElementInfo(gui_node) if gui_info: + gui_type = gui_info.get("type","") + #处理2d图片 + if gui_type =="2d_image" and "image_path" in gui_info: + original_path = gui_info["image_path"] + if original_path and os.path.exists(original_path): + resource_name = os.path.basename(original_path) + new_path = os.path.join(resources_dir,resource_name) + if original_path not in copied_resources: + try: + shutil.copy2(original_path,new_path) + copied_resources[original_path] = new_path + print(f"复制图片资源: {original_path} -> {new_path}") + except Exception as e: + print(f"复制图片资源失败: {original_path}, 错误: {e}") + gui_info["image_path"] = new_path + + # 处理3D图片 + elif gui_type == "3d_image" and "image_path" in gui_info: + original_path = gui_info["image_path"] + # 确保original_path是有效字符串且文件存在 + if original_path and isinstance(original_path, str) and os.path.exists(original_path): + resource_name = os.path.basename(original_path) + new_path = os.path.join(resources_dir, resource_name) + if original_path not in copied_resources: + try: + shutil.copy2(original_path, new_path) + copied_resources[original_path] = new_path + print(f"复制3D图片资源: {original_path} -> {new_path}") + except Exception as e: + print(f"复制3D图片资源失败: {original_path}, 错误: {e}") + gui_info["image_path"] = new_path + + # 处理背景图片 + if "bg_image_path" in gui_info and gui_info["bg_image_path"]: + original_path = gui_info["bg_image_path"] + # 确保original_path是有效字符串且文件存在 + if original_path and isinstance(original_path, str) and os.path.exists(original_path): + resource_name = os.path.basename(original_path) + new_path = os.path.join(resources_dir, resource_name) + if original_path not in copied_resources: + try: + shutil.copy2(original_path, new_path) + copied_resources[original_path] = new_path + print(f"复制背景图片资源: {original_path} -> {new_path}") + except Exception as e: + print(f"复制背景图片资源失败: {original_path}, 错误: {e}") + gui_info["bg_image_path"] = new_path + + # 处理视频资源 + if gui_type in ["video_screen", "2d_video_screen"] and "video_path" in gui_info: + original_path = gui_info["video_path"] + # 确保original_path是有效字符串且文件存在 + if original_path and isinstance(original_path, str) and os.path.exists(original_path): + resource_name = os.path.basename(original_path) + new_path = os.path.join(resources_dir, resource_name) + if original_path not in copied_resources: + try: + shutil.copy2(original_path, new_path) + copied_resources[original_path] = new_path + print(f"复制视频资源: {original_path} -> {new_path}") + except Exception as e: + print(f"复制视频资源失败: {original_path}, 错误: {e}") + gui_info["video_path"] = new_path + gui_data.append(gui_info) - print(f"添加GUI信息{gui_info['name']}") + print(f"添加GUI信息: {gui_info['name']}") # 保存GUI信息到JSON文件(确保即使没有GUI元素也创建有效的空JSON数组) try: @@ -1151,16 +1154,6 @@ class SceneManager: import traceback traceback.print_exc() - # 添加tilesets节点 - for tileset_info in self.tilesets: - if tileset_info.get('node') and not tileset_info['node'].isEmpty(): - all_nodes.append(tileset_info['node']) - - # 添加Cesium tilesets节点 - for tileset_name, tileset_info in self.cesium_integration.tilesets.items(): - if tileset_info.get('node') and not tileset_info['node'].isEmpty(): - all_nodes.append(tileset_info['node']) - # 保存所有节点的信息 for node in all_nodes: if node.isEmpty(): @@ -1436,6 +1429,10 @@ class SceneManager: #存储节点以便后续处理父子关系 loaded_nodes[nodePath.getName()] = nodePath + if nodePath.getName().startswith('ground'): + print(f"{indent}跳过ground节点: {nodePath.getName()}") + return + # 跳过render节点的递归 if nodePath.getName() == "render" and depth > 0: print(f"{indent}跳过重复的render节点") @@ -1529,6 +1526,8 @@ class SceneManager: nodePath.setScale(scale) print(f"{indent}恢复缩放: {scale}") + nodePath.show() + if nodePath.hasTag("has_scripts") and nodePath.getTag("has_scripts") == "true": if hasattr(self.world,'script_manager') and self.world.script_manager: try: @@ -1782,6 +1781,10 @@ class SceneManager: def _shouldSkipNodeInTree(self, nodePath): """判断节点是否应该在场景树中跳过显示""" + + if nodePath.getName().startswith('ground'): + return True + # 跳过render节点的递归 if nodePath.getName() == "render": return True @@ -1805,12 +1808,13 @@ class SceneManager: return True return False + def _recreateGUIElementsFromData(self, gui_data): """根据保存的GUI数据重新创建GUI元素""" try: gui_manager = getattr(self.world, 'gui_manager', None) property_manager = getattr(self.world, 'property_panel', None) - info_panel_manager = getattr(self.world,'info_panel_manager',None) + info_panel_manager = getattr(self.world, 'info_panel_manager', None) if not gui_manager: print("GUI管理器未找到,无法重建GUI元素") return @@ -1818,7 +1822,7 @@ class SceneManager: processed_names = set() created_elements = {} - #存储原始的缩放和位置信息,用于后续计算 + # 存储原始的缩放和位置信息,用于后续计算 element_original_data = {} # 第一遍:收集所有元素信息 @@ -1832,10 +1836,10 @@ class SceneManager: valid_parents = set() for gui_info in gui_data: - name = gui_info.get("name",f"gui_element_{gui_info.get('index',0)}") + name = gui_info.get("name", f"gui_element_{gui_info.get('index', 0)}") valid_parents.add(name) - if hasattr(self.world,'gui_elements'): + if hasattr(self.world, 'gui_elements'): for elem in self.world.gui_elements: if elem and not elem.isEmpty(): valid_parents.add(elem.getName()) @@ -1877,7 +1881,7 @@ class SceneManager: print(f" 文本: {text}") print(f" 图像路径: {image_path}") print(f" 背景图片路径: {bg_image_path}") - print(f"视频路径:{video_path}") + print(f" 视频路径: {video_path}") absolute_position = list(position) absolute_scale = list(scale) @@ -1922,37 +1926,30 @@ class SceneManager: size=absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 1.0 ) elif gui_type == "2d_image" and hasattr(gui_manager, 'createGUI2DImage'): - scale_value = absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 0.2 new_element = gui_manager.createGUI2DImage( pos=tuple(absolute_position), image_path=image_path, - size=scale_value*0.2 + size=(0.8,0.8,0.8) ) - elif gui_type == "3d_text" and hasattr(gui_manager,'createGUI3DText'): + elif gui_type == "3d_text" and hasattr(gui_manager, 'createGUI3DText'): size = absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 0.5 new_element = gui_manager.createGUI3DText( pos=tuple(absolute_position), text=text, - size=size + size=absolute_scale ) elif gui_type == "3d_image" and hasattr(gui_manager, 'createGUI3DImage'): # 处理3D图像 # 根据缩放值的数量处理尺寸 - if len(absolute_scale) >= 3: - size = (absolute_scale[0] * 2, absolute_scale[1] * 2) - elif len(absolute_scale) >= 2: - size = (absolute_scale[0] * 2, absolute_scale[1] * 2) - elif len(scale) >= 1: - size = (absolute_scale[0] * 2, absolute_scale[0] * 2) - else: - size = (1.0, 1.0) + size = (absolute_scale[0] * 0.2, absolute_scale[1] * 0.2, absolute_scale[2] * 0.2) new_element = gui_manager.createGUI3DImage( pos=tuple(absolute_position), image_path=image_path, size=size ) - elif gui_type == "video_screen" and hasattr(gui_manager,'createVideoScreen'): + elif gui_type == "video_screen" and hasattr(gui_manager, 'createVideoScreen'): + print(f"重建的3d视频屏幕视频地址是{video_path}") new_element = gui_manager.createVideoScreen( pos=tuple(absolute_position), size=absolute_scale, @@ -1960,31 +1957,19 @@ class SceneManager: ) if video_path and new_element: if video_path.startswith("http://") or video_path.startswith("https://"): - from direct.task.TaskManagerGlobal import taskMgr - def load_video_stream_task(task): - if hasattr(property_manager,'_loadVideoFromURLWithOpenCV_3D'): - property_manager._loadVideoFromURLWithOpenCV_3D(new_element,video_path) - return task.done - - taskMgr.doMethodLater(0.5, load_video_stream_task, 'loadVideoStreamTask') + pass else: - if hasattr(gui_manager,'loadVideoFile'): + if hasattr(gui_manager, 'loadVideoFile'): from direct.task.TaskManagerGlobal import taskMgr + def load_video_file_task(task): - gui_manager.loadVideoFile(new_element,video_path) + gui_manager.loadVideoFile(new_element, video_path) return task.done - taskMgr.doMethodLater(0.1,load_video_file_task,'loadVideoFileTask') - # if video_path and new_element and hasattr(gui_manager, 'loadVideoFile'): - # # 延迟一帧执行,确保节点完全初始化 - # from direct.task.TaskManagerGlobal import taskMgr - # def load_video_task(task): - # gui_manager.loadVideoFile(new_element, video_path) - # return task.done - # - # taskMgr.doMethodLater(0.1, load_video_task, 'loadVideoTask') + taskMgr.doMethodLater(0.1, load_video_file_task, 'loadVideoFileTask') - elif gui_type == "2d_video_screen" and hasattr(gui_manager,'createGUI2DVideoScreen'): + elif gui_type == "2d_video_screen" and hasattr(gui_manager, 'createGUI2DVideoScreen'): + print(f"重建的2d视频屏幕视频地址是{video_path}") new_element = gui_manager.createGUI2DVideoScreen( pos=tuple(absolute_position), size=absolute_scale, @@ -1993,21 +1978,17 @@ class SceneManager: if video_path and new_element: if video_path.startswith("http://") or video_path.startswith("https://"): pass - # from direct.task.TaskManagerGlobal import taskMgr - # def load_2d_video_stream_task(task): - # if hasattr(property_manager,'_loadVideoFromURLWithOpenCV'): - # property_manager._loadVideoFromURLWithOpenCV(new_element,video_path) - # return task.done - # taskMgr.doMethodLater(0.1,load_2d_video_stream_task,'load2DVideoStreamTask') else: - if hasattr(property_manager,'load2DVideoFile'): + if hasattr(property_manager, 'load2DVideoFile'): from direct.task.TaskManagerGlobal import taskMgr + def load_2d_video_file_task(task): - property_manager.load2DVideoFile(new_element,video_path) + property_manager.load2DVideoFile(new_element, video_path) return task.done - taskMgr.doMethodLater(0.1,load_2d_video_file_task,'load2DVideoFileTask') - + taskMgr.doMethodLater(0.1, load_2d_video_file_task, 'load2DVideoFileTask') + elif gui_type == "info_panel": + new_element = self.world.info_panel_manager.onCreateSampleInfoPanel() # 如果创建成功,设置属性 if new_element: # 如果返回的是列表(多选创建),取第一个 @@ -2048,84 +2029,84 @@ class SceneManager: traceback.print_exc() continue - # 第二遍:设置父子级关系并更新Qt树 - print("开始设置父子级关系...") - try: - # 创建父子级关系映射 - parent_child_map = {} - for gui_info in gui_data: - name = gui_info.get("name") - parent_name = gui_info.get("parent_name") + # 第二遍:设置父子级关系并更新Qt树 + print("开始设置父子级关系...") + try: + # 创建父子级关系映射 + parent_child_map = {} + for gui_info in gui_data: + name = gui_info.get("name") + parent_name = gui_info.get("parent_name") - if name and parent_name and parent_name in created_elements: - parent_child_map[name] = parent_name - print(f"父子级关系映射: {parent_name} -> {name}") + if name and parent_name and parent_name in created_elements: + parent_child_map[name] = parent_name + print(f"父子级关系映射: {parent_name} -> {name}") - # 按正确的顺序设置父子级关系并更新Qt树 - tree_widget = self._get_tree_widget() - if tree_widget: - # 先将所有元素添加到Qt树中 - qt_tree_items = {} - for name, element in created_elements.items(): - # 尝试在Qt树中找到对应的项,如果找不到则创建 - qt_item = self._findOrCreateQtTreeItem(tree_widget, element, name) - if qt_item: - qt_tree_items[name] = qt_item + # 按正确的顺序设置父子级关系并更新Qt树 + tree_widget = self._get_tree_widget() + if tree_widget: + # 先将所有元素添加到Qt树中 + qt_tree_items = {} + for name, element in created_elements.items(): + # 尝试在Qt树中找到对应的项,如果找不到则创建 + qt_item = self._findOrCreateQtTreeItem(tree_widget, element, name) + if qt_item: + qt_tree_items[name] = qt_item - # 然后设置父子级关系 - for child_name, parent_name in parent_child_map.items(): - try: - if child_name in created_elements and parent_name in created_elements: - child_element = created_elements[child_name] - parent_element = created_elements[parent_name] + # 然后设置父子级关系 + for child_name, parent_name in parent_child_map.items(): + try: + if child_name in created_elements and parent_name in created_elements: + child_element = created_elements[child_name] + parent_element = created_elements[parent_name] - # 设置父子级关系 - if hasattr(child_element, 'reparentTo'): - child_element.reparentTo(parent_element) - print(f"成功设置父子级关系: {parent_name} -> {child_name}") + # 设置父子级关系 + if hasattr(child_element, 'reparentTo'): + child_element.reparentTo(parent_element) + print(f"成功设置父子级关系: {parent_name} -> {child_name}") - # 更新Qt树显示 - if child_name in qt_tree_items and parent_name in qt_tree_items: - child_item = qt_tree_items[child_name] - parent_item = qt_tree_items[parent_name] + # 更新Qt树显示 + if child_name in qt_tree_items and parent_name in qt_tree_items: + child_item = qt_tree_items[child_name] + parent_item = qt_tree_items[parent_name] - # 从当前位置移除子项 - if child_item.parent(): - child_item.parent().removeChild(child_item) - else: - # 如果是顶级项,从树中移除 - index = tree_widget.indexOfTopLevelItem(child_item) - if index >= 0: - tree_widget.takeTopLevelItem(index) + # 从当前位置移除子项 + if child_item.parent(): + child_item.parent().removeChild(child_item) + else: + # 如果是顶级项,从树中移除 + index = tree_widget.indexOfTopLevelItem(child_item) + if index >= 0: + tree_widget.takeTopLevelItem(index) - # 将子项添加到新的父项下 - parent_item.addChild(child_item) - print(f"Qt树更新: {child_name} 移动到 {parent_name} 下") - else: - print(f"元素 {child_name} 不支持 reparentTo 操作") + # 将子项添加到新的父项下 + parent_item.addChild(child_item) + print(f"Qt树更新: {child_name} 移动到 {parent_name} 下") else: - print(f"元素未找到: 父级={parent_name}, 子级={child_name}") - except Exception as e: - print(f"设置父子级关系失败 {parent_name} -> {child_name}: {e}") - continue - else: - # 如果没有tree_widget,只设置父子级关系 - for child_name, parent_name in parent_child_map.items(): - try: - if child_name in created_elements and parent_name in created_elements: - child_element = created_elements[child_name] - parent_element = created_elements[parent_name] + print(f"元素 {child_name} 不支持 reparentTo 操作") + else: + print(f"元素未找到: 父级={parent_name}, 子级={child_name}") + except Exception as e: + print(f"设置父子级关系失败 {parent_name} -> {child_name}: {e}") + continue + else: + # 如果没有tree_widget,只设置父子级关系 + for child_name, parent_name in parent_child_map.items(): + try: + if child_name in created_elements and parent_name in created_elements: + child_element = created_elements[child_name] + parent_element = created_elements[parent_name] - # 设置父子级关系 - if hasattr(child_element, 'reparentTo'): - child_element.reparentTo(parent_element) - print(f"成功设置父子级关系: {parent_name} -> {child_name}") - except Exception as e: - print(f"设置父子级关系失败 {parent_name} -> {child_name}: {e}") - continue + # 设置父子级关系 + if hasattr(child_element, 'reparentTo'): + child_element.reparentTo(parent_element) + print(f"成功设置父子级关系: {parent_name} -> {child_name}") + except Exception as e: + print(f"设置父子级关系失败 {parent_name} -> {child_name}: {e}") + continue - except Exception as e: - print(f"设置父子级关系时出错: {e}") + except Exception as e: + print(f"设置父子级关系时出错: {e}") # 第三遍:重新挂载脚本 print("开始重新挂载脚本...") for gui_info in gui_data: @@ -2173,13 +2154,15 @@ class SceneManager: else: print(f"为 {name} 添加脚本失败: {script_name}") except Exception as e: - print(f"重新挂载脚本失败 {gui_info.get('name', 'unknown')}: {e}") + print(f"重新挂载脚本失败: {e}") + import traceback + traceback.print_exc() continue - print("GUI元素重建完成") + print(f"GUI元素重建完成,共创建 {len(created_elements)} 个元素") except Exception as e: - print(f"重建GUI元素时发生错误: {e}") + print(f"重建GUI元素时出错: {e}") import traceback traceback.print_exc() @@ -2276,7 +2259,6 @@ class SceneManager: """重新创建聚光灯""" try: from RenderPipelineFile.rpcore import SpotLight - from QPanda3D.Panda3DWorld import get_render_pipeline from panda3d.core import Vec3 # 创建聚光灯对象 @@ -2506,6 +2488,7 @@ class SceneManager: light.radius = 1000 light.casts_shadows = True light.shadow_map_resolution = 256 + light.setPos(pos) # 添加到渲染管线 render_pipeline.add_light(light) @@ -2682,177 +2665,6 @@ class SceneManager: pass return None - # def createSpotLight(self, pos=(0, 0, 0)): - # """创建聚光灯 - 使用统一的create_item方法""" - # try: - # # 调用CustomTreeWidget的create_item方法创建聚光灯节点 - # if hasattr(self.world, 'interface_manager') and hasattr(self.world.interface_manager, 'treeWidget'): - # tree_widget = self.world.interface_manager.treeWidget - # if tree_widget and hasattr(tree_widget, 'create_item'): - # # 创建聚光灯节点 - # created_nodes = tree_widget.create_item("spot_light") - # - # if created_nodes: - # # 获取创建的节点 - # light_np, qt_item = created_nodes[0] - # - # # 设置位置(如果指定了非默认位置) - # if pos != (0, 0, 0): - # light_np.setPos(*pos) - # # 同时更新光源对象的位置 - # light_obj = light_np.getPythonTag("rp_light_object") - # if light_obj: - # light_obj.setPos(*pos) - # - # print(f"✅ 通过create_item创建聚光灯成功: {light_np.getName()}") - # return light_np - # else: - # print("❌ create_item创建聚光灯失败") - # return None - # else: - # print("❌ 无法访问树形控件的create_item方法") - # return None - # else: - # print("❌ 无法访问界面管理器或树形控件") - # return None - # - # except Exception as e: - # print(f"❌ 创建聚光灯时发生错误: {str(e)}") - # import traceback - # traceback.print_exc() - # return None - - # def createSpotLight(self, pos=(0, 0, 0)): - # from RenderPipelineFile.rpcore import SpotLight, RenderPipeline - # from panda3d.core import Vec3,NodePath - # - # render_pipeline = get_render_pipeline() - # - # # 创建一个挂载节点(你控制的) - # light_np = NodePath("SpotlightAttachNode") - # light_np.reparentTo(self.world.render) - # #light_np.setPos(*pos) - # - # self.half_energy = 5000 - # self.lamp_fov = 70 - # self.lamp_radius = 1000 - # - # light = SpotLight() - # light.direction = Vec3(0, 0, -1) # 光照方向 - # light.fov = self.lamp_fov # 光源角度(类似手电筒) - # light.set_color_from_temperature(5 * 1000.0) # 色温(K) - # light.energy = self.half_energy # 光照强度 - # light.radius = self.lamp_radius # 影响范围 - # light.casts_shadows = True # 是否投射阴影 - # light.shadow_map_resolution = 256 # 阴影分辨率 - # light.setPos(*pos) - # #light_np.setPos(*pos) - # - # #light_np = render_pipeline.add_light(light, parent=self.world.render) - # render_pipeline.add_light(light) # 添加到渲染管线 - # - # light_name = f"Spotlight_{len(self.Spotlight)}" - # - # light_np.setName(light_name) # 设置唯一名称 - # #light_np.reparentTo(self.world.render) # 挂载到场景根节点 - # - # light_np.setTag("light_type", "spot_light") - # light_np.setTag("is_scene_element", "1") - # light_np.setTag("light_energy", str(light.energy)) - # - # light_np.setPythonTag("rp_light_object", light) - # - # self.Spotlight.append(light_np) - # - # if hasattr(self.world, 'updateSceneTree'): - # self.world.updateSceneTree() - # - # #print("nikan"+light_np.getHpr()) - - # def createPointLight(self, pos=(0, 0, 0)): - # """创建点光源 - 使用统一的create_item方法""" - # try: - # # 调用CustomTreeWidget的create_item方法创建点光源节点 - # if hasattr(self.world, 'interface_manager') and hasattr(self.world.interface_manager, 'treeWidget'): - # tree_widget = self.world.interface_manager.treeWidget - # if tree_widget and hasattr(tree_widget, 'create_item'): - # # 创建点光源节点 - # created_nodes = tree_widget.create_item("point_light") - # - # if created_nodes: - # # 获取创建的节点 - # light_np, qt_item = created_nodes[0] - # - # # 设置位置(如果指定了非默认位置) - # if pos != (0, 0, 0): - # light_np.setPos(*pos) - # # 同时更新光源对象的位置 - # light_obj = light_np.getPythonTag("rp_light_object") - # if light_obj: - # light_obj.setPos(*pos) - # - # print(f"✅ 通过create_item创建点光源成功: {light_np.getName()}") - # return light_np - # else: - # print("❌ create_item创建点光源失败") - # return None - # else: - # print("❌ 无法访问树形控件的create_item方法") - # return None - # else: - # print("❌ 无法访问界面管理器或树形控件") - # return None - # - # except Exception as e: - # print(f"❌ 创建点光源时发生错误: {str(e)}") - # import traceback - # traceback.print_exc() - # return None - - # def createPointLight(self, pos=(0, 0, 0)): - # from RenderPipelineFile.rpcore import PointLight, RenderPipeline - # from panda3d.core import Vec3, NodePath - # - # render_pipeline = get_render_pipeline() - # - # # 创建一个挂载节点(你控制的) - # light_np = NodePath("PointlightAttachNode") - # light_np.reparentTo(self.world.render) - # - # - # light = PointLight() - # light.setPos(*pos) - # light_np.setPos(*pos) - # light.energy = 5000 - # light.radius = 1000 - # light.inner_radius = 0.4 - # light.set_color_from_temperature(5 * 1000.0) # 色温(K) - # light.casts_shadows = True # 是否投射阴影 - # light.shadow_map_resolution = 256 # 阴影分辨率 - # - # render_pipeline.add_light(light) # 添加到渲染管线 - # - # light_name = f"Pointlight{len(self.Pointlight)}" - # - # light_np.setName(light_name) # 设置唯一名称 - # - # #light_np = NodePath(f"PointLight_{len(self.Pointlight)}") - # #light_np.reparentTo(self.world.render) - # #light_np.setPos(*pos) - # - # light_np.setTag("light_type", "point_light") - # light_np.setTag("is_scene_element", "1") - # light_np.setTag("light_energy", str(light.energy)) - # - # # 保存光源对象引用(重要!用于属性面板) - # light_np.setPythonTag("rp_light_object", light) - # - # self.Pointlight.append(light_np) - # - # if hasattr(self.world, 'updateSceneTree'): - # self.world.updateSceneTree() - # - # return light,light_np # ==================== GLB 转换方法 ==================== @@ -3762,7 +3574,7 @@ except Exception as e: elif node_type == "CESIUM_TILESET_NODE": new_node = self._recreateTilesetFromData(node_data, parent_node, unique_name) elif node_type in ["GUI_BUTTON", "GUI_LABEL", "GUI_ENTRY", "GUI_IMAGE", - "GUI_3DTEXT", "GUI_3DIMAGE", "GUI_VIRTUAL_SCREEN"]: + "GUI_3DTEXT", "GUI_3DIMAGE", "GUI_VIDEO_SCREEN","GUI_2D_VIDEO_SCREEN"]: new_node = self._recreateGUIFromData(node_data, parent_node, unique_name) elif node_type == "IMPORTED_MODEL_NODE": new_node = self._recreateModelFromData(node_data, parent_node, unique_name) @@ -3947,7 +3759,7 @@ except Exception as e: new_gui_element = self.world.createGUI3DText(pos, text, scale) elif gui_type == "3d_image" and hasattr(self.world, 'createGUI3DImage'): pos = node_data.get('pos', (0, 0, 0)) - image_path = node_data.get('tags').get('image_path', '') + image_path = node_data.get('tags').get('gui_image_path', '') scale = node_data.get('scale', (1, 1)) if isinstance(scale, (int, float)): scale = (scale, scale) @@ -3956,7 +3768,17 @@ except Exception as e: else: scale = (1, 1) print(f"正在创建3D图片: 位置={pos}, 路径={image_path}, 大小={scale}") - new_gui_element = self.world.createGUI3DImage(pos, image_path, scale) + new_gui_element = self.world.gui_manager.createGUI3DImage(pos, image_path, scale) + elif gui_type == "video_screen" and hasattr(self.world.gui_manager, 'createVideoScreen'): + pos = node_data.get('pos', (0, 0, 0)) + video_path = node_data.get('tags').get('video_path', '') + scale = node_data.get('scale', (1, 1,1)) + new_gui_element = self.world.gui_manager.createVideoScreen(pos,scale,video_path) + elif gui_type == "2d_video_screen" and hasattr(self.world.gui_manager, 'createGUI2DVideoScreen'): + pos = node_data.get('pos', (0, 0, 0)) + video_path = node_data.get('tags').get('video_path', '') + scale = node_data.get('scale', (1, 1, 1)) + new_gui_element = self.world.gui_manager.createGUI2DVideoScreen(pos,scale,video_path) if new_gui_element: # 设置名称和变换 diff --git a/scripts/a.py b/scripts/a.py new file mode 100644 index 00000000..34bd5408 --- /dev/null +++ b/scripts/a.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +a - 自定义脚本 +""" + +from core.script_system import ScriptBase + +class A(ScriptBase): + """自定义脚本类""" + + def __init__(self): + super().__init__() + # 在这里初始化您的变量 + + def start(self): + """脚本开始时调用""" + self.log("脚本开始运行!") + + def update(self, dt): + """每帧更新""" + # 在这里编写更新逻辑 + pass + + def on_destroy(self): + """脚本销毁时调用""" + self.log("脚本被销毁") diff --git a/templates/main_template.py b/templates/main_template.py new file mode 100644 index 00000000..9108750a --- /dev/null +++ b/templates/main_template.py @@ -0,0 +1,1567 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +测试动画模型 - Panda3D应用程序 +使用Panda3D引擎编辑器创建 +""" + +from __future__ import print_function + +import json + +from direct.actor.Actor import Actor +from direct.showbase.ShowBaseGlobal import globalClock +from panda3d.core import TextNode, CardMaker, TextureStage, NodePath, Texture, TransparencyAttrib, CollisionTraverser, \ + Point3 +from core.InfoPanelManager import InfoPanelManager +# 获取渲染管线路径 +# 在文件开头添加sys导入(如果还没有的话) +import sys +import os + +# 修改工作目录设置部分 +if getattr(sys, 'frozen', False): + # 打包后的环境 + project_root = os.path.dirname(sys.executable) +else: + # 开发环境 + try: + project_root = os.path.dirname(os.path.abspath(__file__)) + except NameError: + project_root = os.getcwd() + +os.chdir(project_root) + +render_pipeline_path = 'RenderPipelineFile' +sys.path.insert(0, render_pipeline_path) + +# 改进路径设置逻辑 +pipeline_path = os.path.join(project_root, "RenderPipelineFile") +if os.path.exists(pipeline_path) and pipeline_path not in sys.path: + sys.path.insert(0, pipeline_path) + # 同时添加子目录以确保所有模块都能正确导入 + for root, dirs, files in os.walk(pipeline_path): + if root not in sys.path: + sys.path.insert(0, root) + + +import math +from random import random, randint, seed +from panda3d.core import Vec3, load_prc_file_data, Filename +from direct.showbase.ShowBase import ShowBase +from direct.task.TaskManagerGlobal import taskMgr + +# os.chdir(os.path.dirname(os.path.realpath(__file__))) +from core.script_system import ScriptManager +from core.CustomMouseController import CustomMouseController +from panda3d.core import CollisionTraverser + +class MainApp(ShowBase): + def __init__(self): + # 在调用父类构造函数前确保必要的属性存在 + if not hasattr(self, 'appRunner'): + self.appRunner = None + + load_prc_file_data("", """ + win-size 1380 750 + window-title + support-threads #t + """) + + # 简化 sys.path 设置逻辑 + pipeline_path = os.path.join(project_root, "RenderPipelineFile") + if os.path.exists(pipeline_path): + if pipeline_path not in sys.path: + sys.path.insert(0, pipeline_path) + else: + print(f"错误: 找不到渲染管线目录: {pipeline_path}") + return + + try: + from rpcore import RenderPipeline + self.render_pipeline = RenderPipeline() + self.render_pipeline.create(self) + + except ImportError as e: + + print(f"导入RenderPipeline模块失败: {e}") + import traceback + traceback.print_exc() + ShowBase.__init__(self) + self.render_pipeline = None + return + + self.script_manager = ScriptManager(self) + self.script_manager.start_system() + + # 加载所有脚本e + self.script_manager.load_all_scripts_from_directory() + + self.info_panel_manager = InfoPanelManager(self) + + try: + # 再导入controller模块 + from rpcore.util.movement_controller import MovementController + + self.render_pipeline._showbase.camera = self.render_pipeline._showbase.cam + + self.controller = MovementController(self) + self.camLens.set_fov(80) + self.controller.set_initial_position( + Vec3(0, -50, 20), Vec3(0, 0, 0)) + self.controller.setup() + except ImportError as e: + print(f"导入MovementController失败: {e}") + self.controller = None + + self._last_click_time = 0 + self._last_clicked_node = None + self._double_click_threshold = 0.3 + + self.cameraSpeed = 20.0 + self.cameraRotateSpeed=10.0 + + self.lastMouseX=0 + self.lastMouseY=0 + self.mouseRightPressed=False + + self._loadFont() + self.loadFullScene() + self.loadGUIFromJSON() + self.setupMouseClickHandler() + self.cTrav = CollisionTraverser() + + self.createFrameRateDisplay() + + self.women_actor = None + self.current_actor = None + + if hasattr(self, 'accept'): + base.accept("l", self.tour) + + def _loadFont(self): + """加载中文字体""" + self.chinese_font = None + try: + self.chinese_font = self.loader.loadFont('/usr/share/fonts/truetype/wqy/wqy-microhei.ttc') + if not self.chinese_font: + print("警告: 无法加载中文字体,将使用默认字体") + else: + print("✓ 中文字体加载成功") + except: + print("警告: 无法加载中文字体,将使用默认字体") + self.chinese_font = None + + def getChineseFont(self): + """获取中文字体""" + return self.chinese_font + + def getResourcePath(self,relative_path): + if getattr(sys,'frozen',False): + base_path = os.path.dirname(sys.executable) + else: + base_path = os.path.dirname(os.path.abspath(__file__)) + + return os.path.join(base_path,relative_path) + + def loadFullScene(self): + if not hasattr(self, 'loader') or not hasattr(self, 'render'): + print("错误: Panda3D核心组件未正确初始化") + return + try: + scene_file = self.getResourcePath("scene.bam") + + if os.path.exists(scene_file): + # 使用readBamFile加载完整场景 + from panda3d.core import BamCache + BamCache.getGlobalPtr().setActive(False) # 禁用缓存以避免问题 + + scene = self.loader.loadModel(Filename.fromOsSpecific(scene_file)) + if scene: + scene.reparentTo(self.render) + self.render_pipeline.prepare_scene(scene) + print("✓ 完整场景加载成功") + + # 检测并播放模型动画 + #self._processModelAnimations(scene) + + # 处理场景中的各种元素 + self.processSceneElements(scene) + else: + print("⚠️ 场景文件加载失败") + else: + print("⚠️ 未找到场景文件") + except Exception as e: + print(f"加载完整场景时出错: {str(e)}") + import traceback + traceback.print_exc() + + def checkAnimationStatus(self): + """检查场景中所有Actor的动画状态""" + try: + all_actors = self.render.findAllMatches("**/+ActorNode") + print(f"=== 动画状态检查 ===") + print(f"场景中总共有 {len(all_actors)} 个Actor") + + for i, actor_np in enumerate(all_actors): + actor_node = actor_np.node() + if isinstance(actor_node, Actor): + # 确保Actor可见 + actor_np.show() + + current_anim = actor_node.getCurrentAnim() + anim_names = actor_node.getAnimNames() + print(f"Actor {i + 1} ({actor_np.getName()}):") + print(f" 位置: {actor_np.getPos()}") + print(f" 可见性: {actor_np.isHidden()}") + print(f" 可用动画: {anim_names}") + print(f" 当前播放: {current_anim}") + if current_anim: + frame = actor_node.getCurrentFrame(current_anim) + num_frames = actor_node.getNumFrames(current_anim) + play_rate = actor_node.getPlayRate(current_anim) + print(f" 播放进度: {frame}/{num_frames} 帧") + print(f" 播放速度: {play_rate}") + else: + print(f"节点 {actor_np.getName()} 不是Actor类型") + print("==================") + except Exception as e: + print(f"检查动画状态时出错: {str(e)}") + + def _processModelAnimations(self, node_path): + """处理节点中的动画模型""" + try: + # 查找场景中所有可能的动画模型 + char_nodes = node_path.findAllMatches("**/+Character") + + for char_node in char_nodes: + try: + # 获取父节点(通常是模型根节点) + model_root = char_node.getParent() + model_name = model_root.getName() + + print(f"检测到可能的动画模型: {model_name}") + + # 尝试创建Actor + actor = Actor(model_root) + actor.reparentTo(self.render) + + actor.setPos(node_path.getPos()) + actor.setHpr(node_path.getHpr()) + actor.setScale(node_path.getScale()) + + + + # 获取动画名称 + anim_names = actor.getAnimNames() + if anim_names: + print(f"✓ 成功创建动画模型: {model_name}") + print(f" 可用动画: {anim_names}") + + # 循环播放所有动画 + for anim_name in anim_names: + print(f" 循环播放动画: {anim_name}") + actor.loop(anim_name) + break # 只播放第一个动画,避免同时播放多个动画 + + # 替换原始模型 + model_root.detachNode() + + else: + # 没有动画,使用原始模型 + print(f"模型 {model_name} 不包含动画") + actor.detachNode() # 移除创建的Actor + + except Exception as e: + print(f"处理动画模型 {char_node.getName()} 时出错: {str(e)}") + + except Exception as e: + print(f"处理模型动画时出错: {str(e)}") + import traceback + traceback.print_exc() + + def processSceneElements(self, scene): + """处理场景中的各种元素""" + try: + processed_lights = [] + loaded_nodes = {} + + def processNode(nodePath, depth=0): + loaded_nodes[nodePath.getName()] = nodePath + + # 为模型添加碰撞体 - 修复版本 + if nodePath.hasTag("is_scene_element") and not nodePath.hasTag("is_gizmo"): + if nodePath.hasTag("gui_type"): + gui_type = nodePath.getTag("gui_type") + if gui_type in ["video_screen"]: + print(f"移除GUI视频节点: {nodePath.getName()}") + nodePath.removeNode() # 移除视频屏幕节点 + return + # 使用更精确的包围盒 + from panda3d.core import CollisionNode, CollisionBox + bounds = nodePath.getBounds() + if not bounds.isEmpty(): + min_point = bounds.getMin() + max_point = bounds.getMax() + + # 创建碰撞节点 + collision_node = CollisionNode(f'collision_{nodePath.getName()}') + collision_node.addSolid(CollisionBox(min_point, max_point)) + collision_np = nodePath.attachNewNode(collision_node) + # 隐藏碰撞体 + collision_np.hide() + + if nodePath.hasTag("scripts_info"): + try: + import json + scripts_info = json.loads(nodePath.getTag("scripts_info")) + self.processScripts(nodePath, scripts_info) + except Exception as e: + print(f"处理节点 {nodePath.getName()} 的脚本时出错: {str(e)}") + + if nodePath.hasTag("light_type"): + light_type = nodePath.getTag("light_type") + + if nodePath.hasTag("is_auxiliary_light") and nodePath.getTag("is_auxiliary_light").lower() == "true": + print(f"跳过辅助灯光节点:{nodePath.getName()}") + return + + if nodePath not in processed_lights: + if light_type == "spot_light": + self._recreateSpotLight(nodePath) + elif light_type == "point_light": + self._recreatePointLight(nodePath) + processed_lights.append(nodePath) + + for child in nodePath.getChildren(): + processNode(child, depth + 1) + + processNode(scene) + + except Exception as e: + print(f"处理场景元素时出错: {str(e)}") + + def _recreateSpotLight(self,light_node): + try: + from RenderPipelineFile.rpcore import SpotLight + from panda3d.core import Vec3 + + light = SpotLight() + light.direction = Vec3(0,0,-1) + light.fov = 70 + light.set_color_from_temperature(5*1000.0) + + if light_node.hasTag("light_energy"): + light.energy = float(light_node.getTag("light_energy")) + else: + light.energy = 5000 + + light.radius = 1000 + light.casts_shadows = True + light.shadow_map_resolution = 256 + + light_pos = light_node.getPos() + light.setPos(light_pos) + + self.render_pipeline.add_light(light) + except Exception as e: + print(f"创建点光源 {light_node.getName()} 失败: {str(e)}") + import traceback + traceback.print_exc() + + def _recreatePointLight(self,light_node): + try: + from RenderPipelineFile.rpcore import PointLight + + light = PointLight() + + if light_node.hasTag("light_energy"): + light.energy = float(light_node.getTag("light_energy")) + else: + 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 + + light.setPos(light_node.getPos()) + + self.render_pipeline.add_light(light) + except Exception as e: + print(f"创建点光源 {light_node.getName()} 失败: {str(e)}") + import traceback + traceback.print_exc() + + def processGUIElements(self, scene): + """处理场景中的GUI元素""" + try: + # 查找并处理2D图像 + images_2d = scene.findAllMatches("**/=gui_type=image_2d") + for img_node in images_2d: + try: + # GUI元素通常在场景加载时自动处理 + print(f"✓ 2D图像 {img_node.getName()} 已加载") + except Exception as e: + print(f"处理2D图像 {img_node.getName()} 失败: {str(e)}") + + except Exception as e: + print(f"处理GUI元素时出错: {str(e)}") + + def tour(self): + mopath = ( + (Vec3(-10.8645000458, 9.76458263397, 2.13306283951), Vec3(-133.556228638, -4.23447799683, 0.0)), + (Vec3(-10.6538448334, -5.98406457901, 1.68028640747), Vec3(-59.3999938965, -3.32706642151, 0.0)), + (Vec3(9.58458328247, -5.63625621796, 2.63269257545), Vec3(58.7906494141, -9.40668964386, 0.0)), + (Vec3(6.8135137558, 11.0153560638, 2.25509500504), Vec3(148.762527466, -6.41223621368, 0.0)), + (Vec3(-9.07093334198, 3.65908527374, 1.42396306992), Vec3(245.362503052, -3.59927511215, 0.0)), + (Vec3(-8.75390911102, -3.82727789879, 0.990055501461), Vec3(296.090484619, -0.604830980301, 0.0)), + ) + self.controller.play_motion_path(mopath, 3.0) + + def loadGUIFromJSON(self): + try: + gui_json_path = self.getResourcePath("gui/gui_elements.json") + if os.path.exists(gui_json_path): + with open(gui_json_path, 'r', encoding='utf-8') as f: + content = f.read().strip() + if content: + gui_data = json.loads(content) + self.createGUIElement(gui_data) + else: + print("GUI配置文件为空 ") + except Exception as e: + print(f"加载GUI元素时出错: {str(e)}") + import traceback + traceback.print_exc() + + def createGUIElement(self, element_data): + try: + processed_names = set() + element_original_data = {} + + for i, gui_info in enumerate(element_data): + name = gui_info.get("name", f"gui_element_{i}") + element_original_data[name] = { + "scale": gui_info.get("scale", [1, 1, 1]), + "position": gui_info.get("position", [0, 0, 0]), + "parent_name": gui_info.get("parent_name") + } + valid_parents = set() + for gui_info in element_data: + name = gui_info.get("name", f"gui_element_{gui_info.get('index', 0)}") + valid_parents.add(name) + + for i, gui_info in enumerate(element_data): + try: + gui_type = gui_info.get("type", "unknown") + name = gui_info.get("name", f"gui_element_{i}") + position = gui_info.get("position", [0, 0, 0]) + scale = gui_info.get("scale", [1, 1, 1]) + tags = gui_info.get("tags", {}) + text = gui_info.get("text", "") + image_path = gui_info.get("image_path", "") + video_path = gui_info.get("video_path", "") + bg_image_path = gui_info.get("bg_image_path", "") + parent_name = gui_info.get("parent_name") + + if name in processed_names: + continue + + processed_names.add(name) + + absolute_position = list(position) + absolute_scale = list(scale) + + if parent_name and parent_name in element_original_data: + parent_data = element_original_data[parent_name] + parent_scale = parent_data["scale"] + + if gui_type in ["3d_text", "3d_image", "button", "label", "entry", "2d_image", + "2d_video_screen"]: + # 位置需要乘以父级缩放来得到绝对位置 + for j in range(min(len(absolute_position), len(parent_scale))): + absolute_position[j] *= parent_scale[j] if len(parent_scale) > j else parent_scale[0] + + # 缩放需要乘以父级缩放来得到绝对缩放 + for j in range(min(len(absolute_scale), len(parent_scale))): + absolute_scale[j] *= parent_scale[j] if len(parent_scale) > j else parent_scale[0] + + new_element = None + + if gui_type == "3d_text": + size = absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 0.5 + new_element = self.createGUI3DText( + pos=tuple(absolute_position), + text=text, + size=size + ) + elif gui_type == "button": + new_element = self.createGUIButton( + pos=tuple(absolute_position), + text=text, + size=absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 1.0, + command=self.playModelAnimation + ) + elif gui_type == "label": + new_element = self.createGUILabel( + pos=tuple(absolute_position), + text=text, + size=absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 1.0 + ) + elif gui_type == "entry": + new_element = self.createGUIEntry( + pos=tuple(absolute_position), + placeholder=text, + size=absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 1.0, + command=self.onGUIEntrySubmit + ) + elif gui_type == "2d_image": + + scale_value = absolute_scale[0] + print(f"2d_image{scale_value}") + new_element = self.createGUI2DImage( + pos=tuple(absolute_position), + image_path=image_path, + size=absolute_scale + ) + elif gui_type == "2d_video_screen": + new_element = self.createGUI2DVideoScreen( + pos=tuple(absolute_position), + video_path=video_path, + size=absolute_scale + ) + elif gui_type == "video_screen": + new_element = self.createGUIVideoScreen( + pos=tuple(absolute_position), + video_path=video_path, + size=absolute_scale + ) + elif gui_type == "info_panel": + new_element = self.info_panel_manager.onCreateSampleInfoPanel() + + if "scripts" in gui_info and new_element: + self.processScripts(new_element,gui_info["scripts"]) + + except Exception as e: + print(f"重建GUI元素失败 {name}: {e}") + import traceback + traceback.print_exc() + continue + + except Exception as e: + print(f"创建GUI元素时出错: {str(e)}") + + def createGUIButton(self, pos=(0, 0, 0), text="按钮", size=0.1, command=None): + from direct.gui.DirectGui import DirectButton + + button = DirectButton( + text=text, + pos=(pos[0], pos[1], pos[2]), # 保持正确的坐标格式 + scale=size, # size 应该是数值而不是元组 + frameColor=(0.2, 0.6, 0.8, 1), + text_font=self.getChineseFont() if self.getChineseFont() else None, + rolloverSound=None, + clickSound=None, + parent=None, + command=command + ) + + def createGUI3DText(self, pos=(0, 0, 0), text="3D文本", size=0.5): + """创建3D文本GUI元素""" + try: + # 创建文本节点 + text_node = TextNode("gui_3d_text") + text_node.setText(text) + text_node.setAlign(TextNode.ACenter) + + # 设置字体(如果可用) + if self.getChineseFont(): + text_node.setFont(self.getChineseFont()) + + # 创建节点路径并添加到场景 + text_np = self.render.attachNewNode(text_node) + + # 设置位置和大小 + text_np.setPos(Vec3(pos[0], pos[1], pos[2])) + text_np.setScale(size) + + # 设置面向摄像机 + # text_np.setBillboardPointEye() + + # 设置渲染属性 + text_np.setBin("fixed", 40) + text_np.setDepthWrite(False) + + return text_np + except Exception as e: + print(f"❌ 创建3D文本失败: {str(e)}") + import traceback + traceback.print_exc() + return None + + def createGUILabel(self, pos=(0, 0, 0), text="标签", size=0.08): + from direct.gui.DirectGui import DirectLabel + label = DirectLabel( + text=text, + pos=(pos[0], pos[1], pos[2]), + scale=size, + frameColor=(0, 0, 0, 0), + text_fg=(1, 1, 1, 1), + text_font=self.getChineseFont() if self.getChineseFont() else None, + text_align=TextNode.ACenter, + text_wordwrap=None, + text_mayChange=True, + parent=None + ) + + def createGUIEntry(self, pos=(0, 0, 0), placeholder="输入文本...", size=0.08, command=None): + from direct.gui.DirectGui import DirectEntry + + entry = DirectEntry( + text="", + pos=(pos[0], pos[1], pos[2]), + scale=size, + command=command, + initialText=placeholder, + numLines=1, + width=12, + focus=0, + frameColor=(0, 0, 0, 0), + text_fg=(1, 1, 1, 1), + text_font=self.getChineseFont() if self.getChineseFont() else None, + text_align=TextNode.ACenter, + text_wordwrap=None, + text_mayChange=True, + parent=None, + rolloverSound=None, + clickSound=None, + # 添加焦点管理命令 + focusInCommand=self.disableCameraControl, + focusOutCommand=self.enableCameraControl, + # 确保输入框能正确捕获所有键盘事件 + suppressKeys=True, # 这个参数很重要,它会阻止按键事件传播到其他处理器 + suppressMouse=True + ) + return entry + + def disableCameraControl(self): + """禁用相机控制""" + try: + if hasattr(self, 'controller'): + # 如果控制器有内置的禁用方法 + if hasattr(self.controller, 'disable'): + self.controller.disable() + else: + # 否则手动禁用事件监听 + self.controller.ignoreAll() # 忽略所有已注册的事件 + print("相机控制已禁用") + except Exception as e: + print(f"禁用相机控制时出错: {e}") + + def enableCameraControl(self): + """启用相机控制""" + try: + if hasattr(self, 'controller'): + # 如果控制器有内置的启用方法 + if hasattr(self.controller, 'enable'): + self.controller.enable() + else: + # 重新设置控制器 + self.controller.setup() + print("相机控制已启用") + except Exception as e: + print(f"启用相机控制时出错: {e}") + + def onGUIEntrySubmit(self, text, entry_id=None): + """GUI输入框提交事件处理""" + try: + print(f"GUI输入框提交: {entry_id} = {text}") + + # 重新启用相机控制 + self.enableCameraControl() + + # 清除输入框焦点(如果需要) + # base.win.focus() + + # 在这里添加您需要的文本处理逻辑 + # 例如保存文本、更新UI等 + + except Exception as e: + print(f"处理输入框提交时出错: {e}") + import traceback + traceback.print_exc() + + def createGUI2DImage(self, pos=(0, 0, 0), image_path=None, size=0.2): + # 添加属性检查 + if not hasattr(self, 'aspect2d'): + print("错误: aspect2d未初始化") + return None + if image_path and not os.path.isabs(image_path): + image_path = self.getResourcePath(image_path) + # 处理非均匀缩放 + if isinstance(size, (list, tuple)) and len(size) >= 2: + # 分别处理宽度和高度的缩放 + width_scale = size[0] * 0.2 + height_scale = size[2] * 0.2 + else: + # 如果只提供了一个缩放值,则使用相同值 + width_scale = size * 0.1 if isinstance(size, (int, float)) else 0.2 + height_scale = width_scale + + cm = CardMaker("gui-2d-image") + cm.setFrame(-width_scale, width_scale, -height_scale, height_scale) + #cm.setFrame(-size, size, -size, size) + + image_node = self.aspect2d.attachNewNode(cm.generate()) + image_node.setPos(pos) + image_node.setBin("fixed", 0) + image_node.setDepthWrite(False) + image_node.setDepthTest(False) + image_node.setColor(1, 1, 1, 1) + + # 设置透明度支持 + image_node.setTransparency(TransparencyAttrib.MAlpha) + + if image_path: + try: + texture = self.loader.loadTexture(image_path) + if texture: + image_node.setTexture(texture, 1) + texture.setWrapU(Texture.WM_clamp) + texture.setWrapV(Texture.WM_clamp) + texture.setMinfilter(Texture.FT_linear) + texture.setMagfilter(Texture.FT_linear) + image_node.setColor(1, 1, 1, 1) + else: + print(f"无法加载图片: {image_path}") + except Exception as e: + print(f"加载图片时出错: {e}") + return image_node + + def createGUIVideoScreen(self,pos=(0,0,0),size=1,video_path=None): + import os + from panda3d.core import TransparencyAttrib,Texture,TextureStage + + if isinstance(size,(list,tuple)) and len(size) >= 2: + width_scale = size[0] + height_scale = size[2] + else: + width_scale = size * 0.1 if isinstance(size, (int, float)) else 0.2 + height_scale = width_scale + + cm = CardMaker("gui-video-screen") + cm.setFrame(-width_scale,width_scale,-height_scale,height_scale) + + video_screen = self.render.attachNewNode(cm.generate()) + video_screen.setPos(pos) + + video_screen.setBin("fixed", 0) + + #video_screen.setTransparency(TransparencyAttrib.MAlpha) + + video_screen.setColor(1, 1, 1, 1) + + self._ensureVideoScreenMaterial(video_screen) + + if video_path: + if video_path.startswith(("http://", "https://")): + try: + success = self._loadVideoFromURLWithOpenCV3D(video_screen, video_path) + if success: + print("视频已从URL加载成功") + else: + print("从URL加载视频时出错") + except Exception as e: + print(f"从URL加载视频时出错: {e}") + elif os.path.exists(video_path): + movie_texture = self._loadMovieTexture(video_path) + if movie_texture: + video_screen.setTexture(movie_texture, 1) + else: + print(f"视频文件不存在: {video_path}") + else: + # 设置占位符纹理 + placeholder_texture = Texture() + placeholder_texture.setup2dTexture(1, 1, Texture.TUnsignedByte, Texture.FRgb) + placeholder_data = b'\x19\x19\x4c' # 深蓝色占位符 + placeholder_texture.setRamImage(placeholder_data) + video_screen.setTexture(placeholder_texture, 1) + + return video_screen + + def createGUI2DVideoScreen(self, pos=(0, 0, 0), size=0.2, video_path=None): + import os + from direct.gui.DirectGui import DirectFrame + from panda3d.core import TransparencyAttrib, Texture, TextureStage + + if isinstance(size,(list,tuple)) and len(size) >= 2: + width_scale = size[0]*0.2 + height_scale = size[2]*0.2 + else: + width_scale = size*0.1 if isinstance(size,(int,float)) else 0.2 + height_scale = width_scale + + video_screen = DirectFrame( + frameSize=(-width_scale, width_scale, -height_scale, height_scale), + frameColor=(1, 1, 1, 1), + pos=pos, + parent=None, + suppressMouse=True + ) + video_screen.setTransparency(TransparencyAttrib.MAlpha) + + placeholder_texture = Texture() + placeholder_texture.setup2dTexture(1, 1, Texture.TUnsignedByte, Texture.FRgb) + placeholder_data = b'\x19\x19\x4c' + placeholder_texture.setRamImage(placeholder_data) + + if video_path: + if video_path.startswith(("http://","https://")): + try: + success = self._loadVideoFromURLWithOpenCV(video_screen,video_path) + if success: + print("视频已从URL加载成功") + else: + print("从URL加载视频时出错") + except Exception as e: + print(f"从URL加载视频时出错: {e}") + elif os.path.exists(video_path): + movie_texture = self._loadMovieTexture(video_path) + if movie_texture: + video_screen["frameTexture"] = movie_texture + + return video_screen + + def _loadMovieTexture(self, video_path): + from panda3d.core import Texture, MovieTexture + import os + movie_texture = MovieTexture(video_path) + if movie_texture.read(video_path): + self._configureVideoTexture(movie_texture) + return movie_texture + + def _configureVideoTexture(self, texture): + from panda3d.core import Texture + + texture.setWrapU(Texture.WM_clamp) + texture.setWrapV(Texture.WM_clamp) + texture.setMinfilter(Texture.FT_linear) + texture.setMagfilter(Texture.FT_linear) + + texture.setFormat(Texture.FRgb8) + + if hasattr(texture, 'set_loop') and hasattr(texture, 'set_play_rate'): + texture.set_loop(True) + texture.set_play_rate(1.0) + + def _loadVideoFromURLWithOpenCV(self, video_screen, url): + try: + import cv2 + import threading + from panda3d.core import Texture,PNMImage + import numpy as np + import time + + def video_stream_thread(): + cap = cv2.VideoCapture(url) + + if not cap.isOpened(): + print(f"无法打开视频流: {url}") + return False + + cap.set(cv2.CAP_PROP_BUFFERSIZE,1) + + fps = cap.get(cv2.CAP_PROP_FPS) + frame_delay = 1.0 / fps if fps > 0 else 0.033 # 默认30fps + + while hasattr(self, 'video_stream_active') and self.video_stream_active: + ret, frame = cap.read() + + if not ret: + print("视频流读取失败,尝试重新连接...") + cap.release() + time.sleep(1) + cap = cv2.VideoCapture(url) + continue + + frame_height,frame_width = frame.shape[:2] + if frame_width > 256: + scale = 256.0/frame_width + new_width = int(frame_width * scale) + new_height = int(frame_height * scale) + frame = cv2.resize(frame,(new_width,new_height)) + + frame_rgb = cv2.cvtColor(frame,cv2.COLOR_BGR2RGB) + + height,width = frame_rgb.shape[:2] + + img = PNMImage(width,height,3) + img.set_maxval(255) + + for y in range(height): + for x in range(width): + r,g,b = frame_rgb[y,x] + img.setXelVal(x,y,r,g,b) + + texture = Texture("video_texture") + texture.setMagfilter(Texture.FTLinear) + texture.setMinfilter(Texture.FTLinear) + texture.setWrapU(Texture.WMClamp) + texture.setWrapV(Texture.WMClamp) + texture.load(img) + + # 在主线程中更新GUI纹理 + def update_texture(): + if hasattr(self, 'aspect2d') and not video_screen.isEmpty(): + video_screen["frameTexture"] = texture + + # 使用taskMgr在主线程中更新纹理 + if hasattr(self, 'taskMgr'): + self.taskMgr.doMethodLater(0, lambda task: update_texture() or task.done, + f"updateVideoTexture_{time.time()}") + + # 控制帧率 + time.sleep(frame_delay) + + cap.release() + print("视频流线程结束") + return True + + # 启动视频流线程 + self.video_stream_active = True + self.video_thread = threading.Thread(target=video_stream_thread, daemon=True) + self.video_thread.start() + + return True + + except Exception as e: + print(f"加载视频流失败: {e}") + import traceback + traceback.print_exc() + return False + + def _loadVideoFromURLWithOpenCV3D(self,video_screen,url): + try: + import cv2 + import threading + from panda3d.core import Texture,PNMImage + import time + + def video_stream_thread(): + cap = cv2.VideoCapture(url) + + if not cap.isOpened(): + return False + + cap.set(cv2.CAP_PROP_BUFFERSIZE,1) + + fps = cap.get(cv2.CAP_PROP_FPS) + frame_delay = 1.0 / fps if fps > 0 else 0.033 + + while hasattr(self,'video_stream_active') and self.video_stream_active: + ret,frame = cap.read() + + if not ret: + cap.release() + time.sleep(1) + cap = cv2.VideoCapture(url) + continue + + frame_height,frame_width = frame.shape[:2] + if frame_width > 256: + scale = 256.0/frame_width + new_width = int(frame_width*scale) + new_height = int(frame_height*scale) + frame = cv2.resize(frame,(new_width,new_height)) + + frame_rgb = cv2.cvtColor(frame,cv2.COLOR_BGR2RGB) + + height,width = frame_rgb.shape[:2] + + img = PNMImage(width,height,3) + img.set_maxval(255) + + for y in range(height): + for x in range(width): + r,g,b = frame_rgb[y,x] + img.setXelVal(x,y,r,g,b) + + texture = Texture("video_texture_3d") + texture.setMagfilter(Texture.FTLinear) + texture.setMinfilter(Texture.FTLinear) + texture.setWrapU(Texture.WMClamp) + texture.setWrapV(Texture.WMClamp) + texture.load(img) + + def update_texture(): + if hasattr(self,'render') and not video_screen.isEmpty(): + video_screen.setTexture(texture,1) + + if hasattr(self,'taskMgr'): + self.taskMgr.doMethodLater(0,lambda task:update_texture() or task.done,f"updateVideoTexture_{time.time()}") + + time.sleep(frame_delay) + + cap.release() + return True + self.video_stream_active = True + self.video_thread_3d = threading.Thread(target=video_stream_thread,daemon=True) + self.video_thread_3d.start() + + return True + except Exception as e: + print(f"加载3D视频流失败: {e}") + import traceback + traceback.print_exc() + return False + + def _ensureVideoScreenMaterial(self, video_screen): + """确保视频屏幕有正确的材质设置""" + try: + from panda3d.core import Material, LColor + + if not video_screen.hasMaterial(): + material = Material(f"video-material-{video_screen.getName()}") + material.setBaseColor(LColor(1, 1, 1, 1)) + material.setDiffuse(LColor(1, 1, 1, 1)) + material.setAmbient(LColor(1, 1, 1, 1)) # 确保环境光为白色 + material.setEmission(LColor(0, 0, 0, 1)) + material.setSpecular(LColor(0, 0, 0, 1)) + material.setShininess(0) + video_screen.setMaterial(material, 1) + else: + # 更新现有材质确保正确设置 + material = video_screen.getMaterial() + material.setBaseColor(LColor(1, 1, 1, 1)) + material.setAmbient(LColor(1, 1, 1, 1)) # 确保环境光为白色 + video_screen.setMaterial(material, 1) + + except Exception as e: + print(f"⚠️ 设置视频屏幕材质时出错: {e}") + + def playModelAnimation(self): + """播放场景中所有 .glb 模型的动画(仅转换有动画的模型为 Actor)""" + try: + glb_models = self.render.findAllMatches("**/*.glb*") + + # 修复过滤逻辑,确保正确排除碰撞体节点 + filtered_models = [] + for model in glb_models: + model_name = model.getName().lower() + # 排除碰撞体节点 + if ("collision_" not in model_name and + "modelcollision_" not in model_name and + not model_name.endswith("_bounds")): + filtered_models.append(model) + + if not filtered_models: + print("⚠️ 场景中没有找到 .glb 模型") + return + + print(f"找到 {len(filtered_models)} 个 glb 模型") + + self.actors = [] # 存储所有 Actor,避免被垃圾回收 + + for model in filtered_models: + print(f"正在处理模型: {model.getName()}") + + actor = None + + # 尝试把模型当作Actor加载,判断是否有动画 + try: + # 在创建Actor之前先检查节点是否可能包含动画 + if not self._isValidAnimationNode(model): + print(f"⚠️ {model.getName()} 不是有效的动画节点,跳过") + continue + + test_actor = Actor(model) + anim_names = test_actor.getAnimNames() + except Exception as e: + print(f"⚠️ {model.getName()} 无法作为Actor加载: {str(e)}") + anim_names = [] + + if anim_names: # ✅ 只有有动画才转为Actor + if not isinstance(model, Actor): + model_parent = model.getParent() + model_pos = model.getPos() + model_hpr = model.getHpr() + model_scale = model.getScale() + + actor = Actor(model) + actor.reparentTo(model_parent) + actor.setPos(model_pos) + actor.setHpr(model_hpr) + actor.setScale(model_scale) + + model.detachNode() + else: + actor = model + + # 播放动画 + actor.show() + self.actors.append(actor) + + print(f"✓ {actor.getName()} 可用动画: {anim_names}") + first_anim = anim_names[0] + + actor.stop() + actor.setPlayRate(1.0, first_anim) + actor.loop(first_anim) + print(f"✓ {actor.getName()} 正在播放动画: {first_anim}") + + actor.update() + + else: # 没有动画 + print(f"⚠️ {model.getName()} 没有动画,不转为Actor") + + except Exception as e: + print(f"播放模型动画时出错: {str(e)}") + import traceback + traceback.print_exc() + + def _isValidAnimationNode(self, nodePath): + """检查节点是否可能是有效的动画节点""" + # 排除明显不是动画节点的节点 + name = nodePath.getName().lower() + if ("collision_" in name or + "modelcollision_" in name or + "bound" in name or + name.endswith("_bounds")): + return False + + # 检查节点是否有网格数据(简单判断) + from panda3d.core import GeomNode + geom_nodes = nodePath.findAllMatches("**/+GeomNode") + if not geom_nodes: + return False + + return True + + def focusOnWomenModel(self): + """定位并聚焦到Women模型""" + try: + women_models = self.render.findAllMatches("**/Women_1.glb*") + if women_models: + model = women_models[0] + # 确保模型可见 + model.show() + + # 将模型放置在原点附近 + model.setPos(0, 0, 0) + model.setScale(1.0) + + # 确保是Actor的话设置动画 + if isinstance(model, Actor): + anim_names = model.getAnimNames() + if anim_names: + model.loop(anim_names[0]) + model.setPlayRate(1.0, anim_names[0]) + print(f"为模型设置动画: {anim_names[0]}") + + print(f"已定位模型: {model.getName()}") + else: + print("未找到Women模型") + except Exception as e: + print(f"定位模型时出错: {str(e)}") + + def processScripts(self, element, script_info_list): + """处理元素上挂载的脚本 - 使用新的脚本系统""" + try: + print(f"正在为元素 {element.getName()} 挂载脚本") + print(f"可用脚本列表: {self.script_manager.get_available_scripts()}") + + if not hasattr(self,'script_manager'): + print("脚本管理器未初始化") + return + + for script_info in script_info_list: + script_name = script_info["name"] + script_file = script_info.get("file", "") + + if script_name: + script_component = self.script_manager.add_script_to_object(element,script_name) + + if script_component: + print(f"✓ 脚本 {script_name} 已挂载到元素 {element.getName()}") + else: + print(f"⚠️ 脚本 {script_name} 挂载失败") + # 列出可用脚本帮助调试 + available_scripts = self.script_manager.get_available_scripts() + print(f"当前可用脚本: {available_scripts}") + else: + print(f"⚠️ 脚本信息不完整: {script_info}") + + # # 从文件路径中提取脚本类名 + # if script_file: + # # 获取脚本文件名(不含路径和扩展名) + # script_filename = os.path.basename(script_file) + # script_class_name = os.path.splitext(script_filename)[0] + # print(f"尝试挂载脚本: {script_class_name} (来自文件: {script_file})") + # + # # 使用脚本管理器为元素添加脚本 + # script_component = self.script_manager.add_script_to_object(element, script_class_name) + # + # if script_component: + # print(f"✓ 脚本 {script_class_name} 已挂载到元素 {element.getName()}") + # else: + # print(f"⚠️ 脚本 {script_class_name} 挂载失败") + # # 列出可用脚本帮助调试 + # available_scripts = self.script_manager.get_available_scripts() + # print(f"当前可用脚本: {available_scripts}") + # else: + # print(f"⚠️ 脚本信息不完整: {script_name}") + + except Exception as e: + print(f"挂载脚本到元素 {element.getName()} 失败: {str(e)}") + import traceback + traceback.print_exc() + + def checkDoubleClick(self, nodePath): + """检查是否为双击,返回布尔值 - 改进版本""" + try: + import time + current_time = time.time() + + # 必须是同一节点且在时间阈值内 + is_double_click = (self._last_clicked_node is not None and + self._last_clicked_node == nodePath and + nodePath is not None and + current_time - self._last_click_time < self._double_click_threshold) + + if is_double_click: + # 双击成功,重置状态 + self._last_click_time = 0 + self._last_clicked_node = None + print(f"✓ 检测到双击: {nodePath.getName()}") + return True + else: + # 单击,更新状态 + self._last_click_time = current_time + self._last_clicked_node = nodePath + return False + + except Exception as e: + print(f"双击检测失败: {e}") + return False + + def focusCameraOnNode(self, nodePath): + """带动画效果的聚焦到节点方法""" + try: + if not nodePath or nodePath.isEmpty(): + print("无效的节点") + return + + minPoint = Point3() + maxPoint = Point3() + + if not nodePath.calcTightBounds(minPoint, maxPoint,self.render): + print("无法计算选中节点的边界框,使用节点为位置作为替代方案") + node_pos = nodePath.getPos(self.render) + optimal_distance = 10.0 + current_cam_pos = self.cam.getPos() + view_direction = node_pos - current_cam_pos + if view_direction.length()<0.001: + view_direction = Vec3(5,-5,2) + view_direction.normalize() + target_cam_pos = node_pos-(view_direction * optimal_distance) + + temp_node = self.render.attachNewNode("temp_lookat_target") + temp_node.setPos(node_pos) + dummy_cam = self.render.attachNewNode("dummy_camera") + dummy_cam.setPos(target_cam_pos) + dummy_cam.lookAt(temp_node) + target_cam_hpr = Vec3(dummy_cam.getHpr()) + + temp_node.removeNode() + dummy_cam.removeNode() + + current_cam_pos = Point3(self.cam.getPos()) + current_cam_hpr = Vec3(self.cam.getHpr()) + self._startCameraFocusAnimation(current_cam_pos,target_cam_pos,current_cam_hpr,target_cam_hpr) + print(f"开始聚焦到节点(使用位置): {nodePath.getName()}") + + return + + center = Point3( + (minPoint.x + maxPoint.x)*0.5, + (minPoint.y+maxPoint.y)*0.5, + (minPoint.z+maxPoint.z)*0.5 + ) + + size = (maxPoint - minPoint).length() + if size < 0.01: + size = 1.0 + + current_cam_pos = Point3(self.cam.getPos()) + current_cam_hpr = Vec3(self.cam.getHpr()) + + view_direction = current_cam_pos - center + if view_direction.length() < 0.001: + view_direction = Vec3(5,-5,2) + + view_direction.normalize() + + optimal_distance = max(size * 2.5,1.0) + + target_cam_pos = center + (view_direction * optimal_distance) + + temp_node = self.render.attachNewNode("temp_lookat_target") + temp_node.setPos(center) + + dummy_cam = self.render.attachNewNode("dummy_camera") + dummy_cam.setPos(target_cam_pos) + dummy_cam.lookAt(temp_node) + target_cam_hpr = Vec3(dummy_cam.getHpr()) + + temp_node.removeNode() + dummy_cam.removeNode() + + self._startCameraFocusAnimation(current_cam_pos,target_cam_pos, + current_cam_hpr,target_cam_hpr) + + print(f"开始聚焦到节点: {nodePath.getName()}") + return True + + except Exception as e: + print(f"聚焦相机失败: {str(e)}") + import traceback + traceback.print_exc() + + def _startCameraFocusAnimation(self, start_pos, end_pos, start_hpr, end_hpr): + """启动相机聚焦动画 - 改进版本""" + try: + class CameraFocusData: + def __init__(self, start_pos, end_pos, start_hpr, end_hpr): + self.start_pos = Point3(start_pos) + self.end_pos = Point3(end_pos) + self.start_hpr = Vec3(start_hpr) + self.end_hpr = Vec3(end_hpr) + self.elapsed_time = 0.0 + self.duration = 0.5 # 增加动画时间使移动更平滑 + + self._camera_focus_data = CameraFocusData(start_pos, end_pos, start_hpr, end_hpr) + + from direct.task.TaskManagerGlobal import taskMgr + taskMgr.remove("cameraFocusTask") + + taskMgr.add(self._cameraFocusTask, "cameraFocusTask") + print(f"开始相机聚焦动画: {start_pos} -> {end_pos}") + + except Exception as e: + print(f"相机聚焦动画启动失败: {str(e)}") + + def _normalizeAngle(self, angle): + """规范化角度到-180到180度之间""" + while angle > 180: + angle -= 360 + while angle < -180: + angle += 360 + return angle + + def _cameraFocusTask(self, task): + """摄像机聚焦动画任务""" + try: + if not hasattr(self, '_camera_focus_data'): + return task.done + + data = self._camera_focus_data + from direct.showbase.ShowBaseGlobal import globalClock + dt = globalClock.getDt() + + if dt <= 0: + dt = 0.016 + + data.elapsed_time += dt + t = min(1.0, data.elapsed_time / data.duration) + + # 使用平滑插值 + smooth_t = t * t * (3 - 2 * t) + + # 插值位置和朝向 + current_pos = data.start_pos + (data.end_pos - data.start_pos) * smooth_t + current_hpr = data.start_hpr + (data.end_hpr - data.start_hpr) * smooth_t + + self.cam.setPos(current_pos) + self.cam.setHpr(current_hpr) + + if t >= 1.0: + self.cam.setPos(data.end_pos) + self.cam.setHpr(data.end_hpr) + + print(f"聚焦完成: 位置 {data.end_pos}, 朝向 {data.end_hpr}") + + if hasattr(self, '_camera_focus_data'): + delattr(self, '_camera_focus_data') + + return task.done + + return task.cont + + except Exception as e: + print(f"摄像机聚焦动画任务失败: {e}") + import traceback + traceback.print_exc() + return task.done + + def setupMouseClickHandler(self): + """设置鼠标点击处理器""" + try: + # 设置鼠标监听 + self.accept("mouse1", self.onMouseClick) + + # 启用鼠标观察器 + if not hasattr(self, 'mouseWatcherNode'): + from panda3d.core import MouseWatcher + self.mouseWatcherNode = MouseWatcher() + + except Exception as e: + print(f"设置鼠标点击处理器失败: {e}") + + def onMouseClick(self): + """处理鼠标左键点击""" + try: + if not hasattr(self, 'mouseWatcherNode') or not self.mouseWatcherNode.hasMouse(): + return + + # 获取鼠标位置 + mouse_pos = self.mouseWatcherNode.getMouse() + + # 创建射线进行点击检测 + from panda3d.core import CollisionRay, CollisionNode, CollisionHandlerQueue, BitMask32 + + # 创建射线 + ray = CollisionRay() + ray.setFromLens(self.camNode, mouse_pos.x, mouse_pos.y) + + # 创建碰撞节点 + ray_node = CollisionNode('mouseRay') + ray_node.addSolid(ray) + ray_node.setFromCollideMask(BitMask32.allOn()) + + # 附加到相机 + ray_np = self.camera.attachNewNode(ray_node) + + # 创建碰撞队列 + handler = CollisionHandlerQueue() + + # 确保碰撞遍历器存在 + if not hasattr(self, 'cTrav'): + self.cTrav = CollisionTraverser() + + self.cTrav.addCollider(ray_np, handler) + self.cTrav.traverse(self.render) + + # 检查碰撞结果 + target_node = None + if handler.getNumEntries() > 0: + # 按距离排序 + handler.sortEntries() + + # 遍历所有碰撞结果,找到最近的有效节点 + for i in range(handler.getNumEntries()): + entry = handler.getEntry(i) + clicked_node = entry.getIntoNodePath() + + # 向上遍历找到可选择的父节点 + current_node = clicked_node + found_valid_node = False + + while current_node and not current_node.isEmpty(): + # 检查节点是否应该被选择 + if (current_node.hasTag("is_scene_element") and + not current_node.hasTag("is_gizmo") and + current_node.getName() not in ["render", "camera", "ambient_light", + "directional_light", "mouseRay"] and + not current_node.getName().startswith("collision_") and + "ground" not in current_node.getName().lower()): + target_node = current_node + found_valid_node = True + break + + # 如果当前节点是碰撞体,获取其父节点作为目标 + if current_node.getName().startswith("collision_"): + parent = current_node.getParent() + if (parent.hasTag("is_scene_element") and + not parent.hasTag("is_gizmo") and + parent.getName() not in ["render", "camera", "ambient_light", + "directional_light"] and + "ground" not in parent.getName().lower()): + target_node = parent + found_valid_node = True + break + + current_node = current_node.getParent() + if current_node.getName() == "render": + break + + if found_valid_node: + break + + # 如果找到了目标节点,则进行双击检测 + if target_node: + print(f"找到可选择节点: {target_node.getName()}") + print(f"节点位置: {target_node.getPos()}") + print(f"节点边界: {target_node.getBounds()}") + # 检查是否为双击 + if self.checkDoubleClick(target_node): + print(f"双击节点: {target_node.getName()}") + self.focusCameraOnNode(target_node) + else: + print(f"单击节点: {target_node.getName()}") + else: + print("未找到有效的目标节点") + + # 清理碰撞器 + ray_np.removeNode() + + except Exception as e: + print(f"处理鼠标点击失败: {e}") + import traceback + traceback.print_exc()\ + + def createFrameRateDisplay(self): + from direct.gui.DirectGui import DirectLabel + from panda3d.core import TextNode + + self.fps_label = DirectLabel( + text="FPS:0", + pos=(-1.8,0,0.9), + scale=0.05, + text_fg=(1,1,1,1), + text_align=TextNode.ALeft, + frameSize=(-0.1,0.3,-0.05,0.05), + frameColor=(0,0,0,0.5), + text_font=self.getChineseFont() if self.getChineseFont() else None, + #parent=self.a2dTopLeft + ) + + taskMgr.add(self.updateFrameRate,"updateFrameRateTask") + + def updateFrameRate(self,task): + try: + fps = globalClock.getAverageFrameRate() + + if self.fps_label: + self.fps_label.setText(f"FPS:{fps:.1f}") + except Exception as e: + print(f"更新帧率失败: {e}") + return task.cont + + +# 在 main.py 的最后部分,修改为: +if __name__ == "__main__": + try: + app = MainApp() + if hasattr(app, 'run'): + app.run() + else: + print("应用程序初始化失败") + except Exception as e: + print(f"应用程序启动失败: {str(e)}") + import traceback + traceback.print_exc() + + diff --git a/test_metallic_gradient.png b/test_metallic_gradient.png deleted file mode 100644 index 11dbcc81..00000000 Binary files a/test_metallic_gradient.png and /dev/null differ diff --git a/test_metallic_stripes.png b/test_metallic_stripes.png deleted file mode 100644 index 956c8661..00000000 Binary files a/test_metallic_stripes.png and /dev/null differ diff --git a/test_roughness_checkerboard.png b/test_roughness_checkerboard.png deleted file mode 100644 index 3ee2bd65..00000000 Binary files a/test_roughness_checkerboard.png and /dev/null differ diff --git a/test_roughness_circle.png b/test_roughness_circle.png deleted file mode 100644 index 5ab3d4d6..00000000 Binary files a/test_roughness_circle.png and /dev/null differ diff --git a/test_roughness_gradient.png b/test_roughness_gradient.png deleted file mode 100644 index 11dbcc81..00000000 Binary files a/test_roughness_gradient.png and /dev/null differ diff --git a/ui/assembly_disassembly_config.py b/ui/assembly_disassembly_config.py new file mode 100644 index 00000000..e663decc --- /dev/null +++ b/ui/assembly_disassembly_config.py @@ -0,0 +1,944 @@ +# -*- coding: utf-8 -*- +""" +拆装交互配置界面 +实现模型的分步拆装交互功能配置 +""" + +from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QTabWidget, + QWidget, QLabel, QListWidget, QListWidgetItem, + QPushButton, QComboBox, QSpinBox, QLineEdit, + QTextEdit, QGroupBox, QGridLayout, QCheckBox, + QSplitter, QTreeWidget, QTreeWidgetItem, + QMessageBox, QFileDialog, QScrollArea, + QFrame, QDoubleSpinBox, QSlider, QProgressBar) +from PyQt5.QtCore import Qt, pyqtSignal, QTimer +from PyQt5.QtGui import QIcon, QPixmap, QFont +import os +import json + + +class AssemblyDisassemblyConfigDialog(QDialog): + """拆装配置主对话框""" + + def __init__(self, parent=None, world=None): + super().__init__(parent) + self.world = world + self.config_data = { + 'models': [], # 参与拆装的模型列表 + 'steps': [], # 拆装步骤配置 + 'tools': [], # 使用的工具列表 + 'settings': {} # 全局设置 + } + + self.setupUI() + self.loadSceneModels() + + def setupUI(self): + """设置界面""" + self.setWindowTitle("拆装交互配置") + self.setModal(False) # 设置为非模态对话框 + self.resize(1200, 800) + + # 主布局 + main_layout = QVBoxLayout(self) + + # 创建分割器 + splitter = QSplitter(Qt.Horizontal) + main_layout.addWidget(splitter) + + # 左侧面板 - 模型和工具选择 + left_panel = self.createLeftPanel() + splitter.addWidget(left_panel) + + # 右侧面板 - 步骤配置 + right_panel = self.createRightPanel() + splitter.addWidget(right_panel) + + # 设置分割器比例 + splitter.setSizes([400, 800]) + + # 底部按钮 + button_layout = QHBoxLayout() + + self.save_button = QPushButton("保存配置") + self.load_button = QPushButton("加载配置") + self.preview_button = QPushButton("预览效果") + self.apply_button = QPushButton("应用配置") + + button_layout.addWidget(self.save_button) + button_layout.addWidget(self.load_button) + button_layout.addStretch() + button_layout.addWidget(self.preview_button) + button_layout.addWidget(self.apply_button) + + main_layout.addLayout(button_layout) + + # 连接信号 + self.connectSignals() + + def createLeftPanel(self): + """创建左侧面板""" + left_widget = QWidget() + left_layout = QVBoxLayout(left_widget) + + # 场景模型选择区域 + models_group = QGroupBox("场景模型") + models_layout = QVBoxLayout(models_group) + + # 模型搜索框 + search_layout = QHBoxLayout() + search_label = QLabel("搜索:") + self.model_search_edit = QLineEdit() + self.model_search_edit.setPlaceholderText("输入模型名称搜索...") + search_layout.addWidget(search_label) + search_layout.addWidget(self.model_search_edit) + models_layout.addLayout(search_layout) + + # 场景模型列表 + self.scene_models_list = QTreeWidget() + self.scene_models_list.setHeaderLabel("场景中的模型") + self.scene_models_list.setSelectionMode(QTreeWidget.ExtendedSelection) + models_layout.addWidget(self.scene_models_list) + + # 选中模型操作按钮 + model_buttons_layout = QHBoxLayout() + self.add_to_config_button = QPushButton("添加到配置") + self.remove_from_config_button = QPushButton("从配置移除") + model_buttons_layout.addWidget(self.add_to_config_button) + model_buttons_layout.addWidget(self.remove_from_config_button) + models_layout.addLayout(model_buttons_layout) + + left_layout.addWidget(models_group) + + # 已配置模型列表 + config_models_group = QGroupBox("已配置模型") + config_models_layout = QVBoxLayout(config_models_group) + + self.config_models_list = QListWidget() + config_models_layout.addWidget(self.config_models_list) + + left_layout.addWidget(config_models_group) + + # 工具配置区域 + tools_group = QGroupBox("工具配置") + tools_layout = QVBoxLayout(tools_group) + + # 工具添加 + tool_add_layout = QHBoxLayout() + self.tool_name_edit = QLineEdit() + self.tool_name_edit.setPlaceholderText("工具名称") + self.add_tool_button = QPushButton("添加工具") + tool_add_layout.addWidget(self.tool_name_edit) + tool_add_layout.addWidget(self.add_tool_button) + tools_layout.addLayout(tool_add_layout) + + # 工具列表 + self.tools_list = QListWidget() + tools_layout.addWidget(self.tools_list) + + # 工具操作按钮 + tool_buttons_layout = QHBoxLayout() + self.edit_tool_button = QPushButton("编辑") + self.remove_tool_button = QPushButton("删除") + tool_buttons_layout.addWidget(self.edit_tool_button) + tool_buttons_layout.addWidget(self.remove_tool_button) + tools_layout.addLayout(tool_buttons_layout) + + left_layout.addWidget(tools_group) + + return left_widget + + def createRightPanel(self): + """创建右侧面板""" + right_widget = QWidget() + right_layout = QVBoxLayout(right_widget) + + # 步骤配置标签页 + self.tab_widget = QTabWidget() + + # 步骤列表标签页 + self.steps_tab = self.createStepsTab() + self.tab_widget.addTab(self.steps_tab, "步骤配置") + + # 全局设置标签页 + self.settings_tab = self.createSettingsTab() + self.tab_widget.addTab(self.settings_tab, "全局设置") + + right_layout.addWidget(self.tab_widget) + + return right_widget + + def createStepsTab(self): + """创建步骤配置标签页""" + steps_widget = QWidget() + steps_layout = QHBoxLayout(steps_widget) + + # 左侧步骤列表 + steps_list_layout = QVBoxLayout() + + # 步骤列表标题和操作按钮 + steps_header_layout = QHBoxLayout() + steps_label = QLabel("拆装步骤") + steps_label.setFont(QFont("", 12, QFont.Bold)) + self.add_step_button = QPushButton("添加步骤") + self.remove_step_button = QPushButton("删除步骤") + steps_header_layout.addWidget(steps_label) + steps_header_layout.addStretch() + steps_header_layout.addWidget(self.add_step_button) + steps_header_layout.addWidget(self.remove_step_button) + steps_list_layout.addLayout(steps_header_layout) + + # 步骤列表 + self.steps_list = QListWidget() + steps_list_layout.addWidget(self.steps_list) + + # 步骤顺序调整按钮 + step_order_layout = QHBoxLayout() + self.move_step_up_button = QPushButton("上移") + self.move_step_down_button = QPushButton("下移") + step_order_layout.addWidget(self.move_step_up_button) + step_order_layout.addWidget(self.move_step_down_button) + steps_list_layout.addLayout(step_order_layout) + + # 右侧步骤详细配置 + self.step_config_widget = self.createStepConfigWidget() + + # 添加到布局 + steps_layout.addLayout(steps_list_layout, 1) + steps_layout.addWidget(self.step_config_widget, 2) + + return steps_widget + + def createStepConfigWidget(self): + """创建步骤详细配置部件""" + config_widget = QWidget() + config_layout = QVBoxLayout(config_widget) + + # 滚动区域 + scroll_area = QScrollArea() + scroll_area.setWidgetResizable(True) + scroll_content = QWidget() + scroll_layout = QVBoxLayout(scroll_content) + + # 步骤基本信息 + basic_info_group = QGroupBox("基本信息") + basic_info_layout = QGridLayout(basic_info_group) + + # 步骤名称 + basic_info_layout.addWidget(QLabel("步骤名称:"), 0, 0) + self.step_name_edit = QLineEdit() + basic_info_layout.addWidget(self.step_name_edit, 0, 1) + + # 步骤描述 + basic_info_layout.addWidget(QLabel("步骤描述:"), 1, 0) + self.step_description_edit = QTextEdit() + self.step_description_edit.setMaximumHeight(80) + basic_info_layout.addWidget(self.step_description_edit, 1, 1) + + # 步骤类型 + basic_info_layout.addWidget(QLabel("步骤类型:"), 2, 0) + self.step_type_combo = QComboBox() + self.step_type_combo.addItems(["拆卸", "安装"]) + basic_info_layout.addWidget(self.step_type_combo, 2, 1) + + scroll_layout.addWidget(basic_info_group) + + # 目标模型配置 + target_model_group = QGroupBox("目标模型") + target_model_layout = QGridLayout(target_model_group) + + # 目标模型选择 + target_model_layout.addWidget(QLabel("目标模型:"), 0, 0) + self.target_model_combo = QComboBox() + target_model_layout.addWidget(self.target_model_combo, 0, 1) + + # 交互方式 + target_model_layout.addWidget(QLabel("交互方式:"), 1, 0) + self.interaction_type_combo = QComboBox() + self.interaction_type_combo.addItems(["鼠标拖拽", "点击触发"]) + target_model_layout.addWidget(self.interaction_type_combo, 1, 1) + + scroll_layout.addWidget(target_model_group) + + # 拖拽交互配置 + self.drag_config_group = QGroupBox("拖拽交互配置") + drag_config_layout = QGridLayout(self.drag_config_group) + + # 目标位置 + drag_config_layout.addWidget(QLabel("目标位置 X:"), 0, 0) + self.target_pos_x_spin = QDoubleSpinBox() + self.target_pos_x_spin.setRange(-1000, 1000) + self.target_pos_x_spin.setSingleStep(0.1) + drag_config_layout.addWidget(self.target_pos_x_spin, 0, 1) + + drag_config_layout.addWidget(QLabel("目标位置 Y:"), 1, 0) + self.target_pos_y_spin = QDoubleSpinBox() + self.target_pos_y_spin.setRange(-1000, 1000) + self.target_pos_y_spin.setSingleStep(0.1) + drag_config_layout.addWidget(self.target_pos_y_spin, 1, 1) + + drag_config_layout.addWidget(QLabel("目标位置 Z:"), 2, 0) + self.target_pos_z_spin = QDoubleSpinBox() + self.target_pos_z_spin.setRange(-1000, 1000) + self.target_pos_z_spin.setSingleStep(0.1) + drag_config_layout.addWidget(self.target_pos_z_spin, 2, 1) + + # 拖拽约束 + self.constrain_x_check = QCheckBox("约束X轴") + self.constrain_y_check = QCheckBox("约束Y轴") + self.constrain_z_check = QCheckBox("约束Z轴") + + constraint_layout = QHBoxLayout() + constraint_layout.addWidget(self.constrain_x_check) + constraint_layout.addWidget(self.constrain_y_check) + constraint_layout.addWidget(self.constrain_z_check) + + drag_config_layout.addWidget(QLabel("拖拽约束:"), 3, 0) + drag_config_layout.addLayout(constraint_layout, 3, 1) + + scroll_layout.addWidget(self.drag_config_group) + + # 点击触发配置 + self.click_config_group = QGroupBox("点击触发配置") + click_config_layout = QGridLayout(self.click_config_group) + + # 位移距离 + click_config_layout.addWidget(QLabel("位移距离 X:"), 0, 0) + self.move_distance_x_spin = QDoubleSpinBox() + self.move_distance_x_spin.setRange(-100, 100) + self.move_distance_x_spin.setSingleStep(0.1) + click_config_layout.addWidget(self.move_distance_x_spin, 0, 1) + + click_config_layout.addWidget(QLabel("位移距离 Y:"), 1, 0) + self.move_distance_y_spin = QDoubleSpinBox() + self.move_distance_y_spin.setRange(-100, 100) + self.move_distance_y_spin.setSingleStep(0.1) + click_config_layout.addWidget(self.move_distance_y_spin, 1, 1) + + click_config_layout.addWidget(QLabel("位移距离 Z:"), 2, 0) + self.move_distance_z_spin = QDoubleSpinBox() + self.move_distance_z_spin.setRange(-100, 100) + self.move_distance_z_spin.setSingleStep(0.1) + click_config_layout.addWidget(self.move_distance_z_spin, 2, 1) + + # 位移时间 + click_config_layout.addWidget(QLabel("位移时间(秒):"), 3, 0) + self.move_duration_spin = QDoubleSpinBox() + self.move_duration_spin.setRange(0.1, 10.0) + self.move_duration_spin.setSingleStep(0.1) + self.move_duration_spin.setValue(1.0) + click_config_layout.addWidget(self.move_duration_spin, 3, 1) + + # 完成后隐藏 + self.hide_after_move_check = QCheckBox("位移完成后隐藏模型") + click_config_layout.addWidget(self.hide_after_move_check, 4, 0, 1, 2) + + scroll_layout.addWidget(self.click_config_group) + + # 提示音频配置 + audio_config_group = QGroupBox("步骤提示") + audio_config_layout = QGridLayout(audio_config_group) + + # 提示音频文件 + audio_config_layout.addWidget(QLabel("提示音频:"), 0, 0) + audio_file_layout = QHBoxLayout() + self.audio_file_edit = QLineEdit() + self.browse_audio_button = QPushButton("浏览...") + audio_file_layout.addWidget(self.audio_file_edit) + audio_file_layout.addWidget(self.browse_audio_button) + audio_config_layout.addLayout(audio_file_layout, 0, 1) + + # 提示文字 + audio_config_layout.addWidget(QLabel("提示文字:"), 1, 0) + self.hint_text_edit = QTextEdit() + self.hint_text_edit.setMaximumHeight(60) + audio_config_layout.addWidget(self.hint_text_edit, 1, 1) + + scroll_layout.addWidget(audio_config_group) + + # 使用工具配置 + tool_config_group = QGroupBox("使用工具") + tool_config_layout = QGridLayout(tool_config_group) + + tool_config_layout.addWidget(QLabel("所需工具:"), 0, 0) + self.required_tool_combo = QComboBox() + self.required_tool_combo.addItem("无") + tool_config_layout.addWidget(self.required_tool_combo, 0, 1) + + scroll_layout.addWidget(tool_config_group) + + # 设置滚动区域 + scroll_area.setWidget(scroll_content) + config_layout.addWidget(scroll_area) + + # 默认隐藏点击配置 + self.click_config_group.setVisible(False) + + return config_widget + + def createSettingsTab(self): + """创建全局设置标签页""" + settings_widget = QWidget() + settings_layout = QVBoxLayout(settings_widget) + + # 全局设置组 + global_settings_group = QGroupBox("全局设置") + global_settings_layout = QGridLayout(global_settings_group) + + # 自动播放提示音 + self.auto_play_audio_check = QCheckBox("自动播放步骤提示音") + self.auto_play_audio_check.setChecked(True) + global_settings_layout.addWidget(self.auto_play_audio_check, 0, 0, 1, 2) + + # 显示步骤提示文字 + self.show_hint_text_check = QCheckBox("显示步骤提示文字") + self.show_hint_text_check.setChecked(True) + global_settings_layout.addWidget(self.show_hint_text_check, 1, 0, 1, 2) + + # 步骤完成确认 + self.require_confirmation_check = QCheckBox("步骤完成需要确认") + global_settings_layout.addWidget(self.require_confirmation_check, 2, 0, 1, 2) + + # 拖拽灵敏度 + global_settings_layout.addWidget(QLabel("拖拽灵敏度:"), 3, 0) + self.drag_sensitivity_slider = QSlider(Qt.Horizontal) + self.drag_sensitivity_slider.setRange(1, 10) + self.drag_sensitivity_slider.setValue(5) + global_settings_layout.addWidget(self.drag_sensitivity_slider, 3, 1) + + # 动画速度 + global_settings_layout.addWidget(QLabel("动画速度:"), 4, 0) + self.animation_speed_slider = QSlider(Qt.Horizontal) + self.animation_speed_slider.setRange(1, 10) + self.animation_speed_slider.setValue(5) + global_settings_layout.addWidget(self.animation_speed_slider, 4, 1) + + settings_layout.addWidget(global_settings_group) + + # 预设配置组 + presets_group = QGroupBox("预设配置") + presets_layout = QVBoxLayout(presets_group) + + # 预设列表 + self.presets_list = QListWidget() + presets_layout.addWidget(self.presets_list) + + # 预设操作按钮 + preset_buttons_layout = QHBoxLayout() + self.save_preset_button = QPushButton("保存为预设") + self.load_preset_button = QPushButton("加载预设") + self.delete_preset_button = QPushButton("删除预设") + preset_buttons_layout.addWidget(self.save_preset_button) + preset_buttons_layout.addWidget(self.load_preset_button) + preset_buttons_layout.addWidget(self.delete_preset_button) + presets_layout.addLayout(preset_buttons_layout) + + settings_layout.addWidget(presets_group) + + # 添加弹性空间 + settings_layout.addStretch() + + return settings_widget + + def connectSignals(self): + """连接信号槽""" + # 模型搜索 + self.model_search_edit.textChanged.connect(self.filterSceneModels) + + # 模型操作 + self.add_to_config_button.clicked.connect(self.addModelsToConfig) + self.remove_from_config_button.clicked.connect(self.removeModelsFromConfig) + + # 工具操作 + self.add_tool_button.clicked.connect(self.addTool) + self.edit_tool_button.clicked.connect(self.editTool) + self.remove_tool_button.clicked.connect(self.removeTool) + + # 步骤操作 + self.add_step_button.clicked.connect(self.addStep) + self.remove_step_button.clicked.connect(self.removeStep) + self.move_step_up_button.clicked.connect(self.moveStepUp) + self.move_step_down_button.clicked.connect(self.moveStepDown) + self.steps_list.itemSelectionChanged.connect(self.onStepSelectionChanged) + + # 步骤配置变化 + self.interaction_type_combo.currentTextChanged.connect(self.onInteractionTypeChanged) + self.browse_audio_button.clicked.connect(self.browseAudioFile) + + # 步骤配置实时更新 + self.step_name_edit.textChanged.connect(self.updateCurrentStep) + self.step_description_edit.textChanged.connect(self.updateCurrentStep) + self.step_type_combo.currentTextChanged.connect(self.updateCurrentStep) + self.target_model_combo.currentTextChanged.connect(self.updateCurrentStep) + + # 按钮操作 + self.save_button.clicked.connect(self.saveConfiguration) + self.load_button.clicked.connect(self.loadConfiguration) + self.preview_button.clicked.connect(self.previewConfiguration) + self.apply_button.clicked.connect(self.applyConfiguration) + + def loadSceneModels(self): + """加载场景中的模型""" + if not self.world: + return + + # 清空现有列表 + self.scene_models_list.clear() + + # 遍历场景中的所有节点 + self._addNodeToTree(self.world.render, self.scene_models_list) + + # 展开根节点 + self.scene_models_list.expandAll() + + def _addNodeToTree(self, node, parent_item): + """递归添加节点到树形控件""" + if node.getName() in ['render', 'camera', 'aspect2d', 'pixel2d']: + # 跳过系统节点 + for child in node.getChildren(): + self._addNodeToTree(child, parent_item) + return + + # 创建树形项 + if isinstance(parent_item, QTreeWidget): + item = QTreeWidgetItem(parent_item) + else: + item = QTreeWidgetItem(parent_item) + + item.setText(0, node.getName()) + item.setData(0, Qt.UserRole, node) + + # 添加子节点 + for child in node.getChildren(): + self._addNodeToTree(child, item) + + def filterSceneModels(self, text): + """过滤场景模型列表""" + # 简单的文本过滤实现 + def filterItems(item): + item_text = item.text(0).lower() + text_lower = text.lower() + + # 检查当前项是否匹配 + matches = text_lower in item_text + + # 检查子项 + child_matches = False + for i in range(item.childCount()): + child_item = item.child(i) + if filterItems(child_item): + child_matches = True + + # 显示/隐藏项 + item.setHidden(not (matches or child_matches)) + + return matches or child_matches + + # 从根项开始过滤 + for i in range(self.scene_models_list.topLevelItemCount()): + filterItems(self.scene_models_list.topLevelItem(i)) + + def addModelsToConfig(self): + """添加选中的模型到配置""" + selected_items = self.scene_models_list.selectedItems() + + for item in selected_items: + node = item.data(0, Qt.UserRole) + if node: + model_name = node.getName() + + # 检查是否已经存在 + existing_items = self.config_models_list.findItems(model_name, Qt.MatchExactly) + if not existing_items: + # 添加到配置模型列表 + list_item = QListWidgetItem(model_name) + list_item.setData(Qt.UserRole, node) + self.config_models_list.addItem(list_item) + + # 添加到配置数据 + self.config_data['models'].append({ + 'name': model_name, + 'node': node, + 'original_pos': node.getPos(), + 'original_hpr': node.getHpr() + }) + + # 更新目标模型下拉框 + self.target_model_combo.addItem(model_name) + + def removeModelsFromConfig(self): + """从配置中移除选中的模型""" + selected_items = self.config_models_list.selectedItems() + + for item in selected_items: + model_name = item.text() + + # 从列表中移除 + row = self.config_models_list.row(item) + self.config_models_list.takeItem(row) + + # 从配置数据中移除 + self.config_data['models'] = [ + model for model in self.config_data['models'] + if model['name'] != model_name + ] + + # 从目标模型下拉框中移除 + index = self.target_model_combo.findText(model_name) + if index >= 0: + self.target_model_combo.removeItem(index) + + def addTool(self): + """添加工具""" + tool_name = self.tool_name_edit.text().strip() + if not tool_name: + QMessageBox.warning(self, "警告", "请输入工具名称") + return + + # 检查是否已存在 + existing_items = self.tools_list.findItems(tool_name, Qt.MatchExactly) + if existing_items: + QMessageBox.warning(self, "警告", "工具已存在") + return + + # 添加到工具列表 + self.tools_list.addItem(tool_name) + self.required_tool_combo.addItem(tool_name) + + # 添加到配置数据 + self.config_data['tools'].append({ + 'name': tool_name, + 'description': '', + 'icon': '' + }) + + # 清空输入框 + self.tool_name_edit.clear() + + def editTool(self): + """编辑工具""" + current_item = self.tools_list.currentItem() + if not current_item: + QMessageBox.warning(self, "警告", "请选择要编辑的工具") + return + + # 这里可以添加工具编辑对话框 + QMessageBox.information(self, "提示", "工具编辑功能待实现") + + def removeTool(self): + """删除工具""" + current_item = self.tools_list.currentItem() + if not current_item: + QMessageBox.warning(self, "警告", "请选择要删除的工具") + return + + tool_name = current_item.text() + + # 从列表中移除 + row = self.tools_list.row(current_item) + self.tools_list.takeItem(row) + + # 从下拉框中移除 + index = self.required_tool_combo.findText(tool_name) + if index >= 0: + self.required_tool_combo.removeItem(index) + + # 从配置数据中移除 + self.config_data['tools'] = [ + tool for tool in self.config_data['tools'] + if tool['name'] != tool_name + ] + + def addStep(self): + """添加步骤""" + step_count = len(self.config_data['steps']) + 1 + step_name = f"步骤 {step_count}" + + # 创建新步骤数据 + step_data = { + 'name': step_name, + 'description': '', + 'type': '拆卸', + 'target_model': '', + 'interaction_type': '鼠标拖拽', + 'target_position': [0, 0, 0], + 'constraints': {'x': False, 'y': False, 'z': False}, + 'move_distance': [0, 0, 0], + 'move_duration': 1.0, + 'hide_after_move': False, + 'audio_file': '', + 'hint_text': '', + 'required_tool': '无' + } + + # 添加到配置数据 + self.config_data['steps'].append(step_data) + + # 添加到步骤列表 + list_item = QListWidgetItem(step_name) + list_item.setData(Qt.UserRole, step_data) + self.steps_list.addItem(list_item) + + # 选中新添加的步骤 + self.steps_list.setCurrentItem(list_item) + + def removeStep(self): + """删除步骤""" + current_item = self.steps_list.currentItem() + if not current_item: + QMessageBox.warning(self, "警告", "请选择要删除的步骤") + return + + # 从列表中移除 + row = self.steps_list.row(current_item) + self.steps_list.takeItem(row) + + # 从配置数据中移除 + self.config_data['steps'].pop(row) + + # 重新编号步骤 + self.renumberSteps() + + def moveStepUp(self): + """上移步骤""" + current_row = self.steps_list.currentRow() + if current_row <= 0: + return + + # 交换数据 + self.config_data['steps'][current_row], self.config_data['steps'][current_row - 1] = \ + self.config_data['steps'][current_row - 1], self.config_data['steps'][current_row] + + # 交换列表项 + current_item = self.steps_list.takeItem(current_row) + self.steps_list.insertItem(current_row - 1, current_item) + self.steps_list.setCurrentRow(current_row - 1) + + # 重新编号 + self.renumberSteps() + + def moveStepDown(self): + """下移步骤""" + current_row = self.steps_list.currentRow() + if current_row >= self.steps_list.count() - 1: + return + + # 交换数据 + self.config_data['steps'][current_row], self.config_data['steps'][current_row + 1] = \ + self.config_data['steps'][current_row + 1], self.config_data['steps'][current_row] + + # 交换列表项 + current_item = self.steps_list.takeItem(current_row) + self.steps_list.insertItem(current_row + 1, current_item) + self.steps_list.setCurrentRow(current_row + 1) + + # 重新编号 + self.renumberSteps() + + def renumberSteps(self): + """重新编号步骤""" + for i in range(self.steps_list.count()): + item = self.steps_list.item(i) + step_data = item.data(Qt.UserRole) + new_name = f"步骤 {i + 1}" + step_data['name'] = new_name + item.setText(new_name) + + def onStepSelectionChanged(self): + """步骤选择改变""" + current_item = self.steps_list.currentItem() + if not current_item: + return + + step_data = current_item.data(Qt.UserRole) + if not step_data: + return + + # 更新步骤配置界面 + self.loadStepConfiguration(step_data) + + def loadStepConfiguration(self, step_data): + """加载步骤配置到界面""" + # 基本信息 + self.step_name_edit.setText(step_data.get('name', '')) + self.step_description_edit.setPlainText(step_data.get('description', '')) + self.step_type_combo.setCurrentText(step_data.get('type', '拆卸')) + + # 目标模型 + target_model = step_data.get('target_model', '') + index = self.target_model_combo.findText(target_model) + if index >= 0: + self.target_model_combo.setCurrentIndex(index) + + # 交互方式 + interaction_type = step_data.get('interaction_type', '鼠标拖拽') + self.interaction_type_combo.setCurrentText(interaction_type) + self.onInteractionTypeChanged(interaction_type) + + # 拖拽配置 + target_pos = step_data.get('target_position', [0, 0, 0]) + self.target_pos_x_spin.setValue(target_pos[0]) + self.target_pos_y_spin.setValue(target_pos[1]) + self.target_pos_z_spin.setValue(target_pos[2]) + + constraints = step_data.get('constraints', {'x': False, 'y': False, 'z': False}) + self.constrain_x_check.setChecked(constraints.get('x', False)) + self.constrain_y_check.setChecked(constraints.get('y', False)) + self.constrain_z_check.setChecked(constraints.get('z', False)) + + # 点击触发配置 + move_distance = step_data.get('move_distance', [0, 0, 0]) + self.move_distance_x_spin.setValue(move_distance[0]) + self.move_distance_y_spin.setValue(move_distance[1]) + self.move_distance_z_spin.setValue(move_distance[2]) + + self.move_duration_spin.setValue(step_data.get('move_duration', 1.0)) + self.hide_after_move_check.setChecked(step_data.get('hide_after_move', False)) + + # 提示配置 + self.audio_file_edit.setText(step_data.get('audio_file', '')) + self.hint_text_edit.setPlainText(step_data.get('hint_text', '')) + + # 工具配置 + required_tool = step_data.get('required_tool', '无') + index = self.required_tool_combo.findText(required_tool) + if index >= 0: + self.required_tool_combo.setCurrentIndex(index) + + def onInteractionTypeChanged(self, interaction_type): + """交互方式改变""" + if interaction_type == "鼠标拖拽": + self.drag_config_group.setVisible(True) + self.click_config_group.setVisible(False) + else: # 点击触发 + self.drag_config_group.setVisible(False) + self.click_config_group.setVisible(True) + + def updateCurrentStep(self): + """更新当前步骤数据""" + current_item = self.steps_list.currentItem() + if not current_item: + return + + step_data = current_item.data(Qt.UserRole) + if not step_data: + return + + # 更新步骤数据 + step_data['name'] = self.step_name_edit.text() + step_data['description'] = self.step_description_edit.toPlainText() + step_data['type'] = self.step_type_combo.currentText() + step_data['target_model'] = self.target_model_combo.currentText() + step_data['interaction_type'] = self.interaction_type_combo.currentText() + + # 更新列表项显示 + current_item.setText(step_data['name']) + + def browseAudioFile(self): + """浏览音频文件""" + file_path, _ = QFileDialog.getOpenFileName( + self, "选择音频文件", "", + "音频文件 (*.wav *.mp3 *.ogg *.flac);;所有文件 (*)" + ) + + if file_path: + self.audio_file_edit.setText(file_path) + + def saveConfiguration(self): + """保存配置""" + file_path, _ = QFileDialog.getSaveFileName( + self, "保存拆装配置", "", + "JSON文件 (*.json);;所有文件 (*)" + ) + + if file_path: + try: + # 准备保存的数据(去除不能序列化的对象) + save_data = { + 'models': [ + { + 'name': model['name'], + 'original_pos': list(model['original_pos']), + 'original_hpr': list(model['original_hpr']) + } for model in self.config_data['models'] + ], + 'steps': self.config_data['steps'], + 'tools': self.config_data['tools'], + 'settings': { + 'auto_play_audio': self.auto_play_audio_check.isChecked(), + 'show_hint_text': self.show_hint_text_check.isChecked(), + 'require_confirmation': self.require_confirmation_check.isChecked(), + 'drag_sensitivity': self.drag_sensitivity_slider.value(), + 'animation_speed': self.animation_speed_slider.value() + } + } + + with open(file_path, 'w', encoding='utf-8') as f: + json.dump(save_data, f, ensure_ascii=False, indent=2) + + QMessageBox.information(self, "成功", "配置保存成功") + + except Exception as e: + QMessageBox.critical(self, "错误", f"保存配置失败: {str(e)}") + + def loadConfiguration(self): + """加载配置""" + file_path, _ = QFileDialog.getOpenFileName( + self, "加载拆装配置", "", + "JSON文件 (*.json);;所有文件 (*)" + ) + + if file_path: + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + + # 加载配置数据 + self.loadConfigurationData(data) + QMessageBox.information(self, "成功", "配置加载成功") + + except Exception as e: + QMessageBox.critical(self, "错误", f"加载配置失败: {str(e)}") + + def loadConfigurationData(self, data): + """加载配置数据到界面""" + # 清空现有配置 + self.config_models_list.clear() + self.target_model_combo.clear() + self.steps_list.clear() + + # 加载模型配置 + # 注意:这里需要重新匹配场景中的模型节点 + + # 加载步骤配置 + self.config_data['steps'] = data.get('steps', []) + for step_data in self.config_data['steps']: + list_item = QListWidgetItem(step_data['name']) + list_item.setData(Qt.UserRole, step_data) + self.steps_list.addItem(list_item) + + # 加载工具配置 + self.tools_list.clear() + self.required_tool_combo.clear() + self.required_tool_combo.addItem("无") + + self.config_data['tools'] = data.get('tools', []) + for tool in self.config_data['tools']: + self.tools_list.addItem(tool['name']) + self.required_tool_combo.addItem(tool['name']) + + # 加载全局设置 + settings = data.get('settings', {}) + self.auto_play_audio_check.setChecked(settings.get('auto_play_audio', True)) + self.show_hint_text_check.setChecked(settings.get('show_hint_text', True)) + self.require_confirmation_check.setChecked(settings.get('require_confirmation', False)) + self.drag_sensitivity_slider.setValue(settings.get('drag_sensitivity', 5)) + self.animation_speed_slider.setValue(settings.get('animation_speed', 5)) + + def previewConfiguration(self): + """预览配置效果""" + QMessageBox.information(self, "提示", "预览功能待实现") + + def applyConfiguration(self): + """应用配置""" + QMessageBox.information(self, "提示", "应用配置功能待实现") \ No newline at end of file diff --git a/ui/assembly_disassembly_config_simple.py b/ui/assembly_disassembly_config_simple.py new file mode 100644 index 00000000..9397f650 --- /dev/null +++ b/ui/assembly_disassembly_config_simple.py @@ -0,0 +1,1069 @@ +# -*- coding: utf-8 -*- +""" +拆装交互配置界面 - 简化版本 +""" + +from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QTabWidget, + QWidget, QLabel, QListWidget, QListWidgetItem, + QPushButton, QComboBox, QLineEdit, QTextEdit, + QGroupBox, QGridLayout, QCheckBox, QSplitter, + QTreeWidget, QTreeWidgetItem, QMessageBox, + QFileDialog, QScrollArea, QDoubleSpinBox) +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QFont +import json + + +class AssemblyDisassemblyConfigDialog(QDialog): + """拆装配置主对话框 - 简化版本""" + + def __init__(self, parent=None, world=None): + super().__init__(parent) + self.world = world + self.config_data = { + 'models': [], # 参与拆装的模型列表 + 'steps': [], # 拆装步骤配置 + 'tools': [], # 使用的工具列表 + 'settings': {} # 全局设置 + } + + self.setupUI() + self.loadSceneModels() + + # 初始化当前编辑的步骤项 + self._current_step_item = None + + def setupUI(self): + """设置界面""" + self.setWindowTitle("拆装交互配置") + self.setModal(False) + self.resize(1000, 700) + + # 主布局 + main_layout = QVBoxLayout(self) + + # 标题 + title_label = QLabel("模型拆装交互配置") + title_label.setFont(QFont("", 16, QFont.Bold)) + title_label.setAlignment(Qt.AlignCenter) + main_layout.addWidget(title_label) + + # 创建标签页 + self.tab_widget = QTabWidget() + main_layout.addWidget(self.tab_widget) + + # 模型选择标签页 + self.models_tab = self.createModelsTab() + self.tab_widget.addTab(self.models_tab, "模型选择") + + # 步骤配置标签页 + self.steps_tab = self.createStepsTab() + self.tab_widget.addTab(self.steps_tab, "步骤配置") + + # 工具配置标签页 + self.tools_tab = self.createToolsTab() + self.tab_widget.addTab(self.tools_tab, "工具配置") + + # 全局设置标签页 + self.settings_tab = self.createSettingsTab() + self.tab_widget.addTab(self.settings_tab, "全局设置") + + # 底部按钮 + button_layout = QHBoxLayout() + + self.save_button = QPushButton("保存配置") + self.load_button = QPushButton("加载配置") + self.preview_button = QPushButton("预览效果") + self.apply_button = QPushButton("应用配置") + + button_layout.addWidget(self.save_button) + button_layout.addWidget(self.load_button) + button_layout.addStretch() + button_layout.addWidget(self.preview_button) + button_layout.addWidget(self.apply_button) + + main_layout.addLayout(button_layout) + + # 连接信号 + self.connectSignals() + + def createModelsTab(self): + """创建模型选择标签页""" + widget = QWidget() + layout = QVBoxLayout(widget) + + # 分割器 + splitter = QSplitter(Qt.Horizontal) + layout.addWidget(splitter) + + # 左侧 - 场景模型 + left_group = QGroupBox("场景模型") + left_layout = QVBoxLayout(left_group) + + # 添加搜索框 + search_layout = QHBoxLayout() + search_layout.addWidget(QLabel("搜索模型:")) + self.model_search_edit = QLineEdit() + self.model_search_edit.setPlaceholderText("输入模型名称进行搜索...") + search_layout.addWidget(self.model_search_edit) + left_layout.addLayout(search_layout) + + self.scene_models_tree = QTreeWidget() + self.scene_models_tree.setHeaderLabel("场景中的模型") + left_layout.addWidget(self.scene_models_tree) + + # 操作按钮 + button_layout = QHBoxLayout() + self.add_model_button = QPushButton("添加到配置 →") + self.remove_model_button = QPushButton("← 从配置移除") + button_layout.addWidget(self.add_model_button) + button_layout.addWidget(self.remove_model_button) + left_layout.addLayout(button_layout) + + splitter.addWidget(left_group) + + # 右侧 - 已配置模型 + right_group = QGroupBox("已配置模型") + right_layout = QVBoxLayout(right_group) + + self.config_models_list = QListWidget() + right_layout.addWidget(self.config_models_list) + + splitter.addWidget(right_group) + + return widget + + def createStepsTab(self): + """创建步骤配置标签页""" + widget = QWidget() + layout = QVBoxLayout(widget) + + # 分割器 + splitter = QSplitter(Qt.Horizontal) + layout.addWidget(splitter) + + # 左侧 - 步骤列表 + left_group = QGroupBox("拆装步骤") + left_layout = QVBoxLayout(left_group) + + # 步骤操作按钮 + step_buttons = QHBoxLayout() + self.add_step_button = QPushButton("添加步骤") + self.remove_step_button = QPushButton("删除步骤") + self.move_up_button = QPushButton("上移") + self.move_down_button = QPushButton("下移") + + step_buttons.addWidget(self.add_step_button) + step_buttons.addWidget(self.remove_step_button) + step_buttons.addWidget(self.move_up_button) + step_buttons.addWidget(self.move_down_button) + left_layout.addLayout(step_buttons) + + self.steps_list = QListWidget() + left_layout.addWidget(self.steps_list) + + splitter.addWidget(left_group) + + # 右侧 - 步骤详细配置 + right_group = QGroupBox("步骤详细配置") + right_layout = QVBoxLayout(right_group) + + # 滚动区域 + scroll_area = QScrollArea() + scroll_area.setWidgetResizable(True) + scroll_content = QWidget() + scroll_layout = QVBoxLayout(scroll_content) + + # 基本信息 + basic_group = QGroupBox("基本信息") + basic_layout = QGridLayout(basic_group) + + basic_layout.addWidget(QLabel("步骤名称:"), 0, 0) + self.step_name_edit = QLineEdit() + basic_layout.addWidget(self.step_name_edit, 0, 1) + + basic_layout.addWidget(QLabel("步骤描述:"), 1, 0) + self.step_desc_edit = QTextEdit() + self.step_desc_edit.setMaximumHeight(80) + basic_layout.addWidget(self.step_desc_edit, 1, 1) + + basic_layout.addWidget(QLabel("步骤类型:"), 2, 0) + self.step_type_combo = QComboBox() + self.step_type_combo.addItems(["拆卸", "安装"]) + basic_layout.addWidget(self.step_type_combo, 2, 1) + + # 步骤分值(考核模式用) + basic_layout.addWidget(QLabel("步骤分值:"), 3, 0) + self.step_score_spin = QDoubleSpinBox() + self.step_score_spin.setRange(0, 100) + self.step_score_spin.setValue(10) # 默认10分 + self.step_score_spin.setSingleStep(1) + self.step_score_spin.setDecimals(0) + self.step_score_spin.setSuffix(" 分") + basic_layout.addWidget(self.step_score_spin, 3, 1) + + # 添加分值说明 + score_note = QLabel("注:在考核模式下,此分值将用于计算该步骤的得分。") + score_note.setWordWrap(True) + score_note.setStyleSheet("color: #666; font-style: italic; font-size: 10px;") + basic_layout.addWidget(score_note, 4, 0, 1, 2) + + scroll_layout.addWidget(basic_group) + + # 目标模型 + target_group = QGroupBox("目标模型") + target_layout = QGridLayout(target_group) + + target_layout.addWidget(QLabel("目标模型:"), 0, 0) + self.target_model_combo = QComboBox() + target_layout.addWidget(self.target_model_combo, 0, 1) + + target_layout.addWidget(QLabel("交互方式:"), 1, 0) + self.interaction_combo = QComboBox() + self.interaction_combo.addItems(["鼠标拖拽", "点击触发"]) + target_layout.addWidget(self.interaction_combo, 1, 1) + + scroll_layout.addWidget(target_group) + + # 位置配置 + pos_group = QGroupBox("位置配置") + pos_layout = QGridLayout(pos_group) + + # 添加说明文字 + pos_note = QLabel("注:拖拽交互支持自由拖拽,无轴向限制。以下为参考目标位置:") + pos_note.setWordWrap(True) + pos_note.setStyleSheet("color: #666; font-style: italic;") + pos_layout.addWidget(pos_note, 0, 0, 1, 2) + + pos_layout.addWidget(QLabel("参考位置 X:"), 1, 0) + self.pos_x_spin = QDoubleSpinBox() + self.pos_x_spin.setRange(-1000, 1000) + pos_layout.addWidget(self.pos_x_spin, 1, 1) + + pos_layout.addWidget(QLabel("参考位置 Y:"), 2, 0) + self.pos_y_spin = QDoubleSpinBox() + self.pos_y_spin.setRange(-1000, 1000) + pos_layout.addWidget(self.pos_y_spin, 2, 1) + + pos_layout.addWidget(QLabel("参考位置 Z:"), 3, 0) + self.pos_z_spin = QDoubleSpinBox() + self.pos_z_spin.setRange(-1000, 1000) + pos_layout.addWidget(self.pos_z_spin, 3, 1) + + scroll_layout.addWidget(pos_group) + + # 动画配置(仅点击触发模式) + animation_group = QGroupBox("动画配置") + animation_layout = QGridLayout(animation_group) + + # 动画时长 + animation_layout.addWidget(QLabel("动画时长(秒):"), 0, 0) + self.animation_duration_spin = QDoubleSpinBox() + self.animation_duration_spin.setRange(0.1, 10.0) + self.animation_duration_spin.setValue(2.0) + self.animation_duration_spin.setSingleStep(0.1) + self.animation_duration_spin.setDecimals(1) + animation_layout.addWidget(self.animation_duration_spin, 0, 1) + + # 说明文字 + animation_note = QLabel("注:仅在'点击触发'交互方式下有效,用于控制模型自动移动的速度。") + animation_note.setWordWrap(True) + animation_note.setStyleSheet("color: #666; font-style: italic;") + animation_layout.addWidget(animation_note, 1, 0, 1, 2) + + scroll_layout.addWidget(animation_group) + + # 提示配置 + hint_group = QGroupBox("提示配置") + hint_layout = QGridLayout(hint_group) + + hint_layout.addWidget(QLabel("提示音频:"), 0, 0) + audio_layout = QHBoxLayout() + self.audio_edit = QLineEdit() + self.browse_audio_button = QPushButton("浏览...") + audio_layout.addWidget(self.audio_edit) + audio_layout.addWidget(self.browse_audio_button) + hint_layout.addLayout(audio_layout, 0, 1) + + hint_layout.addWidget(QLabel("提示文字:"), 1, 0) + self.hint_text_edit = QTextEdit() + self.hint_text_edit.setMaximumHeight(60) + hint_layout.addWidget(self.hint_text_edit, 1, 1) + + scroll_layout.addWidget(hint_group) + + # 工具配置 + tool_group = QGroupBox("使用工具") + tool_layout = QGridLayout(tool_group) + + tool_layout.addWidget(QLabel("所需工具:"), 0, 0) + self.required_tool_combo = QComboBox() + self.required_tool_combo.addItem("无") + tool_layout.addWidget(self.required_tool_combo, 0, 1) + + scroll_layout.addWidget(tool_group) + + scroll_area.setWidget(scroll_content) + right_layout.addWidget(scroll_area) + + splitter.addWidget(right_group) + + return widget + + def createToolsTab(self): + """创建工具配置标签页""" + widget = QWidget() + layout = QVBoxLayout(widget) + + # 说明文字 + info_label = QLabel("配置拆装过程中需要使用的工具:") + layout.addWidget(info_label) + + # 工具添加 + add_layout = QHBoxLayout() + add_layout.addWidget(QLabel("工具名称:")) + self.tool_name_edit = QLineEdit() + self.add_tool_button = QPushButton("添加工具") + add_layout.addWidget(self.tool_name_edit) + add_layout.addWidget(self.add_tool_button) + add_layout.addStretch() + layout.addLayout(add_layout) + + # 工具列表 + self.tools_list = QListWidget() + layout.addWidget(self.tools_list) + + # 工具操作按钮 + tool_buttons = QHBoxLayout() + self.edit_tool_button = QPushButton("编辑工具") + self.delete_tool_button = QPushButton("删除工具") + tool_buttons.addWidget(self.edit_tool_button) + tool_buttons.addWidget(self.delete_tool_button) + tool_buttons.addStretch() + layout.addLayout(tool_buttons) + + return widget + + def createSettingsTab(self): + """创建全局设置标签页""" + widget = QWidget() + layout = QVBoxLayout(widget) + + # 说明文字 + info_label = QLabel("配置拆装交互的全局设置:") + layout.addWidget(info_label) + + # 设置组 + settings_group = QGroupBox("全局设置") + settings_layout = QGridLayout(settings_group) + + # 自动播放提示音 + self.auto_audio_check = QCheckBox("自动播放步骤提示音") + self.auto_audio_check.setChecked(True) + settings_layout.addWidget(self.auto_audio_check, 0, 0, 1, 2) + + # 显示提示文字 + self.show_hint_check = QCheckBox("显示步骤提示文字") + self.show_hint_check.setChecked(True) + settings_layout.addWidget(self.show_hint_check, 1, 0, 1, 2) + + # 步骤完成确认 + self.confirm_check = QCheckBox("步骤完成需要确认") + settings_layout.addWidget(self.confirm_check, 2, 0, 1, 2) + + layout.addWidget(settings_group) + + # 预设配置 + presets_group = QGroupBox("预设配置") + presets_layout = QVBoxLayout(presets_group) + + self.presets_list = QListWidget() + presets_layout.addWidget(self.presets_list) + + preset_buttons = QHBoxLayout() + self.save_preset_button = QPushButton("保存为预设") + self.load_preset_button = QPushButton("加载预设") + self.delete_preset_button = QPushButton("删除预设") + preset_buttons.addWidget(self.save_preset_button) + preset_buttons.addWidget(self.load_preset_button) + preset_buttons.addWidget(self.delete_preset_button) + presets_layout.addLayout(preset_buttons) + + layout.addWidget(presets_group) + + layout.addStretch() + + return widget + + def connectSignals(self): + """连接信号槽""" + # 模型搜索 + self.model_search_edit.textChanged.connect(self.filterSceneModels) + + # 模型操作 + self.add_model_button.clicked.connect(self.addModelsToConfig) + self.remove_model_button.clicked.connect(self.removeModelsFromConfig) + + # 步骤操作 + self.add_step_button.clicked.connect(self.addStep) + self.remove_step_button.clicked.connect(self.removeStep) + self.move_up_button.clicked.connect(self.moveStepUp) + self.move_down_button.clicked.connect(self.moveStepDown) + self.steps_list.itemSelectionChanged.connect(self.onStepSelectionChanged) + + # 步骤配置实时保存 - 使用专门的连接方法 + self.connectStepConfigSignals() + + # 工具操作 + self.add_tool_button.clicked.connect(self.addTool) + self.edit_tool_button.clicked.connect(self.editTool) + self.delete_tool_button.clicked.connect(self.deleteTool) + + # 文件操作 + self.browse_audio_button.clicked.connect(self.browseAudioFile) + + # 按钮操作 + self.save_button.clicked.connect(self.saveConfiguration) + self.load_button.clicked.connect(self.loadConfiguration) + self.preview_button.clicked.connect(self.previewConfiguration) + self.apply_button.clicked.connect(self.applyConfiguration) + + def loadSceneModels(self): + """加载场景中的模型""" + if not self.world: + return + + self.scene_models_tree.clear() + self._addNodeToTree(self.world.render, self.scene_models_tree) + self.scene_models_tree.expandAll() + + def _addNodeToTree(self, node, parent_item): + """递归添加节点到树形控件""" + if node.getName() in ['render', 'camera', 'aspect2d', 'pixel2d']: + for child in node.getChildren(): + self._addNodeToTree(child, parent_item) + return + + if isinstance(parent_item, QTreeWidget): + item = QTreeWidgetItem(parent_item) + else: + item = QTreeWidgetItem(parent_item) + + item.setText(0, node.getName()) + item.setData(0, Qt.UserRole, node) + + for child in node.getChildren(): + self._addNodeToTree(child, item) + + def filterSceneModels(self, search_text): + """根据搜索文本过滤场景模型""" + def filterItems(item): + item_text = item.text(0).lower() + search_lower = search_text.lower() + + # 检查当前项是否匹配 + matches = search_lower in item_text if search_text else True + + # 检查子项 + child_matches = False + for i in range(item.childCount()): + child_item = item.child(i) + if filterItems(child_item): + child_matches = True + + # 显示/隐藏项 + item.setHidden(not (matches or child_matches)) + + return matches or child_matches + + # 从根项开始过滤 + for i in range(self.scene_models_tree.topLevelItemCount()): + filterItems(self.scene_models_tree.topLevelItem(i)) + + def addModelsToConfig(self): + """添加选中的模型到配置""" + selected_items = self.scene_models_tree.selectedItems() + + for item in selected_items: + node = item.data(0, Qt.UserRole) + if node: + model_name = node.getName() + existing_items = self.config_models_list.findItems(model_name, Qt.MatchExactly) + if not existing_items: + list_item = QListWidgetItem(model_name) + list_item.setData(Qt.UserRole, node) + self.config_models_list.addItem(list_item) + + self.config_data['models'].append({ + 'name': model_name, + 'node': node, + 'original_pos': node.getPos(), + 'original_hpr': node.getHpr() + }) + + self.target_model_combo.addItem(model_name) + + def removeModelsFromConfig(self): + """从配置中移除选中的模型""" + selected_items = self.config_models_list.selectedItems() + + for item in selected_items: + model_name = item.text() + row = self.config_models_list.row(item) + self.config_models_list.takeItem(row) + + self.config_data['models'] = [ + model for model in self.config_data['models'] + if model['name'] != model_name + ] + + index = self.target_model_combo.findText(model_name) + if index >= 0: + self.target_model_combo.removeItem(index) + + def addStep(self): + """添加步骤""" + step_count = len(self.config_data['steps']) + 1 + step_name = f"步骤 {step_count}" + + step_data = { + 'name': step_name, + 'description': '', + 'type': '拆卸', + 'target_model': '', + 'interaction_type': '鼠标拖拽', + 'target_position': [0, 0, 0], + 'audio_file': '', + 'hint_text': '', + 'required_tool': '无', + 'score': 10 # 默认分值 + } + + self.config_data['steps'].append(step_data) + + # 显示步骤名称和分值 + display_text = f"{step_name} ({step_data['score']}分)" + list_item = QListWidgetItem(display_text) + list_item.setData(Qt.UserRole, step_data) + self.steps_list.addItem(list_item) + + # 设置当前编辑的步骤项并选中 + self._current_step_item = list_item + self.steps_list.setCurrentItem(list_item) + + # 清空界面并显示新步骤的默认配置 + self.loadStepConfiguration(step_data) + + def removeStep(self): + """删除步骤""" + current_item = self.steps_list.currentItem() + if not current_item: + QMessageBox.warning(self, "警告", "请选择要删除的步骤") + return + + row = self.steps_list.row(current_item) + self.steps_list.takeItem(row) + self.config_data['steps'].pop(row) + + # 如果删除的是当前编辑的步骤,清空当前步骤项 + if self._current_step_item == current_item: + self._current_step_item = None + + self.renumberSteps() + + def moveStepUp(self): + """上移步骤""" + current_row = self.steps_list.currentRow() + if current_row <= 0: + return + + self.config_data['steps'][current_row], self.config_data['steps'][current_row - 1] = \ + self.config_data['steps'][current_row - 1], self.config_data['steps'][current_row] + + current_item = self.steps_list.takeItem(current_row) + self.steps_list.insertItem(current_row - 1, current_item) + self.steps_list.setCurrentRow(current_row - 1) + self.renumberSteps() + + def moveStepDown(self): + """下移步骤""" + current_row = self.steps_list.currentRow() + if current_row >= self.steps_list.count() - 1: + return + + self.config_data['steps'][current_row], self.config_data['steps'][current_row + 1] = \ + self.config_data['steps'][current_row + 1], self.config_data['steps'][current_row] + + current_item = self.steps_list.takeItem(current_row) + self.steps_list.insertItem(current_row + 1, current_item) + self.steps_list.setCurrentRow(current_row + 1) + self.renumberSteps() + + def renumberSteps(self): + """重新编号步骤""" + for i in range(self.steps_list.count()): + item = self.steps_list.item(i) + step_data = item.data(Qt.UserRole) + new_name = f"步骤 {i + 1}" + step_data['name'] = new_name + step_score = step_data.get('score', 10) + display_text = f"{new_name} ({step_score}分)" + item.setText(display_text) + + def onStepSelectionChanged(self): + """步骤选择改变""" + # 简单直接的切换逻辑 + current_item = self.steps_list.currentItem() + if not current_item: + self._current_step_item = None + self._clearStepConfigurationUI() + return + + # 记录当前步骤项并加载配置 + self._current_step_item = current_item + self.loadStepConfiguration(current_item.data(Qt.UserRole)) + + def _clearStepConfigurationUI(self): + """清空步骤配置界面""" + self.disconnectStepConfigSignals() + try: + self.step_name_edit.setText("") + self.step_desc_edit.setPlainText("") + self.step_type_combo.setCurrentIndex(0) + self.target_model_combo.setCurrentIndex(0) + self.interaction_combo.setCurrentIndex(0) + self.pos_x_spin.setValue(0) + self.pos_y_spin.setValue(0) + self.pos_z_spin.setValue(0) + self.audio_edit.setText("") + self.hint_text_edit.setPlainText("") + self.required_tool_combo.setCurrentIndex(0) + finally: + self.connectStepConfigSignals() + + def loadStepConfiguration(self, step_data): + """加载步骤配置到界面""" + # 临时断开信号,避免加载时触发保存 + self.disconnectStepConfigSignals() + try: + # 加载所有配置项到界面 + self.step_name_edit.setText(step_data.get('name', '')) + self.step_desc_edit.setPlainText(step_data.get('description', '')) + self.step_type_combo.setCurrentText(step_data.get('type', '拆卸')) + + target_model = step_data.get('target_model', '') + index = self.target_model_combo.findText(target_model) + if index >= 0: + self.target_model_combo.setCurrentIndex(index) + else: + self.target_model_combo.setCurrentIndex(0) + + self.interaction_combo.setCurrentText(step_data.get('interaction_type', '鼠标拖拽')) + + target_pos = step_data.get('target_position', [0, 0, 0]) + self.pos_x_spin.setValue(target_pos[0]) + self.pos_y_spin.setValue(target_pos[1]) + self.pos_z_spin.setValue(target_pos[2]) + + # 加载动画时长 + self.animation_duration_spin.setValue(step_data.get('animation_duration', 2.0)) + + self.audio_edit.setText(step_data.get('audio_file', '')) + self.hint_text_edit.setPlainText(step_data.get('hint_text', '')) + + # 加载工具配置 + required_tool = step_data.get('required_tool', '无') + index = self.required_tool_combo.findText(required_tool) + if index >= 0: + self.required_tool_combo.setCurrentIndex(index) + else: + self.required_tool_combo.setCurrentIndex(0) + + # 加载分值配置 + self.step_score_spin.setValue(step_data.get('score', 10)) + + finally: + # 确保信号重新连接 + self.connectStepConfigSignals() + + def disconnectStepConfigSignals(self): + """断开步骤配置的信号连接""" + try: + # 精确断开特定的信号连接 + self.step_name_edit.textChanged.disconnect(self.saveCurrentStepConfig) + self.step_desc_edit.textChanged.disconnect(self.saveCurrentStepConfig) + self.step_type_combo.currentTextChanged.disconnect(self.saveCurrentStepConfig) + self.target_model_combo.currentTextChanged.disconnect(self.saveCurrentStepConfig) + self.interaction_combo.currentTextChanged.disconnect(self.saveCurrentStepConfig) + self.pos_x_spin.valueChanged.disconnect(self.saveCurrentStepConfig) + self.pos_y_spin.valueChanged.disconnect(self.saveCurrentStepConfig) + self.pos_z_spin.valueChanged.disconnect(self.saveCurrentStepConfig) + self.animation_duration_spin.valueChanged.disconnect(self.saveCurrentStepConfig) + self.audio_edit.textChanged.disconnect(self.saveCurrentStepConfig) + self.hint_text_edit.textChanged.disconnect(self.saveCurrentStepConfig) + self.required_tool_combo.currentTextChanged.disconnect(self.saveCurrentStepConfig) + self.step_score_spin.valueChanged.disconnect(self.saveCurrentStepConfig) + except: + # 忽略断开连接时的错误 + pass + + def connectStepConfigSignals(self): + """连接步骤配置的信号""" + # 重新连接信号到保存方法 + self.step_name_edit.textChanged.connect(self.saveCurrentStepConfig) + self.step_desc_edit.textChanged.connect(self.saveCurrentStepConfig) + self.step_type_combo.currentTextChanged.connect(self.saveCurrentStepConfig) + self.target_model_combo.currentTextChanged.connect(self.saveCurrentStepConfig) + self.interaction_combo.currentTextChanged.connect(self.saveCurrentStepConfig) + self.pos_x_spin.valueChanged.connect(self.saveCurrentStepConfig) + self.pos_y_spin.valueChanged.connect(self.saveCurrentStepConfig) + self.pos_z_spin.valueChanged.connect(self.saveCurrentStepConfig) + self.animation_duration_spin.valueChanged.connect(self.saveCurrentStepConfig) + self.audio_edit.textChanged.connect(self.saveCurrentStepConfig) + self.hint_text_edit.textChanged.connect(self.saveCurrentStepConfig) + self.required_tool_combo.currentTextChanged.connect(self.saveCurrentStepConfig) + self.step_score_spin.valueChanged.connect(self.saveCurrentStepConfig) + + def saveCurrentStepConfig(self): + """保存当前步骤的配置到list_item的UserRole数据中""" + # 检查当前步骤项是否有效 + if not hasattr(self, '_current_step_item') or self._current_step_item is None: + return + + # 获取当前步骤的数据 + step_data = self._current_step_item.data(Qt.UserRole) + if not step_data: + return + + # 从界面控件收集所有配置数据 + step_data['name'] = self.step_name_edit.text() + step_data['description'] = self.step_desc_edit.toPlainText() + step_data['type'] = self.step_type_combo.currentText() + step_data['target_model'] = self.target_model_combo.currentText() + step_data['interaction_type'] = self.interaction_combo.currentText() + step_data['target_position'] = [ + self.pos_x_spin.value(), + self.pos_y_spin.value(), + self.pos_z_spin.value() + ] + step_data['animation_duration'] = self.animation_duration_spin.value() + step_data['audio_file'] = self.audio_edit.text() + step_data['hint_text'] = self.hint_text_edit.toPlainText() + step_data['required_tool'] = self.required_tool_combo.currentText() + step_data['score'] = int(self.step_score_spin.value()) # 保存分值 + + # 确保数据更新到list_item的UserRole中 + self._current_step_item.setData(Qt.UserRole, step_data) + + # 更新列表项的显示文本,包含分值信息 + step_name = step_data['name'] + step_score = step_data.get('score', 10) + display_text = f"{step_name} ({step_score}分)" + self._current_step_item.setText(display_text) + + # 同时更新config_data中的对应数据(保持数据一致性) + current_row = self.steps_list.row(self._current_step_item) + if 0 <= current_row < len(self.config_data['steps']): + self.config_data['steps'][current_row] = step_data + + def addTool(self): + """添加工具""" + tool_name = self.tool_name_edit.text().strip() + if not tool_name: + QMessageBox.warning(self, "警告", "请输入工具名称") + return + + existing_items = self.tools_list.findItems(tool_name, Qt.MatchExactly) + if existing_items: + QMessageBox.warning(self, "警告", "工具已存在") + return + + self.tools_list.addItem(tool_name) + self.config_data['tools'].append({'name': tool_name, 'description': ''}) + + # 同时添加到步骤配置的工具下拉框中 + self.required_tool_combo.addItem(tool_name) + + self.tool_name_edit.clear() + + def editTool(self): + """编辑工具""" + current_item = self.tools_list.currentItem() + if not current_item: + QMessageBox.warning(self, "警告", "请选择要编辑的工具") + return + QMessageBox.information(self, "提示", "工具编辑功能待实现") + + def deleteTool(self): + """删除工具""" + current_item = self.tools_list.currentItem() + if not current_item: + QMessageBox.warning(self, "警告", "请选择要删除的工具") + return + + tool_name = current_item.text() + row = self.tools_list.row(current_item) + self.tools_list.takeItem(row) + + self.config_data['tools'] = [ + tool for tool in self.config_data['tools'] + if tool['name'] != tool_name + ] + + # 同时从步骤配置的工具下拉框中移除 + index = self.required_tool_combo.findText(tool_name) + if index >= 0: + self.required_tool_combo.removeItem(index) + + def browseAudioFile(self): + """浏览音频文件""" + file_path, _ = QFileDialog.getOpenFileName( + self, "选择音频文件", "", + "音频文件 (*.wav *.mp3 *.ogg);;所有文件 (*)" + ) + + if file_path: + self.audio_edit.setText(file_path) + + def saveConfiguration(self): + """保存配置""" + file_path, _ = QFileDialog.getSaveFileName( + self, "保存拆装配置", "", + "JSON文件 (*.json);;所有文件 (*)" + ) + + if file_path: + try: + save_data = { + 'models': [ + { + 'name': model['name'], + 'original_pos': list(model['original_pos']), + 'original_hpr': list(model['original_hpr']), + 'node_path': self._getNodePath(model['node']), + 'parent_name': model['node'].getParent().getName() if model['node'].getParent() else None, + 'node_type': type(model['node']).__name__ + } for model in self.config_data['models'] + ], + 'steps': self.config_data['steps'], + 'tools': self.config_data['tools'], + 'settings': { + 'auto_play_audio': self.auto_audio_check.isChecked(), + 'show_hint_text': self.show_hint_check.isChecked(), + 'require_confirmation': self.confirm_check.isChecked() + } + } + + with open(file_path, 'w', encoding='utf-8') as f: + json.dump(save_data, f, ensure_ascii=False, indent=2) + + QMessageBox.information(self, "成功", "配置保存成功") + + except Exception as e: + QMessageBox.critical(self, "错误", f"保存配置失败: {str(e)}") + + def loadConfiguration(self): + """加载配置""" + file_path, _ = QFileDialog.getOpenFileName( + self, "加载拆装配置", "", + "JSON文件 (*.json);;所有文件 (*)" + ) + + if file_path: + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + + self.loadConfigurationData(data) + QMessageBox.information(self, "成功", "配置加载成功") + + except Exception as e: + QMessageBox.critical(self, "错误", f"加载配置失败: {str(e)}") + + def loadConfigurationData(self, data): + """加载配置数据到界面""" + # 清空现有配置 + self.config_models_list.clear() + self.target_model_combo.clear() + self.steps_list.clear() + + # 清空配置数据中的模型信息 + self.config_data['models'] = [] + + # 重置当前编辑的步骤项 + self._current_step_item = None + + # 加载模型配置 + self._loadModelConfiguration(data.get('models', [])) + + # 加载步骤配置 + self.config_data['steps'] = data.get('steps', []) + for step_data in self.config_data['steps']: + step_name = step_data['name'] + step_score = step_data.get('score', 10) + display_text = f"{step_name} ({step_score}分)" + list_item = QListWidgetItem(display_text) + list_item.setData(Qt.UserRole, step_data) + self.steps_list.addItem(list_item) + + # 加载工具配置 + self.tools_list.clear() + self.required_tool_combo.clear() + self.required_tool_combo.addItem("无") + + self.config_data['tools'] = data.get('tools', []) + for tool in self.config_data['tools']: + self.tools_list.addItem(tool['name']) + self.required_tool_combo.addItem(tool['name']) + + # 加载全局设置 + settings = data.get('settings', {}) + self.auto_audio_check.setChecked(settings.get('auto_play_audio', True)) + self.show_hint_check.setChecked(settings.get('show_hint_text', True)) + self.confirm_check.setChecked(settings.get('require_confirmation', False)) + + def _loadModelConfiguration(self, saved_models): + """加载模型配置,通过路径精确匹配场景中的模型节点""" + if not saved_models or not self.world: + return + + # 遍历保存的模型配置 + for saved_model in saved_models: + model_name = saved_model.get('name', '') + node_path = saved_model.get('node_path', '') + parent_name = saved_model.get('parent_name', '') + + scene_node = None + + # 方法1: 优先使用路径匹配(最精确) + if node_path: + scene_node = self._findNodeByPath(self.world.render, node_path) + if scene_node: + print(f"通过路径找到模型: {model_name} -> {node_path}") + + # 方法2: 如果路径匹配失败,尝试名称+父节点匹配 + if not scene_node and parent_name: + scene_node = self._findNodeByNameAndParent(self.world.render, model_name, parent_name) + if scene_node: + print(f"通过名称+父节点找到模型: {model_name} (父节点: {parent_name})") + + # 方法3: 最后尝试简单的名称匹配 + if not scene_node: + scene_model_map = {} + self._buildSceneModelMap(self.world.render, scene_model_map) + if model_name in scene_model_map: + scene_node = scene_model_map[model_name] + print(f"通过名称找到模型: {model_name}") + + # 如果找到了对应的节点,添加到配置中 + if scene_node: + # 添加到已配置模型列表 + list_item = QListWidgetItem(model_name) + list_item.setData(Qt.UserRole, scene_node) + self.config_models_list.addItem(list_item) + + # 添加到目标模型下拉框 + self.target_model_combo.addItem(model_name) + + # 更新config_data中的模型信息,保留原始位置信息 + original_pos = saved_model.get('original_pos', [0, 0, 0]) + original_hpr = saved_model.get('original_hpr', [0, 0, 0]) + + self.config_data['models'].append({ + 'name': model_name, + 'node': scene_node, + 'original_pos': tuple(original_pos), + 'original_hpr': tuple(original_hpr) + }) + else: + # 如果场景中找不到对应的模型,给出详细提示 + print(f"警告: 无法在场景中找到模型 '{model_name}'") + if node_path: + print(f" - 尝试的路径: {node_path}") + if parent_name: + print(f" - 预期父节点: {parent_name}") + print(f" - 请检查场景结构是否发生变化") + + def _buildSceneModelMap(self, node, model_map): + """递归构建场景模型名称到节点的映射""" + if node.getName() in ['render', 'camera', 'aspect2d', 'pixel2d']: + # 跳过系统节点,但遍历其子节点 + for child in node.getChildren(): + self._buildSceneModelMap(child, model_map) + return + + # 将当前节点添加到映射中 + model_name = node.getName() + model_map[model_name] = node + + # 递归处理子节点 + for child in node.getChildren(): + self._buildSceneModelMap(child, model_map) + + def _getNodePath(self, node): + """获取节点的完整路径,用于精确定位""" + if not node: + return "" + + path_parts = [] + current_node = node + + # 从当前节点向上遍历到根节点,构建路径 + while current_node and current_node.getName() != 'render': + path_parts.append(current_node.getName()) + current_node = current_node.getParent() + + # 反转路径,从根到叶 + path_parts.reverse() + return '/'.join(path_parts) if path_parts else node.getName() + + def _findNodeByPath(self, root_node, node_path): + """根据路径在场景中查找节点""" + if not node_path: + return None + + # 分割路径 + path_parts = node_path.split('/') + current_node = root_node + + # 沿着路径查找 + for part in path_parts: + found = False + for child in current_node.getChildren(): + if child.getName() == part: + current_node = child + found = True + break + + if not found: + return None + + return current_node + + def _findNodeByNameAndParent(self, root_node, node_name, parent_name): + """根据节点名称和父节点名称查找节点""" + def search_recursive(node): + # 检查当前节点的所有子节点 + for child in node.getChildren(): + # 如果找到匹配的节点名称和父节点名称 + if (child.getName() == node_name and + child.getParent() and + child.getParent().getName() == parent_name): + return child + + # 递归搜索子节点 + result = search_recursive(child) + if result: + return result + + return None + + return search_recursive(root_node) + + def previewConfiguration(self): + """预览配置效果""" + QMessageBox.information(self, "提示", "预览功能待实现") + + def applyConfiguration(self): + """应用配置""" + QMessageBox.information(self, "提示", "应用配置功能待实现") \ No newline at end of file diff --git a/ui/icon_manager.py b/ui/icon_manager.py index 944c3aa6..5d07611d 100644 --- a/ui/icon_manager.py +++ b/ui/icon_manager.py @@ -52,6 +52,26 @@ class IconManager: 'warning': 'warning.png', 'error': 'error.png', 'info': 'info.png', + + 'up_arrow': 'up_arrows.png', + 'down_arrow': 'down_arrows.png', + 'left_arrow': 'left_arrows.png', + 'right_arrow': 'right_arrows.png', + + 'solid_down_arrows': 'solid_down_arrows.png', + 'solid_right_arrows': 'solid_right_arrows.png', + + 'minimize_icon': 'minimize_icon.png', + 'windowing_icon': 'windowing_icon.png', + 'close_icon': 'close_icon.png', + + # 弹窗图标 + 'success_icon': 'success_icon.png', + 'warning_icon': 'warning_icon.png', + 'fail_icon': 'delete_fail_icon.png', + + # 属性面板图标 + 'property_select_image': 'property_select_image.png', } # 初始化默认图标 diff --git a/ui/interface_manager.py b/ui/interface_manager.py index e5e0c626..474f0c37 100644 --- a/ui/interface_manager.py +++ b/ui/interface_manager.py @@ -1,5 +1,5 @@ -from PIL.ImageChops import lighter from PyQt5.QtWidgets import QTreeWidget, QTreeWidgetItem, QMenu, QStyle +from PyQt5.QtGui import QFont from PyQt5.QtCore import Qt from PyQt5.sip import delete from panda3d.core import GeomNode, ModelRoot @@ -51,7 +51,7 @@ class InterfaceManager: def onTreeItemClicked(self, item, column): """处理树形控件项目点击事件""" - print(f"树形控件点击事件触发,item: {item}, column: {column}") + #print(f"树形控件点击事件触发,item: {item}, column: {column}") # 检查是否点击了空白区域 # 当点击空白区域时,item可能是一个空的QTreeWidgetItem对象 @@ -75,84 +75,6 @@ class InterfaceManager: self.world.property_panel.clearPropertyPanel() print("点击了无数据项,清除选中状态") - # def showTreeContextMenu(self, position): - # """显示树形控件的右键菜单""" - # item = self.treeWidget.itemAt(position) - # if not item: - # return - # - # # 获取节点对象 - # nodePath = item.data(0, Qt.UserRole) - # if not nodePath: - # return - # - # # 创建菜单 - # menu = QMenu() - # - # # 检查是否是GUI元素 - # if hasattr(nodePath, 'getTag') and nodePath.getTag("gui_type"): - # # GUI元素菜单 - # editAction = menu.addAction("编辑") - # editAction.triggered.connect(lambda: self.world.gui_manager.editGUIElementDialog(nodePath)) - # - # deleteAction = menu.addAction("删除GUI元素") - # deleteAction.triggered.connect(lambda: self.world.gui_manager.deleteGUIElement(nodePath)) - # - # duplicateAction = menu.addAction("复制") - # duplicateAction.triggered.connect(lambda: self.world.gui_manager.duplicateGUIElement(nodePath)) - # - # elif hasattr(nodePath,'getTag') and nodePath.getTag("element_type") == "cesium_tileset": - # deleteAction = menu.addAction("删除 Cesium Tileset") - # deleteAction.triggered.connect(lambda:self.deleteCesiumTileset(nodePath,item)) - # - # else: - # # 为模型节点或其子节点添加删除选项 - # parentItem = item.parent() - # if parentItem: - # if self.isModelOrChild(item): - # deleteAction = menu.addAction("删除") - # deleteAction.triggered.connect(lambda: self.deleteNode(nodePath, item)) - # else: - # deleteAction = menu.addAction("删除") - # deleteAction.triggered.connect(lambda: self.deleteNode(nodePath, item)) - # - # # 显示菜单 - # menu.exec_(self.treeWidget.viewport().mapToGlobal(position)) - # - # def deleteCesiumTileset(self, nodePath, item): - # """删除 Cesium tileset""" - # try: - # # 从场景中移除 - # nodePath.removeNode() - # - # # 从 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'] == nodePath: - # tilesets_to_remove.append(i) - # - # # 从后往前删除,避免索引问题 - # for i in reversed(tilesets_to_remove): - # del self.world.scene_manager.tilesets[i] - # - # # 从树形控件中移除 - # parentItem = item.parent() - # if parentItem: - # parentItem.removeChild(item) - # - # print(f"成功删除 Cesium tileset: {nodePath.getName()}") - # - # # 清空属性面板和选择框 - # self.world.property_panel.clearPropertyPanel() - # self.world.selection.updateSelection(None) - # - # # 更新场景树 - # self.updateSceneTree() - # - # except Exception as e: - # print(f"删除 Cesium tileset 失败: {str(e)}") - def isModelOrChild(self, item): """检查是否是模型节点或其子节点""" while item and item.parent(): @@ -292,6 +214,8 @@ class InterfaceManager: sceneRoot = QTreeWidgetItem(self.treeWidget, ['场景']) sceneRoot.setData(0, Qt.UserRole, self.world.render) sceneRoot.setData(0, Qt.UserRole + 1, "SCENE_ROOT") + font = QFont("Microsoft YaHei", 10, QFont.Light) # 假设您希望字体大小为10 + sceneRoot.setFont(0, font) # 添加相机节点 cameraItem = QTreeWidgetItem(sceneRoot, ['相机']) cameraItem.setData(0, Qt.UserRole, self.world.cam) @@ -339,6 +263,23 @@ class InterfaceManager: groundItem.setData(0, Qt.UserRole, self.world.ground) groundItem.setData(0,Qt.UserRole + 1, "SCENE_NODE") + ground_nodes = [ + ('ground2','地板2'), + ('ground3','地板3'), + ('ground4','地板4'), + ('ground5','地板5'), + ('ground6','地板6') + ] + + for attr_name,display_name in ground_nodes: + if hasattr(self.world,attr_name): + ground_node = getattr(self.world,attr_name) + if ground_node: + extraGroundItem = QTreeWidgetItem(sceneRoot,[display_name]) + extraGroundItem.setData(0,Qt.UserRole,ground_node) + extraGroundItem.setData(0,Qt.UserRole+1,"SCENE_NODE") + + #添加灯光节点 for light in self.world.Spotlight: if light: diff --git a/ui/main_window.py b/ui/main_window.py index 4fb010b5..5f520077 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -1,4 +1,4 @@ -""" +""" 主窗口设置模块 负责主窗口的界面构建和事件绑定: @@ -10,19 +10,352 @@ import os import sys from PyQt5.QtGui import QKeySequence, QIcon, QPalette, QColor -from PyQt5.QtWebEngineWidgets import QWebEngineView +# 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) + QSpinBox, QFrame, QRadioButton, QTextEdit, QCheckBox,QTabWidget, QSizePolicy) from PyQt5.QtCore import Qt, QDir, QTimer, QSize, QPoint, QUrl, QRect from direct.showbase.ShowBaseGlobal import aspect2d from panda3d.core import OrthographicLens -from ui.widgets import CustomPanda3DWidget, CustomFileView, CustomTreeWidget,CustomAssetsTreeWidget, CustomConsoleDockWidget +from ui.widgets import (CustomPanda3DWidget, CustomFileView, CustomTreeWidget, + CustomAssetsTreeWidget, CustomConsoleDockWidget, + UniversalMessageDialog) from ui.icon_manager import get_icon_manager, get_icon + + +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 @@ -49,106 +382,134 @@ class MainWindow(QMainWindow): self.setStyleSheet(""" QMainWindow { - background-color: #1e1e2e; + background-color: #000000; } QMenuBar { - background-color: #252538; - color: #e0e0ff; - border-bottom: 1px solid #3a3a4a; + 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: 4px 8px; + padding: 6px 12px; + color: #D4D4D4; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 12px; } QMenuBar::item:selected { - background-color: rgba(139, 92, 246, 100); + background-color: rgba(48, 103, 192, 0.4); } QMenuBar::item:pressed { - background-color: rgba(139, 92, 246, 150); + background-color: rgba(48, 103, 192, 0.6); } QMenu { - background-color: #2d2d44; - color: #e0e0ff; - border: 1px solid #3a3a4a; + background-color: #2E3035; + color: #ebebeb; + border: 1px solid #4c5c6e; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 12px; } QMenu::item { - padding: 4px 20px; + padding: 6px 20px; } QMenu::item:selected { - background-color: rgba(139, 92, 246, 100); + background-color: rgba(48, 103, 192, 0.4); } QPushButton { - background-color: #8b5cf6; - color: white; + background-color: rgba(89, 98, 118, 0.4); + color: #ebebeb; border: none; padding: 6px 12px; - border-radius: 4px; - font-weight: 500; + border-radius: 2px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-weight: 300; + font-size: 10px; } QPushButton:hover { - background-color: #7c3aed; + background-color: #3067c0; + color: #ffffff; } QPushButton:pressed { - background-color: #6d28d9; + background-color: #2556a0; } QPushButton:disabled { - background-color: #4c4c6e; - color: #8888aa; + background-color: #394560; + color: rgba(235, 235, 235, 0.5); } QComboBox { - background-color: #2d2d44; - color: #e0e0ff; - border: 1px solid #3a3a4a; - border-radius: 4px; + 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: #2d2d44; - color: #e0e0ff; - selection-background-color: rgba(139, 92, 246, 100); + background-color: #596471; + color: #ebebeb; + selection-background-color: rgba(48, 103, 192, 0.4); } QLineEdit { - background-color: #2d2d44; - color: #e0e0ff; - border: 1px solid #3a3a4a; - border-radius: 4px; + 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: #2d2d44; - color: #e0e0ff; - border: 1px solid #3a3a4a; - border-radius: 4px; + 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: #252538; - width: 15px; + background-color: #19191b; + width: 12px; border: none; } QScrollBar::handle:vertical { - background-color: #3a3a4a; - border-radius: 4px; + background-color: #596471; + border-radius: 2px; min-height: 20px; } QScrollBar::handle:vertical:hover { - background-color: #8b5cf6; + background-color: #3067c0; } QScrollBar:horizontal { - background-color: #252538; - width: 15px; + background-color: #19191b; + height: 12px; border: none; } QScrollBar::handle:horizontal { - background-color: #3a3a4a; - border-radius: 4px; - min-height: 20px; + background-color: #596471; + border-radius: 2px; + min-width: 20px; } QScrollBar::handle:horizontal:hover { - background-color: #8b5cf6; + background-color: #3067c0; } """) # 设置 QMessageBox 样式表 @@ -177,31 +538,37 @@ class MainWindow(QMainWindow): # 设置 QMessageBox 的样式表 msg_box_style = """ QMessageBox { - background-color: #252538; - color: #e0e0ff; - border: 1px solid #3a3a4a; + background-color: #19191b; + color: #ebebeb; + border: 1px solid #4c5c6e; } QMessageBox QLabel { - color: #e0e0ff; + color: #ebebeb; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 12px; + font-weight: 300; } QMessageBox QPushButton { - background-color: #8b5cf6; - color: white; + background-color: rgba(89, 98, 118, 0.4); + color: #ebebeb; border: none; padding: 6px 12px; - border-radius: 4px; - font-weight: 500; + border-radius: 2px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-weight: 300; + font-size: 10px; min-width: 80px; } QMessageBox QPushButton:hover { - background-color: #7c3aed; + background-color: #3067c0; + color: #ffffff; } QMessageBox QPushButton:pressed { - background-color: #6d28d9; + background-color: #2556a0; } QMessageBox QPushButton:disabled { - background-color: #4c4c6e; - color: #8888aa; + background-color: #394560; + color: rgba(235, 235, 235, 0.5); } """ @@ -219,38 +586,45 @@ class MainWindow(QMainWindow): # 设置样式表 dialog.setStyleSheet(""" QInputDialog { - background-color: #252538; - color: #e0e0ff; + background-color: #19191b; + color: #ebebeb; } QLabel { - color: #e0e0ff; + color: #ffffff; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-weight: 500; + font-size: 14px; } QLineEdit { - background-color: #2d2d44; - color: #e0e0ff; - border: 1px solid #3a3a4a; - border-radius: 4px; + 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: #8b5cf6; - color: white; + background-color: rgba(89, 98, 118, 0.4); + color: #ebebeb; border: none; padding: 6px 12px; - border-radius: 4px; - font-weight: 500; + border-radius: 2px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-weight: 300; + font-size: 10px; min-width: 80px; } QPushButton:hover { - background-color: #7c3aed; + background-color: #3067c0; + color: #ffffff; } QPushButton:pressed { - background-color: #6d28d9; + background-color: #2556a0; } QPushButton:disabled { - background-color: #4c4c6e; - color: #8888aa; + background-color: #394560; + color: rgba(235, 235, 235, 0.5); } """) @@ -264,77 +638,91 @@ class MainWindow(QMainWindow): # 设置样式表 dialog.setStyleSheet(""" QFileDialog { - background-color: #252538; - color: #e0e0ff; + background-color: #19191b; + color: #ebebeb; } QLabel { - color: #e0e0ff; + color: #ebebeb; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 10px; + font-weight: 300; } QListView { - background-color: #1e1e2e; - color: #e0e0ff; - border: 1px solid #3a3a4a; - alternate-background-color: #252538; + 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: #3a3a4a; + background-color: #394560; } QListView::item:selected { - background-color: rgba(139, 92, 246, 100); - color: white; + background-color: rgba(48, 103, 192, 0.4); + color: #ffffff; } QTreeView { - background-color: #1e1e2e; - color: #e0e0ff; - border: 1px solid #3a3a4a; - alternate-background-color: #252538; + 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: #3a3a4a; + background-color: #394560; } QTreeView::item:selected { - background-color: rgba(139, 92, 246, 100); - color: white; + background-color: rgba(48, 103, 192, 0.4); + color: #ffffff; } QComboBox { - background-color: #2d2d44; - color: #e0e0ff; - border: 1px solid #3a3a4a; - border-radius: 4px; + 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: #2d2d44; - color: #e0e0ff; - selection-background-color: rgba(139, 92, 246, 100); + background-color: #596471; + color: #ebebeb; + selection-background-color: rgba(48, 103, 192, 0.4); } QPushButton { - background-color: #8b5cf6; - color: white; + background-color: rgba(89, 98, 118, 0.4); + color: #ebebeb; border: none; padding: 6px 12px; - border-radius: 4px; - font-weight: 500; + border-radius: 2px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-weight: 300; + font-size: 10px; } QPushButton:hover { - background-color: #7c3aed; + background-color: #3067c0; + color: #ffffff; } QPushButton:pressed { - background-color: #6d28d9; + background-color: #2556a0; } QPushButton:disabled { - background-color: #4c4c6e; - color: #8888aa; + background-color: #394560; + color: rgba(235, 235, 235, 0.5); } QLineEdit { - background-color: #2d2d44; - color: #e0e0ff; - border: 1px solid #3a3a4a; - border-radius: 4px; + 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; } """) @@ -412,49 +800,53 @@ class MainWindow(QMainWindow): # """) self.embeddedToolbar.setStyleSheet(""" QFrame#UnityToolbar { - background-color: rgba(40, 40, 60, 200); /* 深蓝灰色背景 */ - border: 1px solid rgba(80, 80, 120, 200); /* 深蓝灰边框 */ - border-radius: 6px; - padding: 4px; + background-color: transparent; + border: none; + padding: 0px; } QToolButton { - background-color: rgba(60, 60, 90, 180); /* 稍亮的深蓝灰 */ - border: 1px solid rgba(100, 100, 150, 150); - color: #e0e0ff; /* 浅蓝白色文字 */ - padding: 6px 8px; - border-radius: 4px; - font-weight: 500; + 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(80, 80, 130, 200); /* 悬停时更亮 */ - border: 1px solid rgba(139, 92, 246, 150); /* 紫色边框 */ + background-color: rgba(89, 100, 113, 0.3); + border: 1px solid rgba(184, 211, 241, 0.3); } QToolButton:checked { - background-color: rgba(139, 92, 246, 180); /* 紫色背景 */ - color: white; - border: 1px solid rgba(180, 140, 255, 200); + background-color: #3067c0; + color: #ffffff; + border: 1px solid rgba(48, 103, 192, 0.6); } QToolButton:pressed { - background-color: rgba(120, 75, 220, 200); /* 按下时稍暗的紫色 */ + background-color: #2556a0; } """) # 水平布局 self.toolbarLayout = QHBoxLayout(self.embeddedToolbar) - self.toolbarLayout.setContentsMargins(5, 5, 5, 5) - self.toolbarLayout.setSpacing(2) + self.toolbarLayout.setContentsMargins(0, 0, 0, 0) + self.toolbarLayout.setSpacing(4) # 创建工具按钮组 self.toolGroup = QButtonGroup() # 选择工具 self.selectTool = QToolButton() - select_icon = get_icon('select_tool', QSize(16, 16)) + select_icon = get_icon('select_tool', QSize(20, 20)) if not select_icon.isNull(): self.selectTool.setIcon(select_icon) - else: - self.selectTool.setText('选择') # 如果没有图标则显示文字 - self.selectTool.setIconSize(QSize(16, 16)) + self.selectTool.setText('选择') # 如果没有图标则显示文字 + self.selectTool.setIconSize(QSize(20, 20)) self.selectTool.setCheckable(True) self.selectTool.setToolTip("选择工具 (Q)") # self.selectTool.setShortcut(QKeySequence("Q")) @@ -467,7 +859,7 @@ class MainWindow(QMainWindow): if icon_path and os.path.exists(icon_path): self.moveTool.setIcon(QIcon(icon_path)) self.moveTool.setText("移动") - self.moveTool.setIconSize(QSize(16, 16)) + self.moveTool.setIconSize(QSize(20, 20)) self.moveTool.setCheckable(True) self.moveTool.setToolTip("移动工具 (W)") # self.moveTool.setShortcut(QKeySequence("W")) @@ -476,11 +868,11 @@ class MainWindow(QMainWindow): # 旋转工具 self.rotateTool = QToolButton() - rotate_icon = get_icon('rotate_tool', QSize(16, 16)) + 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(16, 16)) + self.rotateTool.setIconSize(QSize(20, 20)) self.rotateTool.setCheckable(True) self.rotateTool.setToolTip("旋转工具 (E)") # self.rotateTool.setShortcut(QKeySequence("E")) @@ -489,11 +881,11 @@ class MainWindow(QMainWindow): # 缩放工具 self.scaleTool = QToolButton() - scale_icon = get_icon('scale_tool', QSize(16, 16)) + 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(16, 16)) + self.scaleTool.setIconSize(QSize(20, 20)) self.scaleTool.setCheckable(True) self.scaleTool.setToolTip("缩放工具 (R)") # self.scaleTool.setShortcut(QKeySequence("R")) @@ -501,12 +893,12 @@ class MainWindow(QMainWindow): 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) + # 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) @@ -714,6 +1106,17 @@ class MainWindow(QMainWindow): self.scriptMenu.addSeparator() self.openScriptsManagerAction = self.scriptMenu.addAction('脚本管理器') + # 交互菜单 + self.interactionMenu = menubar.addMenu('交互') + self.assemblyDisassemblyConfigAction = self.interactionMenu.addAction('拆装配置') + self.assemblyDisassemblyConfigAction.triggered.connect(self.onOpenAssemblyDisassemblyConfig) + self.interactionMenu.addSeparator() + self.startAssemblyInteractionAction = self.interactionMenu.addAction('开始拆装交互') + self.startAssemblyInteractionAction.triggered.connect(self.onStartAssemblyInteraction) + self.interactionMenu.addSeparator() + self.maintenanceSystemAction = self.interactionMenu.addAction('🔧 维修系统') + self.maintenanceSystemAction.triggered.connect(self.onOpenMaintenanceSystem) + self.cesiumMenu = menubar.addMenu('Cesium') self.loadCesiumTilesetAction = self.cesiumMenu.addAction('加载3Dtiles') self.loadCesiumTilesetAction.triggered.connect(self.onLoadCesiumTileset) @@ -735,16 +1138,7 @@ class MainWindow(QMainWindow): self.infoPanelMenu = menubar.addMenu('信息面板') # 创建示例面板动作 self.createSamplePanelAction = self.infoPanelMenu.addAction('创建示例面板') - self.createSamplePanelAction.triggered.connect(self.onCreateSampleInfoPanel) - # 添加更多面板创建选项 - # self.createSystemStatusPanelAction = self.infoPanelMenu.addAction('创建系统状态面板') - # self.createSystemStatusPanelAction.triggered.connect(self.onCreateSystemStatusPanel) - # - # self.createSensorDataPanelAction = self.infoPanelMenu.addAction('创建传感器数据面板') - # self.createSensorDataPanelAction.triggered.connect(self.onCreateSensorDataPanel) - # - # self.createSceneInfoPanelAction = self.infoPanelMenu.addAction('创建场景信息面板') - # self.createSceneInfoPanelAction.triggered.connect(self.onCreateSceneInfoPanel) + self.createSamplePanelAction.triggered.connect(self.world.info_panel_manager.onCreateSampleInfoPanel) self.infoPanelMenu.addSeparator() self.create3DSamplePanelAction = self.infoPanelMenu.addAction('创建3D实例面板') @@ -752,14 +1146,6 @@ class MainWindow(QMainWindow): # 添加网页浏览器菜单项 self.webBrowserAction = self.infoPanelMenu.addAction("信息面板") self.webBrowserAction.triggered.connect(self.openWebBrowser) - # - # self.create3DSystemStatusPanelAction = self.infoPanelMenu.addAction('创建3D系统状态面板') - # self.create3DSystemStatusPanelAction.triggered.connect(self.onCreate3DSystemStatusPanel) - - # 添加分隔符和批量创建选项 - # self.infoPanelMenu.addSeparator() - # self.createAllPanelsAction = self.infoPanelMenu.addAction('创建所有面板') - # self.createAllPanelsAction.triggered.connect(self.onCreateAllInfoPanels) @@ -768,6 +1154,105 @@ class MainWindow(QMainWindow): self.refreshAssetsAction = self.assetsMenu.addAction('刷新资源') self.refreshAssetsAction.triggered.connect(self.refreshAssetsView) + # VR菜单 + self.vrMenu = menubar.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.helpMenu = menubar.addMenu('帮助') self.aboutAction = self.helpMenu.addAction('关于') @@ -817,11 +1302,11 @@ class MainWindow(QMainWindow): """设置视图菜单动作""" # 连接视图菜单事件 self.viewPerspectiveAction.triggered.connect(self.onViewPerspective) - self.viewTopAction.triggered.connect(self.onViewTop) - self.viewFrontAction.triggered.connect(self.onViewFront) + #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.viewOrthographicAction.triggered.connect(self.onViewOrthographic) + #self.viewGridAction.triggered.connect(self.onViewGrid) # 添加网格显示的信号连接 # 保存原始相机设置 self._original_camera_fov = 80 @@ -1094,115 +1579,395 @@ class MainWindow(QMainWindow): self.leftDock = QDockWidget("层级", self) self.leftDock.setStyleSheet(""" QDockWidget { - background-color: #252538; - color: #e0e0ff; - border: 1px solid #3a3a4a; + background-color: #19191b; + color: #ffffff; + border: none; } QDockWidget::title { - background-color: #2d2d44; - padding: 0px 0px; /* 增加内边距,提供更多的垂直空间 */ - border-bottom: 0px solid #3a3a4a; + 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: #8b5cf6; - border: none; - icon-size: 8px; /* 调整图标大小 */ - border-radius: 4px; /* 增加圆角 */ - right: 5px; - top: 2px; + 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: #8b5cf6; - border: none; - icon-size: 8px; /* 调整图标大小 */ - border-radius: 4px; /* 增加圆角 */ - right: 25px; - top: 2px; + 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: #7c3aed; /* 悬停时显示较亮的背景 */ + 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: #8b5cf6; /* 按下时显示紫色高亮 */ + background-color: #3067c0; + border: 1px solid rgba(48, 103, 192, 0.6); } QTreeView { - background-color: #1e1e2e; - color: #e0e0ff; - border: 1px solid #3a3a4a; - alternate-background-color: #252538; + 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: #3a3a4a; + background-color: rgba(89, 100, 113, 0.3); } QTreeView::item:selected { - background-color: rgba(139, 92, 246, 100); - color: white; + 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) # 设置树形控件引用 - self.leftDock.setWidget(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: #252538; - color: #e0e0ff; - border: 1px solid #3a3a4a; + background-color: #19191b; + color: #ffffff; + border: none; } QDockWidget::title { - background-color: #2d2d44; - padding: 0px 0px; /* 增加内边距,提供更多的垂直空间 */ - border-bottom: 0px solid #3a3a4a; + 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: #8b5cf6; - border: none; - icon-size: 8px; /* 调整图标大小 */ - border-radius: 4px; /* 增加圆角 */ - right: 5px; - top: 2px; + 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: #8b5cf6; - border: none; - icon-size: 8px; /* 调整图标大小 */ - border-radius: 4px; /* 增加圆角 */ - right: 25px; - top: 2px; + 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: #7c3aed; /* 悬停时显示较亮的背景 */ + 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: #8b5cf6; /* 按下时显示紫色高亮 */ + background-color: #3067c0; + border: 1px solid rgba(48, 103, 192, 0.6); } QScrollArea { - background-color: #1e1e2e; + background-color: #19191b; border: none; } QWidget#PropertyContainer { - background-color: #1e1e2e; + background-color: #19191b; } QLabel { - color: #e0e0ff; + 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 { - background-color: #252538; - border: 1px solid #3a3a4a; - border-radius: 6px; - margin-top: 1ex; - color: #e0e0ff; - font-weight: 500; - padding-top: 10px; /* 增加顶部内边距,使标题和内容分离 */ + /* 减少上边距,使相邻组更紧凑 */ + 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 { - padding: 0 8px; - color: #c0c0e0; + /* 将标题定位到控件内部,在样条线下方 */ + 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; + } """) # 创建属性面板的主容器和布局 @@ -1211,11 +1976,14 @@ class MainWindow(QMainWindow): 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;") # 使用灰色字体 + tipLabel.setStyleSheet("color: gray; padding: 10px 0px;") # 使用灰色字体 self.propertyLayout.addWidget(tipLabel) print("✓ 提示标签添加完成") @@ -1225,8 +1993,31 @@ class MainWindow(QMainWindow): self.scrollArea.setWidget(self.propertyContainer) print("✓ 滚动区域设置完成") - # 设置滚动区域为停靠窗口的主部件 - self.rightDock.setWidget(self.scrollArea) + # 创建包装容器,添加标题下方的分隔线 + 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("✓ 右侧停靠窗口添加完成") @@ -1254,61 +2045,108 @@ class MainWindow(QMainWindow): self.scriptDock = QDockWidget("脚本管理", self) self.scriptDock.setStyleSheet(""" QDockWidget { - background-color: #252538; - color: #e0e0ff; - border: 1px solid #3a3a4a; + background-color: #19191b; + color: #ffffff; + border: none; } QDockWidget::title { - background-color: #2d2d44; - padding: 0px 0px; /* 增加内边距,提供更多的垂直空间 */ - border-bottom: 0px solid #3a3a4a; + 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: #8b5cf6; - border: none; - icon-size: 8px; /* 调整图标大小 */ - border-radius: 4px; /* 增加圆角 */ - right: 5px; - top: 2px; + 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: #8b5cf6; - border: none; - icon-size: 8px; /* 调整图标大小 */ - border-radius: 4px; /* 增加圆角 */ - right: 25px; - top: 2px; + 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: #7c3aed; /* 悬停时显示较亮的背景 */ + 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: #8b5cf6; /* 按下时显示紫色高亮 */ + background-color: #3067c0; + border: 1px solid rgba(48, 103, 192, 0.6); } QScrollArea { - background-color: #1e1e2e; + background-color: #19191b; border: none; } - QWidget#PropertyContainer { - background-color: #1e1e2e; + QWidget#ScriptContainer { + background-color: #19191b; } QLabel { - color: #e0e0ff; + color: #ebebeb; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 10px; + font-weight: 300; } - QGroupBox { - background-color: #252538; - border: 1px solid #3a3a4a; - border-radius: 6px; - margin-top: 1ex; - color: #e0e0ff; - font-weight: 500; - padding-top: 10px; /* 增加顶部内边距,使标题和内容分离 */ + 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 { - padding: 0 8px; - color: #c0c0e0; + /* 将标题定位到控件内部,在样条线下方 */ + 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; + } """) # 创建脚本面板的主容器和布局(与属性面板相同结构) @@ -1316,19 +2154,45 @@ class MainWindow(QMainWindow): # 设置脚本容器的背景色 self.scriptContainer.setStyleSheet(""" QWidget#ScriptContainer { - background-color: #1e1e2e; + 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) - # 设置滚动区域为停靠窗口的主部件 - self.scriptDock.setWidget(self.scriptScrollArea) + # 创建包装容器,添加标题下方的分隔线 + 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) # 设置脚本面板内容 - 这里添加调用 @@ -1365,36 +2229,46 @@ class MainWindow(QMainWindow): self.bottomDock = QDockWidget("资源", self) self.bottomDock.setStyleSheet(""" QDockWidget { - background-color: #252538; - color: #e0e0ff; - border: 1px solid #3a3a4a; + background-color: #19191b; + color: #ffffff; + border: none; } QDockWidget::title { - background-color: #2d2d44; - padding: 0px 0px; /* 增加内边距,提供更多的垂直空间 */ - border-bottom: 0px solid #3a3a4a; + 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: #8b5cf6; - border: none; - icon-size: 8px; /* 调整图标大小 */ - border-radius: 4px; /* 增加圆角 */ - right: 5px; - top: 2px; + 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: #8b5cf6; - border: none; - icon-size: 8px; /* 调整图标大小 */ - border-radius: 4px; /* 增加圆角 */ - right: 25px; - top: 2px; + 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: #7c3aed; /* 悬停时显示较亮的背景 */ + 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: #8b5cf6; /* 按下时显示紫色高亮 */ + background-color: #3067c0; + border: 1px solid rgba(48, 103, 192, 0.6); } """) @@ -1402,132 +2276,263 @@ class MainWindow(QMainWindow): # 为资源树添加样式 self.fileView.setStyleSheet(""" QTreeWidget { - background-color: #1e1e2e; - color: #e0e0ff; - border: 1px solid #3a3a4a; - alternate-background-color: #252538; + 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: #3a3a4a; + background-color: rgba(89, 100, 113, 0.3); } QTreeWidget::item:selected { - background-color: rgba(139, 92, 246, 100); - color: white; + 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: #2d2d44; - color: #e0e0ff; - border: 1px solid #3a3a4a; - padding: 4px; + 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; } """) - self.bottomDock.setWidget(self.fileView) + + # 创建包装容器,添加标题下方的分隔线 + 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: #252538; - color: #e0e0ff; - border: 1px solid #3a3a4a; + background-color: #19191b; + color: #ffffff; + border: none; } QDockWidget::title { - background-color: #2d2d44; - padding: 0px 0px; /* 增加内边距,提供更多的垂直空间 */ - border-bottom: 0px solid #3a3a4a; + 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: #8b5cf6; - border: none; - icon-size: 8px; /* 调整图标大小 */ - border-radius: 4px; /* 增加圆角 */ - right: 5px; - top: 2px; + 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: #8b5cf6; - border: none; - icon-size: 8px; /* 调整图标大小 */ - border-radius: 4px; /* 增加圆角 */ - right: 25px; - top: 2px; + 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: #7c3aed; /* 悬停时显示较亮的背景 */ + 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: #8b5cf6; /* 按下时显示紫色高亮 */ + background-color: #3067c0; + border: 1px solid rgba(48, 103, 192, 0.6); } """) self.consoleView = CustomConsoleDockWidget(self.world) # 为控制台添加样式 self.consoleView.setStyleSheet(""" QTextEdit { - background-color: #1e1e2e; - color: #e0e0ff; - border: 1px solid #3a3a4a; - font-family: 'Consolas', 'Monaco', monospace; + background-color: #19191b; + color: #ebebeb; + border: none; + font-family: 'Consolas', 'Monaco', 'Microsoft YaHei', monospace; + font-size: 10px; + font-weight: 300; } """) - self.consoleDock.setWidget(self.consoleView) + + # 创建包装容器,添加标题下方的分隔线 + 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.tabifyDockWidget(self.bottomDock, self.consoleDock) - # 设定默认显示的标签 - self.bottomDock.raise_() + + # 设定默认显示的标签(属性面板在前) self.rightDock.raise_() - self.scriptDock.raise_() + + # 确保其他面板也正常显示 + self.bottomDock.raise_() self.consoleDock.raise_() self.leftDock.raise_() # ========================================================================= - # ↓↓↓ 新增代码:为停靠窗口的标签栏(QTabBar)设置统一样式 ↓↓↓ + # ↓↓↓ 为停靠窗口的标签栏(QTabBar)设置统一样式(根据Figma设计)↓↓↓ # ========================================================================= # 这段样式会应用到主窗口内的所有 QTabBar,特别是停靠区域的标签栏。 tab_bar_style = """ /* QTabBar 的整体样式 */ QTabBar { - qproperty-drawBase: 0; /* 移除标签栏底部的线条 */ + qproperty-drawBase: 0; + qproperty-expanding: 0; + background-color: #19191b; + border: none; } + /* 标签的基础样式 - 未选中状态 */ QTabBar::tab { - background-color: #2d2d44; /* 未选中标签的背景色,与标题栏一致 */ - color: #c0c0e0; /* 未选中标签的文字颜色 */ - border: 1px solid #3a3a4a; /* 边框颜色 */ - border-bottom: none; /* 移除底部边框 */ - border-top-left-radius: 6px; - border-top-right-radius: 6px; - padding: 8px 16px; /* 内边距,让文字不拥挤 */ + 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; - margin-right: 2px; /* 标签之间的间距 */ + font-size: 12px; + margin-right: 4px; + min-height: 20px; + max-height: 20px; + min-width: 80px; /* 增加一个最小宽度,根据您的标签文字长度调整 */ } /* 鼠标悬停在标签上时的样式 */ QTabBar::tab:hover { - background-color: #3a3a4a; /* 悬停时使用稍亮的背景色 */ + background-color: #4a5568; } /* 当前选中的标签页的样式 */ QTabBar::tab:selected { - background-color: #252538; /* 选中时使用 Dock 背景色 */ - color: #ffffff; /* 选中时使用更亮的文字颜色 */ - font-weight: bold; /* 字体加粗 */ - border-color: #3a3a4a; - border-bottom: 2px solid #8b5cf6; /* 选中标签的底部高亮线 */ + background-color: #3067c0; + color: #ffffff; + font-weight: 500; } /* 未选中的标签 */ QTabBar::tab:!selected { - margin-top: 2px; /* 未选中标签稍微下移,创建层次感 */ + margin-top: 0px; } - /* 标签栏底部的线条 */ + /* 标签栏的对齐方式 */ QTabBar::tab-bar { alignment: left; - border: none; /* 移除标签栏边框 */ + } + + /* 标签栏的关闭按钮(如果有) */ + QTabBar::close-button { + image: none; + subcontrol-position: right; + } + + /* 移除标签栏的滚动按钮样式冲突 */ + QTabBar::scroller { + width: 0px; } """ @@ -1630,15 +2635,73 @@ class MainWindow(QMainWindow): """创建脚本管理面板""" # 脚本状态组 statusGroup = QGroupBox("脚本系统状态") + + statusGroup.setProperty("groupRole", "first") statusLayout = QVBoxLayout() - self.scriptStatusLabel = QLabel("脚本系统: 已启动") - self.scriptStatusLabel.setStyleSheet("color: green; font-weight: bold;") - statusLayout.addWidget(self.scriptStatusLabel) + # 脚本系统状态行 + 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.hotReloadLabel = QLabel("热重载: 已启用") - self.hotReloadLabel.setStyleSheet("color: blue;") - statusLayout.addWidget(self.hotReloadLabel) + 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) @@ -1649,23 +2712,123 @@ class MainWindow(QMainWindow): # 脚本名称输入 nameLayout = QHBoxLayout() - nameLayout.addWidget(QLabel("脚本名称:")) + 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() - templateLayout.addWidget(QLabel("模板:")) + 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) @@ -1679,27 +2842,35 @@ class MainWindow(QMainWindow): self.scriptsList = QListWidget() self.scriptsList.setStyleSheet(""" QListWidget { - background-color: #1e1e2e; - color: #e0e0ff; - border: 1px solid #3a3a4a; - border-radius: 4px; - alternate-background-color: #252538; - selection-background-color: rgba(139, 92, 246, 100); - selection-color: white; + 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-bottom: 1px solid #2d2d44; + border: none; + border-bottom: 1px solid rgba(76, 92, 110, 0.3); } - QListWidget::item:last-child { + QListWidget::item:last { border-bottom: none; } QListWidget::item:hover { - background-color: #3a3a4a; + background-color: rgba(89, 100, 113, 0.3); } QListWidget::item:selected { - background-color: rgba(139, 92, 246, 120); - color: white; + background-color: rgba(48, 103, 192, 0.4); + color: #ffffff; } """) self.scriptsList.itemDoubleClicked.connect(self.onScriptDoubleClick) @@ -1710,10 +2881,52 @@ class MainWindow(QMainWindow): 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) @@ -1726,7 +2939,16 @@ class MainWindow(QMainWindow): # 当前选中对象显示 self.selectedObjectLabel = QLabel("未选择对象") - self.selectedObjectLabel.setStyleSheet("color: gray; font-style: italic;") + 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) # 脚本选择和挂载 @@ -1734,11 +2956,71 @@ class MainWindow(QMainWindow): 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) @@ -1747,37 +3029,77 @@ class MainWindow(QMainWindow): self.mountedScriptsList = QListWidget() self.mountedScriptsList.setStyleSheet(""" QListWidget { - background-color: #1e1e2e; - color: #e0e0ff; - border: 1px solid #3a3a4a; - border-radius: 4px; - alternate-background-color: #252538; - selection-background-color: rgba(139, 92, 246, 100); - selection-color: white; - max-height: 120px; + 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-bottom: 1px solid #2d2d44; + border: none; + border-bottom: 1px solid rgba(76, 92, 110, 0.3); } - QListWidget::item:last-child { + QListWidget::item:last { border-bottom: none; } QListWidget::item:hover { - background-color: #3a3a4a; + background-color: rgba(89, 100, 113, 0.3); } QListWidget::item:selected { - background-color: rgba(139, 92, 246, 120); - color: white; + background-color: rgba(48, 103, 192, 0.4); + color: #ffffff; } """) self.mountedScriptsList.setMaximumHeight(100) - mountLayout.addWidget(QLabel("已挂载脚本:")) + + 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) @@ -1858,12 +3180,47 @@ class MainWindow(QMainWindow): # 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: - QMessageBox.information(self, "提示", "请先选择要复制的节点") + UniversalMessageDialog.show_warning( + self, + "提示", + "请先选择要复制的节点", + show_cancel=False, + confirm_text="确认" + ) return # 获取选中的节点 @@ -1875,35 +3232,71 @@ class MainWindow(QMainWindow): selected_node = getattr(self.world.selection, 'selectedNode', None) if not selected_node or selected_node.isEmpty(): - QMessageBox.warning(self, "错误", "无法获取选中节点") + UniversalMessageDialog.show_warning( + self, + "错误", + "无法获取选中节点", + show_cancel=False, + confirm_text="确认" + ) return # 检查是否是根节点 if selected_node.getName() == "render": - QMessageBox.warning(self, "错误", "不能复制根节点") + UniversalMessageDialog.show_warning( + self, + "错误", + "不能复制根节点", + show_cancel=False, + confirm_text="确认" + ) return # 序列化节点数据 node_data = self.world.scene_manager.serializeNodeForCopy(selected_node) if not node_data: - QMessageBox.warning(self, "错误", "无法序列化选中节点") + UniversalMessageDialog.show_warning( + self, + "错误", + "无法序列化选中节点", + show_cancel=False, + confirm_text="确认" + ) return # 存储到剪切板 self.clipboard = [node_data] self.clipboard_mode = "copy" - QMessageBox.information(self, "成功", "节点已复制到剪切板") + UniversalMessageDialog.show_success( + self, + "成功", + "节点已复制到剪切板", + show_cancel=False, + confirm_text="确认" + ) except Exception as e: - QMessageBox.critical(self, "错误", f"复制操作失败: {str(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: - QMessageBox.information(self, "提示", "请先选择要剪切的节点") + UniversalMessageDialog.show_warning( + self, + "提示", + "请先选择要剪切的节点", + show_cancel=False, + confirm_text="确认" + ) return # 获取选中的节点 @@ -1915,18 +3308,36 @@ class MainWindow(QMainWindow): selected_node = getattr(self.world.selection, 'selectedNode', None) if not selected_node or selected_node.isEmpty(): - QMessageBox.warning(self, "错误", "无法获取选中节点") + UniversalMessageDialog.show_warning( + self, + "错误", + "无法获取选中节点", + show_cancel=False, + confirm_text="确认" + ) return # 检查是否是根节点或特殊节点 if selected_node.getName() in ["render", "camera", "ambientLight", "directionalLight"]: - QMessageBox.warning(self, "错误", "不能剪切根节点或系统节点") + UniversalMessageDialog.show_warning( + self, + "错误", + "不能剪切根节点或系统节点", + show_cancel=False, + confirm_text="确认" + ) return # 序列化节点数据 node_data = self.world.scene_manager.serializeNodeForCopy(selected_node) if not node_data: - QMessageBox.warning(self, "错误", "无法序列化选中节点") + UniversalMessageDialog.show_warning( + self, + "错误", + "无法序列化选中节点", + show_cancel=False, + confirm_text="确认" + ) return # 存储到剪切板 @@ -1936,16 +3347,34 @@ class MainWindow(QMainWindow): # 删除原节点 self.treeWidget.delete_items([selected_item]) - QMessageBox.information(self, "成功", "节点已剪切到剪切板") + UniversalMessageDialog.show_success( + self, + "成功", + "节点已剪切到剪切板", + show_cancel=False, + confirm_text="确认" + ) except Exception as e: - QMessageBox.critical(self, "错误", f"剪切操作失败: {str(e)}") + UniversalMessageDialog.show_error( + self, + "错误", + f"剪切操作失败: {str(e)}", + show_cancel=False, + confirm_text="确认" + ) def onPaste(self): """粘贴操作""" try: if not self.clipboard: - QMessageBox.information(self, "提示", "剪切板为空") + UniversalMessageDialog.show_warning( + self, + "提示", + "剪切板为空", + show_cancel=False, + confirm_text="确认" + ) return # 获取粘贴目标节点 @@ -1986,13 +3415,25 @@ class MainWindow(QMainWindow): # 检查父节点有效性 if not parent_node or parent_node.isEmpty(): - QMessageBox.warning(self, "错误", "无法获取有效的父节点") + UniversalMessageDialog.show_warning( + self, + "错误", + "无法获取有效的父节点", + show_cancel=False, + confirm_text="确认" + ) return # 检查目标节点是否为允许的父节点类型 parent_name = parent_node.getName() if parent_name in ["camera", "ambientLight", "directionalLight"]: - QMessageBox.warning(self, "错误", "不能粘贴到该类型节点下") + UniversalMessageDialog.show_warning( + self, + "错误", + "不能粘贴到该类型节点下", + show_cancel=False, + confirm_text="确认" + ) return # 粘贴节点 @@ -2011,10 +3452,22 @@ class MainWindow(QMainWindow): self.clipboard.clear() self.clipboard_mode = None - QMessageBox.information(self, "成功", f"已粘贴 {len(pasted_nodes)} 个节点") + UniversalMessageDialog.show_success( + self, + "成功", + f"已粘贴 {len(pasted_nodes)} 个节点", + show_cancel=False, + confirm_text="确认" + ) except Exception as e: - QMessageBox.critical(self, "错误", f"粘贴操作失败: {str(e)}") + UniversalMessageDialog.show_error( + self, + "错误", + f"粘贴操作失败: {str(e)}", + show_cancel=False, + confirm_text="确认" + ) def _serializeNode(self, node): """序列化节点数据""" @@ -2134,12 +3587,73 @@ class MainWindow(QMainWindow): # 添加撤销/重做功能的基础实现 def onUndo(self): """撤销操作""" - QMessageBox.information(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): """重做操作""" - QMessageBox.information(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: @@ -2190,15 +3704,16 @@ class MainWindow(QMainWindow): break if not cesium_view_exists: - reply = QMessageBox.question( + result = UniversalMessageDialog.show_info( self, - '提示', - 'Cesium 地图视图尚未打开,是否先打开地图视图?', - QMessageBox.Yes | QMessageBox.No, - QMessageBox.Yes + "提示", + "Cesium 地图视图尚未打开,是否先打开地图视图?", + show_cancel=True, + confirm_text="打开视图", + cancel_text="取消" ) - if reply == QMessageBox.Yes: + if result == QDialog.Accepted: self.onCreateCesiumView() # 给一点时间让 Cesium 视图加载 QTimer.singleShot(1000, self.showAddModelDialog) @@ -2243,22 +3758,28 @@ class MainWindow(QMainWindow): ) if success: - QMessageBox.information( + UniversalMessageDialog.show_success( self, "成功", - f"模型已成功添加到地图!\n模型ID: {model_id}" + f"模型已成功添加到地图!\n模型ID: {model_id}", + show_cancel=False, + confirm_text="确认" ) else: - QMessageBox.warning( + UniversalMessageDialog.show_warning( self, "失败", - "添加模型失败,请检查控制台输出" + "添加模型失败,请检查控制台输出", + show_cancel=False, + confirm_text="确认" ) except Exception as e: - QMessageBox.critical( + UniversalMessageDialog.show_error( self, "错误", - f"添加模型时发生错误:\n{str(e)}" + f"添加模型时发生错误:\n{str(e)}", + show_cancel=False, + confirm_text="确认" ) @@ -2333,43 +3854,103 @@ class MainWindow(QMainWindow): return None, False def onLoadCesiumTileset(self): - dialog = self.createStyledInputDialog( - self, - "加载 Cesium 3D Tiles", - "输入 tileset.json URL:", - QLineEdit.Normal, - "https://assets.ion.cesium.com/96128/tileset.json" - ) + """加载 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.textValue() - if url: - try: - # 生成唯一的 tileset 名称 - import uuid - tileset_name = f"tileset_{uuid.uuid4().hex[:8]}" + url = dialog.get_value("url") + if not url or not url.strip(): + UniversalMessageDialog.show_warning( + self, + "提示", + "请输入有效的 tileset URL", + show_cancel=False, + confirm_text="确认" + ) + return - # 加载 tileset - if hasattr(self.world, 'addCesiumTileset'): - success = self.world.addCesiumTileset(tileset_name, url, (0, 0, 0)) - if success: - QMessageBox.information( - self, - "成功", - f"Cesium 3D Tiles 已加载到场景中!\n名称: {tileset_name}" - ) - else: - QMessageBox.warning( - self, - "失败", - "加载 Cesium 3D Tiles 失败" - ) - except Exception as e: - QMessageBox.critical( - self, - "错误", - f"加载 Cesium 3D Tiles 时发生错误:\n{str(e)}" + 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): """工具切换事件处理""" @@ -2448,7 +4029,8 @@ class MainWindow(QMainWindow): # 加载百度网页 #print("🌐 加载百度网页: https://www.baidu.com") - self.web_view.load(QUrl("https://www.bootstrapmb.com/item/15762/preview")) + #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) @@ -2469,113 +4051,7 @@ class MainWindow(QMainWindow): traceback.print_exc() return None - def onCreateSampleInfoPanel(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) - - # 使用唯一的面板ID - import time - unique_id = f"weather_info_{int(time.time())}" - - # 创建示例面板 - weather_panel = info_manager.createInfoPanel( - panel_id=unique_id, # 使用唯一ID - position=(1.32, 0.68), - size=(1, 0.6), - bg_color=(0.15, 0.25, 0.35, 0), # 蓝色背景 - border_color=(0.3, 0.5, 0.7, 0), # 蓝色边框 - title_color=(0.7, 0.9, 1.0, 1.0), # 浅蓝色标题 - content_color=(0.95, 0.95, 0.95, 1.0), - font=font, - bg_image="/home/tiger/图片/内部信息框2@2x.png" - ) - - # 更新面板标题 - info_manager.updatePanelContent(unique_id, title="北京天气") - - # 添加到场景树 - self.addInfoPanelToTree(weather_panel, "天气信息面板") - - # 立即显示加载中信息 - info_manager.updatePanelContent(unique_id, content="正在获取天气数据...") - - info_manager.registerDataSource(unique_id, self.getRealWeatherData, update_interval=5.0) - - # # 立即显示示例数据 - # sample_data = self.getSampleWeatherData() - # info_manager.updatePanelContent(unique_id, content=sample_data) - # - # # 注册数据源,定期更新示例数据 - # info_manager.registerDataSource(unique_id, self.getSampleWeatherData, update_interval=2.0) - - print("✓ 示例天气信息面板已创建") - - except Exception as e: - print(f"✗ 创建示例天气信息面板失败: {e}") - import traceback - traceback.print_exc() - QMessageBox.critical(self, "错误", f"创建示例天气信息面板时出错: {str(e)}") - - def getRealWeatherData(self): - """获取真实天气数据""" - try: - import requests - import json - from datetime import datetime - - # 请求天气数据 - url = "https://wttr.in/Beijing?format=j1" - response = requests.get(url, timeout=10) - response.raise_for_status() - - # 解析JSON数据 - weather_data = response.json() - - # 提取当前天气信息 - current_condition = weather_data['current_condition'][0] - weather_desc = current_condition['weatherDesc'][0]['value'] - temp_c = current_condition['temp_C'] - feels_like = current_condition['FeelsLikeC'] - humidity = current_condition['humidity'] - pressure = current_condition['pressure'] - visibility = current_condition['visibility'] - wind_speed = current_condition['windspeedKmph'] - wind_dir = current_condition['winddir16Point'] - - # 提取空气质量(如果可用) - air_quality = "N/A" - if 'air_quality' in weather_data and weather_data['air_quality']: - if 'us-epa-index' in current_condition: - air_quality_index = current_condition['air_quality_index'] - air_quality = f"指数: {air_quality_index}" - - # 获取更新时间 - update_time = datetime.now().strftime("%Y-%m-%d %H:%M") - - # 格式化显示内容 - content = f"天气状况: {weather_desc}\n温度: {temp_c}°C (体感 {feels_like}°C)\n湿度: {humidity}%\n气压: {pressure} hPa\n能见度: {visibility} km\n风速: {wind_speed} km/h ({wind_dir})\n空气质量: {air_quality}\n更新时间: {update_time}" - - return content - - except requests.exceptions.Timeout: - return "错误: 获取天气数据超时" - except requests.exceptions.ConnectionError: - return "错误: 网络连接失败" - except requests.exceptions.HTTPError as e: - return f"HTTP错误: {e}" - except json.JSONDecodeError: - return "错误: 无法解析天气数据" - except KeyError as e: - return f"错误: 天气数据格式不正确 (缺少字段: {e})" - except Exception as e: - return f"获取天气数据失败: {str(e)}" def getSampleWeatherData(self): """获取示例天气数据""" @@ -2600,7 +4076,6 @@ class MainWindow(QMainWindow): except Exception as e: return f"获取示例数据失败: {str(e)}" - # 在 main_window.py 中修改 onCreate3DSampleInfoPanel 方法 def onCreate3DSampleInfoPanel(self): """创建3D示例天气信息面板(修复透明度问题)""" try: @@ -2627,15 +4102,7 @@ class MainWindow(QMainWindow): content_color=(0.95, 0.95, 0.95, 1.0), font=font ) - - # 重要:手动设置面板的透明度渲染模式 - # if weather_panel: - # # 确保面板支持透明度 - # weather_panel.setTransparency(True) - # # 设置合适的渲染顺序,确保透明对象正确渲染 - # weather_panel.setBin("transparent", 0) - # # 启用深度写入,但保持透明度 - # weather_panel.setDepthWrite(False) + weather_panel.setTag("name",unique_id) # 更新面板标题 info_manager.update3DPanelContent(unique_id, title="3D北京天气") @@ -2646,7 +4113,7 @@ class MainWindow(QMainWindow): # 显示加载中信息 info_manager.update3DPanelContent(unique_id, content="正在获取天气数据...") - info_manager.registerDataSource(unique_id, self.getRealWeatherData, update_interval=5.0) + info_manager.registerDataSource(unique_id, info_manager.getRealWeatherData, update_interval=5.0) print("✓ 3D示例天气信息面板已创建") @@ -2656,48 +4123,6 @@ class MainWindow(QMainWindow): traceback.print_exc() QMessageBox.critical(self, "错误", f"创建3D示例天气信息面板时出错: {str(e)}") - def onCreate3DSystemStatusPanel(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"system_status_3d_{int(time.time())}" - - panel = info_manager.create3DInfoPanel( - panel_id=unique_id, - position=(2, 0, 0), - 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, "3D系统状态信息面板") - - # 立即显示初始数据 - initial_data = self.getSystemStatusData() - info_manager.update3DPanelContent(unique_id, content=initial_data) - - # 注册数据源,每5秒更新一次 - info_manager.registerDataSource(unique_id, self.getSystemStatusData, update_interval=5.0) - - except Exception as e: - print(f"✗ 创建3D系统状态信息面板失败: {e}") - import traceback - traceback.print_exc() - QMessageBox.critical(self, "错误", f"创建3D系统状态信息面板时出错: {str(e)}") - # 更新 addInfoPanelToTree 方法以支持3D面板 def addInfoPanelToTree(self, panel, panel_name): """ @@ -2948,17 +4373,6 @@ class MainWindow(QMainWindow): except Exception as e: return f"获取场景信息失败: {str(e)}" - def onCreateAllInfoPanels(self): - """创建所有信息面板""" - try: - self.onCreateSampleInfoPanel() - self.onCreateSystemStatusPanel() - self.onCreateSensorDataPanel() - self.onCreateSceneInfoPanel() - QMessageBox.information(self, "成功", - "所有信息面板已创建完成!\n快捷键:\nF1 - 示例面板\nF2 - 系统状态面板\nF3 - 传感器数据面板\nF4 - 场景信息面板") - except Exception as e: - QMessageBox.critical(self, "错误", f"创建信息面板时出错: {str(e)}") # ==================== 脚本管理事件处理 ==================== @@ -2976,17 +4390,72 @@ class MainWindow(QMainWindow): """更新脚本面板状态""" # 更新热重载状态 hot_reload_enabled = self.world.script_manager.hot_reload_enabled - self.hotReloadLabel.setText(f"热重载: {'已启用' if hot_reload_enabled else '已禁用'}") - self.hotReloadLabel.setStyleSheet(f"color: {'blue' if hot_reload_enabled else 'gray'};") + + # 更新热重载标签文本和样式 + 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("color: green; font-weight: bold;") + 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) @@ -2994,7 +4463,17 @@ class MainWindow(QMainWindow): self.updateMountedScriptsList(selected_object) else: self.selectedObjectLabel.setText("未选择对象") - self.selectedObjectLabel.setStyleSheet("color: gray; font-style: italic;") + 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() @@ -3031,7 +4510,9 @@ class MainWindow(QMainWindow): """创建脚本按钮事件""" script_name = self.scriptNameEdit.text().strip() if not script_name: - QMessageBox.warning(self, "错误", "请输入脚本名称!") + UniversalMessageDialog.show_warning( + self, "警告", "请输入脚本名称!", show_cancel=False, confirm_text="确认" + ) return template = self.templateCombo.currentText() @@ -3039,17 +4520,35 @@ class MainWindow(QMainWindow): try: success = self.world.createScript(script_name, template) if success: - QMessageBox.information(self, "成功", f"脚本 '{script_name}' 创建成功!") + UniversalMessageDialog.show_success( + self, + "成功", + f"脚本 '{script_name}' 创建成功!", + show_cancel=False, + confirm_text="确认" + ) self.scriptNameEdit.clear() self.refreshScriptsList() else: - QMessageBox.warning(self, "错误", f"脚本 '{script_name}' 创建失败!") + UniversalMessageDialog.show_warning( + self, + "警告", + f"脚本 '{script_name}' 创建失败!", + show_cancel=False, + confirm_text="确认" + ) except Exception as e: - QMessageBox.critical(self, "错误", f"创建脚本时出错: {str(e)}") + UniversalMessageDialog.show_error( + self, + "错误", + f"创建脚本时发生错误: {str(e)}", + show_cancel=False, + confirm_text="确认" + ) def onCreateScriptDialog(self): """菜单创建脚本事件""" - dialog = self.createStyledInputDialog(self, "创建脚本", "输入脚本名称:") + dialog = self.createStyledInputDialog(self, "创建脚本", "请输入脚本名称:") if dialog.exec_() == QDialog.Accepted: script_name = dialog.textValue() @@ -3057,30 +4556,55 @@ class MainWindow(QMainWindow): try: success = self.world.createScript(script_name.strip(), "basic") if success: - QMessageBox.information(self, "成功", f"脚本 '{script_name}' 创建成功!") + UniversalMessageDialog.show_success( + self, + "成功", + f"脚本 '{script_name}' 创建成功!", + show_cancel=False, + confirm_text="确认" + ) self.refreshScriptsList() else: - QMessageBox.warning(self, "错误", f"脚本 '{script_name}' 创建失败!") + UniversalMessageDialog.show_warning( + self, + "警告", + f"脚本 '{script_name}' 创建失败!", + show_cancel=False, + confirm_text="确认" + ) except Exception as e: - QMessageBox.critical(self, "错误", f"创建脚本时出错: {str(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: - QMessageBox.warning(self, "错误", "请选择要加载的脚本!") + UniversalMessageDialog.show_warning( + self, "警告", "请选择要加载的脚本!", show_cancel=False, confirm_text="确认" + ) return script_name = current_item.text() try: success = self.world.reloadScript(script_name) if success: - QMessageBox.information(self, "成功", f"脚本 '{script_name}' 重载成功!") + UniversalMessageDialog.show_success( + self, "成功", f"脚本 '{script_name}' 加载成功!", show_cancel=False, confirm_text="确认" + ) else: - QMessageBox.warning(self, "错误", f"脚本 '{script_name}' 重载失败!") + UniversalMessageDialog.show_warning( + self, "警告", f"脚本 '{script_name}' 加载失败!", show_cancel=False, confirm_text="确认" + ) except Exception as e: - QMessageBox.critical(self, "错误", f"重载脚本时出错: {str(e)}") + UniversalMessageDialog.show_error( + self, "错误", f"加载脚本时发生错误: {str(e)}", show_cancel=False, confirm_text="确认" + ) def onLoadScriptFile(self): """加载脚本文件菜单事件""" @@ -3097,29 +4621,52 @@ class MainWindow(QMainWindow): try: success = self.world.loadScript(file_path) if success: - QMessageBox.information(self, "成功", "脚本文件加载成功!") + UniversalMessageDialog.show_success( + self, "成功", "脚本文件加载成功!", show_cancel=False, confirm_text="确认" + ) self.refreshScriptsList() else: - QMessageBox.warning(self, "错误", "脚本文件加载失败!") + UniversalMessageDialog.show_warning( + self, "警告", "脚本文件加载失败!", show_cancel=False, confirm_text="确认" + ) except Exception as e: - QMessageBox.critical(self, "错误", f"加载脚本文件时出错: {str(e)}") - + UniversalMessageDialog.show_error( + self, "错误", f"加载脚本文件时发生错误: {str(e)}", show_cancel=False, confirm_text="确认" + ) def onReloadAllScripts(self): - """重载所有脚本事件""" + """重新加载所有脚本事件""" try: scripts_loaded = self.world.loadAllScripts() - QMessageBox.information(self, "成功", f"重载完成,共加载 {len(scripts_loaded)} 个脚本!") + UniversalMessageDialog.show_success( + self, + "成功", + f"更新完成,共加载 {len(scripts_loaded)} 个脚本!", + show_cancel=False, + confirm_text="确认" + ) self.refreshScriptsList() except Exception as e: - QMessageBox.critical(self, "错误", f"重载脚本时出错: {str(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 "禁用" - QMessageBox.information(self, "热重载", f"热重载已{status}") + status = "已开启" if enabled else "已关闭" + UniversalMessageDialog.show_info( + self, + "脚本热更", + f"脚本热更{status}", + show_cancel=False, + confirm_text="确认" + ) def onOpenScriptsManager(self): """打开脚本管理器""" @@ -3129,65 +4676,107 @@ class MainWindow(QMainWindow): def onScriptDoubleClick(self, item): """脚本列表双击事件""" - # 可以在这里添加打开外部编辑器的功能 script_name = item.text() - QMessageBox.information(self, "提示", f"双击了脚本: {script_name}\n\n可以使用外部编辑器编辑脚本文件。") + 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: - QMessageBox.warning(self, "错误", "请先选择一个对象!") + UniversalMessageDialog.show_warning( + self, "警告", "请选择一个对象!", show_cancel=False, confirm_text="确认" + ) return script_name = self.mountScriptCombo.currentText() if not script_name: - QMessageBox.warning(self, "错误", "请选择要挂载的脚本!") + UniversalMessageDialog.show_warning( + self, "警告", "请选择要挂载的脚本!", show_cancel=False, confirm_text="确认" + ) return try: success = self.world.addScript(selected_object, script_name) if success: - QMessageBox.information(self, "成功", f"脚本 '{script_name}' 已挂载到对象!") + 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: - QMessageBox.warning(self, "错误", f"挂载脚本失败!") + UniversalMessageDialog.show_warning( + self, "警告", "挂载脚本失败!", show_cancel=False, confirm_text="确认" + ) except Exception as e: - QMessageBox.critical(self, "错误", f"挂载脚本时出错: {str(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: - QMessageBox.warning(self, "错误", "请先选择一个对象!") + UniversalMessageDialog.show_warning( + self, + "警告", + "请选择一个对象!", + show_cancel=False, + confirm_text="确认" + ) return current_item = self.mountedScriptsList.currentItem() if not current_item: - QMessageBox.warning(self, "错误", "请选择要卸载的脚本!") + UniversalMessageDialog.show_warning( + self, + "警告", + "请选择要卸载的脚本!", + show_cancel=False, + confirm_text="确认" + ) return - # 解析脚本名称(移除状态标记) item_text = current_item.text() - script_name = item_text[2:] # 移除 "✓ " 或 "✗ " 前缀 + script_name = item_text[2:] try: success = self.world.removeScript(selected_object, script_name) if success: - QMessageBox.information(self, "成功", f"脚本 '{script_name}' 已从对象卸载!") + 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: - QMessageBox.warning(self, "错误", f"卸载脚本失败!") + UniversalMessageDialog.show_warning( + self, + "警告", + "卸载脚本失败!", + show_cancel=False, + confirm_text="确认" + ) except Exception as e: - QMessageBox.critical(self, "错误", f"卸载脚本时出错: {str(e)}") + UniversalMessageDialog.show_error( + self, + "错误", + f"卸载脚本时发生错误: {str(e)}", + show_cancel=False, + confirm_text="确认" + ) def onOpenIconManager(self): """打开图标管理器""" @@ -3204,16 +4793,34 @@ class MainWindow(QMainWindow): 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("🧹 清理工具管理器进程...") - self.world.tool_manager.cleanup_processes() + 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资源...") @@ -3228,178 +4835,788 @@ class MainWindow(QMainWindow): def onCreateFlatTerrain(self): """创建平面地形""" - dialog = QDialog(self) - dialog.setWindowTitle("创建平面地形") - dialog.setModal(True) - dialog.resize(300,200) - # 设置对话框样式 - dialog.setStyleSheet(""" - QDialog { - background-color: #252538; - color: #e0e0ff; - } - QLabel { - color: #e0e0ff; - font-weight: 500; - } - QPushButton { - background-color: #8b5cf6; - color: white; - border: none; - padding: 6px 12px; - border-radius: 4px; - font-weight: 500; - min-width: 80px; - } - QPushButton:hover { - background-color: #7c3aed; - } - QPushButton:pressed { - background-color: #6d28d9; - } - QPushButton:disabled { - background-color: #4c4c6e; - color: #8888aa; - } - QDoubleSpinBox, QSpinBox { - background-color: #2d2d44; - color: #e0e0ff; - border: 1px solid #3a3a4a; - border-radius: 4px; - padding: 4px; - } - """) + 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="取消") - layout = QVBoxLayout(dialog) - - width_layout = QHBoxLayout() - width_layout.addWidget(QLabel("宽度:")) - width_spin = QDoubleSpinBox() - width_spin.setRange(0,10000) - width_spin.setValue(0.3) - width_layout.addWidget(width_spin) - layout.addLayout(width_layout) - - # 高度 - height_layout = QHBoxLayout() - height_layout.addWidget(QLabel("高度:")) - height_spin = QDoubleSpinBox() - height_spin.setRange(0, 10000) - height_spin.setValue(0.3) - height_layout.addWidget(height_spin) - layout.addLayout(height_layout) - - # 分辨率 - resolution_layout = QHBoxLayout() - resolution_layout.addWidget(QLabel("分辨率:")) - resolution_spin = QSpinBox() - resolution_spin.setRange(16, 2048) - resolution_spin.setValue(256) - resolution_spin.setSingleStep(16) - resolution_layout.addWidget(resolution_spin) - layout.addLayout(resolution_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: - width = width_spin.value() - height = height_spin.value() - resolution = resolution_spin.value() + 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: - QMessageBox.information(self, "成功", "平面地形创建成功!") + UniversalMessageDialog.show_success( + self, + "成功", + "平面地形创建成功!", + show_cancel=False, + confirm_text="确认" + ) else: - QMessageBox.warning(self, "错误", "平面地形创建失败!") + UniversalMessageDialog.show_warning( + self, + "警告", + "平面地形创建失败!", + show_cancel=False, + confirm_text="确认" + ) def onCreateHeightmapTerrain(self): """从高度图创建地形""" - dialog = self.createStyledFileDialog( + file_dialog = self.createStyledFileDialog( self, "选择高度图文件", "", "图像文件 (*.png *.jpg *.jpeg *.bmp *.tga);;所有文件 (*)" ) - if dialog.exec_() == QDialog.Accepted: - file_path = dialog.selectedFiles()[0] + if file_dialog.exec_() == QDialog.Accepted: + file_path = file_dialog.selectedFiles()[0] if file_path: - #创建对话框获取地形参数 - dialog = QDialog(self) - dialog.setWindowTitle("设置地形参数") - dialog.setModal(True) - dialog.resize(300,250) + 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 + }, + ] - layout = QVBoxLayout(dialog) + params_dialog = StyledTerrainDialog(self, "设置地形参数", fields, primary_text="创建", secondary_text="取消") - x_scale_layout = QHBoxLayout() - x_scale_layout.addWidget(QLabel("X缩放:")) - x_scale_spin = QDoubleSpinBox() - x_scale_spin.setRange(0.1,1000) - x_scale_spin.setValue(0.3) - x_scale_spin.setSingleStep(10) - x_scale_layout.addWidget(x_scale_spin) - layout.addLayout(x_scale_layout) + 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") - # Y缩放 - y_scale_layout = QHBoxLayout() - y_scale_layout.addWidget(QLabel("Y缩放:")) - y_scale_spin = QDoubleSpinBox() - y_scale_spin.setRange(0.1, 1000) - y_scale_spin.setValue(0.3) - y_scale_spin.setSingleStep(10) - y_scale_layout.addWidget(y_scale_spin) - layout.addLayout(y_scale_layout) - - # Z缩放 - z_scale_layout = QHBoxLayout() - z_scale_layout.addWidget(QLabel("Z缩放:")) - z_scale_spin = QDoubleSpinBox() - z_scale_spin.setRange(0.1, 1000) - z_scale_spin.setValue(50) - z_scale_spin.setSingleStep(5) - z_scale_layout.addWidget(z_scale_spin) - layout.addLayout(z_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: - x_scale = x_scale_spin.value() - y_scale = y_scale_spin.value() - z_scale = z_scale_spin.value() - - # 调用世界对象创建地形 terrain_info = self.world.createTerrainFromHeightMap( file_path, (x_scale, y_scale, z_scale) ) if terrain_info: - QMessageBox.information(self, "成功", "高度图地形创建成功!") + UniversalMessageDialog.show_success( + self, + "成功", + "高度图地形创建成功!", + show_cancel=False, + confirm_text="确认" + ) else: - QMessageBox.warning(self, "错误", "高度图地形创建失败!") + 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: + QMessageBox.critical(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: + QMessageBox.critical(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("❌ 维修科目启动失败") + QMessageBox.warning(self, "错误", "维修科目启动失败") + + except Exception as e: + print(f"❌ 启动维修科目失败: {e}") + QMessageBox.critical(self, "错误", f"启动维修科目失败:\n{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}") + QMessageBox.critical(self, "错误", f"打开维修系统失败:\n{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) + QMessageBox.information(self, "成功", "VR模式已启用!\n请确保您的VR头显已正确连接。") + else: + QMessageBox.warning(self, "错误", "无法启用VR模式!\n请检查:\n1. SteamVR是否正在运行\n2. VR头显是否已连接\n3. OpenVR库是否已正确安装") + else: + QMessageBox.warning(self, "错误", "VR管理器不可用!") + except Exception as e: + QMessageBox.critical(self, "错误", f"启用VR模式时发生错误:\n{str(e)}") + + 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) + QMessageBox.information(self, "成功", "已退出VR模式") + else: + QMessageBox.warning(self, "错误", "VR管理器不可用!") + except Exception as e: + QMessageBox.critical(self, "错误", f"退出VR模式时发生错误:\n{str(e)}") + + 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 +""" + + QMessageBox.information(self, "VR状态", status_text) + else: + QMessageBox.warning(self, "错误", "VR管理器不可用!") + except Exception as e: + QMessageBox.critical(self, "错误", f"获取VR状态时发生错误:\n{str(e)}") + + def onShowVRSettings(self): + """显示VR设置对话框""" + try: + if hasattr(self.world, 'vr_manager') and self.world.vr_manager: + dialog = self.createVRSettingsDialog() + dialog.exec_() + else: + QMessageBox.warning(self, "错误", "VR管理器不可用!") + except Exception as e: + QMessageBox.critical(self, "错误", f"打开VR设置时发生错误:\n{str(e)}") + + def createVRSettingsDialog(self): + """创建VR设置对话框""" + dialog = QDialog(self) + dialog.setWindowTitle("VR设置") + dialog.setModal(True) + dialog.resize(400, 300) + + layout = QVBoxLayout(dialog) + + # VR状态显示 + status_group = QGroupBox("VR状态") + status_layout = QVBoxLayout() + + 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: {'green' if status['available'] else 'red'};") + status_layout.addWidget(available_label) + + enabled_label = QLabel(f"VR状态: {'已启用' if status['enabled'] else '未启用'}") + enabled_label.setStyleSheet(f"color: {'green' if status['enabled'] else 'gray'};") + status_layout.addWidget(enabled_label) + + resolution_label = QLabel(f"渲染分辨率: {status['eye_resolution'][0]}x{status['eye_resolution'][1]}") + status_layout.addWidget(resolution_label) + + status_group.setLayout(status_layout) + layout.addWidget(status_group) + + # 渲染设置 + render_group = QGroupBox("渲染设置") + render_layout = QFormLayout() + + # 渲染质量 + quality_combo = QComboBox() + quality_combo.addItems(["低", "中", "高", "超高"]) + quality_combo.setCurrentText("高") + render_layout.addRow("渲染质量:", quality_combo) + + # 抗锯齿 + aa_combo = QComboBox() + aa_combo.addItems(["无", "2x", "4x", "8x"]) + aa_combo.setCurrentText("4x") + render_layout.addRow("抗锯齿:", aa_combo) + + render_group.setLayout(render_layout) + layout.addWidget(render_group) + + # 性能设置 + perf_group = QGroupBox("性能设置") + perf_layout = QFormLayout() + + # 刷新率 + refresh_combo = QComboBox() + refresh_combo.addItems(["72Hz", "90Hz", "120Hz", "144Hz"]) + refresh_combo.setCurrentText("90Hz") + perf_layout.addRow("刷新率:", refresh_combo) + + # 异步重投影 + async_check = QCheckBox("启用异步重投影") + async_check.setChecked(True) + perf_layout.addRow("", async_check) + + perf_group.setLayout(perf_layout) + layout.addWidget(perf_group) + + # 按钮 + button_layout = QHBoxLayout() + + apply_button = QPushButton("应用") + ok_button = QPushButton("确定") + cancel_button = QPushButton("取消") + + button_layout.addWidget(apply_button) + button_layout.addStretch() + button_layout.addWidget(ok_button) + button_layout.addWidget(cancel_button) + + layout.addLayout(button_layout) + + # 连接信号 + apply_button.clicked.connect(lambda: self.applyVRSettings(dialog)) + ok_button.clicked.connect(dialog.accept) + cancel_button.clicked.connect(dialog.reject) + + return dialog + + def applyVRSettings(self, dialog): + """应用VR设置""" + try: + # 这里可以实现设置的保存和应用逻辑 + QMessageBox.information(dialog, "成功", "VR设置已应用!") + except Exception as e: + QMessageBox.critical(dialog, "错误", f"应用VR设置时发生错误:\n{str(e)}") + + # ==================== 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 "禁用" + QMessageBox.information(self, "VR调试", f"VR调试输出已{status}") + else: + QMessageBox.warning(self, "错误", "VR管理器不可用!") + except Exception as e: + QMessageBox.critical(self, "错误", f"切换VR调试时发生错误:\n{str(e)}") + + 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() + QMessageBox.information(self, "VR性能", "性能报告已输出到控制台") + else: + QMessageBox.warning(self, "提示", "请先启用VR模式") + else: + QMessageBox.warning(self, "错误", "VR管理器不可用!") + except Exception as e: + QMessageBox.critical(self, "错误", f"显示VR性能报告时发生错误:\n{str(e)}") + + 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 "详细" + QMessageBox.information(self, "VR调试", f"调试模式已设置为:{mode_name}") + else: + QMessageBox.warning(self, "错误", "VR管理器不可用!") + except Exception as e: + QMessageBox.critical(self, "错误", f"设置VR调试模式时发生错误:\n{str(e)}") + + 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 "禁用" + QMessageBox.information(self, "VR性能监控", f"VR性能监控已{status}") + else: + QMessageBox.warning(self, "错误", "VR管理器不可用!") + self.vrPerformanceMonitorAction.setChecked(False) + except Exception as e: + QMessageBox.critical(self, "错误", f"切换VR性能监控时发生错误:\n{str(e)}") + + 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 "禁用" + QMessageBox.information(self, "VR GPU监控", f"VR GPU时间监控已{status}") + else: + QMessageBox.warning(self, "错误", "VR管理器不可用!") + self.vrGpuTimingAction.setChecked(False) + except Exception as e: + QMessageBox.critical(self, "错误", f"切换VR GPU时间监控时发生错误:\n{str(e)}") + + 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: + QMessageBox.warning(self, "错误", "VR管理器不可用!") + self.vrPipelineMonitorAction.setChecked(False) + except Exception as e: + QMessageBox.critical(self, "错误", f"切换VR管线监控时发生错误:\n{str(e)}") + + 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': '更新任务策略' + } + QMessageBox.information(self, "VR姿态策略", + f"姿态策略已切换为:{strategy_names.get(strategy, strategy)}") + else: + QMessageBox.warning(self, "错误", f"无效的姿态策略:{strategy}") + else: + QMessageBox.warning(self, "错误", "VR管理器不可用!") + except Exception as e: + QMessageBox.critical(self, "错误", f"设置VR姿态策略时发生错误:\n{str(e)}") + + def onTestVRPipeline(self): + """测试VR管线监控功能""" + try: + if hasattr(self.world, 'vr_manager') and self.world.vr_manager: + self.world.vr_manager.test_pipeline_monitoring() + QMessageBox.information(self, "VR管线测试", "管线监控测试已完成,请查看控制台输出。") + else: + QMessageBox.warning(self, "错误", "VR管理器不可用!") + except Exception as e: + QMessageBox.critical(self, "错误", f"测试VR管线监控时发生错误:\n{str(e)}") + + def onShowVRDebugSettings(self): + """显示VR调试设置对话框""" + try: + if hasattr(self.world, 'vr_manager') and self.world.vr_manager: + dialog = self.createVRDebugSettingsDialog() + dialog.exec_() + else: + QMessageBox.warning(self, "错误", "VR管理器不可用!") + except Exception as e: + QMessageBox.critical(self, "错误", f"打开VR调试设置时发生错误:\n{str(e)}") + + def createVRDebugSettingsDialog(self): + """创建VR调试设置对话框""" + from PyQt5.QtWidgets import QCheckBox, QSlider + from PyQt5.QtCore import Qt + + dialog = QDialog(self) + dialog.setWindowTitle("VR调试设置") + dialog.setModal(True) + dialog.resize(450, 400) + + layout = QVBoxLayout(dialog) + + # 获取当前设置 + vr_manager = self.world.vr_manager + debug_status = vr_manager.get_debug_status() + perf_config = vr_manager.get_performance_monitoring_config() + + # 调试状态显示 + status_group = QGroupBox("调试状态") + status_layout = QVBoxLayout() + + debug_enabled_label = QLabel(f"调试输出: {'启用' if debug_status['debug_enabled'] else '禁用'}") + debug_enabled_label.setStyleSheet(f"color: {'green' 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: {'green' if debug_status['performance_monitoring'] else 'red'};") + status_layout.addWidget(performance_label) + + status_group.setLayout(status_layout) + layout.addWidget(status_group) + + # 报告设置 + report_group = QGroupBox("报告设置") + report_layout = QFormLayout() + + # 报告间隔滑块 (5-120秒) + 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}秒")) + + interval_layout = QHBoxLayout() + interval_layout.addWidget(interval_slider) + interval_layout.addWidget(interval_label) + report_layout.addRow("报告间隔:", interval_layout) + + # 性能检查间隔 + 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) + report_layout.addRow("性能检查间隔:", check_interval_combo) + + # 帧历史大小 + 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(" 帧") + report_layout.addRow("帧时间历史:", frame_history_spin) + + report_group.setLayout(report_layout) + layout.addWidget(report_group) + + # 监控项目 + monitor_group = QGroupBox("监控项目") + monitor_layout = QVBoxLayout() + + 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) + + monitor_group.setLayout(monitor_layout) + layout.addWidget(monitor_group) + + # 按钮 + button_layout = QHBoxLayout() + + apply_button = QPushButton("应用") + reset_button = QPushButton("重置计数器") + ok_button = QPushButton("确定") + 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) + + layout.addLayout(button_layout) + + # 连接信号 + def apply_settings(): + try: + # 应用报告间隔 + new_interval_seconds = interval_slider.value() + new_interval_frames = int(new_interval_seconds * 60) # 假设60fps + 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()) + + QMessageBox.information(dialog, "成功", "VR调试设置已应用!") + except Exception as e: + QMessageBox.critical(dialog, "错误", f"应用设置时发生错误:\n{str(e)}") + + def reset_counters(): + try: + vr_manager.reset_performance_counters() + QMessageBox.information(dialog, "成功", "性能计数器已重置!") + except Exception as e: + QMessageBox.critical(dialog, "错误", f"重置计数器时发生错误:\n{str(e)}") + + 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: + QMessageBox.information(self, "VR测试模式", + "VR测试模式已启用!\n\n现在VR渲染内容将直接显示在PC屏幕上,无需VR头显。\n\n特点:\n- 显示VR左右眼视图\n- 实时性能监控HUD\n- 复用完整VR渲染管线\n- 可测量纯渲染性能") + print("✅ VR测试模式已启用") + + # 可选:自动开启性能测试 + self.world.vr_manager.run_vr_performance_test(duration_seconds=10) + else: + self.vrTestModeAction.setChecked(False) + QMessageBox.warning(self, "错误", "启用VR测试模式失败!") + else: + # 禁用VR测试模式 + self.world.vr_manager.disable_vr_test_mode() + QMessageBox.information(self, "VR测试模式", "VR测试模式已禁用") + print("✅ VR测试模式已禁用") + else: + self.vrTestModeAction.setChecked(False) + QMessageBox.warning(self, "错误", "VR管理器不可用!") + except Exception as e: + self.vrTestModeAction.setChecked(False) + QMessageBox.critical(self, "错误", f"切换VR测试模式时发生错误:\n{str(e)}") + + 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: + QMessageBox.critical(self, "错误", f"设置纹理提交功能时发生错误:\n{str(e)}") + + 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: + QMessageBox.critical(self, "错误", f"设置姿态等待功能时发生错误:\n{str(e)}") + + 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) + QMessageBox.information(self, "VR测试", "已重置为基线状态:两个功能都禁用") + 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) + QMessageBox.information(self, "VR测试", "步骤1:只启用纹理提交\n观察FPS变化来判断submit_texture是否影响性能") + 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) + QMessageBox.information(self, "VR测试", "步骤2:只启用姿态等待\n观察FPS变化来判断waitGetPoses是否影响性能") + 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) + QMessageBox.information(self, "VR测试", "步骤3:同时启用两者\n这应该完全复现普通VR模式的36FPS问题") + except Exception as e: + QMessageBox.critical(self, "错误", f"设置VR测试步骤时发生错误:\n{str(e)}") def setup_main_window(world,path = None): """设置主窗口的便利函数""" @@ -3413,4 +5630,129 @@ def setup_main_window(world,path = None): if path: openProjectForPath(path,main_window) - return app, main_window \ No newline at end of file + 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: + 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 diff --git a/ui/maintenance_system.py b/ui/maintenance_system.py new file mode 100644 index 00000000..77d609ea --- /dev/null +++ b/ui/maintenance_system.py @@ -0,0 +1,658 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +维修系统界面模块 +包含登录界面、科目列表、模式选择等功能 +""" + +import os +import json +from PyQt5.QtWidgets import ( + QDialog, QVBoxLayout, QHBoxLayout, QFormLayout, QGroupBox, + QLineEdit, QPushButton, QLabel, QListWidget, QListWidgetItem, + QMessageBox, QRadioButton, QTextEdit, QSplitter +) +from PyQt5.QtCore import Qt, pyqtSignal +from PyQt5.QtGui import QFont, QPixmap + + +class MaintenanceLoginDialog(QDialog): + """维修系统登录对话框""" + + login_success = pyqtSignal() # 登录成功信号 + + def __init__(self, parent=None): + super().__init__(parent) + self.setupUI() + + def setupUI(self): + """设置用户界面""" + self.setWindowTitle("维修系统 - 登录") + self.setFixedSize(400, 300) + self.setModal(True) + + # 应用简化样式 - 确保可见性和可用性 + self.setStyleSheet(""" + QDialog { + background-color: #f5f5f5; + color: #333333; + } + QLabel { + color: #333333; + font-size: 14px; + } + QLineEdit { + background-color: white; + color: #333333; + border: 2px solid #cccccc; + border-radius: 4px; + padding: 8px; + font-size: 14px; + min-height: 20px; + } + QLineEdit:focus { + border-color: #0078d4; + } + QPushButton { + background-color: #0078d4; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + font-size: 14px; + min-height: 30px; + } + QPushButton:hover { + background-color: #106ebe; + } + QPushButton:pressed { + background-color: #005a9e; + } + QGroupBox { + font-size: 16px; + font-weight: bold; + color: #0078d4; + border: 2px solid #cccccc; + border-radius: 4px; + margin-top: 10px; + padding-top: 10px; + } + QGroupBox::title { + subcontrol-origin: margin; + left: 10px; + padding: 0 5px 0 5px; + } + """) + + layout = QVBoxLayout(self) + layout.setSpacing(15) + layout.setContentsMargins(20, 20, 20, 20) + + # 标题 + title_label = QLabel("🔧 维修系统") + title_label.setAlignment(Qt.AlignCenter) + title_label.setStyleSheet(""" + font-size: 20px; + font-weight: bold; + color: #0078d4; + margin: 10px 0px; + """) + layout.addWidget(title_label) + + # 登录表单 + login_group = QGroupBox("身份验证") + login_layout = QFormLayout(login_group) + login_layout.setSpacing(10) + login_layout.setContentsMargins(15, 15, 15, 15) + + # 用户名 + self.username_edit = QLineEdit() + self.username_edit.setPlaceholderText("请输入用户名") + self.username_edit.setText("") # 确保清空 + login_layout.addRow("用户名:", self.username_edit) + + # 密码 + self.password_edit = QLineEdit() + self.password_edit.setEchoMode(QLineEdit.Password) + self.password_edit.setPlaceholderText("请输入密码") + self.password_edit.setText("") # 确保清空 + login_layout.addRow("密码:", self.password_edit) + + layout.addWidget(login_group) + + # 提示信息 + hint_label = QLabel("💡 默认账号密码均为: admin") + hint_label.setAlignment(Qt.AlignCenter) + hint_label.setStyleSheet(""" + color: #666666; + font-size: 12px; + margin: 5px 0px; + """) + layout.addWidget(hint_label) + + # 按钮 + button_layout = QHBoxLayout() + button_layout.setSpacing(10) + + self.cancel_btn = QPushButton("取消") + self.login_btn = QPushButton("登录") + + button_layout.addStretch() + button_layout.addWidget(self.cancel_btn) + button_layout.addWidget(self.login_btn) + + layout.addLayout(button_layout) + + # 连接信号 + self.login_btn.clicked.connect(self.on_login) + self.cancel_btn.clicked.connect(self.reject) + self.password_edit.returnPressed.connect(self.on_login) + + # 设置默认焦点和tab顺序 + self.username_edit.setFocus() + self.setTabOrder(self.username_edit, self.password_edit) + self.setTabOrder(self.password_edit, self.login_btn) + self.setTabOrder(self.login_btn, self.cancel_btn) + + print("🔧 登录界面创建完成") + print(f"📝 用户名输入框: {self.username_edit}") + print(f"📝 密码输入框: {self.password_edit}") + print(f"📝 登录按钮: {self.login_btn}") + print(f"📝 取消按钮: {self.cancel_btn}") + + def on_login(self): + """处理登录""" + username = self.username_edit.text().strip() + password = self.password_edit.text().strip() + + # 验证账号密码 + if username == "admin" and password == "admin": + print("✅ 维修系统登录成功") + self.login_success.emit() + self.accept() + else: + QMessageBox.warning(self, "登录失败", + "用户名或密码错误!\n请使用: admin / admin") + self.password_edit.clear() + self.password_edit.setFocus() + + +class MaintenanceSubjectDialog(QDialog): + """维修系统科目选择对话框""" + + subject_selected = pyqtSignal(str, str) # 科目路径, 模式 + + def __init__(self, project_path, parent=None): + super().__init__(parent) + self.project_path = project_path + self.subjects_path = os.path.join(project_path, "Subjects") if project_path else None + self.current_subject_path = None + self.setupUI() + self.load_subjects() + + def setupUI(self): + """设置用户界面""" + self.setWindowTitle("维修系统 - 科目选择") + self.setFixedSize(800, 600) + self.setModal(True) + + # 应用样式 + self.setStyleSheet(""" + QDialog { + background-color: #1e1e2e; + color: #e0e0ff; + } + QLabel { + color: #e0e0ff; + } + QListWidget { + background-color: #2d2d44; + color: #e0e0ff; + border: 2px solid #3a3a4a; + border-radius: 8px; + padding: 5px; + font-size: 14px; + } + QListWidget::item { + padding: 8px; + border-radius: 4px; + margin: 2px; + } + QListWidget::item:hover { + background-color: #3a3a4a; + } + QListWidget::item:selected { + background-color: #8b5cf6; + color: white; + } + QTextEdit { + background-color: #2d2d44; + color: #e0e0ff; + border: 2px solid #3a3a4a; + border-radius: 8px; + padding: 8px; + font-size: 12px; + } + QPushButton { + background-color: #8b5cf6; + color: white; + border: none; + padding: 10px 20px; + border-radius: 8px; + font-weight: bold; + font-size: 14px; + } + QPushButton:hover { + background-color: #7c3aed; + } + QPushButton:pressed { + background-color: #6d28d9; + } + QPushButton:disabled { + background-color: #4c4c6e; + color: #8888aa; + } + QGroupBox { + font-size: 14px; + font-weight: bold; + color: #8b5cf6; + border: 2px solid #3a3a4a; + border-radius: 8px; + margin: 10px 0px; + padding-top: 15px; + } + QGroupBox::title { + subcontrol-origin: margin; + left: 10px; + padding: 0 5px 0 5px; + } + QRadioButton { + color: #e0e0ff; + font-size: 14px; + spacing: 8px; + } + QRadioButton::indicator { + width: 16px; + height: 16px; + } + QRadioButton::indicator:unchecked { + border: 2px solid #3a3a4a; + border-radius: 8px; + background-color: #2d2d44; + } + QRadioButton::indicator:checked { + border: 2px solid #8b5cf6; + border-radius: 8px; + background-color: #8b5cf6; + } + """) + + layout = QVBoxLayout(self) + + # 标题 + title_label = QLabel("📚 选择维修科目") + title_label.setAlignment(Qt.AlignCenter) + title_label.setStyleSheet(""" + font-size: 20px; + font-weight: bold; + color: #8b5cf6; + margin: 15px 0px; + """) + layout.addWidget(title_label) + + # 主要内容区域 + main_splitter = QSplitter(Qt.Horizontal) + + # 左侧:科目列表 + left_widget = QGroupBox("科目列表") + left_layout = QVBoxLayout(left_widget) + + self.subject_list = QListWidget() + self.subject_list.itemClicked.connect(self.on_subject_selected) + left_layout.addWidget(self.subject_list) + + main_splitter.addWidget(left_widget) + + # 右侧:科目详情和模式选择 + right_widget = QGroupBox("科目详情") + right_layout = QVBoxLayout(right_widget) + + # 科目描述 + self.subject_description = QTextEdit() + self.subject_description.setReadOnly(True) + self.subject_description.setMaximumHeight(150) + self.subject_description.setPlainText("请选择一个科目查看详情...") + right_layout.addWidget(self.subject_description) + + # 模式选择 + mode_group = QGroupBox("选择模式") + mode_layout = QVBoxLayout(mode_group) + + self.training_radio = QRadioButton("🎓 训练模式") + self.training_radio.setChecked(True) + mode_layout.addWidget(self.training_radio) + + training_desc = QLabel("• 显示详细的步骤描述和操作提示\n• 提供工具选择的正确性提示\n• 播放语音指导") + training_desc.setStyleSheet("color: #94a3b8; font-size: 12px; margin-left: 20px;") + mode_layout.addWidget(training_desc) + + self.exam_radio = QRadioButton("📋 考核模式") + mode_layout.addWidget(self.exam_radio) + + exam_desc = QLabel("• 不显示步骤描述\n• 不提供工具选择提示\n• 无语音指导,独立完成操作") + exam_desc.setStyleSheet("color: #94a3b8; font-size: 12px; margin-left: 20px;") + mode_layout.addWidget(exam_desc) + + right_layout.addWidget(mode_group) + + main_splitter.addWidget(right_widget) + + # 设置分割器比例 + main_splitter.setSizes([300, 500]) + layout.addWidget(main_splitter) + + # 底部按钮 + button_layout = QHBoxLayout() + + self.refresh_btn = QPushButton("🔄 刷新") + self.cancel_btn = QPushButton("取消") + self.start_btn = QPushButton("开始") + self.start_btn.setEnabled(False) + + button_layout.addWidget(self.refresh_btn) + button_layout.addStretch() + button_layout.addWidget(self.cancel_btn) + button_layout.addWidget(self.start_btn) + + layout.addLayout(button_layout) + + # 连接信号 + self.refresh_btn.clicked.connect(self.load_subjects) + self.cancel_btn.clicked.connect(self.reject) + self.start_btn.clicked.connect(self.on_start) + + def load_subjects(self): + """加载科目列表""" + self.subject_list.clear() + self.current_subject_path = None + self.start_btn.setEnabled(False) + self.subject_description.setPlainText("请选择一个科目查看详情...") + + if not self.subjects_path or not os.path.exists(self.subjects_path): + # 添加提示项 + item = QListWidgetItem("📁 未找到Subjects目录") + item.setData(Qt.UserRole, None) + self.subject_list.addItem(item) + + if not self.project_path: + self.subject_description.setPlainText( + "❌ 错误:没有打开的项目\n\n" + "请先通过 文件 -> 打开 菜单打开一个项目,\n" + "然后再使用维修系统功能。" + ) + else: + self.subject_description.setPlainText( + f"❌ 错误:找不到Subjects目录\n\n" + f"项目路径: {self.project_path}\n" + f"期望目录: {self.subjects_path}\n\n" + "请在项目根目录下创建Subjects文件夹,\n" + "并在其中放置科目的JSON配置文件。" + ) + return + + # 扫描JSON文件 + json_files = [] + try: + for file in os.listdir(self.subjects_path): + if file.endswith('.json'): + json_path = os.path.join(self.subjects_path, file) + json_files.append((file, json_path)) + except Exception as e: + print(f"❌ 扫描Subjects目录失败: {e}") + item = QListWidgetItem("❌ 读取目录失败") + item.setData(Qt.UserRole, None) + self.subject_list.addItem(item) + return + + if not json_files: + item = QListWidgetItem("📄 目录中没有JSON文件") + item.setData(Qt.UserRole, None) + self.subject_list.addItem(item) + self.subject_description.setPlainText( + f"ℹ️ 提示:Subjects目录为空\n\n" + f"目录路径: {self.subjects_path}\n\n" + "请在此目录中放置维修科目的JSON配置文件。" + ) + return + + # 加载科目 + loaded_count = 0 + for filename, filepath in json_files: + try: + with open(filepath, 'r', encoding='utf-8') as f: + subject_data = json.load(f) + + # 获取科目名称 + subject_name = subject_data.get('name', filename.replace('.json', '')) + + # 创建列表项 + item = QListWidgetItem(f"📋 {subject_name}") + item.setData(Qt.UserRole, { + 'path': filepath, + 'data': subject_data, + 'filename': filename + }) + self.subject_list.addItem(item) + loaded_count += 1 + + except Exception as e: + print(f"❌ 加载科目文件失败 {filename}: {e}") + item = QListWidgetItem(f"❌ {filename} (加载失败)") + item.setData(Qt.UserRole, None) + self.subject_list.addItem(item) + + print(f"✅ 成功加载 {loaded_count} 个维修科目") + + def on_subject_selected(self, item): + """处理科目选择""" + try: + print(f"📋 科目选择事件触发,item: {item}") + + if not item: + print("❌ item为空") + self.current_subject_path = None + self.start_btn.setEnabled(False) + self.subject_description.setPlainText("请选择一个有效的科目。") + return + + subject_info = item.data(Qt.UserRole) + print(f"📋 科目信息: {subject_info}") + + if not subject_info: + print("❌ 科目信息为空") + self.current_subject_path = None + self.start_btn.setEnabled(False) + self.subject_description.setPlainText("此科目无法加载,请检查文件格式。") + return + + self.current_subject_path = subject_info['path'] + self.start_btn.setEnabled(True) + + print(f"✅ 选择科目: {subject_info.get('filename', 'unknown')}") + + # 显示科目详情 + subject_data = subject_info['data'] + description = self.format_subject_description(subject_data, subject_info['filename']) + self.subject_description.setPlainText(description) + + print("✅ 科目详情显示完成") + + except Exception as e: + print(f"❌ 科目选择处理失败: {e}") + import traceback + traceback.print_exc() + + self.current_subject_path = None + self.start_btn.setEnabled(False) + self.subject_description.setPlainText(f"处理科目信息时出错:\n{str(e)}") + + # 显示错误对话框 + QMessageBox.critical(self, "错误", f"处理科目选择时出错:\n{str(e)}") + + def format_subject_description(self, subject_data, filename): + """格式化科目描述""" + try: + print(f"🔧 格式化科目描述: {filename}") + + if not subject_data: + return "❌ 科目数据为空" + + lines = [] + + # 基本信息 + name = subject_data.get('name', '未知科目') + lines.append(f"📋 科目: {name}") + lines.append(f"📁 文件: {filename}") + lines.append("") + + # 描述信息 + if 'description' in subject_data and subject_data['description']: + lines.append(f"📝 描述: {subject_data['description']}") + lines.append("") + + # 步骤信息 + steps = subject_data.get('steps', []) + if steps and isinstance(steps, list): + lines.append(f"📊 步骤数量: {len(steps)}") + + # 计算总分 + try: + total_score = 0 + for step in steps: + if isinstance(step, dict): + score = step.get('score', 0) + if isinstance(score, (int, float)): + total_score += score + + if total_score > 0: + lines.append(f"💯 总分: {total_score} 分") + except Exception as e: + print(f"⚠️ 计算总分时出错: {e}") + + lines.append("") + lines.append("📋 步骤列表:") + + for i, step in enumerate(steps, 1): + try: + if isinstance(step, dict): + step_name = step.get('name', f'步骤{i}') + step_score = step.get('score', 0) + + if isinstance(step_score, (int, float)) and step_score > 0: + lines.append(f" {i}. {step_name} ({step_score}分)") + else: + lines.append(f" {i}. {step_name}") + else: + lines.append(f" {i}. 步骤{i} (格式错误)") + except Exception as e: + print(f"⚠️ 处理步骤{i}时出错: {e}") + lines.append(f" {i}. 步骤{i} (处理错误)") + else: + lines.append("⚠️ 此科目没有配置步骤") + + # 工具信息 + tools = subject_data.get('tools', []) + if tools and isinstance(tools, list): + try: + valid_tools = [str(tool) for tool in tools if tool] + if valid_tools: + lines.append("") + lines.append(f"🔧 可用工具: {', '.join(valid_tools)}") + except Exception as e: + print(f"⚠️ 处理工具列表时出错: {e}") + lines.append("") + lines.append("🔧 可用工具: (处理错误)") + + result = '\n'.join(lines) + print(f"✅ 科目描述格式化完成,长度: {len(result)}") + return result + + except Exception as e: + print(f"❌ 格式化科目描述失败: {e}") + import traceback + traceback.print_exc() + + return f"❌ 格式化科目描述时出错:\n{str(e)}\n\n文件: {filename}" + + def on_start(self): + """开始科目""" + if not self.current_subject_path: + QMessageBox.warning(self, "错误", "请先选择一个科目!") + return + + # 获取选择的模式 + mode = "training" if self.training_radio.isChecked() else "exam" + + print(f"🚀 开始维修科目: {self.current_subject_path}, 模式: {mode}") + + # 发射信号 + self.subject_selected.emit(self.current_subject_path, mode) + self.accept() + + +class MaintenanceSystemManager: + """维修系统管理器""" + + def __init__(self, world): + self.world = world + self.login_dialog = None + self.subject_dialog = None + + def show_maintenance_system(self, parent_window): + """显示维修系统""" + # 创建登录对话框 + self.login_dialog = MaintenanceLoginDialog(parent_window) + self.login_dialog.login_success.connect( + lambda: self.show_subject_selection(parent_window) + ) + self.login_dialog.exec_() + + def show_subject_selection(self, parent_window): + """显示科目选择界面""" + # 获取当前项目路径 + project_path = None + if hasattr(self.world, 'project_manager') and self.world.project_manager: + project_path = self.world.project_manager.getCurrentProjectPath() + + # 创建科目选择对话框 + self.subject_dialog = MaintenanceSubjectDialog(project_path, parent_window) + self.subject_dialog.subject_selected.connect(self.start_maintenance_subject) + self.subject_dialog.exec_() + + def start_maintenance_subject(self, subject_path, mode): + """开始维修科目""" + try: + print(f"🎯 启动维修科目: {subject_path}") + print(f"📝 模式: {mode}") + + # 加载科目配置 + with open(subject_path, 'r', encoding='utf-8') as f: + subject_config = json.load(f) + + # 启动拆装交互系统 + if hasattr(self.world, 'assembly_interaction') and self.world.assembly_interaction: + # 设置配置 + self.world.assembly_interaction.config_data = subject_config + + # 启动交互模式 + self.world.assembly_interaction.start_interaction_mode(mode=mode) + + print(f"✅ 维修科目启动成功") + else: + print("❌ 错误:拆装交互系统未初始化") + + except Exception as e: + print(f"❌ 启动维修科目失败: {e}") + import traceback + traceback.print_exc() \ No newline at end of file diff --git a/ui/property_panel.py b/ui/property_panel.py index cf0942f6..46204ee9 100644 --- a/ui/property_panel.py +++ b/ui/property_panel.py @@ -7,18 +7,19 @@ from typing import Hashable from PyQt5.QtGui import QColor from PyQt5.QtWidgets import (QLabel, QLineEdit, QDoubleSpinBox, QPushButton, QTreeWidget, QTreeWidgetItem, QMenu, QCheckBox, QComboBox, QHBoxLayout, QWidget, - QVBoxLayout, QGroupBox, QGridLayout, QSpinBox, QFileDialog, QMessageBox) + QVBoxLayout, QGroupBox, QGridLayout, QSpinBox, QFileDialog, QMessageBox, QSizePolicy) from PyQt5.QtCore import Qt from deploy_libs.unicodedata import normalize from direct.actor.Actor import Actor from direct.gui import DirectGui +from direct.task.TaskManagerGlobal import taskMgr from idna import check_label from jinja2.compiler import has_safe_repr from panda3d.core import Vec3, Vec4, transpose, TransparencyAttrib, PartGroup, ColorAttrib, NodePath, Point3 from scene import util from direct.gui.DirectGui import DirectLabel, DirectFrame from panda3d.core import TextNode - +from ui.icon_manager import get_icon_manager, has_icon, get_icon class PropertyPanelManager: """属性面板管理器""" @@ -30,6 +31,8 @@ class PropertyPanelManager: self._actor_cache = {} self._spherical_video_controls = {} + self.column_minimum_width = 85 + # 初始化地形编辑参数 if not hasattr(self.world, 'terrain_edit_radius'): self.world.terrain_edit_radius = 3.0 @@ -41,24 +44,552 @@ class PropertyPanelManager: # 初始化碰撞参数加载标志位 self._loading_collision_params = False - # 定义紧凑样式 + # 定义现代化紧凑样式 self.compact_style = """ - QDoubleSpinBox { - min-width: 45px; + QDoubleSpinBox, QSpinBox { + min-width: 60px; + background-color: rgba(89, 100, 113, 0.15); + border: 1px solid rgba(76, 92, 110, 0.4); + border-radius: 4px; + padding: 4px 6px; + font-size: 10px; + + } + QDoubleSpinBox:focus, QSpinBox:focus { + border: 1px solid #4d74bd; + background-color: rgba(77, 116, 189, 0.1); + } + QDoubleSpinBox::up-button, QSpinBox::up-button { + background-color: rgba(77, 116, 189, 0.2); + border: none; + border-radius: 2px; + width: 14px; + 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: 14px; + 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: 10px; + height: 10px; + } + QDoubleSpinBox::down-arrow, QSpinBox::down-arrow { + image: url(icons/down_arrows.png); + width: 10px; + height: 10px; + } + 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); } QPushButton { - min-width: 10px; + min-width: 60px; + 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: 6px 10px; + font-size: 10px; + font-weight: 400; + } + 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); } QComboBox { - min-width: 60px; + min-width: 80px; + background-color: rgba(89, 100, 113, 0.15); + border: 1px solid rgba(76, 92, 110, 0.4); + border-radius: 4px; + padding: 4px 6px; + font-size: 10px; + } + QComboBox:focus { + border: 1px solid #4d74bd; + } + 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: 16px; + } + QComboBox::drop-down:hover { + background-color: rgba(77, 116, 189, 0.4); + } + QComboBox::down-arrow { + image: url(icons/down_arrows.png); + width: 10px; + height: 10px; } QLineEdit { - min-width: 60px; + min-width: 80px; + background-color: rgba(89, 100, 113, 0.15); + border: 1px solid rgba(76, 92, 110, 0.4); + border-radius: 4px; + padding: 4px 6px; + font-size: 10px; + } + QLineEdit:focus { + border: 1px solid #4d74bd; + background-color: rgba(77, 116, 189, 0.1); } QCheckBox { min-width: 20px; + font-size: 10px; + spacing: 6px; + } + QCheckBox::indicator { + width: 14px; + height: 14px; + border: 1px solid rgba(76, 92, 110, 0.6); + border-radius: 3px; + background-color: rgba(89, 100, 113, 0.15); + } + QCheckBox::indicator:checked { + background-color: #4d74bd; + border: 1px solid #4d74bd; } """ + self.compact_style += """ + QGroupBox { + margin-left: 0px; + padding-left: 0px; + } + QGroupBox::title { + left: 0px; + padding: 8px 5px 5px 0; + margin-top: 0px; + } + """ + + # 定义现代化状态徽章样式 + self.badge_style_blue = """ + QLabel { + background-color: rgba(77, 116, 189, 0.15); + border: 1px solid rgba(77, 116, 189, 0.6); + color: #4d74bd; + border-radius: 6px; + padding: 4px 10px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 9px; + font-weight: 500; + letter-spacing: 0.5px; + } + """ + + self.badge_style_green = """ + QLabel { + background-color: rgba(45, 255, 196, 0.15); + border: 1px solid rgba(45, 255, 196, 0.6); + color: #2dffc4; + border-radius: 6px; + padding: 4px 10px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 9px; + font-weight: 500; + letter-spacing: 0.5px; + } + """ + + # 添加新的徽章样式 + self.badge_style_orange = """ + QLabel { + background-color: rgba(255, 165, 0, 0.15); + border: 1px solid rgba(255, 165, 0, 0.6); + color: #ffa500; + border-radius: 6px; + padding: 4px 10px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 9px; + font-weight: 500; + letter-spacing: 0.5px; + } + """ + + self.badge_style_red = """ + QLabel { + background-color: rgba(255, 99, 99, 0.15); + border: 1px solid rgba(255, 99, 99, 0.6); + color: #ff6363; + border-radius: 6px; + padding: 4px 10px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 9px; + font-weight: 500; + letter-spacing: 0.5px; + } + """ + + # 固定宽度的徽章样式(用于碰撞检测状态) + self.badge_style_green_fixed = """ + QLabel { + background-color: rgba(45, 255, 196, 0.15); + border: 1px solid rgba(45, 255, 196, 0.6); + color: #2dffc4; + border-radius: 6px; + padding: 4px 10px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 9px; + font-weight: 500; + letter-spacing: 0.5px; + min-width: 50px; + max-width: 50px; + text-align: center; + } + """ + + self.badge_style_red_fixed = """ + QLabel { + background-color: rgba(255, 99, 99, 0.15); + border: 1px solid rgba(255, 99, 99, 0.6); + color: #ff6363; + border-radius: 6px; + padding: 4px 10px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 9px; + font-weight: 500; + letter-spacing: 0.5px; + min-width: 50px; + max-width: 50px; + text-align: center; + } + """ + + def createStatusBadge(self, text, badge_type="green"): + """创建现代化状态徽章 + + Args: + text: 徽章文字 + badge_type: 徽章类型,支持 "green", "blue", "orange", "red" + + Returns: + QLabel: 配置好样式的标签 + """ + badge = QLabel(text) + if badge_type == "green": + badge.setStyleSheet(self.badge_style_green) + elif badge_type == "blue": + badge.setStyleSheet(self.badge_style_blue) + elif badge_type == "orange": + badge.setStyleSheet(self.badge_style_orange) + elif badge_type == "red": + badge.setStyleSheet(self.badge_style_red) + else: + badge.setStyleSheet(self.badge_style_blue) # 默认蓝色 + return badge + + def createFixedStatusBadge(self, text, badge_type="green"): + """创建固定宽度的状态徽章(用于碰撞检测等需要保持一致宽度的场景) + + Args: + text: 徽章文字 + badge_type: 徽章类型,支持 "green", "red" + + Returns: + QLabel: 配置好固定宽度样式的标签 + """ + badge = QLabel(text) + badge.setAlignment(Qt.AlignCenter) # 确保文字居中 + + if badge_type == "green": + badge.setStyleSheet(self.badge_style_green_fixed) + elif badge_type == "red": + badge.setStyleSheet(self.badge_style_red_fixed) + else: + badge.setStyleSheet(self.badge_style_green_fixed) # 默认绿色 + + return badge + + def createModernButton(self, text, button_type="default"): + """创建现代化按钮 + + Args: + text: 按钮文字 + button_type: 按钮类型 - "default", "primary", "success", "warning", "danger" + + Returns: + QPushButton: 配置好样式的按钮 + """ + button = QPushButton(text) + + if button_type == "primary": + button.setStyleSheet(""" + QPushButton { + background-color: rgba(77, 116, 189, 0.8); + color: #ffffff; + border: 1px solid #4d74bd; + border-radius: 4px; + padding: 8px 12px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 10px; + font-weight: 500; + min-height: 16px; + } + QPushButton:hover { + background-color: rgba(77, 116, 189, 1.0); + border: 1px solid rgba(77, 116, 189, 1.0); + } + QPushButton:pressed { + background-color: rgba(61, 96, 169, 1.0); + border: 1px solid rgba(61, 96, 169, 1.0); + } + """) + elif button_type == "success": + button.setStyleSheet(""" + QPushButton { + background-color: rgba(45, 255, 196, 0.15); + color: #2dffc4; + border: 1px solid rgba(45, 255, 196, 0.6); + 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(45, 255, 196, 0.25); + border: 1px solid #2dffc4; + } + QPushButton:pressed { + background-color: rgba(45, 255, 196, 0.4); + color: #ffffff; + } + """) + elif button_type == "warning": + button.setStyleSheet(""" + QPushButton { + background-color: rgba(255, 165, 0, 0.15); + color: #ffa500; + border: 1px solid rgba(255, 165, 0, 0.6); + 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(255, 165, 0, 0.25); + border: 1px solid #ffa500; + } + QPushButton:pressed { + background-color: rgba(255, 165, 0, 0.4); + color: #ffffff; + } + """) + elif button_type == "danger": + button.setStyleSheet(""" + QPushButton { + background-color: rgba(255, 99, 99, 0.15); + color: #ff6363; + border: 1px solid rgba(255, 99, 99, 0.6); + 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(255, 99, 99, 0.25); + border: 1px solid #ff6363; + } + QPushButton:pressed { + background-color: rgba(255, 99, 99, 0.4); + color: #ffffff; + } + """) + else: # default + # 使用全局默认样式 + pass + return button + + def _collision_button_style(self): + """返回碰撞检测面板按钮统一样式""" + return """ + QPushButton { + background-color: rgba(89, 98, 118, 0.4); + color: rgba(235, 235, 235, 0.85); + border: 1px solid rgba(89, 98, 118, 0.65); + 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, 98, 118, 0.55); + border: 1px solid rgba(89, 98, 118, 0.9); + color: #ffffff; + } + QPushButton:pressed { + background-color: rgba(89, 98, 118, 0.7); + border: 1px solid rgba(89, 98, 118, 0.95); + color: #ffffff; + } + QPushButton:disabled { + background-color: rgba(89, 98, 118, 0.18); + border: 1px solid rgba(89, 98, 118, 0.25); + color: rgba(235, 235, 235, 0.35); + } + """ + + def createModernGroupBox(self, title, parent_layout): + """创建现代化的分组框 + + Args: + title: 分组框标题 + parent_layout: 父布局 + + Returns: + tuple: (QGroupBox, QVBoxLayout) 分组框和其内部布局 + """ + group_box = QGroupBox(title) + group_layout = QVBoxLayout(group_box) + group_layout.setSpacing(0) # 减少内部间距 + group_layout.setContentsMargins(0, 3, 0, 6) # 减少内容边距 + + # 应用现代化样式,使用更紧凑的间距 + group_box.setStyleSheet(""" + QGroupBox { + font-weight: 500; + font-size: 11px; + color: rgba(255, 255, 255, 0.9); + margin-top: 12px; + margin-bottom: 4px; + padding-top: 15px; + border-top: 1px solid rgba(77, 116, 189, 0.3); + } + QGroupBox::title { + subcontrol-origin: padding; + subcontrol-position: top left; + left: 0px; + padding: 8px 5px 5px 0; + margin-top: 0px; + } + """) + + parent_layout.addWidget(group_box) + return group_box, group_layout + + def _getFigmaSpinBoxStyle(self): + """获取符合Figma设计的SpinBox样式""" + return """ + 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: 3px 6px; + font-family: 'Inter', 'Microsoft YaHei', sans-serif; + font-size: 10px; + font-weight: 300; + letter-spacing: 0.5px; + min-width: 70px; + max-width: 70px; + min-height: 18px; + max-height: 18px; + } + 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.25); + } + QDoubleSpinBox::up-button, QSpinBox::up-button { + background-color: rgba(77, 116, 189, 0.2); + border: none; + border-radius: 2px; + width: 14px; + 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: 14px; + 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: 10px; + height: 10px; + } + QDoubleSpinBox::down-arrow, QSpinBox::down-arrow { + image: url(icons/down_arrows.png); + width: 10px; + height: 10px; + } + """ + + def createPropertyRow(self, label_text, widget, layout, tooltip=None): + """创建属性行 + + Args: + label_text: 标签文字 + widget: 控件 + layout: 布局 + tooltip: 工具提示 + """ + row_layout = QHBoxLayout() + row_layout.setContentsMargins(0, 0, 0, 0) + row_layout.setSpacing(8) + + # 创建标签 + label = QLabel(label_text) + label.setMinimumWidth(60) + label.setStyleSheet(""" + QLabel { + color: rgba(255, 255, 255, 0.8); + font-size: 10px; + font-weight: 300; + } + """) + + if tooltip: + label.setToolTip(tooltip) + widget.setToolTip(tooltip) + + row_layout.addWidget(label) + row_layout.addWidget(widget) + row_layout.addStretch() + + layout.addLayout(row_layout) def setPropertyLayout(self, layout): """设置属性面板布局引用""" @@ -137,7 +668,8 @@ class PropertyPanelManager: model.setPythonTag("user_visible", True) self.name_group = QGroupBox("物体名称") - name_layout = QHBoxLayout() + self.name_group.setProperty("groupRole", "first") + name_layout = QGridLayout() self.active_check = QCheckBox() # 根据模型的实际可见性状态设置复选框 self.active_check.setChecked(user_visible) @@ -147,8 +679,9 @@ class PropertyPanelManager: lambda: self.world.treeWidget.update_item_name(self.name_input.text(), item) ) - name_layout.addWidget(self.active_check) - name_layout.addWidget(self.name_input) + name_layout.addWidget(self.active_check, 0, 0) + name_layout.setColumnMinimumWidth(0, self.column_minimum_width) + name_layout.addWidget(self.name_input, 0, 1, 1, 3) self.name_group.setLayout(name_layout) self._propertyLayout.addWidget(self.name_group) @@ -219,19 +752,20 @@ class PropertyPanelManager: info_group = QGroupBox("地形信息") info_layout = QGridLayout() + info_layout.setColumnMinimumWidth(0, self.column_minimum_width) info_layout.addWidget(QLabel("名称:"), 0, 0) name_label = QLabel(terrain_info.get('name', '未知')) - info_layout.addWidget(name_label, 0, 1) + info_layout.addWidget(name_label, 0, 1, 1, 3) info_layout.addWidget(QLabel("类型:"), 1, 0) type_label = QLabel("高度图地形" if terrain_info.get('heightmap') else "平面地形") - info_layout.addWidget(type_label, 1, 1) + info_layout.addWidget(type_label, 1, 1, 1, 3) if terrain_info.get('heightmap'): info_layout.addWidget(QLabel("高度图:"), 2, 0) heightmap_label = QLabel(os.path.basename(terrain_info['heightmap'])) heightmap_label.setWordWrap(True) - info_layout.addWidget(heightmap_label, 2, 1) + info_layout.addWidget(heightmap_label, 2, 1, 1, 3) info_group.setLayout(info_layout) self._propertyLayout.addWidget(info_group) @@ -269,22 +803,16 @@ class PropertyPanelManager: try: transform_group = QGroupBox("变换 Transform") transform_layout = QGridLayout() + transform_layout.setColumnMinimumWidth(0, self.column_minimum_width) pos = terrain_node.getPos() scale = terrain_node.getScale() transform_layout.addWidget(QLabel("位置"), 0, 0) - x_label = QLabel("X") - y_label = QLabel("Y") - z_label = QLabel("Z") - x_label.setAlignment(Qt.AlignCenter) - y_label.setAlignment(Qt.AlignCenter) - z_label.setAlignment(Qt.AlignCenter) - - transform_layout.addWidget(x_label, 0, 1) - transform_layout.addWidget(y_label, 0, 2) - transform_layout.addWidget(z_label, 0, 3) + position_row = QHBoxLayout() + position_row.setContentsMargins(0, 0, 0, 0) + position_row.setSpacing(4) self.pos_x = QDoubleSpinBox() self.pos_y = QDoubleSpinBox() @@ -314,11 +842,20 @@ class PropertyPanelManager: self.pos_z.valueChanged.connect(updateZPosition) - transform_layout.addWidget(self.pos_x, 1, 1) - transform_layout.addWidget(self.pos_y, 1, 2) - transform_layout.addWidget(self.pos_z, 1, 3) + for axis_label, widget in (("X:", self.pos_x), ("Y:", self.pos_y), ("Z:", self.pos_z)): + axis = QLabel(axis_label) + axis.setAlignment(Qt.AlignVCenter) + axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + position_row.addWidget(axis) + position_row.addWidget(widget) + + transform_layout.addLayout(position_row, 0, 1, 1, 3) + + transform_layout.addWidget(QLabel("旋转"), 1, 0) + rotation_row = QHBoxLayout() + rotation_row.setContentsMargins(0, 0, 0, 0) + rotation_row.setSpacing(4) - transform_layout.addWidget(QLabel("旋转"), 2, 0) self.rot_x = QDoubleSpinBox() self.rot_y = QDoubleSpinBox() self.rot_z = QDoubleSpinBox() @@ -334,21 +871,21 @@ class PropertyPanelManager: self.rot_y.valueChanged.connect(lambda v: terrain_node.setP(v)) self.rot_z.valueChanged.connect(lambda v: terrain_node.setR(v)) - h_label = QLabel("H") - p_label = QLabel("P") - r_label = QLabel("R") - h_label.setAlignment(Qt.AlignCenter) - p_label.setAlignment(Qt.AlignCenter) - r_label.setAlignment(Qt.AlignCenter) + for axis_label, widget in (("H:", self.rot_x), ("P:", self.rot_y), ("R:", self.rot_z)): + axis = QLabel(axis_label) + axis.setAlignment(Qt.AlignVCenter) + axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + rotation_row.addWidget(axis) + rotation_row.addWidget(widget) - transform_layout.addWidget(h_label, 2, 1) - transform_layout.addWidget(p_label, 2, 2) - transform_layout.addWidget(r_label, 2, 3) - transform_layout.addWidget(self.rot_x, 3, 1) - transform_layout.addWidget(self.rot_y, 3, 2) - transform_layout.addWidget(self.rot_z, 3, 3) + transform_layout.addLayout(rotation_row, 1, 1, 1, 3) + + transform_layout.addWidget(QLabel("缩放"), 2, 0) + + scale_row = QHBoxLayout() + scale_row.setContentsMargins(0, 0, 0, 0) + scale_row.setSpacing(4) - transform_layout.addWidget(QLabel("缩放"), 4, 0) self.scale_x = QDoubleSpinBox() self.scale_y = QDoubleSpinBox() self.scale_z = QDoubleSpinBox() @@ -364,19 +901,14 @@ class PropertyPanelManager: self.scale_y.valueChanged.connect(lambda v: self._updateYScale(terrain_node, v)) self.scale_z.valueChanged.connect(lambda v: self._updateZScale(terrain_node, v)) - x_label2 = QLabel("X") - y_label2 = QLabel("Y") - z_label2 = QLabel("Z") - x_label2.setAlignment(Qt.AlignCenter) - y_label2.setAlignment(Qt.AlignCenter) - z_label2.setAlignment(Qt.AlignCenter) + for axis_label, widget in (("X:", self.scale_x), ("Y:", self.scale_y), ("Z:", self.scale_z)): + axis = QLabel(axis_label) + axis.setAlignment(Qt.AlignVCenter) + axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + scale_row.addWidget(axis) + scale_row.addWidget(widget) - transform_layout.addWidget(x_label2, 4, 1) - transform_layout.addWidget(y_label2, 4, 2) - transform_layout.addWidget(z_label2, 4, 3) - transform_layout.addWidget(self.scale_x, 5, 1) - transform_layout.addWidget(self.scale_y, 5, 2) - transform_layout.addWidget(self.scale_z, 5, 3) + transform_layout.addLayout(scale_row, 2, 1, 1, 3) transform_group.setLayout(transform_layout) self._propertyLayout.addWidget(transform_group) @@ -387,6 +919,7 @@ class PropertyPanelManager: try: edit_group = QGroupBox("地形编辑") edit_layout = QGridLayout() + edit_layout.setColumnMinimumWidth(0, self.column_minimum_width) # 编辑半径 edit_layout.addWidget(QLabel("编辑半径:"), 0, 0) @@ -395,7 +928,7 @@ class PropertyPanelManager: self.terrain_radius_spin.setSingleStep(0.5) self.terrain_radius_spin.setValue(getattr(self.world, 'terrain_edit_radius', 3.0)) self.terrain_radius_spin.valueChanged.connect(self._onTerrainRadiusChanged) - edit_layout.addWidget(self.terrain_radius_spin, 0, 1) + edit_layout.addWidget(self.terrain_radius_spin, 0, 1, 1, 3) # 编辑强度 edit_layout.addWidget(QLabel("编辑强度:"), 1, 0) @@ -404,7 +937,7 @@ class PropertyPanelManager: self.terrain_strength_spin.setSingleStep(0.1) self.terrain_strength_spin.setValue(getattr(self.world, 'terrain_edit_strength', 0.3)) self.terrain_strength_spin.valueChanged.connect(self._onTerrainStrengthChanged) - edit_layout.addWidget(self.terrain_strength_spin, 1, 1) + edit_layout.addWidget(self.terrain_strength_spin, 1, 1, 1, 3) edit_layout.addWidget(QLabel("操作类型:"), 2, 0) self.terrain_operation_combo = QComboBox() @@ -417,12 +950,12 @@ class PropertyPanelManager: else: self.terrain_operation_combo.setCurrentText("平坦化") self.terrain_operation_combo.currentTextChanged.connect(self._onTerrainOperationChanged) - edit_layout.addWidget(self.terrain_operation_combo, 2, 1) + edit_layout.addWidget(self.terrain_operation_combo, 2, 1, 1, 3) help_label = QLabel("在地形编辑模式下,使用鼠标左键升高地形,右键降低地形") help_label.setWordWrap(True) help_label.setStyleSheet("font-size:10px;color:gray;") - edit_layout.addWidget(help_label, 3, 0, 1, 2) + edit_layout.addWidget(help_label, 3, 0, 1, 4) edit_group.setLayout(edit_layout) self._propertyLayout.addWidget(edit_group) @@ -461,15 +994,17 @@ class PropertyPanelManager: try: material_group = QGroupBox("材质属性") material_layout = QGridLayout() + material_layout.setColumnMinimumWidth(0, self.column_minimum_width) # 颜色设置 material_layout.addWidget(QLabel("颜色:"), 0, 0) color_button = QPushButton("选择颜色") color_button.clicked.connect(lambda: self._selectTerrainColor(terrain_info)) + material_layout.addWidget(color_button, 0, 1, 1, 3) # 纹理设置 material_layout.addWidget(QLabel("纹理:"), 1, 0) texture_button = QPushButton("选择纹理") texture_button.clicked.connect(lambda: self._selectTerrainTexture(terrain_info)) - material_layout.addWidget(texture_button, 1, 1) + material_layout.addWidget(texture_button, 1, 1, 1, 3) material_group.setLayout(material_layout) self._propertyLayout.addWidget(material_group) @@ -570,7 +1105,7 @@ class PropertyPanelManager: # 清理碰撞相关控件引用 collision_controls = [ - 'collision_status_text', 'collision_shape_combo', 'collision_shape_label', + 'collision_status_badge', 'collision_shape_combo', 'collision_shape_label', 'collision_visibility_button', 'collision_button', 'collision_layout', 'collision_group', 'collision_pos_x', 'collision_pos_y', 'collision_pos_z', 'collision_radius', 'collision_width', 'collision_length', 'collision_height', @@ -909,22 +1444,33 @@ class PropertyPanelManager: print(f"显示 Cesium tileset 属性: {nodePath.getName()}") # 标题 - title = QLabel("Cesium 3D Tiles") - title.setStyleSheet("font-size: 14px; font-weight: bold; margin: 10px 0;") - self._propertyLayout.addWidget(title) + tileset_group = QGroupBox("Cesium 3D Tiles") + tileset_layout = QGridLayout() + tileset_layout.setColumnMinimumWidth(0, self.column_minimum_width) + # self._propertyLayout.addWidget(title) # URL 信息 if nodePath.hasTag("tileset_url"): url_label = QLabel("URL:") + tileset_layout.addWidget(url_label, 0, 0) url_value = QLabel(nodePath.getTag("tileset_url")) url_value.setWordWrap(True) url_value.setStyleSheet("font-size: 9px; color: #666;") - self._propertyLayout.addWidget(url_label) - self._propertyLayout.addWidget(url_value) + tileset_layout.addWidget(url_value, 0, 1, 1, 3) + # self._propertyLayout.addWidget(url_label) + # self._propertyLayout.addWidget(url_value) + tileset_group.setLayout(tileset_layout) + self._propertyLayout.addWidget(tileset_group) # 位置控制 pos_group = QGroupBox("位置") - pos_layout = QFormLayout() + pos_layout = QGridLayout() + pos_layout.setColumnMinimumWidth(0, self.column_minimum_width) + + pos_row = QHBoxLayout() + pos_row.setContentsMargins(0, 0, 0, 0) + pos_row.setSpacing(4) + pos_layout.addWidget(QLabel("位置"), 0, 0) # X 坐标 x_spin = QDoubleSpinBox() @@ -934,7 +1480,11 @@ class PropertyPanelManager: pos = nodePath.getPos() x_spin.setValue(pos.getX()) x_spin.valueChanged.connect(lambda v: self._updateTilesetPosition(nodePath, 'x', v)) - pos_layout.addRow("X:", x_spin) + x_label = QLabel("X:") + x_label.setAlignment(Qt.AlignVCenter) + x_label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + pos_row.addWidget(x_label) + pos_row.addWidget(x_spin) # Y 坐标 y_spin = QDoubleSpinBox() @@ -943,7 +1493,11 @@ class PropertyPanelManager: y_spin.setDecimals(2) y_spin.setValue(pos.getY()) y_spin.valueChanged.connect(lambda v: self._updateTilesetPosition(nodePath, 'y', v)) - pos_layout.addRow("Y:", y_spin) + y_label = QLabel("Y:") + y_label.setAlignment(Qt.AlignVCenter) + y_label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + pos_row.addWidget(y_label) + pos_row.addWidget(y_spin) # Z 坐标 z_spin = QDoubleSpinBox() @@ -952,25 +1506,28 @@ class PropertyPanelManager: z_spin.setDecimals(2) z_spin.setValue(pos.getZ()) z_spin.valueChanged.connect(lambda v: self._updateTilesetPosition(nodePath, 'z', v)) - pos_layout.addRow("Z:", z_spin) + z_label = QLabel("Z:") + z_label.setAlignment(Qt.AlignVCenter) + z_label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + pos_row.addWidget(z_label) + pos_row.addWidget(z_spin) + + pos_layout.addLayout(pos_row, 0, 1, 1, 3) pos_group.setLayout(pos_layout) - self._propertyLayout.addWidget(pos_group) # 缩放控制 - scale_group = QGroupBox("缩放") - scale_layout = QFormLayout() - + scale_label = QLabel("缩放") + pos_layout.addWidget(scale_label, 1, 0) scale_spin = QDoubleSpinBox() scale_spin.setRange(0.01, 1000) scale_spin.setSingleStep(0.1) scale_spin.setDecimals(2) scale_spin.setValue(nodePath.getScale().getX()) scale_spin.valueChanged.connect(lambda v: self._updateTilesetScale(nodePath, v)) - scale_layout.addRow("缩放:", scale_spin) + pos_layout.addWidget(scale_spin, 1, 1, 1, 3) - scale_group.setLayout(scale_layout) - self._propertyLayout.addWidget(scale_group) + self._propertyLayout.addWidget(pos_group) # # 删除按钮 # delete_btn = QPushButton("删除 Tileset") @@ -1126,13 +1683,25 @@ class PropertyPanelManager: # 变换属性部分 (Transform) self.transform_group = QGroupBox("变换 Transform") transform_layout = QGridLayout() + transform_layout.setColumnMinimumWidth(0, self.column_minimum_width) + # transform_layout.setColumnStretch(1, 1) + # transform_layout.setColumnStretch(2, 1) + # transform_layout.setColumnStretch(3, 1) + # 获取当前值 relativePos = model.getPos(parent) if parent else model.getPos() worldPos = model.getPos(self.world.render) + position_label = QLabel("位置") + transform_layout.addWidget(position_label, 0, 0) + # 位置 (Position) - transform_layout.addWidget(QLabel("相对位置"), 0, 0) + position_row = QHBoxLayout() + position_row.setContentsMargins(0, 0, 0, 0) + position_row.setSpacing(4) + # position_row.addWidget(QLabel("位置")) + self.pos_x = QDoubleSpinBox() self.pos_y = QDoubleSpinBox() self.pos_z = QDoubleSpinBox() @@ -1146,10 +1715,6 @@ class PropertyPanelManager: self.pos_z.setValue(relativePos.getZ()) # 连接位置变化事件 - # self.pos_x.valueChanged.connect(lambda v: model.setX(parent, v) if parent else model.setX(v)) - # self.pos_y.valueChanged.connect(lambda v: model.setY(parent, v) if parent else model.setY(v)) - # self.pos_z.valueChanged.connect(lambda v: model.setZ(parent, v) if parent else model.setZ(v)) - def updateXPosition(value): model.setX(value) self.refreshModelValues(model) @@ -1168,28 +1733,28 @@ class PropertyPanelManager: self.pos_z.valueChanged.connect(updateZPosition) - # 创建并设置 X, Y, Z 标签居中 - x_label1 = QLabel("X") - y_label1 = QLabel("Y") - z_label1 = QLabel("Z") - x_label1.setAlignment(Qt.AlignCenter) - y_label1.setAlignment(Qt.AlignCenter) - z_label1.setAlignment(Qt.AlignCenter) + # 水平排布 X, Y, Z + for axis_label, widget in (("X:", self.pos_x), ("Y:", self.pos_y), ("Z:", self.pos_z)): + axis = QLabel(axis_label) + axis.setAlignment(Qt.AlignVCenter) + axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + position_row.addWidget(axis) + position_row.addWidget(widget) - transform_layout.addWidget(x_label1, 0, 1) - transform_layout.addWidget(y_label1, 0, 2) - transform_layout.addWidget(z_label1, 0, 3) - transform_layout.addWidget(self.pos_x, 1, 1) - transform_layout.addWidget(self.pos_y, 1, 2) - transform_layout.addWidget(self.pos_z, 1, 3) + transform_layout.addLayout(position_row, 0, 1, 1, 3) # 世界位置 (只读) - transform_layout.addWidget(QLabel("世界位置"), 2, 0) + world_position_row = QHBoxLayout() + world_position_row.setContentsMargins(0, 0, 0, 0) + world_position_row.setSpacing(4) + # world_position_row.addWidget(QLabel("世界位置")) + world_position_label = QLabel("世界位置") + transform_layout.addWidget(world_position_label, 1, 0) + self.world_pos_x = QDoubleSpinBox() self.world_pos_y = QDoubleSpinBox() self.world_pos_z = QDoubleSpinBox() - # 设置世界位置控件属性 for world_pos_widget in [self.world_pos_x, self.world_pos_y, self.world_pos_z]: world_pos_widget.setRange(-1000000.0, 1000000.0) world_pos_widget.setReadOnly(True) @@ -1198,17 +1763,26 @@ class PropertyPanelManager: self.world_pos_y.setValue(worldPos.getY()) self.world_pos_z.setValue(worldPos.getZ()) - transform_layout.addWidget(self.world_pos_x, 3, 1) - transform_layout.addWidget(self.world_pos_y, 3, 2) - transform_layout.addWidget(self.world_pos_z, 3, 3) + for axis_label, widget in (("X:", self.world_pos_x), ("Y:", self.world_pos_y), ("Z:", self.world_pos_z)): + axis = QLabel(axis_label) + axis.setAlignment(Qt.AlignVCenter) + axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + world_position_row.addWidget(axis) + world_position_row.addWidget(widget) + + transform_layout.addLayout(world_position_row, 1, 1, 1, 3) # 旋转 (Rotation) - transform_layout.addWidget(QLabel("旋转"), 4, 0) + rotation_row = QHBoxLayout() + rotation_row.setContentsMargins(0, 0, 0, 0) + rotation_row.setSpacing(4) + rotation_label = QLabel("旋转") + transform_layout.addWidget(rotation_label, 2, 0) + self.rot_x = QDoubleSpinBox() self.rot_y = QDoubleSpinBox() self.rot_z = QDoubleSpinBox() - # 设置旋转控件属性 for rot_widget in [self.rot_x, self.rot_y, self.rot_z]: rot_widget.setRange(-360, 360) @@ -1216,38 +1790,35 @@ class PropertyPanelManager: self.rot_y.setValue(model.getP()) self.rot_z.setValue(model.getR()) - # 连接旋转变化事件 self.rot_x.valueChanged.connect(lambda v: model.setH(v)) self.rot_y.valueChanged.connect(lambda v: model.setP(v)) self.rot_z.valueChanged.connect(lambda v: model.setR(v)) - # 创建并设置 H, P, R 标签居中 - h_label = QLabel("H") - p_label = QLabel("P") - r_label = QLabel("R") - h_label.setAlignment(Qt.AlignCenter) - p_label.setAlignment(Qt.AlignCenter) - r_label.setAlignment(Qt.AlignCenter) + for axis_label, widget in (("H:", self.rot_x), ("P:", self.rot_y), ("R:", self.rot_z)): + axis = QLabel(axis_label) + axis.setAlignment(Qt.AlignVCenter) + axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + rotation_row.addWidget(axis) + rotation_row.addWidget(widget) - transform_layout.addWidget(h_label, 4, 1) - transform_layout.addWidget(p_label, 4, 2) - transform_layout.addWidget(r_label, 4, 3) - transform_layout.addWidget(self.rot_x, 5, 1) - transform_layout.addWidget(self.rot_y, 5, 2) - transform_layout.addWidget(self.rot_z, 5, 3) + transform_layout.addLayout(rotation_row, 2, 1, 1, 3) # 缩放 (Scale) - transform_layout.addWidget(QLabel("缩放"), 6, 0) + scale_row = QHBoxLayout() + scale_row.setContentsMargins(0, 0, 0, 0) + scale_row.setSpacing(4) + scale_label = QLabel("缩放") + transform_layout.addWidget(scale_label, 3, 0) + self.scale_x = QDoubleSpinBox() self.scale_y = QDoubleSpinBox() self.scale_z = QDoubleSpinBox() current_scale = model.getScale() - # 设置缩放控件属性 - for i, (scale_widget, scale_value) in enumerate(zip([self.scale_x, self.scale_y, self.scale_z], - [current_scale.getX(), current_scale.getY(), - current_scale.getZ()])): + for scale_widget, scale_value in zip([self.scale_x, self.scale_y, self.scale_z], + [current_scale.getX(), current_scale.getY(), + current_scale.getZ()]): scale_widget.setRange(-1000, 1000) scale_widget.setSingleStep(0.1) scale_widget.setValue(scale_value) @@ -1256,25 +1827,18 @@ class PropertyPanelManager: self.scale_y.valueChanged.connect(lambda value: self._onScaleValueChanged(self.scale_y, value)) self.scale_z.valueChanged.connect(lambda value: self._onScaleValueChanged(self.scale_z, value)) - # 连接缩放变化事件 self.scale_x.valueChanged.connect(lambda value: self._updateXScale(model, value)) self.scale_y.valueChanged.connect(lambda value: self._updateYScale(model, value)) self.scale_z.valueChanged.connect(lambda value: self._updateZScale(model, value)) - # 创建并设置 X, Y, Z 标签居中 - x_label3 = QLabel("X") - y_label3 = QLabel("Y") - z_label3 = QLabel("Z") - x_label3.setAlignment(Qt.AlignCenter) - y_label3.setAlignment(Qt.AlignCenter) - z_label3.setAlignment(Qt.AlignCenter) + for axis_label, widget in (("X:", self.scale_x), ("Y:", self.scale_y), ("Z:", self.scale_z)): + axis = QLabel(axis_label) + axis.setAlignment(Qt.AlignVCenter) + axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + scale_row.addWidget(axis) + scale_row.addWidget(widget) - transform_layout.addWidget(x_label3, 6, 1) - transform_layout.addWidget(y_label3, 6, 2) - transform_layout.addWidget(z_label3, 6, 3) - transform_layout.addWidget(self.scale_x, 7, 1) - transform_layout.addWidget(self.scale_y, 7, 2) - transform_layout.addWidget(self.scale_z, 7, 3) + transform_layout.addLayout(scale_row, 3, 1, 1, 3) self.transform_group.setLayout(transform_layout) self._propertyLayout.addWidget(self.transform_group) @@ -1362,7 +1926,7 @@ class PropertyPanelManager: gui_element.setPythonTag("user_visible", True) self.name_group = QGroupBox("物体名称") - name_layout = QHBoxLayout() + name_layout = QGridLayout() self.active_check = QCheckBox() # 根据元素的实际可见性状态设置复选框 self.active_check.setChecked(user_visible) @@ -1383,8 +1947,9 @@ class PropertyPanelManager: # 如果失去焦点也更新名称 self.name_input.editingFinished.connect(lambda: updateGUIName(self.name_input.text())) - name_layout.addWidget(self.active_check) - name_layout.addWidget(self.name_input) + name_layout.addWidget(self.active_check, 0, 0) + name_layout.setColumnMinimumWidth(0, self.column_minimum_width) + name_layout.addWidget(self.name_input, 0, 1, 1, 3) self.name_group.setLayout(name_layout) self._propertyLayout.addWidget(self.name_group) @@ -1403,12 +1968,13 @@ class PropertyPanelManager: # GUI基本信息组 gui_info_group = QGroupBox("GUI信息") gui_info_layout = QGridLayout() + gui_info_layout.setColumnMinimumWidth(0, self.column_minimum_width) # GUI类型显示 gui_info_layout.addWidget(QLabel("GUI类型:"), 0, 0) typeValue = QLabel(gui_type) # typeValue.setStyleSheet("color: #00AAFF; font-weight: bold;") - gui_info_layout.addWidget(typeValue, 0, 1) + gui_info_layout.addWidget(typeValue, 0, 1, 1, 3) # 修改 updateGUIPropertyPanel 中的文本属性部分 # 文本属性(如果适用) @@ -1429,7 +1995,7 @@ class PropertyPanelManager: textEdit.returnPressed.connect(updateText) textEdit.editingFinished.connect(updateText) - gui_info_layout.addWidget(textEdit, 1, 1) + gui_info_layout.addWidget(textEdit, 1, 1, 1, 3) gui_info_group.setLayout(gui_info_layout) self._propertyLayout.addWidget(gui_info_group) @@ -1443,6 +2009,7 @@ class PropertyPanelManager: transform_group = QGroupBox("变换 Transform") transform_layout = QGridLayout() + transform_layout.setColumnMinimumWidth(0, self.column_minimum_width) pos = gui_element.getPos() @@ -1452,40 +2019,90 @@ class PropertyPanelManager: logical_x = pos.getX() / 0.1 # 反向转换为逻辑坐标 logical_z = pos.getZ() / 0.1 - # 屏幕位置控件 - transform_layout.addWidget(QLabel("屏幕位置"), 0, 0) + position_row = QHBoxLayout() + position_row.setContentsMargins(0, 0, 0, 0) + position_row.setSpacing(4) - # X, Z 标签居中 - x_label = QLabel("X") - z_label = QLabel("Z") - x_label.setAlignment(Qt.AlignCenter) - z_label.setAlignment(Qt.AlignCenter) - - transform_layout.addWidget(x_label, 0, 1) - transform_layout.addWidget(z_label, 0, 2) + screen_title = QLabel("屏幕位置") + transform_layout.addWidget(screen_title, 0, 0) self.pos_x = QDoubleSpinBox() self.pos_x.setRange(-1000, 1000) self.pos_x.setValue(logical_x) - self.pos_x.valueChanged.connect(lambda v: self.world.gui_manager.editGUI2DPosition(gui_element, "x", v)) - transform_layout.addWidget(self.pos_x, 1, 1) - self.pos_z = QDoubleSpinBox() self.pos_z.setRange(-1000, 1000) self.pos_z.setValue(logical_z) - self.pos_z.valueChanged.connect(lambda v: self.world.gui_manager.editGUI2DPosition(gui_element, "z", v)) - transform_layout.addWidget(self.pos_z, 1, 2) - # 显示实际屏幕坐标(只读) - transform_layout.addWidget(QLabel("实际坐标"), 2, 0) + axis_labels = [] + for col, (axis_label, widget) in enumerate((("X:", self.pos_x), ("Z:", self.pos_z)), start=1): + axis = QLabel(axis_label) + axis.setAlignment(Qt.AlignRight | Qt.AlignVCenter) + axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + axis_labels.append(axis) + position_row.addWidget(axis) + position_row.addWidget(widget) - actualXLabel = QLabel(f"{pos.getX():.3f}") - actualXLabel.setStyleSheet("color: gray; font-size: 10px;") - actualZLabel = QLabel(f"{pos.getZ():.3f}") - actualZLabel.setStyleSheet("color: gray; font-size: 10px;") + transform_layout.addLayout(position_row, 0, 1, 1, 3) - transform_layout.addWidget(actualXLabel, 3, 1) - transform_layout.addWidget(actualZLabel, 3, 2) + # axis_width = max(label.sizeHint().width() for label in axis_labels) + # for label in axis_labels: + # label.setMinimumWidth(axis_width) + + # screen_position_row.setColumnStretch(2, 1) + # screen_position_row.setColumnStretch(4, 1) + + for i in range(4): + transform_layout.setColumnStretch(i, 1) + actual_position_row = QGridLayout() + + actual_row = QHBoxLayout() + actual_row.setContentsMargins(0, 0, 0, 0) + actual_row.setSpacing(4) + + actual_title = QLabel("实际坐标") + # actual_row.addWidget(actual_title) + transform_layout.addWidget(actual_title, 1, 0) + + self.actual_pos_x_label = QLabel(f"{pos.getX():.3f}") + self.actual_pos_x_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) + self.actual_pos_x_label.setStyleSheet("color: gray; font-size: 10px;") + self.actual_pos_z_label = QLabel(f"{pos.getZ():.3f}") + self.actual_pos_z_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) + self.actual_pos_z_label.setStyleSheet("color: gray; font-size: 10px;") + + for col, (axis_label, widget) in enumerate((("X:", self.actual_pos_x_label), + ("Z:", self.actual_pos_z_label)), start=1): + axis = QLabel(axis_label) + axis.setAlignment(Qt.AlignRight | Qt.AlignVCenter) + axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + # axis.setMinimumWidth(axis_width) + # actual_position_row.addWidget(axis, 0, col * 2 - 1, Qt.AlignLeft | Qt.AlignVCenter) + # actual_position_row.addWidget(widget, 0, col * 2, Qt.AlignLeft | Qt.AlignVCenter) + actual_row.addWidget(axis) + actual_row.addWidget(widget) + + # actual_position_row.setColumnStretch(2, 1) + # actual_position_row.setColumnStretch(4, 1) + + transform_layout.addLayout(actual_row, 1, 1, 1, 3) + + def _update_actual_position_labels(): + current_pos = gui_element.getPos() + self.actual_pos_x_label.setText(f"{current_pos.getX():.3f}") + self.actual_pos_z_label.setText(f"{current_pos.getZ():.3f}") + + _update_actual_position_labels() + + def _on_gui_pos_x_changed(value): + self.world.gui_manager.editGUI2DPosition(gui_element, "x", value) + _update_actual_position_labels() + + def _on_gui_pos_z_changed(value): + self.world.gui_manager.editGUI2DPosition(gui_element, "z", value) + _update_actual_position_labels() + + self.pos_x.valueChanged.connect(_on_gui_pos_x_changed) + self.pos_z.valueChanged.connect(_on_gui_pos_z_changed) if gui_type in ["2d_image", "2d_video_screen"]: scale = gui_element.getScale() @@ -1495,93 +2112,101 @@ class PropertyPanelManager: (tuple, list)) and len( scale) > 1 else scale - transform_layout.addWidget(QLabel("宽度"), 4, 0) + scale_row = QHBoxLayout() + scale_row.setContentsMargins(0, 0, 0, 0) + scale_row.setSpacing(4) + transform_layout.addWidget(QLabel("缩放"), 2, 0) + self.scale_x = QDoubleSpinBox() self.scale_x.setRange(0.1, 100) self.scale_x.setSingleStep(0.01) self.scale_x.setValue(width) self.scale_x.valueChanged.connect(lambda v: self.editGUIScale(gui_element, "x", v)) - transform_layout.addWidget(self.scale_x, 4, 1) - transform_layout.addWidget(QLabel("高度"), 4, 2) self.scale_z = QDoubleSpinBox() self.scale_z.setRange(0.1, 10) self.scale_z.setSingleStep(0.01) self.scale_z.setValue(height) self.scale_z.valueChanged.connect(lambda v: self.editGUIScale(gui_element, "z", v)) - transform_layout.addWidget(self.scale_z, 4, 3) + + for axis_label, widget in (("X:", self.scale_x), ("Z:", self.scale_z)): + axis = QLabel(axis_label) + axis.setAlignment(Qt.AlignVCenter) + axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + scale_row.addWidget(axis) + scale_row.addWidget(widget) + + transform_layout.addLayout(scale_row, 2, 1, 1, 3) else: - transform_layout.addWidget(QLabel("缩放"), 4, 0) + scale_row = QHBoxLayout() + scale_row.setContentsMargins(0, 0, 0, 0) + scale_row.setSpacing(4) + transform_layout.addWidget(QLabel("缩放"), 2, 0) + scaleSpinBox = QDoubleSpinBox() scaleSpinBox.setRange(0.01, 100) scaleSpinBox.setSingleStep(0.01) scaleSpinBox.setValue(gui_element.getScale().getX() * 2) scaleSpinBox.valueChanged.connect(lambda v: self._update2DImageScale(gui_element, v)) - transform_layout.addWidget(scaleSpinBox, 4, 1) + + axis = QLabel("X:") + axis.setAlignment(Qt.AlignVCenter) + axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + scale_row.addWidget(axis) + scale_row.addWidget(scaleSpinBox) + transform_layout.addLayout(scale_row, 2, 1, 1, 3) else: # 3D GUI组件使用世界坐标 - transform_layout.addWidget(QLabel("位置"), 0, 0) + position_row = QHBoxLayout() + position_row.setContentsMargins(0, 0, 0, 0) + position_row.setSpacing(4) + # position_row.addWidget(QLabel("位置")) + position_label = QLabel("位置") + transform_layout.addWidget(position_label, 0, 0) - # X, Y, Z 标签居中 - x_label = QLabel("X") - y_label = QLabel("Y") - z_label = QLabel("Z") - x_label.setAlignment(Qt.AlignCenter) - y_label.setAlignment(Qt.AlignCenter) - z_label.setAlignment(Qt.AlignCenter) - - transform_layout.addWidget(x_label, 0, 1) - transform_layout.addWidget(y_label, 0, 2) - transform_layout.addWidget(z_label, 0, 3) - - # 位置数值输入框 self.pos_x = QDoubleSpinBox() self.pos_x.setRange(-100, 100) self.pos_x.setValue(pos.getX()) self.pos_x.setSingleStep(0.01) self.pos_x.valueChanged.connect(lambda v: self.world.gui_manager.editGUI3DPosition(gui_element, "x", v)) - transform_layout.addWidget(self.pos_x, 1, 1) self.pos_y = QDoubleSpinBox() self.pos_y.setRange(-100, 100) self.pos_y.setValue(pos.getY()) self.pos_y.setSingleStep(0.01) self.pos_y.valueChanged.connect(lambda v: self.world.gui_manager.editGUI3DPosition(gui_element, "y", v)) - transform_layout.addWidget(self.pos_y, 1, 2) self.pos_z = QDoubleSpinBox() self.pos_z.setRange(-100, 100) self.pos_z.setValue(pos.getZ()) self.pos_z.setSingleStep(0.01) self.pos_z.valueChanged.connect(lambda v: self.world.gui_manager.editGUI3DPosition(gui_element, "z", v)) - transform_layout.addWidget(self.pos_z, 1, 3) - # 缩放属性 + for axis_label, widget in (("X:", self.pos_x), ("Y:", self.pos_y), ("Z:", self.pos_z)): + axis = QLabel(axis_label) + axis.setAlignment(Qt.AlignVCenter) + axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + position_row.addWidget(axis) + position_row.addWidget(widget) + + transform_layout.addLayout(position_row, 0, 1, 1, 3) + scale = gui_element.getScale() - transform_layout.addWidget(QLabel("缩放"), 2, 0) + scale_row = QHBoxLayout() + scale_row.setContentsMargins(0, 0, 0, 0) + scale_row.setSpacing(4) + # scale_row.addWidget(QLabel("缩放")) + scale_label = QLabel("缩放") + transform_layout.addWidget(scale_label, 1, 0) - # X, Y, Z 缩放标签居中 - sx_label = QLabel("X") - sy_label = QLabel("Y") - sz_label = QLabel("Z") - sx_label.setAlignment(Qt.AlignCenter) - sy_label.setAlignment(Qt.AlignCenter) - sz_label.setAlignment(Qt.AlignCenter) - - transform_layout.addWidget(sx_label, 2, 1) - transform_layout.addWidget(sy_label, 2, 2) - transform_layout.addWidget(sz_label, 2, 3) - - # 缩放数值输入框 self.scale_x = QDoubleSpinBox() self.scale_x.setRange(0.01, 100) self.scale_x.setSingleStep(0.01) self.scale_x.setValue( scale.getX() if hasattr(scale, 'getX') else scale[0] if isinstance(scale, (tuple, list)) else scale) self.scale_x.valueChanged.connect(lambda v: self.world.gui_manager.editGUIScale(gui_element, "x", v)) - transform_layout.addWidget(self.scale_x, 3, 1) self.scale_y = QDoubleSpinBox() self.scale_y.setRange(0.01, 100) @@ -1590,7 +2215,6 @@ class PropertyPanelManager: scale.getY() if hasattr(scale, 'getY') else scale[1] if isinstance(scale, (tuple, list)) and len( scale) > 1 else scale) self.scale_y.valueChanged.connect(lambda v: self.world.gui_manager.editGUIScale(gui_element, "y", v)) - transform_layout.addWidget(self.scale_y, 3, 2) self.scale_z = QDoubleSpinBox() self.scale_z.setRange(0.01, 100) @@ -1599,7 +2223,15 @@ class PropertyPanelManager: scale.getZ() if hasattr(scale, 'getZ') else scale[2] if isinstance(scale, (tuple, list)) and len( scale) > 2 else scale) self.scale_z.valueChanged.connect(lambda v: self.world.gui_manager.editGUIScale(gui_element, "z", v)) - transform_layout.addWidget(self.scale_z, 3, 3) + + for axis_label, widget in (("X:", self.scale_x), ("Y:", self.scale_y), ("Z:", self.scale_z)): + axis = QLabel(axis_label) + axis.setAlignment(Qt.AlignVCenter) + axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + scale_row.addWidget(axis) + scale_row.addWidget(widget) + + transform_layout.addLayout(scale_row, 1, 1, 1, 3) transform_group.setLayout(transform_layout) self._propertyLayout.addWidget(transform_group) @@ -1608,6 +2240,7 @@ class PropertyPanelManager: if gui_type in ["2d_image", "2d_video_screen", "info_panel"]: sort_group = QGroupBox("显示顺序") sort_layout = QGridLayout() + sort_layout.setColumnMinimumWidth(0, self.column_minimum_width) # 获取当前sort值,如果没有设置则默认为0 current_sort = int(gui_element.getTag("sort") or "0") @@ -1628,7 +2261,7 @@ class PropertyPanelManager: print(f"✗ 更新{gui_type}渲染顺序失败: {e}") sort_spin.valueChanged.connect(updateSort) - sort_layout.addWidget(sort_spin, 0, 1) + sort_layout.addWidget(sort_spin, 0, 1, 1, 3) sort_group.setLayout(sort_layout) self._propertyLayout.addWidget(sort_group) @@ -1637,12 +2270,13 @@ class PropertyPanelManager: if gui_type in ["button", "label", "3d_text"]: appearance_group = QGroupBox("外观属性") appearance_layout = QGridLayout() + appearance_layout.setColumnMinimumWidth(0, self.column_minimum_width) # 字体颜色选择 appearance_layout.addWidget(QLabel("字体颜色:"), 0, 0) colorButton = QPushButton("选择颜色") colorButton.clicked.connect(lambda checked, elem=gui_element: self._selectGUIColor(elem)) - appearance_layout.addWidget(colorButton, 0, 1) + appearance_layout.addWidget(colorButton, 0, 1, 1, 3) appearance_group.setLayout(appearance_layout) self._propertyLayout.addWidget(appearance_group) @@ -1651,12 +2285,13 @@ class PropertyPanelManager: if gui_type in ["button", "label"]: appearance_group = QGroupBox("外观属性") appearance_layout = QGridLayout() + appearance_layout.setColumnMinimumWidth(0, self.column_minimum_width) appearance_layout.addWidget(QLabel("背景颜色:"), 0, 0) colorButton = QPushButton("选择颜色") colorButton.clicked.connect(lambda: self.world.gui_manager.selectGUIColor(gui_element)) - appearance_layout.addWidget(colorButton, 0, 1) + appearance_layout.addWidget(colorButton, 0, 1, 1, 3) appearance_group.setLayout(appearance_layout) self._propertyLayout.addWidget(appearance_group) @@ -1664,6 +2299,7 @@ class PropertyPanelManager: if gui_type == "3d_image": image_group = QGroupBox("图片设置") image_layout = QGridLayout() + image_layout.setColumnMinimumWidth(0, self.column_minimum_width) # 当前图片路径标签和 current_image_label = QLabel("当前图片:") @@ -1679,11 +2315,14 @@ class PropertyPanelManager: texture_label = QLabel(display_path) texture_label.setWordWrap(True) texture_label.setToolTip(current_texture_path if current_texture_path != "未设置" else "") - image_layout.addWidget(texture_label, 0, 1) + image_layout.addWidget(texture_label, 0, 1, 1, 3) # 选择图片按钮 - select_texture_button = QPushButton("选择图片...") - image_layout.addWidget(select_texture_button, 1, 0, 1, 2) + if has_icon("property_select_image"): + select_texture_button = QPushButton(get_icon("property_select_image"), "选择图片...") + else: + select_texture_button = QPushButton("选择图片...") + image_layout.addWidget(select_texture_button, 1, 0, 1, 4) def onSelectTexture(): from PyQt5.QtWidgets import QFileDialog @@ -1714,6 +2353,7 @@ class PropertyPanelManager: if gui_type in ["2d_image"]: image_group = QGroupBox("2D图片设置") image_layout = QGridLayout() + image_layout.setColumnMinimumWidth(0, self.column_minimum_width) # 当前图片路径标签 current_image_label = QLabel("当前图片:") @@ -1728,11 +2368,15 @@ class PropertyPanelManager: display_path = current_texture_path texture_label = QLabel(display_path) texture_label.setWordWrap(True) - image_layout.addWidget(texture_label, 0, 1) + image_layout.addWidget(texture_label, 0, 1, 1, 3) # 选择图片按钮 - select_texture_button = QPushButton("选择图片...") - image_layout.addWidget(select_texture_button, 1, 0, 1, 2) + if has_icon("property_select_image"): + select_texture_button = QPushButton(get_icon("property_select_image"), "选择图片...") + else: + select_texture_button = QPushButton("选择图片...") + # select_texture_button = QPushButton("选择图片...") + image_layout.addWidget(select_texture_button, 1, 0, 1, 4) def onSelect2DTexture(): from PyQt5.QtWidgets import QFileDialog @@ -1891,6 +2535,7 @@ class PropertyPanelManager: # 背景图片组 image_group = QGroupBox("背景图片设置") image_layout = QGridLayout() + image_layout.setColumnMinimumWidth(0, self.column_minimum_width) # 当前背景图片路径标签 current_image_label = QLabel("当前背景图片:") @@ -1904,16 +2549,16 @@ class PropertyPanelManager: texture_label = QLabel(display_path) texture_label.setWordWrap(True) texture_label.setToolTip(current_bg_image if current_bg_image else "") - image_layout.addWidget(texture_label, 0, 1) + image_layout.addWidget(texture_label, 0, 1, 1, 3) # 选择背景图片按钮 select_texture_button = QPushButton("选择背景图片...") - image_layout.addWidget(select_texture_button, 1, 0, 1, 2) + image_layout.addWidget(select_texture_button, 1, 0, 1, 4) # 清除背景图片按钮 clear_texture_button = QPushButton("清除背景图片") clear_texture_button.setStyleSheet("color: red;") - image_layout.addWidget(clear_texture_button, 2, 0, 1, 2) + image_layout.addWidget(clear_texture_button, 2, 0, 1, 4) def onSelectBackgroundImage(): """选择背景图片""" @@ -1954,6 +2599,7 @@ class PropertyPanelManager: # 显示顺序组 sort_group = QGroupBox("显示顺序") sort_layout = QGridLayout() + sort_layout.setColumnMinimumWidth(0, self.column_minimum_width) sort_layout.addWidget(QLabel("显示优先度:"), 0, 0) sort_spin = QSpinBox() @@ -1971,28 +2617,42 @@ class PropertyPanelManager: print(f"✗ 更新信息面板渲染顺序失败: {e}") sort_spin.valueChanged.connect(updateSort) - sort_layout.addWidget(sort_spin, 0, 1) + sort_layout.addWidget(sort_spin, 0, 1, 1, 3) sort_group.setLayout(sort_layout) self._propertyLayout.addWidget(sort_group) # 面板大小设置组 - size_group = QGroupBox("面板大小设置") + size_group = QGroupBox() + size_group.setTitle("面板大小设置") size_layout = QGridLayout() + size_layout.setColumnMinimumWidth(0, self.column_minimum_width) + + size_row = QHBoxLayout() + size_row.setContentsMargins(0, 0, 0, 0) + size_row.setSpacing(4) + + size_title = QLabel("面板大小设置:") + size_layout.addWidget(size_title, 0, 0) - size_layout.addWidget(QLabel("宽度:"), 0, 0) width_spin = QDoubleSpinBox() width_spin.setRange(0.1, 5.0) width_spin.setSingleStep(0.1) width_spin.setValue(current_size[0]) - size_layout.addWidget(width_spin, 0, 1) - size_layout.addWidget(QLabel("高度:"), 0, 2) height_spin = QDoubleSpinBox() height_spin.setRange(0.1, 5.0) height_spin.setSingleStep(0.1) height_spin.setValue(current_size[1]) - size_layout.addWidget(height_spin, 0, 3) + + for label_text, spinbox in (("宽度:", width_spin), ("高度:", height_spin)): + label = QLabel(label_text) + label.setAlignment(Qt.AlignVCenter) + label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + size_row.addWidget(label) + size_row.addWidget(spinbox) + + size_layout.addLayout(size_row, 0, 1, 1, 3) # 连接大小变化信号 def onSizeChanged(): @@ -2017,36 +2677,40 @@ class PropertyPanelManager: size_group.setLayout(size_layout) self._propertyLayout.addWidget(size_group) - # 位置设置组 (根据面板类型显示不同的位置控件) - position_group = QGroupBox("位置设置") + # 位置设置:X:[box] Y:[box]/Z:[box],右对齐可伸缩 + position_group = QGroupBox() + position_group.setTitle("位置设置") position_layout = QGridLayout() + position_layout.setColumnMinimumWidth(0, self.column_minimum_width) + + position_row = QHBoxLayout() + position_row.setContentsMargins(0, 0, 0, 0) + position_row.setSpacing(4) + + position_title = QLabel("位置设置:") + position_layout.addWidget(position_title, 0, 0) if is_3d_panel: - # 3D面板位置设置 (X, Y, Z) - position_layout.addWidget(QLabel("X:"), 0, 0) + # 3D 面板位置:X、Y、Z pos_x_spin = QDoubleSpinBox() pos_x_spin.setRange(-1000, 1000) pos_x_spin.setSingleStep(0.1) pos_x_spin.setValue(current_position[0] if len(current_position) > 0 else 0) - position_layout.addWidget(pos_x_spin, 0, 1) - position_layout.addWidget(QLabel("Y:"), 0, 2) pos_y_spin = QDoubleSpinBox() pos_y_spin.setRange(-1000, 1000) pos_y_spin.setSingleStep(0.1) pos_y_spin.setValue(current_position[1] if len(current_position) > 1 else 0) - position_layout.addWidget(pos_y_spin, 0, 3) - position_layout.addWidget(QLabel("Z:"), 1, 0) pos_z_spin = QDoubleSpinBox() pos_z_spin.setRange(-1000, 1000) pos_z_spin.setSingleStep(0.1) pos_z_spin.setValue(current_position[2] if len(current_position) > 2 else 0) - position_layout.addWidget(pos_z_spin, 1, 1) - # 连接位置变化信号 + axis_widgets = (("X:", pos_x_spin), ("Y:", pos_y_spin), ("Z:", pos_z_spin)) + def on3DPositionChanged(): - if hasattr(self.world, 'info_panel_manager'): + if hasattr(self.world, "info_panel_manager"): self.world.info_panel_manager.update3DPanelProperties( panel_id, position=(pos_x_spin.value(), pos_y_spin.value(), pos_z_spin.value())) @@ -2054,76 +2718,107 @@ class PropertyPanelManager: pos_y_spin.valueChanged.connect(on3DPositionChanged) pos_z_spin.valueChanged.connect(on3DPositionChanged) else: - # 2D面板位置设置 (X, Z) - position_layout.addWidget(QLabel("X:"), 0, 0) + # 2D 面板位置:X、Z pos_x_spin = QDoubleSpinBox() pos_x_spin.setRange(-1000, 1000) pos_x_spin.setSingleStep(0.1) pos_x_spin.setValue(current_position[0] if len(current_position) > 0 else 0) - position_layout.addWidget(pos_x_spin, 0, 1) - position_layout.addWidget(QLabel("Z:"), 0, 2) pos_z_spin = QDoubleSpinBox() pos_z_spin.setRange(-1000, 1000) pos_z_spin.setSingleStep(0.1) pos_z_spin.setValue(current_position[1] if len(current_position) > 1 else 0) - position_layout.addWidget(pos_z_spin, 0, 3) - # 连接位置变化信号 + axis_widgets = (("X:", pos_x_spin), ("Z:", pos_z_spin)) + def on2DPositionChanged(): - if hasattr(self.world, 'info_panel_manager'): + if hasattr(self.world, "info_panel_manager"): self.world.info_panel_manager.updatePanelProperties( panel_id, position=(pos_x_spin.value(), pos_z_spin.value())) pos_x_spin.valueChanged.connect(on2DPositionChanged) pos_z_spin.valueChanged.connect(on2DPositionChanged) + for axis_label, spinbox in axis_widgets: + axis = QLabel(axis_label) + axis.setAlignment(Qt.AlignVCenter) + axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + position_row.addWidget(axis) + position_row.addWidget(spinbox) + + position_layout.addLayout(position_row, 0, 1, 1, 3) position_group.setLayout(position_layout) self._propertyLayout.addWidget(position_group) # 边框属性组 border_group = QGroupBox("边框属性") border_layout = QGridLayout() + border_layout.setColumnMinimumWidth(0, self.column_minimum_width) border_layout.addWidget(QLabel("边框颜色:"), 0, 0) + # border_layout.addWidget(border_color_label, 0, 0) + + border_row_top = QHBoxLayout() + border_row_top.setContentsMargins(0, 0, 0, 0) + border_row_top.setSpacing(4) - # 边框R分量 - border_layout.addWidget(QLabel("R:"), 1, 0) border_r_spin = QDoubleSpinBox() border_r_spin.setRange(0.0, 1.0) border_r_spin.setSingleStep(0.01) border_r_spin.setValue(current_border_color[0]) - border_layout.addWidget(border_r_spin, 1, 1) + color_r_bg = QLabel("R:") + color_r_bg.setAlignment(Qt.AlignVCenter) + color_r_bg.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + border_row_top.addWidget(color_r_bg) + border_row_top.addWidget(border_r_spin) - # 边框G分量 - border_layout.addWidget(QLabel("G:"), 1, 2) border_g_spin = QDoubleSpinBox() border_g_spin.setRange(0.0, 1.0) border_g_spin.setSingleStep(0.01) border_g_spin.setValue(current_border_color[1]) - border_layout.addWidget(border_g_spin, 1, 3) + color_g = QLabel("G:") + color_g.setAlignment(Qt.AlignVCenter) + color_g.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + border_row_top.addWidget(color_g) + border_row_top.addWidget(border_g_spin) + + border_layout.addLayout(border_row_top, 0, 1, 1, 3) + + indent_placeholder = QLabel("") + border_layout.addWidget(indent_placeholder, 1, 0) + # indent_placeholder.setFixedWidth(border_color_label.sizeHint().width()) + # border_layout.addWidget(indent_placeholder, 1, 0) + + border_row_bottom = QHBoxLayout() + border_row_bottom.setContentsMargins(0, 0, 0, 0) + border_row_bottom.setSpacing(4) - # 边框B分量 - border_layout.addWidget(QLabel("B:"), 2, 0) border_b_spin = QDoubleSpinBox() border_b_spin.setRange(0.0, 1.0) border_b_spin.setSingleStep(0.01) border_b_spin.setValue(current_border_color[2]) - border_layout.addWidget(border_b_spin, 2, 1) + color_b = QLabel("B:") + color_b.setAlignment(Qt.AlignVCenter) + color_b.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + border_row_bottom.addWidget(color_b) + border_row_bottom.addWidget(border_b_spin) - # 边框A分量 - border_layout.addWidget(QLabel("A:"), 2, 2) border_a_spin = QDoubleSpinBox() border_a_spin.setRange(0.0, 1.0) border_a_spin.setSingleStep(0.01) border_a_spin.setValue(current_border_color[3]) - border_layout.addWidget(border_a_spin, 2, 3) + color_a = QLabel("A:") + color_a.setAlignment(Qt.AlignVCenter) + color_a.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + border_row_bottom.addWidget(color_a) + border_row_bottom.addWidget(border_a_spin) + + border_layout.addLayout(border_row_bottom, 1, 1, 1, 3) - # 边框颜色选择按钮 border_color_button = QPushButton("选择边框颜色") border_color_button.clicked.connect(lambda: self._selectInfoPanelBorderColor( info_panel, border_r_spin, border_g_spin, border_b_spin, border_a_spin)) - border_layout.addWidget(border_color_button, 3, 0, 1, 4) + border_layout.addWidget(border_color_button, 2, 0, 1, 4) # 连接边框颜色变化信号 def onBorderColorChanged(): @@ -2152,45 +2847,74 @@ class PropertyPanelManager: bg_color_group = QGroupBox("背景颜色设置") bg_color_layout = QGridLayout() + bg_color_layout.setColumnMinimumWidth(0, self.column_minimum_width) - bg_color_layout.addWidget(QLabel("背景颜色:"), 0, 0) + bg_color_label = QLabel("背景颜色:") + bg_color_layout.addWidget(bg_color_label, 0, 0) + bg_color_row_top = QHBoxLayout() + bg_color_row_top.setContentsMargins(0, 0, 0, 0) + bg_color_row_top.setSpacing(4) - bg_color_layout.addWidget(QLabel("R:"), 1, 0) + # R分量 + color_r_bg = QLabel("R:") + color_r_bg.setAlignment(Qt.AlignVCenter) + color_r_bg.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) r_spin = QDoubleSpinBox() r_spin.setRange(0.0, 1.0) r_spin.setSingleStep(0.01) r_spin.setValue(current_bg_color[0]) - bg_color_layout.addWidget(r_spin, 1, 1) + bg_color_row_top.addWidget(color_r_bg) + bg_color_row_top.addWidget(r_spin) # G分量 - bg_color_layout.addWidget(QLabel("G:"), 1, 2) + color_g_bg = QLabel("G:") + color_g_bg.setAlignment(Qt.AlignVCenter) + color_g_bg.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) g_spin = QDoubleSpinBox() g_spin.setRange(0.0, 1.0) g_spin.setSingleStep(0.01) g_spin.setValue(current_bg_color[1]) - bg_color_layout.addWidget(g_spin, 1, 3) + bg_color_row_top.addWidget(color_g_bg) + bg_color_row_top.addWidget(g_spin) + + bg_color_layout.addLayout(bg_color_row_top, 0, 1, 1, 3) + + indent_placeholder = QLabel("") + bg_color_layout.addWidget(indent_placeholder, 1, 0) + + bg_color_row_bottom = QHBoxLayout() + bg_color_row_bottom.setContentsMargins(0, 0, 0, 0) + bg_color_row_bottom.setSpacing(4) # B分量 - bg_color_layout.addWidget(QLabel("B:"), 2, 0) + color_b_bg = QLabel("B:") + color_b_bg.setAlignment(Qt.AlignVCenter) + color_b_bg.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) b_spin = QDoubleSpinBox() b_spin.setRange(0.0, 1.0) b_spin.setSingleStep(0.01) b_spin.setValue(current_bg_color[2]) - bg_color_layout.addWidget(b_spin, 2, 1) + bg_color_row_bottom.addWidget(color_b_bg) + bg_color_row_bottom.addWidget(b_spin) # A分量 - bg_color_layout.addWidget(QLabel("A:"), 2, 2) + color_a_bg = QLabel("A:") + color_a_bg.setAlignment(Qt.AlignVCenter) + color_a_bg.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) a_spin = QDoubleSpinBox() a_spin.setRange(0.0, 1.0) a_spin.setSingleStep(0.01) a_spin.setValue(current_bg_color[3]) - bg_color_layout.addWidget(a_spin, 2, 3) + bg_color_row_bottom.addWidget(color_a_bg) + bg_color_row_bottom.addWidget(a_spin) + + bg_color_layout.addLayout(bg_color_row_bottom, 1, 1, 1, 3) # 颜色选择按钮 color_button = QPushButton("选择颜色") color_button.clicked.connect(lambda: self._selectInfoPanelBackgroundColor( info_panel, r_spin, g_spin, b_spin, a_spin)) - bg_color_layout.addWidget(color_button, 3, 0, 1, 4) + bg_color_layout.addWidget(color_button, 2, 0, 1, 4) # 连接颜色数值变化信号,自动更新背景颜色 def onBackgroundColorChanged(): @@ -2208,6 +2932,7 @@ class PropertyPanelManager: # 标题属性组 title_group = QGroupBox("标题属性") title_layout = QGridLayout() + title_layout.setColumnMinimumWidth(0, self.column_minimum_width) # 标题文本 title_layout.addWidget(QLabel("标题文本:"), 0, 0) @@ -2219,37 +2944,62 @@ class PropertyPanelManager: # 标题颜色 title_layout.addWidget(QLabel("标题颜色:"), 1, 0) + title_color_row = QHBoxLayout() # 占位,保持对齐 + title_color_row.setContentsMargins(0, 0, 0, 0) + title_color_row.setSpacing(4) + # 标题R分量 - title_layout.addWidget(QLabel("R:"), 2, 0) + title_color_label_r = QLabel("R:") + title_color_label_r.setAlignment(Qt.AlignVCenter) + title_color_label_r.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + title_color_row.addWidget(title_color_label_r) title_r_spin = QDoubleSpinBox() title_r_spin.setRange(0.0, 1.0) title_r_spin.setSingleStep(0.01) title_r_spin.setValue(current_title_color[0]) - title_layout.addWidget(title_r_spin, 2, 1) + title_color_row.addWidget(title_r_spin) # 标题G分量 - title_layout.addWidget(QLabel("G:"), 2, 2) + title_color_label_g = QLabel("G:") + title_color_label_g.setAlignment(Qt.AlignVCenter) + title_color_label_g.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + title_color_row.addWidget(title_color_label_g) title_g_spin = QDoubleSpinBox() title_g_spin.setRange(0.0, 1.0) title_g_spin.setSingleStep(0.01) title_g_spin.setValue(current_title_color[1]) - title_layout.addWidget(title_g_spin, 2, 3) + title_color_row.addWidget(title_g_spin) + + title_layout.addLayout(title_color_row, 1, 1, 1, 3) + + title_layout.addWidget(QLabel(""), 2, 0) # 占位 + title_color_row_ = QHBoxLayout() # 占位,保持对齐 + title_color_row_.setContentsMargins(0, 0, 0, 0) + title_color_row_.setSpacing(4) # 标题B分量 - title_layout.addWidget(QLabel("B:"), 3, 0) + title_color_label_b = QLabel("B:") + title_color_label_b.setAlignment(Qt.AlignVCenter) + title_color_label_b.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + title_color_row_.addWidget(title_color_label_b) title_b_spin = QDoubleSpinBox() title_b_spin.setRange(0.0, 1.0) title_b_spin.setSingleStep(0.01) title_b_spin.setValue(current_title_color[2]) - title_layout.addWidget(title_b_spin, 3, 1) + title_color_row_.addWidget(title_b_spin) # 标题A分量 - title_layout.addWidget(QLabel("A:"), 3, 2) + title_color_label_a = QLabel("A:") + title_color_label_a.setAlignment(Qt.AlignVCenter) + title_color_label_a.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + title_color_row_.addWidget(title_color_label_a) title_a_spin = QDoubleSpinBox() title_a_spin.setRange(0.0, 1.0) title_a_spin.setSingleStep(0.01) title_a_spin.setValue(current_title_color[3]) - title_layout.addWidget(title_a_spin, 3, 3) + title_color_row_.addWidget(title_a_spin) + + title_layout.addLayout(title_color_row_, 2, 1, 1, 3) # 标题颜色选择按钮 title_color_button = QPushButton("选择标题颜色") @@ -2267,20 +3017,31 @@ class PropertyPanelManager: # 标题位置控制 (仅对2D面板) if not is_3d_panel: - title_layout.addWidget(QLabel("标题位置:"), 6, 0) - title_layout.addWidget(QLabel("X:"), 7, 0) + title_position_row = QHBoxLayout() + title_position_row.setContentsMargins(0, 0, 0, 0) + title_position_row.setSpacing(4) + + title_position_label = QLabel("标题位置") + title_layout.addWidget(title_position_label, 6, 0) + title_x_spin = QDoubleSpinBox() title_x_spin.setRange(-1.0, 1.0) title_x_spin.setSingleStep(0.01) title_x_spin.setValue(current_title_pos[0]) - title_layout.addWidget(title_x_spin, 7, 1) - title_layout.addWidget(QLabel("Y:"), 7, 2) title_y_spin = QDoubleSpinBox() title_y_spin.setRange(-1.0, 1.0) title_y_spin.setSingleStep(0.01) title_y_spin.setValue(current_title_pos[1]) - title_layout.addWidget(title_y_spin, 7, 3) + + for axis_label, spinbox in (("X:", title_x_spin), ("Y:", title_y_spin)): + label = QLabel(axis_label) + label.setAlignment(Qt.AlignVCenter) + label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + title_position_row.addWidget(label) + title_position_row.addWidget(spinbox) + + title_layout.addLayout(title_position_row, 6, 1, 1, 3) # 连接标题属性变化信号 def onTitleTextChanged(): @@ -2329,6 +3090,7 @@ class PropertyPanelManager: # 内容属性组 content_group = QGroupBox("内容属性") content_layout = QGridLayout() + content_layout.setColumnMinimumWidth(0, self.column_minimum_width) # 内容文本 content_layout.addWidget(QLabel("内容文本:"), 0, 0) @@ -2339,38 +3101,63 @@ class PropertyPanelManager: # 内容颜色 content_layout.addWidget(QLabel("内容颜色:"), 1, 0) + content_color_row = QHBoxLayout() + content_color_row.setContentsMargins(0, 0, 0, 0) + content_color_row.setSpacing(4) + # 内容R分量 - content_layout.addWidget(QLabel("R:"), 2, 0) + content_r = QLabel("R:") + content_r.setAlignment(Qt.AlignVCenter) + content_r.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + content_color_row.addWidget(content_r) content_r_spin = QDoubleSpinBox() content_r_spin.setRange(0.0, 1.0) content_r_spin.setSingleStep(0.01) content_r_spin.setValue(current_content_color[0]) - content_layout.addWidget(content_r_spin, 2, 1) + content_color_row.addWidget(content_r_spin) # 内容G分量 - content_layout.addWidget(QLabel("G:"), 2, 2) + content_g = QLabel("G:") + content_g.setAlignment(Qt.AlignVCenter) + content_g.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + content_color_row.addWidget(content_g) content_g_spin = QDoubleSpinBox() content_g_spin.setRange(0.0, 1.0) content_g_spin.setSingleStep(0.01) content_g_spin.setValue(current_content_color[1]) - content_layout.addWidget(content_g_spin, 2, 3) + content_color_row.addWidget(content_g_spin) + + content_layout.addLayout(content_color_row, 1, 1, 1, 3) + + content_color_row_ = QHBoxLayout() + content_color_row_.setContentsMargins(0, 0, 0, 0) + content_color_row_.setSpacing(4) + content_layout.addWidget(QLabel(":"), 2, 0) # 内容B分量 - content_layout.addWidget(QLabel("B:"), 3, 0) + content_b = QLabel("B:") + content_b.setAlignment(Qt.AlignVCenter) + content_b.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + content_color_row_.addWidget(content_b) content_b_spin = QDoubleSpinBox() content_b_spin.setRange(0.0, 1.0) content_b_spin.setSingleStep(0.01) content_b_spin.setValue(current_content_color[2]) - content_layout.addWidget(content_b_spin, 3, 1) + content_color_row_.addWidget(content_b_spin) # 内容A分量 - content_layout.addWidget(QLabel("A:"), 3, 2) + content_a = QLabel("A:") + content_a.setAlignment(Qt.AlignVCenter) + content_a.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + content_color_row_.addWidget(content_a) content_a_spin = QDoubleSpinBox() content_a_spin.setRange(0.0, 1.0) content_a_spin.setSingleStep(0.01) content_a_spin.setValue(current_content_color[3]) - content_layout.addWidget(content_a_spin, 3, 3) + content_color_row_.addWidget(content_a_spin) + + content_layout.addLayout(content_color_row_, 2, 1, 1, 3) # 内容颜色选择按钮 content_color_button = QPushButton("选择内容颜色") @@ -2388,20 +3175,32 @@ class PropertyPanelManager: # 内容位置控制 (仅对2D面板) if not is_3d_panel: - content_layout.addWidget(QLabel("内容位置:"), 6, 0) - content_layout.addWidget(QLabel("X:"), 7, 0) + content_layout.addLayout(QLabel("内容位置:"), 6, 0) + content_pos_layout = QHBoxLayout() + content_pos_layout.setContentsMargins(0, 0, 0, 0) + content_pos_layout.setSpacing(4) + + content_x = QLabel("X:") + content_x.setAlignment(Qt.AlignVCenter) + content_x.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + content_pos_layout.addWidget(content_x) content_x_spin = QDoubleSpinBox() content_x_spin.setRange(-1.0, 1.0) content_x_spin.setSingleStep(0.01) content_x_spin.setValue(current_content_pos[0]) - content_layout.addWidget(content_x_spin, 7, 1) + content_pos_layout.addWidget(content_x_spin) - content_layout.addWidget(QLabel("Y:"), 7, 2) + content_y = QLabel("Y:") + content_y.setAlignment(Qt.AlignVCenter) + content_y.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + content_pos_layout.addWidget(content_y) content_y_spin = QDoubleSpinBox() content_y_spin.setRange(-1.0, 1.0) content_y_spin.setSingleStep(0.01) content_y_spin.setValue(current_content_pos[1]) - content_layout.addWidget(content_y_spin, 7, 3) + content_pos_layout.addWidget(content_y_spin) + + content_layout.addLayout(content_pos_layout, 6, 1, 1, 3) # 连接内容属性变化信号 def onContentTextChanged(): @@ -3131,7 +3930,7 @@ class PropertyPanelManager: current_radius = float(spherical_video.getTag("original_radius") or "5.0") radius_spin.setValue(current_radius) radius_spin.valueChanged.connect(lambda value: self._updateSphericalVideoRadius(spherical_video, value)) - sphere_layout.addWidget(radius_spin, 0, 1) + sphere_layout.addWidget(radius_spin, 0, 1, 1, 3) sphere_group.setLayout(sphere_layout) self._propertyLayout.addWidget(sphere_group) @@ -3176,7 +3975,6 @@ class PropertyPanelManager: is_playing = self._video_playing.get(video_id, False) if is_playing: - # 暂停视频 if hasattr(movie_texture, 'stop'): movie_texture.stop() if hasattr(self, '_spherical_video_controls') and 'play_button' in self._spherical_video_controls: @@ -3184,15 +3982,12 @@ class PropertyPanelManager: play_button.setText("▶️ 播放") print("⏸️ 球形视频已暂停") else: - # 播放视频 if hasattr(movie_texture, 'play'): movie_texture.play() if hasattr(self, '_spherical_video_controls') and 'play_button' in self._spherical_video_controls: play_button = self._spherical_video_controls['play_button'] play_button.setText("⏸️ 暂停") print("▶️ 球形视频开始播放") - - # 更新播放状态 self._video_playing[video_id] = not is_playing except Exception as e: @@ -3216,12 +4011,10 @@ class PropertyPanelManager: if hasattr(movie_texture, 'setTime'): movie_texture.setTime(0.0) - # 更新播放按钮 if hasattr(self, '_spherical_video_controls') and 'play_button' in self._spherical_video_controls: play_button = self._spherical_video_controls['play_button'] play_button.setText("▶️ 播放") - # 更新播放状态 if hasattr(self, '_video_playing'): video_id = id(spherical_video) self._video_playing[video_id] = False @@ -3376,6 +4169,7 @@ class PropertyPanelManager: video_info_group = QGroupBox("视频信息") video_info_layout = QGridLayout() + video_info_layout.setColumnMinimumWidth(0, self.column_minimum_width) # 显示当前视频文件路径 - 改进版本 video_path = video_screen.getTag("video_path") if video_screen.hasTag("video_path") else "" @@ -3392,70 +4186,74 @@ class PropertyPanelManager: display_path = video_path path_label.setText(display_path) path_label.setStyleSheet("color: #00AAFF;") - video_info_layout.addWidget(path_label, 0, 1) + video_info_layout.addWidget(path_label, 0, 1, 1, 3) elif os.path.exists(video_path): # 显示本地文件信息 video_info_layout.addWidget(QLabel("视频文件:"), 0, 0) path_label = QLabel(os.path.basename(video_path)) path_label.setWordWrap(True) path_label.setStyleSheet("color: #00AAFF;") - video_info_layout.addWidget(path_label, 0, 1) + video_info_layout.addWidget(path_label, 0, 1, 1, 3) else: # 文件不存在 video_info_layout.addWidget(QLabel("状态:"), 0, 0) status_label = QLabel("文件不存在") status_label.setStyleSheet("color: red;") - video_info_layout.addWidget(status_label, 0, 1) + video_info_layout.addWidget(status_label, 0, 1, 1, 3) else: # 无视频 video_info_layout.addWidget(QLabel("状态:"), 0, 0) status_label = QLabel("无视频文件") status_label.setStyleSheet("color: red;") - video_info_layout.addWidget(status_label, 0, 1) + video_info_layout.addWidget(status_label, 0, 1, 1, 3) video_info_group.setLayout(video_info_layout) self._propertyLayout.addWidget(video_info_group) video_control_group = QGroupBox("视频控制") - video_control_layout = QHBoxLayout() + video_control_layout = QGridLayout() + video_control_layout.setColumnMinimumWidth(0, self.column_minimum_width) + video_control_h_layout = QHBoxLayout() - # 播放按钮 play_btn = QPushButton("▶️ 播放") play_btn.clicked.connect(lambda: self.world.gui_manager.playVideo(video_screen)) - video_control_layout.addWidget(play_btn) + video_control_h_layout.addWidget(play_btn) # 暂停按钮 pause_btn = QPushButton("⏸️ 暂停") pause_btn.clicked.connect(lambda: self.world.gui_manager.pauseVideo(video_screen)) - video_control_layout.addWidget(pause_btn) + video_control_h_layout.addWidget(pause_btn) # 停止按钮 stop_btn = QPushButton("⏹️ 停止") stop_btn.clicked.connect(lambda: self.world.gui_manager.stopVideo(video_screen)) - video_control_layout.addWidget(stop_btn) + video_control_h_layout.addWidget(stop_btn) - video_control_group.setLayout(video_control_layout) - self._propertyLayout.addWidget(video_control_group) + video_control_layout.addLayout(video_control_h_layout, 0, 0, 1, 4) # 加载新视频按钮 load_btn = QPushButton("📁 加载新视频...") load_btn.clicked.connect(lambda: self._loadNewVideo(video_screen, item)) - self._propertyLayout.addWidget(load_btn) + video_control_layout.addWidget(load_btn, 1, 0, 1, 4) + + video_control_group.setLayout(video_control_layout) + self._propertyLayout.addWidget(video_control_group) # 添加URL输入区域 url_group = QGroupBox("网络视频") - url_layout = QHBoxLayout() + url_layout = QGridLayout() + url_layout.setColumnMinimumWidth(0, self.column_minimum_width) self.url_input = QLineEdit() self.url_input.setPlaceholderText("输入视频流URL") - url_layout.addWidget(self.url_input) + url_layout.addWidget(self.url_input, 0, 0, 1, 3) load_url_btn = QPushButton("加载URL") self._current_load_url_btn_3d = load_url_btn load_url_btn.clicked.connect( lambda: self._loadVideoFromURLWithOpenCV_3D(video_screen, self.url_input.text().strip())) - url_layout.addWidget(load_url_btn) + url_layout.addWidget(load_url_btn, 0, 3) url_group.setLayout(url_layout) self._propertyLayout.addWidget(url_group) @@ -3473,6 +4271,7 @@ class PropertyPanelManager: # 视频信息组 video_info_group = QGroupBox("2D视频信息") video_info_layout = QGridLayout() + video_info_layout.setColumnMinimumWidth(0, self.column_minimum_width) # 显示当前视频文件路径 # 在显示视频信息时区分本地文件和网络URL @@ -3481,29 +4280,30 @@ class PropertyPanelManager: if video_path.startswith("http://") or video_path.startswith("https://"): # 显示URL信息 video_info_layout.addWidget(QLabel("视频流URL:"), 0, 0) - path_label = QLabel(video_path) + display_path = video_path if len(video_path)<=50 else video_path[:47]+"..." + path_label = QLabel(display_path) path_label.setWordWrap(True) path_label.setStyleSheet("color: #00AAFF;") - video_info_layout.addWidget(path_label, 0, 1) + video_info_layout.addWidget(path_label, 0, 1, 1, 3) elif os.path.exists(video_path): # 显示本地文件信息 video_info_layout.addWidget(QLabel("视频文件:"), 0, 0) path_label = QLabel(os.path.basename(video_path)) path_label.setWordWrap(True) path_label.setStyleSheet("color: #00AAFF;") - video_info_layout.addWidget(path_label, 0, 1) + video_info_layout.addWidget(path_label, 0, 1, 1, 3) else: # 文件不存在 video_info_layout.addWidget(QLabel("状态:"), 0, 0) status_label = QLabel("文件不存在") status_label.setStyleSheet("color: red;") - video_info_layout.addWidget(status_label, 0, 1) + video_info_layout.addWidget(status_label, 0, 1, 1, 3) else: # 无视频 video_info_layout.addWidget(QLabel("状态:"), 0, 0) status_label = QLabel("无视频文件") status_label.setStyleSheet("color: red;") - video_info_layout.addWidget(status_label, 0, 1) + video_info_layout.addWidget(status_label, 0, 1, 1, 3) video_screen.setBin("fixed", 0) @@ -3512,45 +4312,48 @@ class PropertyPanelManager: # 视频控制组 video_control_group = QGroupBox("2D视频控制") - video_control_layout = QHBoxLayout() + video_control_layout = QGridLayout() + video_control_h_layout = QHBoxLayout() # 播放按钮 play_btn = QPushButton("▶️ 播放") play_btn.clicked.connect(lambda: self.play2DVideo(video_screen)) - video_control_layout.addWidget(play_btn) + video_control_h_layout.addWidget(play_btn) # 暂停按钮 pause_btn = QPushButton("⏸️ 暂停") pause_btn.clicked.connect(lambda: self.pause2DVideo(video_screen)) - video_control_layout.addWidget(pause_btn) + video_control_h_layout.addWidget(pause_btn) # 停止按钮 stop_btn = QPushButton("⏹️ 停止") stop_btn.clicked.connect(lambda: self._stop2DVideo(video_screen)) - video_control_layout.addWidget(stop_btn) + video_control_h_layout.addWidget(stop_btn) + + video_control_layout.addLayout(video_control_h_layout, 0, 0, 1, 4) video_control_group.setLayout(video_control_layout) - self._propertyLayout.addWidget(video_control_group) # 加载新视频按钮 load_btn = QPushButton("📁 加载新视频...") load_btn.clicked.connect(lambda: self._loadNew2DVideo(video_screen, item)) - self._propertyLayout.addWidget(load_btn) + video_control_layout.addWidget(load_btn, 1, 0, 1, 4) + self._propertyLayout.addWidget(video_control_group) # 添加URL输入区域 url_group = QGroupBox("网络视频") - url_layout = QHBoxLayout() + url_layout = QGridLayout() self.url_input = QLineEdit() self.url_input.setPlaceholderText("输入视频流URL") - url_layout.addWidget(self.url_input) + url_layout.addWidget(self.url_input, 0, 0, 1, 3) load_url_btn = QPushButton("加载URL") self._current_load_url_btn_2d = load_url_btn load_url_btn.clicked.connect( lambda: self._loadVideoFromURLWithOpenCV(video_screen, self.url_input.text().strip())) - url_layout.addWidget(load_url_btn) + url_layout.addWidget(load_url_btn, 0, 3) url_group.setLayout(url_layout) self._propertyLayout.addWidget(url_group) @@ -3848,8 +4651,9 @@ class PropertyPanelManager: if success: print(f"成功加载新视频: {file_path}") # 刷新属性面板以显示新视频信息 - self._stop2DVideo(video_screen) + self.updateGUIPropertyPanel(video_screen, item) + #self._stop2DVideo(video_screen) return True except Exception as e: print(f"加载新视频失败: {e}") @@ -4500,123 +5304,137 @@ class PropertyPanelManager: # 变换属性组 transform_group = QGroupBox("变换 Transform") transform_layout = QGridLayout() + transform_layout.setColumnMinimumWidth(0, self.column_minimum_width) + + transform_layout.addWidget(QLabel("位置"), 0, 0) # 位置属性 current_pos = light_object.pos - transform_layout.addWidget(QLabel("位置"), 0, 0) - - # X, Y, Z 标签居中 - x_label = QLabel("X") - y_label = QLabel("Y") - z_label = QLabel("Z") - x_label.setAlignment(Qt.AlignCenter) - y_label.setAlignment(Qt.AlignCenter) - z_label.setAlignment(Qt.AlignCenter) - - transform_layout.addWidget(x_label, 0, 1) - transform_layout.addWidget(y_label, 0, 2) - transform_layout.addWidget(z_label, 0, 3) + position_row = QHBoxLayout() + position_row.setContentsMargins(0, 0, 0, 0) + position_row.setSpacing(4) xPos = QDoubleSpinBox() xPos.setRange(-1000, 1000) xPos.setValue(current_pos.getX()) xPos.valueChanged.connect(lambda v: self._updateLightPosition(light_object, model, 'x', v)) - transform_layout.addWidget(xPos, 1, 1) yPos = QDoubleSpinBox() yPos.setRange(-1000, 1000) yPos.setValue(current_pos.getY()) yPos.valueChanged.connect(lambda v: self._updateLightPosition(light_object, model, 'y', v)) - transform_layout.addWidget(yPos, 1, 2) zPos = QDoubleSpinBox() zPos.setRange(-1000, 1000) zPos.setValue(current_pos.getZ()) zPos.valueChanged.connect(lambda v: self._updateLightPosition(light_object, model, 'z', v)) - transform_layout.addWidget(zPos, 1, 3) - # 世界位置(只读) + for axis_label, widget in (("X:", xPos), ("Y:", yPos), ("Z:", zPos)): + axis = QLabel(axis_label) + axis.setAlignment(Qt.AlignVCenter) + axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + position_row.addWidget(axis) + position_row.addWidget(widget) + + transform_layout.addLayout(position_row, 0, 1, 1, 3) + worldPos = model.getPos(self.world.render) - transform_layout.addWidget(QLabel("世界位置"), 2, 0) + world_position_row = QHBoxLayout() + world_position_row.setContentsMargins(0, 0, 0, 0) + world_position_row.setSpacing(4) + transform_layout.addWidget(QLabel("世界位置"), 1, 0) worldXPos = QDoubleSpinBox() worldXPos.setRange(-1000, 1000) worldXPos.setValue(worldPos.getX()) worldXPos.setReadOnly(True) - transform_layout.addWidget(worldXPos, 3, 1) worldYPos = QDoubleSpinBox() worldYPos.setRange(-1000, 1000) worldYPos.setValue(worldPos.getY()) worldYPos.setReadOnly(True) - transform_layout.addWidget(worldYPos, 3, 2) worldZPos = QDoubleSpinBox() worldZPos.setRange(-1000, 1000) worldZPos.setValue(worldPos.getZ()) worldZPos.setReadOnly(True) - transform_layout.addWidget(worldZPos, 3, 3) + + for axis_label, widget in (("X:", worldXPos), ("Y:", worldYPos), ("Z:", worldZPos)): + axis = QLabel(axis_label) + axis.setAlignment(Qt.AlignVCenter) + axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + world_position_row.addWidget(axis) + world_position_row.addWidget(widget) + + transform_layout.addLayout(world_position_row, 1, 1, 1, 3) # 旋转属性(仅聚光灯) + next_row = 2 if hasattr(light_object, 'direction'): current_hpr = model.getHpr() - transform_layout.addWidget(QLabel("旋转"), 4, 0) - - # H, P, R 标签居中 - h_label = QLabel("H") - p_label = QLabel("P") - r_label = QLabel("R") - h_label.setAlignment(Qt.AlignCenter) - p_label.setAlignment(Qt.AlignCenter) - r_label.setAlignment(Qt.AlignCenter) - - transform_layout.addWidget(h_label, 4, 1) - transform_layout.addWidget(p_label, 4, 2) - transform_layout.addWidget(r_label, 4, 3) + rotation_row = QHBoxLayout() + rotation_row.setContentsMargins(0, 0, 0, 0) + rotation_row.setSpacing(4) + transform_layout.addWidget(QLabel("旋转"), next_row, 0) hRot = QDoubleSpinBox() hRot.setRange(-180, 180) hRot.setValue(current_hpr.getX()) hRot.valueChanged.connect(lambda v: self._updateLightRotation(light_object, model, 'h', v)) - transform_layout.addWidget(hRot, 5, 1) pRot = QDoubleSpinBox() pRot.setRange(-180, 180) pRot.setValue(current_hpr.getY()) pRot.valueChanged.connect(lambda v: self._updateLightRotation(light_object, model, 'p', v)) - transform_layout.addWidget(pRot, 5, 2) rRot = QDoubleSpinBox() rRot.setRange(-180, 180) rRot.setValue(current_hpr.getZ()) rRot.valueChanged.connect(lambda v: self._updateLightRotation(light_object, model, 'r', v)) - transform_layout.addWidget(rRot, 5, 3) + + for axis_label, widget in (("H:", hRot), ("P:", pRot), ("R:", rRot)): + axis = QLabel(axis_label) + axis.setAlignment(Qt.AlignVCenter) + axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + rotation_row.addWidget(axis) + rotation_row.addWidget(widget) + + transform_layout.addLayout(rotation_row, next_row, 1, 1, 3) + next_row += 1 # 缩放属性 current_scale = model.getScale() - scale_row = 6 if hasattr(light_object, 'direction') else 4 - transform_layout.addWidget(QLabel("缩放"), scale_row, 0) + scale_row = QHBoxLayout() + scale_row.setContentsMargins(0, 0, 0, 0) + scale_row.setSpacing(4) + transform_layout.addWidget(QLabel("缩放"), next_row, 0) xScaleSpinBox = QDoubleSpinBox() xScaleSpinBox.setRange(0.01, 100) xScaleSpinBox.setSingleStep(0.1) xScaleSpinBox.setValue(current_scale.getX()) xScaleSpinBox.valueChanged.connect(lambda v: self._updateLightScale(model, 'x', v)) - transform_layout.addWidget(xScaleSpinBox, scale_row + 1, 1) yScaleSpinBox = QDoubleSpinBox() yScaleSpinBox.setRange(0.01, 100) yScaleSpinBox.setSingleStep(0.1) yScaleSpinBox.setValue(current_scale.getY()) yScaleSpinBox.valueChanged.connect(lambda v: self._updateLightScale(model, 'y', v)) - transform_layout.addWidget(yScaleSpinBox, scale_row + 1, 2) zScaleSpinBox = QDoubleSpinBox() zScaleSpinBox.setRange(0.01, 100) zScaleSpinBox.setSingleStep(0.1) zScaleSpinBox.setValue(current_scale.getZ()) zScaleSpinBox.valueChanged.connect(lambda v: self._updateLightScale(model, 'z', v)) - transform_layout.addWidget(zScaleSpinBox, scale_row + 1, 3) + + for axis_label, widget in (("X:", xScaleSpinBox), ("Y:", yScaleSpinBox), ("Z:", zScaleSpinBox)): + axis = QLabel(axis_label) + axis.setAlignment(Qt.AlignVCenter) + axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + scale_row.addWidget(axis) + scale_row.addWidget(widget) + + transform_layout.addLayout(scale_row, next_row, 1, 1, 3) transform_group.setLayout(transform_layout) self._propertyLayout.addWidget(transform_group) @@ -4631,7 +5449,7 @@ class PropertyPanelManager: energySpinBox.setRange(0, 10000) energySpinBox.setValue(light_object.energy) energySpinBox.valueChanged.connect(lambda v: self._updateLightEnergy(light_object, v)) - light_layout.addWidget(energySpinBox, 0, 1) + light_layout.addWidget(energySpinBox, 0, 1, 1, 3) # 半径 light_layout.addWidget(QLabel("半径:"), 1, 0) @@ -4639,7 +5457,7 @@ class PropertyPanelManager: radiusSpinBox.setRange(1, 2000) radiusSpinBox.setValue(light_object.radius) radiusSpinBox.valueChanged.connect(lambda v: self._updateLightRadius(light_object, v)) - light_layout.addWidget(radiusSpinBox, 1, 1) + light_layout.addWidget(radiusSpinBox, 1, 1, 1, 3) # 视野角度(仅聚光灯) if hasattr(light_object, 'fov'): @@ -4648,7 +5466,7 @@ class PropertyPanelManager: fovSpinBox.setRange(1, 180) fovSpinBox.setValue(light_object.fov) fovSpinBox.valueChanged.connect(lambda v: self._updateLightFOV(light_object, v)) - light_layout.addWidget(fovSpinBox, 2, 1) + light_layout.addWidget(fovSpinBox, 2, 1, 1, 3) light_group.setLayout(light_layout) self._propertyLayout.addWidget(light_group) @@ -4801,12 +5619,12 @@ class PropertyPanelManager: if geom_node: geom_node_name = geom_node.getName() unique_name = f"{geom_node_name}({model_name})" - #print(f"材质 {i}: 使用几何节点名称 '{geom_node_name}'") + print(f"材质 {i}: 使用几何节点名称 '{geom_node_name}'") else: material_name = material.get_name() if hasattr(material, 'get_name') and material.get_name() else f"材质{i + 1}" unique_name = f"{material_name}({model_name})" - #print(f"材质 {i}: 未找到几何节点,使用材质名称 '{material_name}'") + print(f"材质 {i}: 未找到几何节点,使用材质名称 '{material_name}'") # 处理重复名称 if unique_name in name_counter: @@ -4825,6 +5643,7 @@ class PropertyPanelManager: # material_group = QGroupBox(display_name) material_group = QGroupBox("材质属性") material_layout = QGridLayout() + material_layout.setColumnMinimumWidth(0, self.column_minimum_width) material_layout.addWidget(QLabel("名称:"), 0, 0) limited_length = 25 @@ -4840,50 +5659,52 @@ class PropertyPanelManager: material_layout.addWidget(QLabel("状态:"), 1, 0) status_label = QLabel(material_status) # status_label.setStyleSheet("color:#FFA500;font-style:italic;font-size:10px;") - material_layout.addWidget(status_label, 1, 1, 1, 2) + material_layout.addWidget(status_label, 1, 1, 1, 3) # 基础颜色编辑 base_color = self._getOrCreateMaterialBaseColor(material) if base_color is not None: - #print(f"材质基础颜色: {base_color}") + print(f"材质基础颜色: {base_color}") - # 基础颜色标题 + # 基础颜色编辑 color_row = 2 if material_status != "标准PBR材质" else 1 - material_layout.addWidget(QLabel("基础颜色"), color_row, 0) - # R, G, B 标签 - r_label = QLabel("R") - g_label = QLabel("G") - b_label = QLabel("B") - r_label.setAlignment(Qt.AlignCenter) - g_label.setAlignment(Qt.AlignCenter) - b_label.setAlignment(Qt.AlignCenter) - - material_layout.addWidget(r_label, color_row, 1) - material_layout.addWidget(g_label, color_row, 2) - material_layout.addWidget(b_label, color_row, 3) - - # RGB 数值框 r_spinbox = QDoubleSpinBox() r_spinbox.setRange(0.0, 1.0) r_spinbox.setSingleStep(0.01) r_spinbox.setValue(base_color.x) r_spinbox.valueChanged.connect(lambda v, mat=material: self._updateMaterialBaseColor(mat, 'r', v)) - material_layout.addWidget(r_spinbox, color_row + 1, 1) g_spinbox = QDoubleSpinBox() g_spinbox.setRange(0.0, 1.0) g_spinbox.setSingleStep(0.01) g_spinbox.setValue(base_color.y) g_spinbox.valueChanged.connect(lambda v, mat=material: self._updateMaterialBaseColor(mat, 'g', v)) - material_layout.addWidget(g_spinbox, color_row + 1, 2) b_spinbox = QDoubleSpinBox() b_spinbox.setRange(0.0, 1.0) b_spinbox.setSingleStep(0.01) b_spinbox.setValue(base_color.z) b_spinbox.valueChanged.connect(lambda v, mat=material: self._updateMaterialBaseColor(mat, 'b', v)) - material_layout.addWidget(b_spinbox, color_row + 1, 3) + + title_label = QLabel("基础颜色") + material_layout.addWidget(title_label, color_row, 0) + + base_color_layout = QHBoxLayout() + base_color_layout.setContentsMargins(0, 0, 0, 0) + base_color_layout.setSpacing(4) + + for label_text, spinbox in (("R:", r_spinbox), ("G:", g_spinbox), ("B:", b_spinbox)): + channel_label = QLabel(label_text) + channel_label.setAlignment(Qt.AlignVCenter) + channel_label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + base_color_layout.addWidget(channel_label) + spinbox.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + base_color_layout.addWidget(spinbox) + + # base_color_layout.addStretch(1) + + material_layout.addLayout(base_color_layout, color_row, 1, 1, 3) else: no_base_color_label = QLabel("无法获取材质基础颜色") no_base_color_label.setStyleSheet("color:#888;font-style:italic;font-size:10px;") @@ -4891,7 +5712,7 @@ class PropertyPanelManager: material_layout.addWidget(no_base_color_label, 1, 1, 1, 3) # 材质属性行 - current_row = color_row + 2 if base_color is not None else 2 + current_row = color_row + 1 if base_color is not None else 2 # 粗糙度 if hasattr(material, 'roughness') and material.roughness is not None: @@ -4904,18 +5725,18 @@ class PropertyPanelManager: roughness_spinbox.setValue(roughness_value) roughness_spinbox.valueChanged.connect( lambda v, mat=material: self._updateMaterialRoughness(mat, v)) - material_layout.addWidget(roughness_spinbox, current_row, 1) + material_layout.addWidget(roughness_spinbox, current_row, 1, 1, 1) except (TypeError, ValueError) as e: print(f"粗糙度值无效: {material.roughness}, 错误: {e}") material_layout.addWidget(QLabel("粗糙度:"), current_row, 0) no_roughness_label = QLabel("粗糙度值无效") no_roughness_label.setStyleSheet("color:#888;font-style:italic;font-size:10px;") - material_layout.addWidget(no_roughness_label, current_row, 1) + material_layout.addWidget(no_roughness_label, current_row, 1, 1, 1) else: material_layout.addWidget(QLabel("粗糙度:"), current_row, 0) no_roughness_label = QLabel("不支持") no_roughness_label.setStyleSheet("color:#888;font-style:italic;font-size:10px;") - material_layout.addWidget(no_roughness_label, current_row, 1) + material_layout.addWidget(no_roughness_label, current_row, 1, 1, 1) current_row += 1 @@ -4929,18 +5750,18 @@ class PropertyPanelManager: metallic_spinbox.setSingleStep(0.01) metallic_spinbox.setValue(metallic_value) metallic_spinbox.valueChanged.connect(lambda v, mat=material: self._updateMaterialMetallic(mat, v)) - material_layout.addWidget(metallic_spinbox, current_row, 1) + material_layout.addWidget(metallic_spinbox, current_row, 1, 1, 1) except (TypeError, ValueError) as e: print(f"金属性值无效: {material.metallic}, 错误: {e}") material_layout.addWidget(QLabel("金属性:"), current_row, 0) no_metallic_label = QLabel("值无效") no_metallic_label.setStyleSheet("color:#888;font-style:italic;font-size:10px;") - material_layout.addWidget(no_metallic_label, current_row, 1) + material_layout.addWidget(no_metallic_label, current_row, 1, 1, 1) else: material_layout.addWidget(QLabel("金属性:"), current_row, 0) no_metallic_label = QLabel("不支持") no_metallic_label.setStyleSheet("color:#888;font-style:italic;font-size:10px;") - material_layout.addWidget(no_metallic_label, current_row, 1) + material_layout.addWidget(no_metallic_label, current_row, 1, 1, 1) current_row += 1 @@ -4954,18 +5775,18 @@ class PropertyPanelManager: ior_spinbox.setSingleStep(0.01) ior_spinbox.setValue(ior_value) ior_spinbox.valueChanged.connect(lambda v, mat=material: self._updateMaterialIOR(mat, v)) - material_layout.addWidget(ior_spinbox, current_row, 1) + material_layout.addWidget(ior_spinbox, current_row, 1, 1, 1) except (TypeError, ValueError) as e: print(f"折射率值无效: {material.refractive_index}, 错误: {e}") material_layout.addWidget(QLabel("折射率:"), current_row, 0) no_ior_label = QLabel("折射率值无效") no_ior_label.setStyleSheet("color:#888;font-style:italic;font-size:10px;") - material_layout.addWidget(no_ior_label, current_row, 1) + material_layout.addWidget(no_ior_label, current_row, 1, 1, 1) else: material_layout.addWidget(QLabel("折射率:"), current_row, 0) no_ior_label = QLabel("此材质不支持折射率编辑") no_ior_label.setStyleSheet("color:#888;font-style:italic;font-size:10px;") - material_layout.addWidget(no_ior_label, current_row, 1) + material_layout.addWidget(no_ior_label, current_row, 1, 1, 1) current_row += 1 @@ -5231,7 +6052,7 @@ class PropertyPanelManager: try: # 方法1: 尝试获取base_color属性 if hasattr(material, 'base_color') and material.base_color is not None: - #print(f"✓ 找到base_color属性: {material.base_color}") + print(f"✓ 找到base_color属性: {material.base_color}") return material.base_color # 方法2: 尝试调用get_base_color方法 @@ -5249,7 +6070,7 @@ class PropertyPanelManager: try: diffuse_color = material.getDiffuse() if diffuse_color is not None: - #print(f"✓ 从diffuse颜色获取: {diffuse_color}") + print(f"✓ 从diffuse颜色获取: {diffuse_color}") # 同时设置为base_color if hasattr(material, 'set_base_color'): material.set_base_color(diffuse_color) @@ -6857,7 +7678,7 @@ class PropertyPanelManager: # print(f"找到匹配的几何节点: {geom_np.get_name()}") return geom_np - #print("未找到匹配的几何节点") + print("未找到匹配的几何节点") return None def _findSpecificGeomNodeForMaterial(self, target_material): @@ -7467,8 +8288,8 @@ class PropertyPanelManager: print(f"🎯 目标节点: {node.getName()}") # 3. 设置材质金属性为1.0 - material.set_metallic(1.0) - print("🔧 材质金属性设置为1.0") + # material.set_metallic(1.0) + # print("🔧 材质金属性设置为1.0") # # 4. 创建纹理阶段 # metallic_stage = TextureStage("metallic_map") @@ -8026,6 +8847,7 @@ class PropertyPanelManager: # 动画控制组 animation_group = QGroupBox("动画控制") animation_layout = QGridLayout() + animation_layout.setColumnMinimumWidth(0, 115) # 首先检测骨骼动画 has_skeletal_anim = False @@ -8096,8 +8918,8 @@ class PropertyPanelManager: layout.addWidget(self.animation_combo, current_row, 1, 1, 3) current_row += 1 - btn_box = QWidget() - btn_lay = QHBoxLayout(btn_box) + # btn_box = QWidget() + btn_lay = QHBoxLayout() for txt, slot in (("播放", self._playAnimation), ("暂停", self._pauseAnimation), ("停止", self._stopAnimation), @@ -8106,7 +8928,7 @@ class PropertyPanelManager: btn.clicked.connect(lambda _, f=slot: f(origin_model)) btn_lay.addWidget(btn) layout.addWidget(QLabel("控制:"), current_row, 0) - layout.addWidget(btn_box, current_row, 1, 1, 3) + layout.addLayout(btn_lay, current_row, 1, 1, 3) current_row += 1 self.speed_spinbox = QDoubleSpinBox() @@ -8142,7 +8964,7 @@ class PropertyPanelManager: format_info = self._getModelFormat(origin_model) processed = [] - #print(f"[动画分析] 格式: {format_info}, 原始动画名称: {anim_names}") + print(f"[动画分析] 格式: {format_info}, 原始动画名称: {anim_names}") for name in anim_names: display_name = name @@ -8176,7 +8998,7 @@ class PropertyPanelManager: display_name = name processed.append((display_name, original_name)) - #print(f"[动画分析] {original_name} → {display_name}") + print(f"[动画分析] {original_name} → {display_name}") return processed @@ -8210,7 +9032,7 @@ class PropertyPanelManager: if frames > 1: valid_anims += 1 total_frames += frames - #print(f"[动画分析] '{anim_name}': {frames} 帧") + print(f"[动画分析] '{anim_name}': {frames} 帧") else: print(f"[动画分析] '{anim_name}': 无有效帧数 ({frames})") except Exception as e: @@ -8615,12 +9437,32 @@ except Exception as e: actor = self._getActor(origin_model) if not actor: return + + original_world_pos = origin_model.getPos(self.world.render) + original_world_hpr = origin_model.getHpr(self.world.render) + original_world_scale = origin_model.getScale(self.world.render) actor.setPos(origin_model.getPos()) actor.setHpr(origin_model.getHpr()) actor.setScale(origin_model.getScale()) + origin_model.hide() actor.show() + #创建人物来维持世界坐标不变 + def maintainWorldPosition(task): + try: + if not actor.isEmpty(): + actor.setPos(self.world.render,original_world_pos) + actor.setHpr(self.world.render,original_world_hpr) + actor.setScale(self.world.render,original_world_scale) + return task.cont + else: + return task.done + except: + return task.done + + taskMgr.add(maintainWorldPosition,f"maintain_anim_pos_{id(actor)}") + if hasattr(self, 'animation_combo'): # 获取原始动画名称(存储在 userData 中) current_index = self.animation_combo.currentIndex() @@ -8758,18 +9600,45 @@ except Exception as e: # 创建碰撞检测组 collision_group = QGroupBox("碰撞检测") collision_layout = QGridLayout() + collision_layout.setColumnMinimumWidth(0, self.column_minimum_width) + # collision_layout.setColumnStretch(0, 0) + # collision_layout.setColumnStretch(1, 1) + # collision_layout.setColumnStretch(2, 1) + # collision_layout.setColumnStretch(3, 1) # 检查模型是否已有碰撞 has_collision = self._hasCollision(model) - # 碰撞状态标签 - status_label = QLabel("状态:") - collision_layout.addWidget(status_label, 0, 0) + # 创建主容器 + main_container = QWidget() + main_layout = QVBoxLayout(main_container) + main_layout.setContentsMargins(8, 8, 8, 8) + main_layout.setSpacing(12) - # 状态文本(需要保存引用以便更新) - self.collision_status_text = QLabel("已启用" if has_collision else "未启用") - self.collision_status_text.setStyleSheet("color: green;" if has_collision else "color: red;") - collision_layout.addWidget(self.collision_status_text, 0, 1) + # 状态和形状选择区域 + header_container = QWidget() + header_layout = QGridLayout(header_container) + header_layout.setContentsMargins(0, 0, 0, 0) + header_layout.setSpacing(8) + + # 调整列宽,保证控件对齐 + # collision_layout.setColumnStretch(0, 0) + # collision_layout.setColumnStretch(1, 0) + # collision_layout.setColumnStretch(2, 0) + # collision_layout.setColumnStretch(3, 0) + + # 碰撞状态行 + status_layout = QHBoxLayout() + status_label = QLabel("状态:") + status_layout.addWidget(status_label) + + # 状态徽章(使用固定宽度样式) + if has_collision: + self.collision_status_badge = self.createFixedStatusBadge("已启用", "green") + else: + self.collision_status_badge = self.createFixedStatusBadge("未启用", "red") + status_layout.addWidget(self.collision_status_badge) + collision_layout.addLayout(status_layout, 0, 0, 1, 1) # 形状选择标签(始终显示) self.collision_shape_label = QLabel("碰撞形状:") @@ -8784,7 +9653,7 @@ except Exception as e: "平面 (Plane)", "自动选择 (Auto)" ]) - collision_layout.addWidget(self.collision_shape_combo, 1, 1) + collision_layout.addWidget(self.collision_shape_combo, 1, 1, 1, 3) # 保存布局引用,用于动态添加/移除控件 self.collision_layout = collision_layout @@ -8805,16 +9674,21 @@ except Exception as e: # 添加碰撞参数调整控件 current_row = self._addCollisionParameterControls(model, collision_layout, current_row, current_shape) - # 显示/隐藏切换按钮 - self.collision_visibility_button = QPushButton("隐藏碰撞" if is_collision_visible else "显示碰撞") + # 显示/隐藏切换按钮(使用现代化样式) + visibility_text = "隐藏碰撞" if is_collision_visible else "显示碰撞" + self.collision_visibility_button = self.createModernButton(visibility_text) + self.collision_visibility_button.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + self.collision_visibility_button.setStyleSheet(self._collision_button_style()) self.collision_visibility_button.clicked.connect(lambda: self._toggleCollisionVisibility(model)) - collision_layout.addWidget(self.collision_visibility_button, current_row, 0, 1, 2) + collision_layout.addWidget(self.collision_visibility_button, current_row, 0, 1, 4) current_row += 1 - # 移除碰撞按钮 - self.collision_button = QPushButton("移除碰撞") + # 移除碰撞按钮(使用现代化样式) + self.collision_button = self.createModernButton("移除碰撞") + self.collision_button.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + self.collision_button.setStyleSheet(self._collision_button_style()) self.collision_button.clicked.connect(lambda: self._removeCollisionAndUpdate(model)) - collision_layout.addWidget(self.collision_button, current_row, 0, 1, 2) + collision_layout.addWidget(self.collision_button, current_row, 0, 1, 4) else: # 如果没有碰撞,设置默认选择并允许编辑 self.collision_shape_combo.setCurrentText("球形 (Sphere)") @@ -8827,10 +9701,12 @@ except Exception as e: if hasattr(self, 'collision_visibility_button'): self.collision_visibility_button.setVisible(False) - # 添加碰撞按钮 - self.collision_button = QPushButton("添加碰撞") + # 添加碰撞按钮(使用现代化样式) + self.collision_button = self.createModernButton("添加碰撞") + self.collision_button.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + self.collision_button.setStyleSheet(self._collision_button_style()) self.collision_button.clicked.connect(lambda: self._addCollisionAndUpdate(model)) - collision_layout.addWidget(self.collision_button, current_row, 0, 1, 2) + collision_layout.addWidget(self.collision_button, current_row, 0, 1, 4) collision_group.setLayout(collision_layout) self._propertyLayout.addWidget(collision_group) @@ -8846,32 +9722,47 @@ except Exception as e: # 位置调整控件(所有类型都有) pos_label = QLabel("位置偏移:") - layout.addWidget(pos_label, current_row, 0) - current_row += 1 + pos_label.setAlignment(Qt.AlignLeft | Qt.AlignTop) + pos_label.setVisible(True) # 确保可见 # X, Y, Z 位置调整 - self.collision_pos_x = self._createCollisionSpinBox(-1000000, 1000000, 2) - self.collision_pos_y = self._createCollisionSpinBox(-1000000, 1000000, 2) - self.collision_pos_z = self._createCollisionSpinBox(-1000000, 1000000, 2) + self.collision_pos_x = self._createCollisionSpinBox(-100, 100, 2) + self.collision_pos_y = self._createCollisionSpinBox(-100, 100, 2) + self.collision_pos_z = self._createCollisionSpinBox(-100, 100, 2) - # 只在没有现有碰撞时设置默认值,否则由_loadCurrentCollisionParameters加载实际值 - if not self._hasCollision(model): - # 设置默认位置偏移(无偏移) - self.collision_pos_x.setValue(0.0) - self.collision_pos_y.setValue(0.0) - self.collision_pos_z.setValue(0.0) + for spinbox in (self.collision_pos_x, self.collision_pos_y, self.collision_pos_z): + spinbox.setMinimumWidth(140) + spinbox.setVisible(True) - layout.addWidget(QLabel("X:"), current_row, 0) - layout.addWidget(self.collision_pos_x, current_row, 1) - current_row += 1 + axis_controls = [ + ("X:", self.collision_pos_x), + ("Y:", self.collision_pos_y), + ("Z:", self.collision_pos_z), + ] - layout.addWidget(QLabel("Y:"), current_row, 0) - layout.addWidget(self.collision_pos_y, current_row, 1) - current_row += 1 + for offset, (axis_text, spinbox) in enumerate(axis_controls): + row = current_row + offset + axis_layout = QHBoxLayout() + if offset == 0: + layout.addWidget(pos_label, row, 0) + # axis_label.addWidget(pos_label) + else: + spacer = QLabel("") + spacer.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) + spacer.setVisible(True) + layout.addWidget(spacer, row, 0) + # axis_layout.addWidget(spacer) - layout.addWidget(QLabel("Z:"), current_row, 0) - layout.addWidget(self.collision_pos_z, current_row, 1) - current_row += 1 + axis_label = QLabel(axis_text) + axis_label.setAlignment(Qt.AlignVCenter) + axis_label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + axis_layout.addWidget(axis_label) + axis_layout.addWidget(spinbox) + # layout.addWidget(axis_label, row, 1) + # layout.addWidget(spinbox, row, 2, 1, 2) + layout.addLayout(axis_layout, row, 1, 1, 3) + + current_row += len(axis_controls) # 连接位置变化信号 self.collision_pos_x.valueChanged.connect(lambda v: self._updateCollisionPosition(model, 'x', v)) @@ -8912,6 +9803,7 @@ except Exception as e: # 半径调整 radius_label = QLabel("半径:") layout.addWidget(radius_label, current_row, 0) + # layout.addWidget(QLabel(""), current_row, 1) self.collision_radius = self._createCollisionSpinBox(0.01, 100000, 2) @@ -8931,7 +9823,7 @@ except Exception as e: self.collision_radius.setValue(default_radius) self.collision_radius.valueChanged.connect(lambda v: self._updateSphereRadius(model, v)) - layout.addWidget(self.collision_radius, current_row, 1) + layout.addWidget(self.collision_radius, current_row, 1, 1, 3) current_row += 1 return current_row @@ -8942,7 +9834,8 @@ except Exception as e: size_label = QLabel("尺寸:") layout.addWidget(size_label, current_row, 0) - current_row += 1 + # layout.addWidget(QLabel(""), current_row, 1) + # current_row += 1 # 宽度、长度、高度 self.collision_width = self._createCollisionSpinBox(0.001, 100000, 2) @@ -8968,17 +9861,33 @@ except Exception as e: self.collision_length.setValue(model_size.y) self.collision_height.setValue(model_size.z) - layout.addWidget(QLabel("宽度:"), current_row, 0) - layout.addWidget(self.collision_width, current_row, 1) - current_row += 1 + box_controls = [ + ("宽度:", self.collision_width), + ("长度:", self.collision_length), + ("高度:", self.collision_height), + ] - layout.addWidget(QLabel("长度:"), current_row, 0) - layout.addWidget(self.collision_length, current_row, 1) - current_row += 1 + for offset, (text, spinbox) in enumerate(box_controls): + text_layout = QHBoxLayout() + if offset == 0: + layout.addWidget(size_label, current_row, 0) + else: + spacer = QLabel("") + spacer.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) + spacer.setVisible(True) + layout.addWidget(spacer, current_row, 0) + # placeholder = QLabel("") + # layout.addWidget(placeholder, current_row, 0) - layout.addWidget(QLabel("高度:"), current_row, 0) - layout.addWidget(self.collision_height, current_row, 1) - current_row += 1 + axis_label = QLabel(text) + axis_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) + axis_label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + text_layout.addWidget(axis_label) + text_layout.addWidget(spinbox) + layout.addLayout(text_layout, current_row, 1, 1, 3) + # layout.addWidget(axis_label, current_row, 1) + # layout.addWidget(spinbox, current_row, 2) + current_row += 1 # 连接信号 self.collision_width.valueChanged.connect(lambda v: self._updateBoxSize(model, 'width', v)) @@ -8994,6 +9903,7 @@ except Exception as e: # 半径和高度 radius_label = QLabel("半径:") layout.addWidget(radius_label, current_row, 0) + # layout.addWidget(QLabel(""), current_row, 1) self.collision_capsule_radius = self._createCollisionSpinBox(0.01, 100000, 2) @@ -9017,11 +9927,12 @@ except Exception as e: self.collision_capsule_radius.setValue(default_radius) self.collision_capsule_radius.valueChanged.connect(lambda v: self._updateCapsuleRadius(model, v)) - layout.addWidget(self.collision_capsule_radius, current_row, 1) + layout.addWidget(self.collision_capsule_radius, current_row, 1, 1, 3) current_row += 1 height_label = QLabel("高度:") layout.addWidget(height_label, current_row, 0) + # layout.addWidget(QLabel(""), current_row, 1) self.collision_capsule_height = self._createCollisionSpinBox(0.01, 10000, 2) @@ -9041,7 +9952,7 @@ except Exception as e: self.collision_capsule_height.setValue(model_size.z) self.collision_capsule_height.valueChanged.connect(lambda v: self._updateCapsuleHeight(model, v)) - layout.addWidget(self.collision_capsule_height, current_row, 1) + layout.addWidget(self.collision_capsule_height, current_row, 1, 1, 3) current_row += 1 return current_row @@ -9053,7 +9964,8 @@ except Exception as e: # 法向量 normal_label = QLabel("法向量:") layout.addWidget(normal_label, current_row, 0) - current_row += 1 + # layout.addWidget(QLabel(""), current_row, 1) + # current_row += 1 self.collision_normal_x = self._createCollisionSpinBox(-1, 1, 2) self.collision_normal_y = self._createCollisionSpinBox(-1, 1, 2) @@ -9066,16 +9978,40 @@ except Exception as e: self.collision_normal_y.setValue(0.0) self.collision_normal_z.setValue(1.0) - layout.addWidget(QLabel("Nx:"), current_row, 0) - layout.addWidget(self.collision_normal_x, current_row, 1) + # placeholder_nx = QLabel("") + # layout.addWidget(placeholder_nx, current_row, 0) + + hbox_nx = QHBoxLayout() + axis_label_nx = QLabel("Nx:") + axis_label_nx.setAlignment(Qt.AlignRight | Qt.AlignVCenter) + axis_label_nx.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + hbox_nx.addWidget(axis_label_nx) + hbox_nx.addWidget(self.collision_normal_x) + layout.addLayout(hbox_nx, current_row, 1, 1, 3) current_row += 1 - layout.addWidget(QLabel("Ny:"), current_row, 0) - layout.addWidget(self.collision_normal_y, current_row, 1) + placeholder_ny = QLabel("") + layout.addWidget(placeholder_ny, current_row, 0) + + hbox_ny = QHBoxLayout() + axis_label_ny = QLabel("Ny:") + axis_label_ny.setAlignment(Qt.AlignRight | Qt.AlignVCenter) + axis_label_ny.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + hbox_ny.addWidget(axis_label_ny) + hbox_ny.addWidget(self.collision_normal_y) + layout.addLayout(hbox_ny, current_row, 1, 1, 3) current_row += 1 - layout.addWidget(QLabel("Nz:"), current_row, 0) - layout.addWidget(self.collision_normal_z, current_row, 1) + placeholder_nz = QLabel("") + layout.addWidget(placeholder_nz, current_row, 0) + + hbox_nz = QHBoxLayout() + axis_label_nz = QLabel("Nz:") + axis_label_nz.setAlignment(Qt.AlignRight | Qt.AlignVCenter) + axis_label_nz.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + hbox_nz.addWidget(axis_label_nz) + hbox_nz.addWidget(self.collision_normal_z) + layout.addLayout(hbox_nz, current_row, 1, 1, 3) current_row += 1 # 连接信号 @@ -9139,7 +10075,12 @@ except Exception as e: try: if hasattr(self, 'collision_visibility_button'): is_visible = self._isCollisionVisible(model) - self.collision_visibility_button.setText("隐藏碰撞" if is_visible else "显示碰撞") + button_text = "隐藏碰撞" if is_visible else "显示碰撞" + self.collision_visibility_button.setText(button_text) + + # 应用统一按钮样式 + self.collision_visibility_button.setStyleSheet(self._collision_button_style()) + print(f"更新可见性按钮:{model.getName()} - {'可见' if is_visible else '隐藏'}") except Exception as e: print(f"更新碰撞可见性按钮失败: {e}") @@ -9307,6 +10248,17 @@ except Exception as e: except Exception as e: print(f"移除碰撞失败: {e}") + def _findButtonRow(self, layout): + """查找按钮应该在的行数""" + # 简单返回一个合适的行数,基于布局中现有的项目数 + row_count = 0 + for i in range(layout.count()): + item = layout.itemAt(i) + if item and item.widget(): + row, col, rowspan, colspan = layout.getItemPosition(i) + row_count = max(row_count, row + 1) + return row_count + def _updateCollisionPanelState(self, model): """更新碰撞面板状态""" try: @@ -9315,25 +10267,47 @@ except Exception as e: return self._updating_collision_panel = True - - if hasattr(self, 'collision_button') and hasattr(self, 'collision_status_text') and hasattr(self, + print("-------------------------------------") + if hasattr(self, 'collision_button') and hasattr(self, 'collision_status_badge') and hasattr(self, 'collision_shape_combo'): has_collision = self._hasCollision(model) + print(f"模型 {model.getName()} 是否有碰撞体: {has_collision}-------------------------------------------------") - # 更新状态文本和颜色 - self.collision_status_text.setText("已启用" if has_collision else "未启用") - self.collision_status_text.setStyleSheet("color: green;" if has_collision else "color: red;") + # 更新状态徽章(使用固定宽度) + if has_collision: + new_badge = self.createFixedStatusBadge("已启用", "green") + else: + new_badge = self.createFixedStatusBadge("未启用", "red") + + # 替换旧的徽章 + old_badge = self.collision_status_badge + parent_layout = old_badge.parent().layout() + if parent_layout: + # 找到旧徽章在布局中的位置 + for i in range(parent_layout.count()): + item = parent_layout.itemAt(i) + if item and item.widget() == old_badge: + # 移除旧徽章并添加新徽章 + parent_layout.removeWidget(old_badge) + old_badge.deleteLater() + parent_layout.addWidget(new_badge, 0, 0, 1, 1) # 状态徽章在第0行第1列 + self.collision_status_badge = new_badge + break if has_collision: # 有碰撞:显示移除按钮,下拉框变为只读并显示当前类型 - self.collision_button.setText("移除碰撞") + if hasattr(self, 'collision_button'): + # 更新按钮文本和样式(简单方法:直接设置文本和样式) + self.collision_button.setText("移除碰撞") + # 应用统一按钮样式 + self.collision_button.setStyleSheet(self._collision_button_style()) - # 先断开所有连接,再重新连接 - try: - self.collision_button.clicked.disconnect() - except: - pass - self.collision_button.clicked.connect(lambda: self._removeCollisionAndUpdate(model)) + # 重新连接信号 + try: + self.collision_button.clicked.disconnect() + except: + pass + self.collision_button.clicked.connect(lambda: self._removeCollisionAndUpdate(model)) # 获取并显示当前碰撞类型,设置为只读 current_shape = self._getCurrentCollisionShape(model) @@ -9357,14 +10331,17 @@ except Exception as e: else: # 无碰撞:显示添加按钮,下拉框变为可编辑 - self.collision_button.setText("添加碰撞") + if hasattr(self, 'collision_button'): + self.collision_button.setText("添加碰撞") + # 应用统一按钮样式 + self.collision_button.setStyleSheet(self._collision_button_style()) - # 先断开所有连接,再重新连接 - try: - self.collision_button.clicked.disconnect() - except: - pass - self.collision_button.clicked.connect(lambda: self._addCollisionAndUpdate(model)) + # 先断开所有连接,再重新连接 + try: + self.collision_button.clicked.disconnect() + except: + pass + self.collision_button.clicked.connect(lambda: self._addCollisionAndUpdate(model)) # 恢复为可编辑状态 self.collision_shape_combo.setEnabled(True) @@ -9828,35 +10805,47 @@ except Exception as e: # 位置调整控件 pos_label = QLabel("位置偏移:") + pos_label.setAlignment(Qt.AlignLeft | Qt.AlignTop) pos_label.setVisible(True) # 确保可见 - layout.addWidget(pos_label, current_row, 0) - current_row += 1 # X, Y, Z 位置调整 - self.collision_pos_x = self._createCollisionSpinBox(-100, 100, 2) - self.collision_pos_y = self._createCollisionSpinBox(-100, 100, 2) - self.collision_pos_z = self._createCollisionSpinBox(-100, 100, 2) + self.collision_pos_x = self._createCollisionSpinBox(-1000000, 1000000, 2) + self.collision_pos_y = self._createCollisionSpinBox(-1000000, 1000000, 2) + self.collision_pos_z = self._createCollisionSpinBox(-1000000, 1000000, 2) - x_label = QLabel("X:") - x_label.setVisible(True) - layout.addWidget(x_label, current_row, 0) - self.collision_pos_x.setVisible(True) - layout.addWidget(self.collision_pos_x, current_row, 1) - current_row += 1 + for spinbox in (self.collision_pos_x, self.collision_pos_y, self.collision_pos_z): + spinbox.setMinimumWidth(140) + spinbox.setVisible(True) - y_label = QLabel("Y:") - y_label.setVisible(True) - layout.addWidget(y_label, current_row, 0) - self.collision_pos_y.setVisible(True) - layout.addWidget(self.collision_pos_y, current_row, 1) - current_row += 1 + axis_controls = [ + ("X:", self.collision_pos_x), + ("Y:", self.collision_pos_y), + ("Z:", self.collision_pos_z), + ] - z_label = QLabel("Z:") - z_label.setVisible(True) - layout.addWidget(z_label, current_row, 0) - self.collision_pos_z.setVisible(True) - layout.addWidget(self.collision_pos_z, current_row, 1) - current_row += 1 + for offset, (axis_text, spinbox) in enumerate(axis_controls): + row = current_row + offset + axis_layout = QHBoxLayout() + if offset == 0: + layout.addWidget(pos_label, row, 0) + # axis_label.addWidget(pos_label) + else: + spacer = QLabel("") + spacer.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) + spacer.setVisible(True) + layout.addWidget(spacer, row, 0) + # axis_layout.addWidget(spacer) + + axis_label = QLabel(axis_text) + axis_label.setAlignment(Qt.AlignVCenter) + axis_label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + axis_layout.addWidget(axis_label) + axis_layout.addWidget(spinbox) + # layout.addWidget(axis_label, row, 1) + # layout.addWidget(spinbox, row, 2, 1, 2) + layout.addLayout(axis_layout, row, 1, 1, 3) + + current_row += len(axis_controls) # 连接位置变化信号 self.collision_pos_x.valueChanged.connect(lambda v: self._updateCollisionPosition(model, 'x', v)) @@ -9891,11 +10880,12 @@ except Exception as e: radius_label = QLabel("半径:") radius_label.setVisible(True) layout.addWidget(radius_label, current_row, 0) + layout.addWidget(QLabel(""), current_row, 1) self.collision_radius = self._createCollisionSpinBox(0.01, 10000, 2) self.collision_radius.setVisible(True) self.collision_radius.valueChanged.connect(lambda v: self._updateSphereRadius(model, v)) - layout.addWidget(self.collision_radius, current_row, 1) + layout.addWidget(self.collision_radius, current_row, 1, 1, 3) current_row += 1 return current_row @@ -9907,32 +10897,38 @@ except Exception as e: size_label = QLabel("尺寸:") size_label.setVisible(True) layout.addWidget(size_label, current_row, 0) - current_row += 1 + # layout.addWidget(QLabel(""), current_row, 1) + # current_row += 1 self.collision_width = self._createCollisionSpinBox(0.01, 10000, 2) self.collision_length = self._createCollisionSpinBox(0.01, 10000, 2) self.collision_height = self._createCollisionSpinBox(0.01, 10000, 2) - width_label = QLabel("宽度:") - width_label.setVisible(True) - layout.addWidget(width_label, current_row, 0) - self.collision_width.setVisible(True) - layout.addWidget(self.collision_width, current_row, 1) - current_row += 1 + box_controls = [ + ("宽度:", self.collision_width), + ("长度:", self.collision_length), + ("高度:", self.collision_height), + ] - length_label = QLabel("长度:") - length_label.setVisible(True) - layout.addWidget(length_label, current_row, 0) - self.collision_length.setVisible(True) - layout.addWidget(self.collision_length, current_row, 1) - current_row += 1 + for offset,(text, spinbox) in enumerate(box_controls): + axis_layout = QHBoxLayout() + if offset == 0: + layout.addWidget(size_label, current_row, 0) + else: + spacer = QLabel("") + spacer.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) + spacer.setVisible(True) + layout.addWidget(spacer, current_row, 0) - height_label = QLabel("高度:") - height_label.setVisible(True) - layout.addWidget(height_label, current_row, 0) - self.collision_height.setVisible(True) - layout.addWidget(self.collision_height, current_row, 1) - current_row += 1 + axis_label = QLabel(text) + axis_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) + axis_label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + axis_label.setVisible(True) + axis_layout.addWidget(axis_label) + spinbox.setVisible(True) + axis_layout.addWidget(spinbox) + layout.addLayout(axis_layout, current_row, 1, 1, 3) + current_row += 1 # 连接信号 self.collision_width.valueChanged.connect(lambda v: self._updateBoxSize(model, 'width', v)) @@ -9950,7 +10946,7 @@ except Exception as e: self.collision_capsule_radius = self._createCollisionSpinBox(0.01, 10000, 2) self.collision_capsule_radius.valueChanged.connect(lambda v: self._updateCapsuleRadius(model, v)) - layout.addWidget(self.collision_capsule_radius, current_row, 1) + layout.addWidget(self.collision_capsule_radius, current_row, 1, 1 , 3) current_row += 1 height_label = QLabel("高度:") @@ -9958,7 +10954,7 @@ except Exception as e: self.collision_capsule_height = self._createCollisionSpinBox(0.01, 10000, 2) self.collision_capsule_height.valueChanged.connect(lambda v: self._updateCapsuleHeight(model, v)) - layout.addWidget(self.collision_capsule_height, current_row, 1) + layout.addWidget(self.collision_capsule_height, current_row, 1, 1, 3) current_row += 1 return current_row @@ -9969,22 +10965,42 @@ except Exception as e: normal_label = QLabel("法向量:") layout.addWidget(normal_label, current_row, 0) - current_row += 1 self.collision_normal_x = self._createCollisionSpinBox(-1, 1, 2) self.collision_normal_y = self._createCollisionSpinBox(-1, 1, 2) self.collision_normal_z = self._createCollisionSpinBox(-1, 1, 2) - layout.addWidget(QLabel("Nx:"), current_row, 0) - layout.addWidget(self.collision_normal_x, current_row, 1) + nx_layout = QHBoxLayout() + axis_label = QLabel("Nx:") + axis_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) + axis_label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + nx_layout.addWidget(axis_label) + nx_layout.addWidget(self.collision_normal_x) + layout.addLayout(nx_layout, current_row, 1, 1, 3) current_row += 1 - layout.addWidget(QLabel("Ny:"), current_row, 0) - layout.addWidget(self.collision_normal_y, current_row, 1) + ny_layout = QHBoxLayout() + placeholder = QLabel("") + layout.addWidget(placeholder, current_row, 0) + + axis_label = QLabel("Ny:") + axis_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) + axis_label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + ny_layout.addWidget(axis_label) + ny_layout.addWidget(self.collision_normal_y) + layout.addLayout(ny_layout, current_row, 1, 1, 3) current_row += 1 - layout.addWidget(QLabel("Nz:"), current_row, 0) - layout.addWidget(self.collision_normal_z, current_row, 1) + nz_layout = QHBoxLayout() + placeholder = QLabel("") + layout.addWidget(placeholder, current_row, 0) + + axis_label = QLabel("Nz:") + axis_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) + axis_label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + nz_layout.addWidget(axis_label) + nz_layout.addWidget(self.collision_normal_z) + layout.addLayout(nz_layout, current_row, 1, 1, 3) current_row += 1 # 连接信号 @@ -10005,8 +11021,9 @@ except Exception as e: current_row = layout.rowCount() self.collision_visibility_button = QPushButton("隐藏碰撞" if is_collision_visible else "显示碰撞") + self.collision_visibility_button.setStyleSheet(self._collision_button_style()) self.collision_visibility_button.clicked.connect(lambda: self._toggleCollisionVisibility(model)) - layout.addWidget(self.collision_visibility_button, current_row - 1, 0, 1, 2) + layout.addWidget(self.collision_visibility_button, current_row - 1, 0, 1, 4) except Exception as e: print(f"添加可见性按钮失败: {e}") @@ -10019,12 +11036,12 @@ except Exception as e: # 移动可见性按钮 if hasattr(self, 'collision_visibility_button'): - layout.addWidget(self.collision_visibility_button, new_row, 0, 1, 2) + layout.addWidget(self.collision_visibility_button, new_row, 0, 1, 4) new_row += 1 # 移动主按钮 if hasattr(self, 'collision_button'): - layout.addWidget(self.collision_button, new_row, 0, 1, 2) + layout.addWidget(self.collision_button, new_row, 0, 1, 4) except Exception as e: print(f"重新定位按钮失败: {e}") diff --git a/ui/simple_maintenance_login.py b/ui/simple_maintenance_login.py new file mode 100644 index 00000000..2cf5ba45 --- /dev/null +++ b/ui/simple_maintenance_login.py @@ -0,0 +1,220 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +简化的维修系统登录界面 +解决显示和输入问题 +""" + +from PyQt5.QtWidgets import ( + QDialog, QVBoxLayout, QHBoxLayout, QFormLayout, QGroupBox, + QLineEdit, QPushButton, QLabel, QMessageBox +) +from PyQt5.QtCore import Qt, pyqtSignal + + +class SimpleMaintenanceLoginDialog(QDialog): + """简化的维修系统登录对话框""" + + login_success = pyqtSignal() # 登录成功信号 + + def __init__(self, parent=None): + super().__init__(parent) + self.setupUI() + + def setupUI(self): + """设置用户界面""" + self.setWindowTitle("维修系统 - 登录") + self.resize(400, 250) + self.setModal(True) + + # 设置简洁的样式 + self.setStyleSheet(""" + QDialog { + background-color: #ffffff; + font-family: "Microsoft YaHei", Arial, sans-serif; + } + QGroupBox { + font-size: 14px; + font-weight: bold; + color: #2c3e50; + border: 2px solid #bdc3c7; + border-radius: 5px; + margin-top: 10px; + padding-top: 10px; + } + QGroupBox::title { + subcontrol-origin: margin; + left: 10px; + padding: 0 5px; + } + QLabel { + color: #2c3e50; + font-size: 14px; + } + QLineEdit { + background-color: #ffffff; + border: 2px solid #bdc3c7; + border-radius: 3px; + padding: 6px 8px; + font-size: 14px; + color: #2c3e50; + min-height: 16px; + } + QLineEdit:focus { + border-color: #3498db; + background-color: #ffffff; + } + QPushButton { + background-color: #3498db; + color: white; + border: none; + padding: 8px 20px; + border-radius: 3px; + font-size: 14px; + font-weight: bold; + min-height: 16px; + } + QPushButton:hover { + background-color: #2980b9; + } + QPushButton:pressed { + background-color: #21618c; + } + QPushButton#cancel_btn { + background-color: #95a5a6; + } + QPushButton#cancel_btn:hover { + background-color: #7f8c8d; + } + """) + + # 主布局 + main_layout = QVBoxLayout(self) + main_layout.setSpacing(20) + main_layout.setContentsMargins(30, 30, 30, 30) + + # 标题 + title_label = QLabel("🔧 维修系统") + title_label.setAlignment(Qt.AlignCenter) + title_label.setStyleSheet(""" + font-size: 18px; + font-weight: bold; + color: #2c3e50; + margin: 10px 0px; + """) + main_layout.addWidget(title_label) + + # 登录表单组 + form_group = QGroupBox("身份验证") + form_layout = QFormLayout(form_group) + form_layout.setSpacing(15) + form_layout.setContentsMargins(20, 20, 20, 20) + + # 用户名输入框 + self.username_edit = QLineEdit() + self.username_edit.setPlaceholderText("请输入用户名") + form_layout.addRow("用户名:", self.username_edit) + + # 密码输入框 + self.password_edit = QLineEdit() + self.password_edit.setEchoMode(QLineEdit.Password) + self.password_edit.setPlaceholderText("请输入密码") + form_layout.addRow("密码:", self.password_edit) + + main_layout.addWidget(form_group) + + # 提示信息 + hint_label = QLabel("💡 默认账号密码均为: admin") + hint_label.setAlignment(Qt.AlignCenter) + hint_label.setStyleSheet(""" + color: #7f8c8d; + font-size: 12px; + margin: 5px 0px; + """) + main_layout.addWidget(hint_label) + + # 按钮布局 + button_layout = QHBoxLayout() + button_layout.setSpacing(10) + + self.cancel_btn = QPushButton("取消") + self.cancel_btn.setObjectName("cancel_btn") + + self.login_btn = QPushButton("登录") + + button_layout.addStretch() + button_layout.addWidget(self.cancel_btn) + button_layout.addWidget(self.login_btn) + + main_layout.addLayout(button_layout) + + # 连接信号 + self.login_btn.clicked.connect(self.handle_login) + self.cancel_btn.clicked.connect(self.reject) + self.password_edit.returnPressed.connect(self.handle_login) + + # 设置焦点和Tab顺序 + self.username_edit.setFocus() + self.setTabOrder(self.username_edit, self.password_edit) + self.setTabOrder(self.password_edit, self.login_btn) + self.setTabOrder(self.login_btn, self.cancel_btn) + + print("✅ 简化登录界面创建完成") + print(f"📝 窗口大小: {self.size().width()}x{self.size().height()}") + + def handle_login(self): + """处理登录""" + username = self.username_edit.text().strip() + password = self.password_edit.text().strip() + + print(f"🔍 登录尝试: 用户名='{username}', 密码='{password}'") + + # 验证账号密码 + if username == "admin" and password == "admin": + print("✅ 登录验证成功") + self.login_success.emit() + self.accept() + else: + print("❌ 登录验证失败") + QMessageBox.warning( + self, + "登录失败", + "用户名或密码错误!\n\n请使用:\n用户名: admin\n密码: admin" + ) + self.password_edit.clear() + self.password_edit.setFocus() + + +def test_simple_login(): + """测试简化登录界面""" + import sys + from PyQt5.QtWidgets import QApplication + + print("🧪 测试简化登录界面") + print("=" * 30) + + app = QApplication(sys.argv) + + dialog = SimpleMaintenanceLoginDialog() + + print("💡 请测试以下功能:") + print("1. 检查是否显示完整的登录界面") + print("2. 在用户名框输入 'admin'") + print("3. 在密码框输入 'admin'") + print("4. 点击登录按钮") + + def on_success(): + print("🎉 登录成功!") + QMessageBox.information(None, "成功", "登录测试成功!") + + dialog.login_success.connect(on_success) + + result = dialog.exec_() + print(f"📊 测试结果: {'成功' if result == QDialog.Accepted else '取消'}") + + return result == QDialog.Accepted + + +if __name__ == "__main__": + test_simple_login() \ No newline at end of file diff --git a/ui/vr_control_panel.py b/ui/vr_control_panel.py deleted file mode 100644 index fbb75c1b..00000000 --- a/ui/vr_control_panel.py +++ /dev/null @@ -1,412 +0,0 @@ -""" -VR控制面板 - -提供VR系统的GUI控制界面 -包括VR启用/禁用、ALVR串流控制、性能监控等功能 -""" - -from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QGroupBox, - QPushButton, QLabel, QProgressBar, QSlider, - QSpinBox, QCheckBox, QComboBox, QTextEdit, - QGridLayout, QFrame, QSizePolicy) -from PyQt5.QtCore import Qt, QTimer, pyqtSignal -from PyQt5.QtGui import QFont, QColor, QPalette - - -class VRControlPanel(QWidget): - """VR控制面板""" - - # 信号定义 - vr_enabled = pyqtSignal(bool) - alvr_streaming_changed = pyqtSignal(bool) - - def __init__(self, world, parent=None): - super().__init__(parent) - self.world = world - self.setupUI() - self.connectSignals() - - # 设置更新定时器 - self.update_timer = QTimer() - self.update_timer.timeout.connect(self.updateStatus) - self.update_timer.start(1000) # 每秒更新一次 - - def setupUI(self): - """设置界面""" - self.setWindowTitle("VR控制面板") - self.setMinimumSize(400, 600) - - # 主布局 - main_layout = QVBoxLayout(self) - - # VR系统控制组 - vr_control_group = self.createVRControlGroup() - main_layout.addWidget(vr_control_group) - - # ALVR串流控制组 - alvr_control_group = self.createALVRControlGroup() - main_layout.addWidget(alvr_control_group) - - # 性能监控组 - performance_group = self.createPerformanceGroup() - main_layout.addWidget(performance_group) - - # 控制器状态组 - controller_group = self.createControllerGroup() - main_layout.addWidget(controller_group) - - # 日志输出组 - log_group = self.createLogGroup() - main_layout.addWidget(log_group) - - # 添加弹性空间 - main_layout.addStretch() - - def createVRControlGroup(self): - """创建VR系统控制组""" - group = QGroupBox("VR系统控制") - layout = QVBoxLayout(group) - - # 状态指示器 - self.vr_status_label = QLabel("VR状态: 未启用") - self.vr_status_label.setStyleSheet("color: red; font-weight: bold;") - layout.addWidget(self.vr_status_label) - - # 启用/禁用VR按钮 - button_layout = QHBoxLayout() - self.enable_vr_button = QPushButton("启用VR") - self.enable_vr_button.clicked.connect(self.enableVR) - button_layout.addWidget(self.enable_vr_button) - - self.disable_vr_button = QPushButton("禁用VR") - self.disable_vr_button.clicked.connect(self.disableVR) - self.disable_vr_button.setEnabled(False) - button_layout.addWidget(self.disable_vr_button) - - layout.addLayout(button_layout) - - # VR设备信息 - self.vr_info_label = QLabel("VR设备: 未连接") - layout.addWidget(self.vr_info_label) - - # VR选项 - options_layout = QGridLayout() - - # 控制器射线显示 - self.show_rays_checkbox = QCheckBox("显示控制器射线") - self.show_rays_checkbox.stateChanged.connect(self.toggleControllerRays) - options_layout.addWidget(self.show_rays_checkbox, 0, 0) - - # 手势识别 - self.gesture_checkbox = QCheckBox("启用手势识别") - self.gesture_checkbox.stateChanged.connect(self.toggleGestureRecognition) - options_layout.addWidget(self.gesture_checkbox, 0, 1) - - # VR交互 - self.interaction_checkbox = QCheckBox("启用VR交互") - self.interaction_checkbox.setChecked(True) - self.interaction_checkbox.stateChanged.connect(self.toggleVRInteraction) - options_layout.addWidget(self.interaction_checkbox, 1, 0) - - layout.addLayout(options_layout) - - return group - - def createALVRControlGroup(self): - """创建ALVR串流控制组""" - group = QGroupBox("ALVR串流控制") - layout = QVBoxLayout(group) - - # 连接状态 - self.alvr_status_label = QLabel("ALVR状态: 未连接") - self.alvr_status_label.setStyleSheet("color: red; font-weight: bold;") - layout.addWidget(self.alvr_status_label) - - # 串流控制按钮 - button_layout = QHBoxLayout() - self.start_streaming_button = QPushButton("开始串流") - self.start_streaming_button.clicked.connect(self.startStreaming) - self.start_streaming_button.setEnabled(False) - button_layout.addWidget(self.start_streaming_button) - - self.stop_streaming_button = QPushButton("停止串流") - self.stop_streaming_button.clicked.connect(self.stopStreaming) - self.stop_streaming_button.setEnabled(False) - button_layout.addWidget(self.stop_streaming_button) - - layout.addLayout(button_layout) - - # 串流质量设置 - quality_layout = QGridLayout() - - # 分辨率 - quality_layout.addWidget(QLabel("分辨率宽度:"), 0, 0) - self.width_spinbox = QSpinBox() - self.width_spinbox.setRange(1920, 4096) - self.width_spinbox.setValue(2880) - self.width_spinbox.setSuffix(" px") - quality_layout.addWidget(self.width_spinbox, 0, 1) - - quality_layout.addWidget(QLabel("分辨率高度:"), 1, 0) - self.height_spinbox = QSpinBox() - self.height_spinbox.setRange(1080, 2160) - self.height_spinbox.setValue(1700) - self.height_spinbox.setSuffix(" px") - quality_layout.addWidget(self.height_spinbox, 1, 1) - - # 帧率 - quality_layout.addWidget(QLabel("帧率:"), 2, 0) - self.fps_spinbox = QSpinBox() - self.fps_spinbox.setRange(60, 120) - self.fps_spinbox.setValue(72) - self.fps_spinbox.setSuffix(" fps") - quality_layout.addWidget(self.fps_spinbox, 2, 1) - - # 比特率 - quality_layout.addWidget(QLabel("比特率:"), 3, 0) - self.bitrate_spinbox = QSpinBox() - self.bitrate_spinbox.setRange(50, 500) - self.bitrate_spinbox.setValue(150) - self.bitrate_spinbox.setSuffix(" Mbps") - quality_layout.addWidget(self.bitrate_spinbox, 3, 1) - - layout.addLayout(quality_layout) - - # 应用设置按钮 - self.apply_quality_button = QPushButton("应用质量设置") - self.apply_quality_button.clicked.connect(self.applyQualitySettings) - layout.addWidget(self.apply_quality_button) - - return group - - def createPerformanceGroup(self): - """创建性能监控组""" - group = QGroupBox("性能监控") - layout = QGridLayout(group) - - # FPS显示 - layout.addWidget(QLabel("渲染FPS:"), 0, 0) - self.fps_label = QLabel("0") - self.fps_label.setStyleSheet("font-weight: bold; color: blue;") - layout.addWidget(self.fps_label, 0, 1) - - # 串流FPS - layout.addWidget(QLabel("串流FPS:"), 1, 0) - self.stream_fps_label = QLabel("0") - self.stream_fps_label.setStyleSheet("font-weight: bold; color: green;") - layout.addWidget(self.stream_fps_label, 1, 1) - - # 延迟 - layout.addWidget(QLabel("延迟:"), 2, 0) - self.latency_label = QLabel("0 ms") - self.latency_label.setStyleSheet("font-weight: bold; color: orange;") - layout.addWidget(self.latency_label, 2, 1) - - # 性能进度条 - layout.addWidget(QLabel("CPU使用率:"), 3, 0) - self.cpu_progress = QProgressBar() - self.cpu_progress.setRange(0, 100) - layout.addWidget(self.cpu_progress, 3, 1) - - layout.addWidget(QLabel("GPU使用率:"), 4, 0) - self.gpu_progress = QProgressBar() - self.gpu_progress.setRange(0, 100) - layout.addWidget(self.gpu_progress, 4, 1) - - return group - - def createControllerGroup(self): - """创建控制器状态组""" - group = QGroupBox("控制器状态") - layout = QVBoxLayout(group) - - # 控制器列表 - self.controller_list = QTextEdit() - self.controller_list.setMaximumHeight(100) - self.controller_list.setReadOnly(True) - layout.addWidget(self.controller_list) - - # 触觉反馈测试 - haptic_layout = QHBoxLayout() - haptic_layout.addWidget(QLabel("触觉反馈测试:")) - - self.haptic_button = QPushButton("发送震动") - self.haptic_button.clicked.connect(self.sendHapticFeedback) - haptic_layout.addWidget(self.haptic_button) - - layout.addLayout(haptic_layout) - - return group - - def createLogGroup(self): - """创建日志输出组""" - group = QGroupBox("系统日志") - layout = QVBoxLayout(group) - - self.log_text = QTextEdit() - self.log_text.setMaximumHeight(150) - self.log_text.setReadOnly(True) - layout.addWidget(self.log_text) - - # 清空日志按钮 - clear_button = QPushButton("清空日志") - clear_button.clicked.connect(self.clearLog) - layout.addWidget(clear_button) - - return group - - def connectSignals(self): - """连接信号""" - # 连接值变化信号 - self.width_spinbox.valueChanged.connect(self.onQualityChanged) - self.height_spinbox.valueChanged.connect(self.onQualityChanged) - self.fps_spinbox.valueChanged.connect(self.onQualityChanged) - self.bitrate_spinbox.valueChanged.connect(self.onQualityChanged) - - def enableVR(self): - """启用VR""" - self.addLog("正在启用VR模式...") - if self.world.enableVRMode(): - self.addLog("✓ VR模式已启用") - self.vr_enabled.emit(True) - else: - self.addLog("✗ VR模式启用失败") - - def disableVR(self): - """禁用VR""" - self.addLog("正在禁用VR模式...") - self.world.disableVRMode() - self.addLog("✓ VR模式已禁用") - self.vr_enabled.emit(False) - - def startStreaming(self): - """开始串流""" - self.addLog("正在开始ALVR串流...") - if self.world.startALVRStreaming(): - self.addLog("✓ ALVR串流已开始") - self.alvr_streaming_changed.emit(True) - else: - self.addLog("✗ ALVR串流开始失败") - - def stopStreaming(self): - """停止串流""" - self.addLog("正在停止ALVR串流...") - self.world.stopALVRStreaming() - self.addLog("✓ ALVR串流已停止") - self.alvr_streaming_changed.emit(False) - - def toggleControllerRays(self, checked): - """切换控制器射线显示""" - self.world.showControllerRays(checked) - self.addLog(f"控制器射线: {'显示' if checked else '隐藏'}") - - def toggleGestureRecognition(self, checked): - """切换手势识别""" - self.world.setVRGestureEnabled(checked) - self.addLog(f"手势识别: {'启用' if checked else '禁用'}") - - def toggleVRInteraction(self, checked): - """切换VR交互""" - self.world.setVRInteractionEnabled(checked) - self.addLog(f"VR交互: {'启用' if checked else '禁用'}") - - def applyQualitySettings(self): - """应用质量设置""" - width = self.width_spinbox.value() - height = self.height_spinbox.value() - fps = self.fps_spinbox.value() - bitrate = self.bitrate_spinbox.value() - - self.world.setALVRStreamQuality(width, height, fps, bitrate) - self.addLog(f"串流质量已设置: {width}x{height} @ {fps}fps, {bitrate}Mbps") - - def sendHapticFeedback(self): - """发送触觉反馈""" - controllers = self.world.getAllControllers() - if controllers: - controller_id = controllers[0] # 使用第一个控制器 - self.world.sendHapticFeedback(controller_id, 0.5, 0.8) - self.addLog(f"已发送触觉反馈到控制器 {controller_id}") - else: - self.addLog("没有可用的控制器") - - def onQualityChanged(self): - """质量设置变化""" - # 可以在这里实现实时质量调整 - pass - - def updateStatus(self): - """更新状态显示""" - # 更新VR状态 - vr_status = self.world.getVRStatus() - - # VR系统状态 - if vr_status['vr_enabled']: - self.vr_status_label.setText("VR状态: 已启用") - self.vr_status_label.setStyleSheet("color: green; font-weight: bold;") - self.enable_vr_button.setEnabled(False) - self.disable_vr_button.setEnabled(True) - - # 更新VR设备信息 - vr_info = vr_status['vr_info'] - if vr_info: - device_info = f"VR设备: {vr_info.get('hmd_manufacturer', 'Unknown')} {vr_info.get('hmd_model', 'Unknown')}" - self.vr_info_label.setText(device_info) - else: - self.vr_status_label.setText("VR状态: 未启用") - self.vr_status_label.setStyleSheet("color: red; font-weight: bold;") - self.enable_vr_button.setEnabled(True) - self.disable_vr_button.setEnabled(False) - self.vr_info_label.setText("VR设备: 未连接") - - # ALVR状态 - if vr_status['alvr_connected']: - self.alvr_status_label.setText("ALVR状态: 已连接") - self.alvr_status_label.setStyleSheet("color: green; font-weight: bold;") - self.start_streaming_button.setEnabled(not vr_status['alvr_streaming']) - self.stop_streaming_button.setEnabled(vr_status['alvr_streaming']) - else: - self.alvr_status_label.setText("ALVR状态: 未连接") - self.alvr_status_label.setStyleSheet("color: red; font-weight: bold;") - self.start_streaming_button.setEnabled(False) - self.stop_streaming_button.setEnabled(False) - - # 性能统计 - streaming_status = vr_status['streaming_status'] - if streaming_status: - self.stream_fps_label.setText(str(streaming_status.get('fps', 0))) - self.latency_label.setText(f"{streaming_status.get('latency', 0)} ms") - - # 控制器状态 - controllers = vr_status['controllers'] - if controllers: - controller_text = f"已连接 {len(controllers)} 个控制器:\n" - for i, controller_id in enumerate(controllers): - controller_text += f"控制器 {i+1}: ID {controller_id}\n" - self.controller_list.setText(controller_text) - else: - self.controller_list.setText("没有连接的控制器") - - def addLog(self, message): - """添加日志""" - import datetime - timestamp = datetime.datetime.now().strftime("%H:%M:%S") - log_message = f"[{timestamp}] {message}" - self.log_text.append(log_message) - print(log_message) # 同时输出到控制台 - - def clearLog(self): - """清空日志""" - self.log_text.clear() - - def closeEvent(self, event): - """关闭事件""" - # 停止更新定时器 - if hasattr(self, 'update_timer'): - self.update_timer.stop() - - # 如果VR启用,先禁用 - if self.world.isVREnabled(): - self.disableVR() - - event.accept() \ No newline at end of file diff --git a/ui/widgets.py b/ui/widgets.py index cefd9b3f..6f59fc59 100644 --- a/ui/widgets.py +++ b/ui/widgets.py @@ -10,18 +10,19 @@ import os import re -from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QGroupBox, QHBoxLayout, - QLineEdit, QPushButton, QLabel, QDialogButtonBox, +from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, + QLineEdit, QPushButton, QLabel, QTreeView, QTreeWidget, QTreeWidgetItem, QWidget, - QFileDialog, QMessageBox, QAbstractItemView, QMenu, QDockWidget, QButtonGroup, QToolButton) -from PyQt5.QtCore import Qt, QUrl, QMimeData -from PyQt5.QtGui import QDrag, QPainter, QPixmap, QPen, QBrush + 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 QPanda3D.QPanda3DWidget import QPanda3DWidget from scene import util +from ui.icon_manager import get_icon_manager class NewProjectDialog(QDialog): """新建项目对话框""" @@ -29,109 +30,398 @@ class NewProjectDialog(QDialog): def __init__(self, parent=None): super().__init__(parent) self.setWindowTitle("新建项目") - self.setMinimumWidth(500) + 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 { - background-color: #252538; - color: #e0e0ff; + QDialog#newProjectDialog { + background-color: transparent; + color: #EBEBEB; + border: none; } - QGroupBox { - background-color: #2d2d44; - border: 1px solid #3a3a4a; - border-radius: 6px; - margin-top: 1ex; /* 保持这个设置 */ - color: #e0e0ff; - font-weight: 500; - padding-top: 10px; /* 增加顶部内边距,为标题留出空间 */ + QFrame#baseFrame { + background-color: #000000; + border: 1px solid #3E3E42; + border-radius: 5px; } - QGroupBox::title { - padding: 0 8px; - color: #c0c0e0; + 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: #2d2d44; - color: #e0e0ff; - border: 1px solid #3a3a4a; - border-radius: 4px; - padding: 6px; + 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: #1e1e2e; - color: #8888aa; + 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: #8b5cf6; - color: white; + background-color: rgba(89, 98, 118, 0.5); + color: #EBEBEB; border: none; - padding: 6px 12px; - border-radius: 4px; - font-weight: 500; + 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: #7c3aed; + background-color: #3067C0; + color: #FFFFFF; } QPushButton:pressed { - background-color: #6d28d9; + background-color: #2556A0; + color: #FFFFFF; } QPushButton:disabled { - background-color: #4c4c6e; - color: #8888aa; + background-color: rgba(89, 98, 118, 0.3); + color: rgba(235, 235, 235, 0.4); } - QLabel { - color: #e0e0ff; + QPushButton#primaryButton { + background-color: rgba(89, 98, 118, 0.5); + border: none; + color: #EBEBEB; + font-weight: 300; + min-width: 120px; + max-width: 120px; } - QLabel:disabled { - color: #8888aa; + QPushButton#primaryButton:hover { + background-color: #2556A0; } - QDialogButtonBox QPushButton { - min-width: 80px; + 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; } """) - # 创建布局 - layout = QVBoxLayout(self) - - # 创建路径选择部分 - pathGroup = QGroupBox("项目路径") - pathLayout = QHBoxLayout() + # 创建主布局 + 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) - browseButton = QPushButton("浏览...") + 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) - pathLayout.addWidget(self.pathEdit) - pathLayout.addWidget(browseButton) - pathGroup.setLayout(pathLayout) - layout.addWidget(pathGroup) - - # 创建项目名称部分 - nameGroup = QGroupBox("项目名称") - nameLayout = QVBoxLayout() + 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.setText('新项目') + self.nameEdit.setPlaceholderText('请输入项目名称') self.nameEdit.selectAll() - nameLayout.addWidget(self.nameEdit) - # 添加提示标签 - self.tipLabel = QLabel("项目名称只能包含字母、数字、下划线、中划线和中文") - self.tipLabel.setStyleSheet("color: gray;") - nameLayout.addWidget(self.tipLabel) - nameGroup.setLayout(nameLayout) - layout.addWidget(nameGroup) - - # 添加按钮 - buttonBox = QDialogButtonBox( - QDialogButtonBox.Ok | QDialogButtonBox.Cancel - ) - buttonBox.accepted.connect(self.validate) - buttonBox.rejected.connect(self.reject) - layout.addWidget(buttonBox) - - # 存储结果 + 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, "选择项目路径") @@ -143,25 +433,25 @@ class NewProjectDialog(QDialog): # 获取并验证路径 self.projectPath = self.pathEdit.text() if not self.projectPath: - QMessageBox.warning(self, "错误", "请选择项目路径!") + UniversalMessageDialog.show_warning(self, "错误", "请选择项目路径!", show_cancel=False, confirm_text="确认") return # 获取并验证项目名称 self.projectName = self.nameEdit.text().strip() if not self.projectName: - QMessageBox.warning(self, "错误", "请输入项目名称!") + UniversalMessageDialog.show_warning(self, "错误", "请输入项目名称!", show_cancel=False, confirm_text="确认") return # 验证项目名称格式 if not re.match(r'^[a-zA-Z0-9_\-\u4e00-\u9fa5]+$', self.projectName): - QMessageBox.warning(self, "错误", - "项目名称只能包含字母、数字、下划线、中划线和中文!") + 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): - QMessageBox.warning(self, "错误", "项目已存在!") + UniversalMessageDialog.show_warning(self, "错误", "项目已存在!", show_cancel=False, confirm_text="确认") return self.accept() @@ -302,6 +592,13 @@ class CustomFileView(QTreeView): 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() @@ -363,7 +660,7 @@ class CustomFileView(QTreeView): drag.exec_(supportedActions) def mouseDoubleClickEvent(self, event): - """处理双击事件""" + """双击标题栏切换最大化""" index = self.indexAt(event.pos()) if index.isValid(): model = self.model() @@ -486,80 +783,96 @@ class CustomAssetsTreeWidget(QTreeWidget): self._restoreExpandedState(expanded_paths) def createNewFolder(self, parent_item): - """创建新文件夹""" + """新建文件夹""" import os - from PyQt5.QtWidgets import QInputDialog - + parent_path = parent_item.data(0, Qt.UserRole) - folder_name, ok = QInputDialog.getText(self, "新建文件夹", "文件夹名称:") - - if ok and folder_name: - 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}") + 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 - from PyQt5.QtWidgets import QInputDialog - + parent_path = parent_item.data(0, Qt.UserRole) - file_name, ok = QInputDialog.getText(self, "新建文件", "文件名称:") - - if ok and file_name: - 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}") + 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 - from PyQt5.QtWidgets import QInputDialog - + old_path = item.data(0, Qt.UserRole) old_name = os.path.basename(old_path) - - new_name, ok = QInputDialog.getText(self, "重命名", "新名称:", text=old_name) - - if ok and new_name and new_name != old_name: - parent_dir = os.path.dirname(old_path) - new_path = os.path.join(parent_dir, new_name) - - try: - os.rename(old_path, new_path) - self._refreshWithStatePreservation() - print(f"重命名: {old_path} -> {new_path}") - except OSError as e: - print(f"重命名失败: {e}") + + 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 - from PyQt5.QtWidgets import QMessageBox filepath = item.data(0, Qt.UserRole) is_folder = item.data(0, Qt.UserRole + 1) item_type = "文件夹" if is_folder else "文件" - reply = QMessageBox.question( - self, - "确认删除", + result = UniversalMessageDialog.show_warning( + self, + "确认删除", f"确定要删除这个{item_type}吗?\n{filepath}", - QMessageBox.Yes | QMessageBox.No, - QMessageBox.No + show_cancel=True, + confirm_text="删除", + cancel_text="取消" ) - if reply == QMessageBox.Yes: + if result == QDialog.Accepted: try: if is_folder: shutil.rmtree(filepath) @@ -569,6 +882,13 @@ class CustomAssetsTreeWidget(QTreeWidget): 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): """复制路径到剪贴板""" @@ -599,7 +919,6 @@ class CustomAssetsTreeWidget(QTreeWidget): def showProperties(self, item): """显示属性面板""" import os - from PyQt5.QtWidgets import QMessageBox filepath = item.data(0, Qt.UserRole) is_folder = item.data(0, Qt.UserRole + 1) @@ -622,10 +941,23 @@ class CustomAssetsTreeWidget(QTreeWidget): 修改时间: {modified_str} """ - QMessageBox.information(self, "属性", properties.strip()) + UniversalMessageDialog.show_info( + self, + "属性", + properties.strip(), + show_cancel=False, + confirm_text="确认" + ) except OSError as e: - QMessageBox.warning(self, "错误", f"无法获取属性: {e}") + UniversalMessageDialog.show_warning( + self, + "错误", + f"无法获取属性: {e}", + show_cancel=False, + confirm_text="确认" + ) + # def mouseDoubleClickEvent(self, event): # """处理双击事件""" @@ -736,6 +1068,8 @@ class CustomAssetsTreeWidget(QTreeWidget): 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) # 加载当前目录内容 @@ -1254,7 +1588,7 @@ class CustomAssetsTreeWidget(QTreeWidget): self._refreshWithStatePreservation() if failed_files: - QMessageBox.warning( + StyledMessageBox.warning( self, "移动失败", f"以下文件移动失败:\n" + "\n".join(failed_files) @@ -1304,6 +1638,25 @@ class CustomConsoleDockWidget(QWidget): # 清空按钮 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) @@ -1313,26 +1666,25 @@ class CustomConsoleDockWidget(QWidget): self.autoScrollBtn.setChecked(True) self.autoScrollBtn.setStyleSheet(""" QPushButton { - background-color: #2d2d44; - color: #e0e0ff; - border: 1px solid #3a3a4a; - padding: 6px 12px; - border-radius: 4px; - font-weight: 500; + 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: #8b5cf6; - color: white; - border: 1px solid #7c3aed; + background-color: #3067c0; + color: #ffffff; } QPushButton:hover { - background-color: #3a3a4a; - } - QPushButton:checked:hover { - background-color: #7c3aed; + background-color: rgba(84, 89, 98, 0.7); } QPushButton:pressed { - background-color: #6d28d9; + background-color: rgba(84, 89, 98, 0.9); } """) toolbar.addWidget(self.autoScrollBtn) @@ -1343,26 +1695,25 @@ class CustomConsoleDockWidget(QWidget): self.timestampBtn.setChecked(True) self.timestampBtn.setStyleSheet(""" QPushButton { - background-color: #2d2d44; - color: #e0e0ff; - border: 1px solid #3a3a4a; - padding: 6px 12px; - border-radius: 4px; - font-weight: 500; + 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: #8b5cf6; - color: white; - border: 1px solid #7c3aed; + background-color: #3067c0; + color: #ffffff; } QPushButton:hover { - background-color: #3a3a4a; - } - QPushButton:checked:hover { - background-color: #7c3aed; + background-color: rgba(84, 89, 98, 0.7); } QPushButton:pressed { - background-color: #6d28d9; + background-color: rgba(84, 89, 98, 0.9); } """) toolbar.addWidget(self.timestampBtn) @@ -1370,13 +1721,15 @@ class CustomConsoleDockWidget(QWidget): self.fpsLabel = QLabel("FPS:0.0") self.fpsLabel.setStyleSheet(""" QLabel { - background-color: #2d2d44; - color: #80ff80; - border: 1px solid #3a3a4a; - padding: 6px 12px; - border-radius: 4px; - font-weight: 500; - font-family: 'Consolas', 'Monaco', monospace; + 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) @@ -1386,7 +1739,7 @@ class CustomConsoleDockWidget(QWidget): # 帧率更新定时器 self.fpsTimer = QTimer() self.fpsTimer.timeout.connect(self.updateFPS) - self.fpsTimer.start(1000) # 每秒更新一次 + self.fpsTimer.start(105) # 每秒更新一次 toolbar.addStretch() layout.addLayout(toolbar) @@ -1397,11 +1750,14 @@ class CustomConsoleDockWidget(QWidget): self.consoleText.setReadOnly(True) self.consoleText.setStyleSheet(""" QTextEdit { - background-color: #1e1e1e; - color: #ffffff; - font-family: 'Consolas', 'Monaco', monospace; - font-size: 10pt; - border: 1px solid #3e3e3e; + 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) @@ -1445,7 +1801,7 @@ class CustomConsoleDockWidget(QWidget): self.fpsLabel.setStyleSheet(f""" QLabel {{ - background-color: #2d2d44; + background-color: #3067c0; color: {color}; border: 1px solid #3a3a4a; padding: 6px 12px; @@ -1559,6 +1915,897 @@ class ConsoleRedirect: """刷新缓冲区""" pass +class StyledMessageBox: + """统一样式的消息框辅助类""" + + MESSAGEBOX_STYLE = """ + QMessageBox { + background-color: #1e1e1f; + color: #ebebeb; + border: 1px solid rgba(77, 116, 189, 0.3); + border-radius: 8px; + padding: 20px; + min-width: 300px; + } + QMessageBox QLabel { + color: #ffffff; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 13px; + font-weight: 400; + line-height: 1.4; + padding: 10px 0px; + } + QMessageBox QPushButton { + background-color: rgba(89, 100, 113, 0.4); + color: rgba(255, 255, 255, 0.8); + border: 1px solid rgba(76, 92, 110, 0.4); + border-radius: 6px; + padding: 8px 16px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-weight: 400; + font-size: 11px; + min-width: 80px; + min-height: 28px; + } + QMessageBox QPushButton:hover { + background-color: rgba(89, 100, 113, 0.6); + border: 1px solid rgba(77, 116, 189, 0.6); + color: #ffffff; + } + QMessageBox QPushButton:pressed, QMessageBox QPushButton:checked { + background-color: rgba(77, 116, 189, 0.8); + border: 1px solid #4d74bd; + color: #ffffff; + } + QMessageBox QPushButton:disabled { + background-color: rgba(89, 100, 113, 0.2); + color: rgba(235, 235, 235, 0.4); + border: 1px solid rgba(76, 92, 110, 0.2); + } + QMessageBox QPushButton:default { + background-color: rgba(77, 116, 189, 0.8); + border: 1px solid #4d74bd; + color: #ffffff; + font-weight: 500; + } + QMessageBox QPushButton:default:hover { + background-color: rgba(77, 116, 189, 1.0); + border: 1px solid rgba(77, 116, 189, 1.0); + } + QMessageBox QIcon { + padding: 0px 10px 0px 0px; + } + """ + + INPUTDIALOG_STYLE = """ + QInputDialog { + background-color: #1e1e1f; + color: #ebebeb; + border: 1px solid rgba(77, 116, 189, 0.3); + border-radius: 8px; + padding: 20px; + min-width: 350px; + } + QLabel { + color: #ffffff; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-weight: 500; + font-size: 13px; + padding: 0px 0px 10px 0px; + } + QLineEdit { + background-color: rgba(89, 100, 113, 0.15); + color: #ebebeb; + border: 1px solid rgba(76, 92, 110, 0.4); + border-radius: 6px; + padding: 10px 12px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 11px; + font-weight: 400; + min-height: 16px; + } + QLineEdit:focus { + border: 1px solid #4d74bd; + background-color: rgba(77, 116, 189, 0.1); + } + QLineEdit:hover { + border: 1px solid rgba(77, 116, 189, 0.6); + background-color: rgba(89, 100, 113, 0.2); + } + QPushButton { + background-color: rgba(89, 100, 113, 0.4); + color: rgba(255, 255, 255, 0.8); + border: 1px solid rgba(76, 92, 110, 0.4); + border-radius: 6px; + padding: 8px 16px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-weight: 400; + font-size: 11px; + min-width: 80px; + min-height: 28px; + } + QPushButton:hover { + background-color: rgba(89, 100, 113, 0.6); + border: 1px solid rgba(77, 116, 189, 0.6); + color: #ffffff; + } + QPushButton:pressed, QPushButton:checked { + background-color: rgba(77, 116, 189, 0.8); + border: 1px solid #4d74bd; + color: #ffffff; + } + QPushButton:disabled { + background-color: rgba(89, 100, 113, 0.2); + color: rgba(235, 235, 235, 0.4); + border: 1px solid rgba(76, 92, 110, 0.2); + } + QPushButton:default { + background-color: rgba(77, 116, 189, 0.8); + border: 1px solid #4d74bd; + color: #ffffff; + font-weight: 500; + } + """ + + BUTTON_STYLE = """ + QPushButton { + background-color: rgba(89, 100, 113, 0.4); + color: rgba(255, 255, 255, 0.8); + border: 1px solid rgba(76, 92, 110, 0.4); + border-radius: 6px; + padding: 8px 16px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-weight: 400; + font-size: 11px; + min-width: 80px; + min-height: 28px; + } + QPushButton:hover { + background-color: rgba(89, 100, 113, 0.6); + border: 1px solid rgba(77, 116, 189, 0.6); + color: #ffffff; + } + QPushButton:pressed, QPushButton:checked { + background-color: rgba(77, 116, 189, 0.8); + border: 1px solid #4d74bd; + color: #ffffff; + } + QPushButton:disabled { + background-color: rgba(89, 100, 113, 0.2); + color: rgba(235, 235, 235, 0.4); + border: 1px solid rgba(76, 92, 110, 0.2); + } + """ + + @staticmethod + def information(parent, title, message): + """显示信息提示框""" + msg = QMessageBox(QMessageBox.Information, title, message, QMessageBox.Ok, parent) + msg.setStyleSheet(StyledMessageBox.MESSAGEBOX_STYLE) + # 强制设置所有按钮的样式 + for button in msg.buttons(): + button.setStyleSheet(StyledMessageBox.BUTTON_STYLE) + return msg.exec_() + + @staticmethod + def question(parent, title, message, buttons=QMessageBox.Yes | QMessageBox.No, defaultButton=QMessageBox.No): + """显示确认对话框""" + msg = QMessageBox(QMessageBox.Question, title, message, buttons, parent) + msg.setDefaultButton(defaultButton) + msg.setStyleSheet(StyledMessageBox.MESSAGEBOX_STYLE) + # 强制设置所有按钮的样式 + for button in msg.buttons(): + button.setStyleSheet(StyledMessageBox.BUTTON_STYLE) + return msg.exec_() + + @staticmethod + def warning(parent, title, message): + """显示警告对话框""" + msg = QMessageBox(QMessageBox.Warning, title, message, QMessageBox.Ok, parent) + msg.setStyleSheet(StyledMessageBox.MESSAGEBOX_STYLE) + # 强制设置所有按钮的样式 + for button in msg.buttons(): + button.setStyleSheet(StyledMessageBox.BUTTON_STYLE) + return msg.exec_() + + @staticmethod + def critical(parent, title, message): + """显示错误对话框""" + msg = QMessageBox(QMessageBox.Critical, title, message, QMessageBox.Ok, parent) + msg.setStyleSheet(StyledMessageBox.MESSAGEBOX_STYLE) + # 强制设置所有按钮的样式 + for button in msg.buttons(): + button.setStyleSheet(StyledMessageBox.BUTTON_STYLE) + return msg.exec_() + + @staticmethod + def getText(parent, title, label, text=""): + """显示样式统一的文本输入对话框""" + from PyQt5.QtWidgets import QInputDialog + dialog = QInputDialog(parent) + dialog.setWindowTitle(title) + dialog.setLabelText(label) + dialog.setTextValue(text) + dialog.setStyleSheet(StyledMessageBox.INPUTDIALOG_STYLE) + + # 强制设置所有按钮的样式 + for button in dialog.findChildren(QPushButton): + button.setStyleSheet(StyledMessageBox.BUTTON_STYLE) + + ok = dialog.exec_() + return dialog.textValue(), ok + +class UniversalMessageDialog(QDialog): + """通用消息对话框类 - 支持不同图标和按钮配置""" + + # 消息类型枚举 + SUCCESS = "success_icon" + WARNING = "warning_icon" + ERROR = "fail_icon" + INFO = "info" + + def __init__(self, parent, title, message, message_type=INFO, show_cancel=True, + confirm_text="确认", cancel_text="取消", icon_size=QSize(20, 20)): + """ + 初始化通用消息对话框 + + Args: + parent: 父窗口 + title: 对话框标题 + message: 消息内容 + message_type: 消息类型 (SUCCESS, WARNING, ERROR, INFO) + show_cancel: 是否显示取消按钮 + confirm_text: 确认按钮文字 + cancel_text: 取消按钮文字 + icon_size: 图标尺寸 + """ + super().__init__(parent) + self.setWindowTitle(title) + self.setObjectName("universalMessageDialog") + self.setModal(True) + self.resize(508, 134) + self.setMinimumSize(508, 134) + self.setWindowFlags(Qt.Dialog | Qt.FramelessWindowHint) + self.setAttribute(Qt.WA_TranslucentBackground, True) + + # 对话框配置 + self.message_type = message_type + self.show_cancel = show_cancel + self.confirm_text = confirm_text + self.cancel_text = cancel_text + self.icon_size = icon_size + + # 拖拽相关 + self.dragging = False + self.drag_position = QPoint() + + # 图标管理 + self.icon_manager = get_icon_manager() + self._title_icon_size = QSize(18, 18) + self._icon_close = self.icon_manager.get_icon('close_icon', self._title_icon_size) + + # 根据消息类型获取对应图标 + self._message_icon = self._get_message_icon() + + # 设置样式 + self._setup_styles() + + # 创建UI + self._create_ui(message) + + def _get_message_icon(self): + """根据消息类型获取对应图标""" + icon_map = { + self.SUCCESS: 'success_icon', + self.WARNING: 'warning_icon', + self.ERROR: 'fail_icon', + self.INFO: 'success_icon' # 默认使用成功图标 + } + + icon_name = icon_map.get(self.message_type, 'success_icon') + return self.icon_manager.get_icon(icon_name, self.icon_size) + + def _setup_styles(self): + """设置对话框样式""" + self.setStyleSheet(""" + QDialog#universalMessageDialog { + background-color: transparent; + border: none; + } + QFrame#baseFrame { + background-color: #19191B; + border: 1px solid #3E3E42; + border-radius: 5px; + } + QWidget#titleBar { + background-color: transparent; + border: none; + border-radius: 5px 5px 0px 0px; + min-height: 32px; + max-height: 32px; + } + QWidget#titleBar QWidget { + background-color: transparent; + border: none; + } + QLabel#titleLabel { + color: #FFFFFF; + font-family: 'Inter', 'Microsoft YaHei', sans-serif; + font-size: 14px; + font-weight: 500; + letter-spacing: 0.7px; + } + QWidget#controlButtons QPushButton { + background-color: transparent; + border: none; + color: #EBEBEB; + font-size: 14px; + min-width: 18px; + max-width: 18px; + min-height: 18px; + max-height: 18px; + padding: 0px; + border-radius: 3px; + } + QWidget#controlButtons QPushButton:hover { + background-color: #2A2D2E; + color: #FFFFFF; + } + QPushButton#closeButton { + border-radius: 0px 5px 0px 0px; + } + QPushButton#closeButton:hover { + background-color: #2A2D2E; + color: #FFFFFF; + } + QFrame#titleSeparator { + background-color: #3E3E42; + border: none; + min-height: 1px; + max-height: 1px; + } + QWidget#contentWidget { + background-color: transparent; + border: none; + } + QLabel#messageLabel { + color: #EBEBEB; + font-family: 'Inter', 'Microsoft YaHei', sans-serif; + font-size: 13px; + font-weight: 400; + letter-spacing: 0.6px; + line-height: 1.4; + padding: 0px; + margin: 0px; + } + QPushButton { + background-color: rgba(89, 98, 118, 0.5); + color: #EBEBEB; + border: none; + border-radius: 2px; + padding: 0px 12px; + font-family: 'Inter', 'Microsoft YaHei', sans-serif; + font-weight: 300; + font-size: 10px; + letter-spacing: 0.5px; + min-width: 90px; + max-width: 90px; + min-height: 28px; + max-height: 28px; + } + QPushButton:hover { + background-color: #3067C0; + color: #FFFFFF; + } + QPushButton:pressed { + background-color: #2556A0; + color: #FFFFFF; + } + QPushButton#primaryButton { + background-color: rgba(89, 98, 118, 0.5); + border: none; + color: #EBEBEB; + font-weight: 300; + } + QPushButton#primaryButton:hover { + background-color: #2556A0; + } + QPushButton#primaryButton:pressed { + background-color: #1E4A8C; + } + QPushButton#secondaryButton { + background-color: rgba(89, 98, 118, 0.5); + border: none; + color: #EBEBEB; + } + QPushButton#secondaryButton:hover { + background-color: #3067C0; + color: #FFFFFF; + } + QPushButton#secondaryButton:pressed { + background-color: #2556A0; + color: #FFFFFF; + } + """) + + def _create_ui(self, message): + """构建用户界面""" + main_layout = QVBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.setSpacing(0) + + base_frame = QFrame() + base_frame.setObjectName('baseFrame') + base_frame.setFrameShape(QFrame.NoFrame) + base_frame.setAttribute(Qt.WA_StyledBackground, True) + + base_layout = QVBoxLayout(base_frame) + base_layout.setContentsMargins(25, 14, 25, 14) + base_layout.setSpacing(0) + + self._create_title_bar() + base_layout.addWidget(self.title_bar) + base_layout.addSpacing(4) + + title_separator = QFrame() + title_separator.setObjectName('titleSeparator') + title_separator.setFrameShape(QFrame.HLine) + title_separator.setFrameShadow(QFrame.Plain) + base_layout.addWidget(title_separator) + base_layout.addSpacing(10) + + content_widget = QWidget() + content_widget.setObjectName('contentWidget') + content_layout = QVBoxLayout(content_widget) + content_layout.setContentsMargins(0, 0, 0, 0) + content_layout.setSpacing(10) + + message_area = QHBoxLayout() + message_area.setContentsMargins(0, 0, 0, 0) + message_area.setSpacing(10) + + # 用一个垂直布局包裹icon_label,确保顶部对齐 + icon_vbox = QVBoxLayout() + icon_vbox.setContentsMargins(0, 0, 0, 0) + icon_vbox.setSpacing(0) + icon_label = QLabel() + if self._message_icon and not self._message_icon.isNull(): + icon_label.setPixmap(self._message_icon.pixmap(self.icon_size)) + icon_label.setAlignment(Qt.AlignTop | Qt.AlignLeft) + icon_label.setFixedSize(self.icon_size) + icon_vbox.addWidget(icon_label, alignment=Qt.AlignTop) + icon_vbox.addStretch() + message_area.addLayout(icon_vbox) + + self.message_label = QLabel(message) + self.message_label.setObjectName('messageLabel') + self.message_label.setWordWrap(True) + self.message_label.setAlignment(Qt.AlignTop | Qt.AlignLeft) + self.message_label.setMinimumHeight(self.icon_size.height()) + self.message_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum) + message_area.addWidget(self.message_label, 1) + message_area.addStretch() + + content_layout.addLayout(message_area) + base_layout.addWidget(content_widget) + base_layout.addSpacing(10) + + button_widget = QWidget() + button_layout = QHBoxLayout(button_widget) + button_layout.setContentsMargins(0, 0, 0, 0) + button_layout.setSpacing(10) + button_layout.addStretch() + + if self.show_cancel: + self.cancel_button = QPushButton(self.cancel_text) + self.cancel_button.setObjectName("secondaryButton") + self.cancel_button.clicked.connect(self.reject) + self.cancel_button.setFixedSize(90, 28) + button_layout.addWidget(self.cancel_button) + + self.confirm_button = QPushButton(self.confirm_text) + self.confirm_button.setObjectName("primaryButton") + self.confirm_button.clicked.connect(self.accept) + self.confirm_button.setFixedSize(90, 28) + button_layout.addWidget(self.confirm_button) + + self.confirm_button.setDefault(True) + self.confirm_button.setAutoDefault(True) + if self.show_cancel: + self.cancel_button.setAutoDefault(False) + + base_layout.addWidget(button_widget) + + main_layout.addWidget(base_frame) + + def _create_title_bar(self): + """创建自定义标题栏""" + self.title_bar = QFrame() + self.title_bar.setObjectName("titleBar") + + title_layout = QHBoxLayout(self.title_bar) + title_layout.setContentsMargins(0, 0, 0, 0) + title_layout.setSpacing(0) + + self.title_label = QLabel(self.windowTitle()) + self.title_label.setObjectName("titleLabel") + self.title_label.setAlignment(Qt.AlignVCenter | Qt.AlignLeft) + title_layout.addWidget(self.title_label, 1) + + title_layout.addStretch() + + controls = QWidget() + controls.setObjectName("controlButtons") + controls_layout = QHBoxLayout(controls) + controls_layout.setContentsMargins(0, 0, 0, 0) + controls_layout.setSpacing(0) + + self.close_button = QPushButton() + self.close_button.setObjectName("closeButton") + self.close_button.clicked.connect(self.reject) + self.close_button.setFocusPolicy(Qt.NoFocus) + controls_layout.addWidget(self.close_button) + + self._apply_title_bar_icons() + + title_layout.addWidget(controls) + + + def _apply_title_bar_icons(self): + """应用标题栏图标""" + if self._icon_close: + self.close_button.setIcon(self._icon_close) + self.close_button.setIconSize(self._title_icon_size) + self.close_button.setText("") + self.close_button.setToolTip("关闭") + + def setWindowTitle(self, title): + """设置窗口标题""" + super().setWindowTitle(title) + if hasattr(self, "title_label"): + self.title_label.setText(title) + + def mousePressEvent(self, event): + """鼠标按下事件 - 用于拖拽窗口""" + if event.button() == Qt.LeftButton and self.title_bar.geometry().contains(event.pos()): + self.dragging = True + self.drag_position = event.globalPos() - self.frameGeometry().topLeft() + event.accept() + super().mousePressEvent(event) + + def mouseMoveEvent(self, event): + """鼠标移动事件 - 用于拖拽窗口""" + if event.buttons() == Qt.LeftButton and self.dragging: + self.move(event.globalPos() - self.drag_position) + event.accept() + super().mouseMoveEvent(event) + + def mouseReleaseEvent(self, event): + """鼠标释放事件 - 停止拖拽""" + if event.button() == Qt.LeftButton: + self.dragging = False + super().mouseReleaseEvent(event) + + @staticmethod + def show_success(parent, title, message, show_cancel=False, confirm_text="确认"): + """显示成功消息对话框""" + dialog = UniversalMessageDialog( + parent, title, message, + UniversalMessageDialog.SUCCESS, + show_cancel, confirm_text + ) + return dialog.exec_() + + @staticmethod + def show_warning(parent, title, message, show_cancel=True, confirm_text="确认", cancel_text="取消"): + """显示警告消息对话框""" + dialog = UniversalMessageDialog( + parent, title, message, + UniversalMessageDialog.WARNING, + show_cancel, confirm_text, cancel_text + ) + return dialog.exec_() + + @staticmethod + def show_error(parent, title, message, show_cancel=False, confirm_text="确认"): + """显示错误消息对话框""" + dialog = UniversalMessageDialog( + parent, title, message, + UniversalMessageDialog.ERROR, + show_cancel, confirm_text + ) + return dialog.exec_() + + @staticmethod + def show_info(parent, title, message, show_cancel=True, confirm_text="确认", cancel_text="取消"): + """显示信息消息对话框""" + dialog = UniversalMessageDialog( + parent, title, message, + UniversalMessageDialog.INFO, + show_cancel, confirm_text, cancel_text + ) + return dialog.exec_() + + +class StyledTextInputDialog(QDialog): + """与新建项目样式一致的文本输入对话框""" + + def __init__(self, parent, title, label_text="", placeholder="", default_text=""): + super().__init__(parent) + self.setWindowTitle(title) + self.setObjectName("styledTextInputDialog") + self.setModal(True) + self.resize(420, 186) + self.setWindowFlags(Qt.Dialog | Qt.FramelessWindowHint) + self.setAttribute(Qt.WA_TranslucentBackground, True) + + self.dragging = False + self.drag_position = QPoint() + self.icon_manager = get_icon_manager() + self._title_icon_size = QSize(18, 18) + self._icon_close = self.icon_manager.get_icon('close_icon', self._title_icon_size) + + self.setStyleSheet(""" + QDialog#styledTextInputDialog { + background-color: transparent; + border: none; + } + QFrame#baseFrame { + background-color: #000000; + border: 1px solid #3E3E42; + border-radius: 5px; + } + QWidget#titleBar { + background-color: transparent; + border: none; + border-radius: 5px 5px 0px 0px; + min-height: 32px; + max-height: 32px; + } + QWidget#titleBar QWidget { + background-color: transparent; + border: none; + } + QLabel#titleLabel { + color: #FFFFFF; + font-family: 'Inter', 'Microsoft YaHei', sans-serif; + font-size: 14px; + font-weight: 500; + letter-spacing: 0.7px; + } + QWidget#controlButtons QPushButton { + background-color: transparent; + border: none; + color: #EBEBEB; + font-size: 14px; + min-width: 18px; + max-width: 18px; + min-height: 18px; + max-height: 18px; + padding: 0px; + border-radius: 3px; + } + QWidget#controlButtons QPushButton:hover { + background-color: #2A2D2E; + color: #FFFFFF; + } + QPushButton#closeButton { + border-radius: 0px 5px 0px 0px; + } + QPushButton#closeButton:hover { + background-color: #2A2D2E; + color: #FFFFFF; + } + QWidget#contentWidget { + background-color: transparent; + border-radius: 0px 0px 5px 5px; + } + QFrame#contentContainer { + background-color: #19191B; + border: 1px solid #2C2F36; + border-radius: 5px; + } + QLabel[role="fieldLabel"] { + color: #EBEBEB; + font-family: 'Inter', 'Microsoft YaHei', sans-serif; + font-size: 12px; + font-weight: 400; + letter-spacing: 0.6px; + } + QLineEdit { + background-color: rgba(89, 100, 113, 0.2); + color: #EBEBEB; + border: 1px solid rgba(76, 92, 110, 0.6); + border-radius: 2px; + padding: 0px 10px; + font-family: 'Inter', 'Microsoft YaHei', sans-serif; + font-size: 11px; + font-weight: 300; + letter-spacing: 0.55px; + min-height: 30px; + max-height: 30px; + } + QLineEdit:focus { + border: 1px solid #3067C0; + background-color: rgba(48, 103, 192, 0.1); + } + QLineEdit:hover { + border: 1px solid #3067C0; + background-color: rgba(89, 100, 113, 0.3); + } + QPushButton { + background-color: rgba(89, 98, 118, 0.5); + color: #EBEBEB; + border: none; + border-radius: 2px; + padding: 0px 12px; + font-family: 'Inter', 'Microsoft YaHei', sans-serif; + font-weight: 300; + font-size: 10px; + letter-spacing: 0.5px; + min-width: 120px; + min-height: 30px; + max-height: 30px; + } + QPushButton:hover { + background-color: #3067C0; + color: #FFFFFF; + } + QPushButton:pressed { + background-color: #2556A0; + color: #FFFFFF; + } + """) + + main_layout = QVBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.setSpacing(0) + + base_frame = QFrame() + base_frame.setObjectName('baseFrame') + base_frame.setFrameShape(QFrame.NoFrame) + base_frame.setAttribute(Qt.WA_StyledBackground, True) + + base_layout = QVBoxLayout(base_frame) + base_layout.setContentsMargins(0, 0, 0, 0) + base_layout.setSpacing(0) + + self._createTitleBar() + base_layout.addWidget(self.title_bar) + base_layout.addSpacing(10) + + content_widget = QWidget() + content_widget.setObjectName('contentWidget') + content_layout = QVBoxLayout(content_widget) + content_layout.setContentsMargins(10, 0, 10, 10) + content_layout.setSpacing(0) + + content_container = QFrame() + content_container.setObjectName('contentContainer') + content_container.setFrameShape(QFrame.NoFrame) + content_container.setAttribute(Qt.WA_StyledBackground, True) + content_container.setFixedWidth(400) + + container_layout = QVBoxLayout(content_container) + container_layout.setContentsMargins(15, 10, 15, 10) + container_layout.setSpacing(10) + + if label_text: + label = QLabel(label_text) + label.setProperty('role', 'fieldLabel') + container_layout.addWidget(label) + + self.line_edit = QLineEdit() + self.line_edit.setPlaceholderText(placeholder) + self.line_edit.setText(default_text) + container_layout.addWidget(self.line_edit) + + separator = QFrame() + separator.setFrameShape(QFrame.HLine) + separator.setFrameShadow(QFrame.Plain) + separator.setFixedHeight(1) + separator.setStyleSheet("background-color: #2C2F36; border: none;") + container_layout.addWidget(separator) + + button_row = QHBoxLayout() + button_row.setContentsMargins(0, 0, 0, 0) + button_row.setSpacing(10) + button_row.addStretch() + + self.confirmButton = QPushButton("确认") + self.confirmButton.setObjectName("primaryButton") + self.confirmButton.clicked.connect(self._onAccept) + button_row.addWidget(self.confirmButton) + + self.cancelButton = QPushButton("取消") + self.cancelButton.setObjectName("secondaryButton") + self.cancelButton.clicked.connect(self.reject) + button_row.addWidget(self.cancelButton) + + container_layout.addLayout(button_row) + + content_layout.addWidget(content_container, 0, Qt.AlignTop) + base_layout.addWidget(content_widget) + main_layout.addWidget(base_frame) + + self.confirmButton.setDefault(True) + self.confirmButton.setAutoDefault(True) + self.line_edit.selectAll() + self.line_edit.setFocus() + + def _createTitleBar(self): + self.title_bar = QFrame() + self.title_bar.setObjectName("titleBar") + + title_layout = QHBoxLayout(self.title_bar) + title_layout.setContentsMargins(12, 6, 12, 6) + title_layout.setSpacing(0) + + controls = QWidget() + controls.setObjectName("controlButtons") + controls_layout = QHBoxLayout(controls) + controls_layout.setContentsMargins(0, 0, 0, 0) + controls_layout.setSpacing(0) + + self.close_button = QPushButton() + self.close_button.setObjectName("closeButton") + self.close_button.clicked.connect(self.reject) + self.close_button.setFocusPolicy(Qt.NoFocus) + self.close_button.setFixedSize(18, 18) + controls_layout.addWidget(self.close_button) + + self._applyTitleBarIcons() + + left_placeholder = QWidget() + left_placeholder.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + title_layout.addWidget(left_placeholder) + + self.title_label = QLabel(self.windowTitle()) + self.title_label.setObjectName("titleLabel") + self.title_label.setAlignment(Qt.AlignCenter) + title_layout.addWidget(self.title_label, 1) + + title_layout.addWidget(controls) + left_placeholder.setFixedWidth(controls.sizeHint().width()) + + def _applyTitleBarIcons(self): + if self._icon_close: + self.close_button.setIcon(self._icon_close) + self.close_button.setIconSize(self._title_icon_size) + self.close_button.setText("") + self.close_button.setToolTip("关闭") + + def setWindowTitle(self, title): + super().setWindowTitle(title) + if hasattr(self, "title_label"): + self.title_label.setText(title) + + def _onAccept(self): + if self.line_edit.text().strip(): + self.accept() + + def text(self): + return self.line_edit.text().strip() + + def mousePressEvent(self, event): + if event.button() == Qt.LeftButton and self.title_bar.geometry().contains(event.pos()): + self.dragging = True + self.drag_position = event.globalPos() - self.frameGeometry().topLeft() + event.accept() + super().mousePressEvent(event) + + def mouseMoveEvent(self, event): + if event.buttons() == Qt.LeftButton and self.dragging: + self.move(event.globalPos() - self.drag_position) + event.accept() + super().mouseMoveEvent(event) + + def mouseReleaseEvent(self, event): + if event.button() == Qt.LeftButton: + self.dragging = False + super().mouseReleaseEvent(event) + + class CustomTreeWidget(QTreeWidget): """自定义场景树部件""" @@ -1573,24 +2820,7 @@ class CustomTreeWidget(QTreeWidget): self.setupContextMenu() # 初始化右键菜单 self.setupDragDrop() # 设置拖拽功能 self.original_scales={} - - self.setStyleSheet(""" - /* 设置折叠状态下,带子节点的箭头颜色 */ - QTreeWidget::branch:has-children:!open { - color: #8b5cf6; /* 紫色 */ - } - - /* 设置展开状态下,带子节点的箭头颜色 */ - QTreeWidget::branch:has-children:open { - color: #9ca3af; /* 灰色,提供状态变化反馈 */ - } - - /* 鼠标悬停在任意箭头上时,颜色变亮 */ - QTreeWidget::branch:hover { - color: #a78bfa; /* 亮紫色 */ - } - """) - + def initData(self): """初始化变量""" # 定义2D GUI元素类型 @@ -2119,7 +3349,7 @@ class CustomTreeWidget(QTreeWidget): deletable_items.append(item) if not deletable_items: - QMessageBox.information(self, "提示", "没有可删除的节点") + StyledMessageBox.information(self, "提示", "没有可删除的节点") return # 2. 确认删除 @@ -2129,12 +3359,17 @@ class CustomTreeWidget(QTreeWidget): else: message = f"确定要删除 {item_count} 个节点吗?" - reply = QMessageBox.question( - self, "确认删除", message, - QMessageBox.Yes | QMessageBox.No, QMessageBox.No + dialog = UniversalMessageDialog( + self, + "确认删除", + message, + message_type=UniversalMessageDialog.WARNING, + show_cancel=True, + confirm_text="确认", + cancel_text="取消" ) - if reply != QMessageBox.Yes: + if dialog.exec_() != QDialog.Accepted: return # 默认选中场景根节点,通常是第一个顶级节点 @@ -2165,7 +3400,6 @@ class CustomTreeWidget(QTreeWidget): if hasattr(panda_node, 'getPythonTag'): light_object = panda_node.getPythonTag('rp_light_object') if light_object and hasattr(self.world, 'render_pipeline'): - print(f'11111111111111111111111111,{light_object.casts_shadows}') self.world.render_pipeline.remove_light(light_object) # 从world列表中移除 @@ -2231,6 +3465,14 @@ class CustomTreeWidget(QTreeWidget): 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() @@ -2290,15 +3532,51 @@ class CustomTreeWidget(QTreeWidget): 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节点相关的所有资源。""" @@ -3029,5 +4307,3 @@ class CustomTreeWidget(QTreeWidget): - - diff --git a/vr_actions/actions.json b/vr_actions/actions.json new file mode 100644 index 00000000..7e55618f --- /dev/null +++ b/vr_actions/actions.json @@ -0,0 +1,89 @@ +{ + "actions": [ + { + "name": "/actions/default/in/Pose", + "type": "pose" + }, + { + "name": "/actions/default/in/Trigger", + "type": "boolean" + }, + { + "name": "/actions/default/in/Grip", + "type": "boolean" + }, + { + "name": "/actions/default/in/Menu", + "type": "boolean" + }, + { + "name": "/actions/default/in/System", + "type": "boolean" + }, + { + "name": "/actions/default/in/TrackpadClick", + "type": "boolean" + }, + { + "name": "/actions/default/in/TrackpadTouch", + "type": "boolean" + }, + { + "name": "/actions/default/in/AButton", + "type": "boolean" + }, + { + "name": "/actions/default/in/BButton", + "type": "boolean" + }, + { + "name": "/actions/default/in/Trackpad", + "type": "vector2" + }, + { + "name": "/actions/default/in/Joystick", + "type": "vector2" + }, + { + "name": "/actions/default/in/Squeeze", + "type": "vector1" + }, + { + "name": "/actions/default/out/Haptic", + "type": "vibration" + } + ], + "action_sets": [ + { + "name": "/actions/default", + "usage": "single" + } + ], + "default_bindings": [ + { + "controller_type": "vive_controller", + "binding_url": "bindings_vive.json" + }, + { + "controller_type": "oculus_touch", + "binding_url": "bindings_oculus.json" + }, + { + "controller_type": "knuckles", + "binding_url": "bindings_index.json" + } + ], + "localization": [ + { + "language_tag": "zh_CN", + "/actions/default/in/Trigger": "扳机", + "/actions/default/in/Grip": "握把", + "/actions/default/in/Menu": "菜单", + "/actions/default/in/System": "系统", + "/actions/default/in/TrackpadClick": "触摸板点击", + "/actions/default/in/TrackpadTouch": "触摸板触摸", + "/actions/default/in/Pose": "手部姿态", + "/actions/default/out/Haptic": "震动反馈" + } + ] +} \ No newline at end of file diff --git a/vr_actions/bindings_index.json b/vr_actions/bindings_index.json new file mode 100644 index 00000000..f7198f13 --- /dev/null +++ b/vr_actions/bindings_index.json @@ -0,0 +1,133 @@ +{ + "controller_type": "knuckles", + "description": "Valve Index\u63a7\u5236\u5668\u7ed1\u5b9a", + "name": "EG VR Editor - Index", + "bindings": { + "/actions/default": { + "sources": [ + { + "inputs": { + "click": { + "output": "/actions/default/in/Trigger" + } + }, + "mode": "button", + "path": "/user/hand/left/input/trigger" + }, + { + "inputs": { + "click": { + "output": "/actions/default/in/Trigger" + } + }, + "mode": "button", + "path": "/user/hand/right/input/trigger" + }, + { + "inputs": { + "value": { + "output": "/actions/default/in/Squeeze" + } + }, + "mode": "trigger", + "path": "/user/hand/left/input/grip" + }, + { + "inputs": { + "value": { + "output": "/actions/default/in/Squeeze" + } + }, + "mode": "trigger", + "path": "/user/hand/right/input/grip" + }, + { + "inputs": { + "click": { + "output": "/actions/default/in/AButton" + } + }, + "mode": "button", + "path": "/user/hand/right/input/a" + }, + { + "inputs": { + "click": { + "output": "/actions/default/in/BButton" + } + }, + "mode": "button", + "path": "/user/hand/right/input/b" + }, + { + "inputs": { + "position": { + "output": "/actions/default/in/Trackpad" + }, + "click": { + "output": "/actions/default/in/TrackpadClick" + }, + "touch": { + "output": "/actions/default/in/TrackpadTouch" + } + }, + "mode": "trackpad", + "path": "/user/hand/left/input/trackpad" + }, + { + "inputs": { + "position": { + "output": "/actions/default/in/Trackpad" + }, + "click": { + "output": "/actions/default/in/TrackpadClick" + }, + "touch": { + "output": "/actions/default/in/TrackpadTouch" + } + }, + "mode": "trackpad", + "path": "/user/hand/right/input/trackpad" + }, + { + "inputs": { + "position": { + "output": "/actions/default/in/Joystick" + } + }, + "mode": "joystick", + "path": "/user/hand/left/input/thumbstick" + }, + { + "inputs": { + "position": { + "output": "/actions/default/in/Joystick" + } + }, + "mode": "joystick", + "path": "/user/hand/right/input/thumbstick" + } + ], + "poses": [ + { + "output": "/actions/default/in/Pose", + "path": "/user/hand/left/pose/raw" + }, + { + "output": "/actions/default/in/Pose", + "path": "/user/hand/right/pose/raw" + } + ], + "haptics": [ + { + "output": "/actions/default/out/Haptic", + "path": "/user/hand/left/output/haptic" + }, + { + "output": "/actions/default/out/Haptic", + "path": "/user/hand/right/output/haptic" + } + ] + } + } +} \ No newline at end of file diff --git a/vr_actions/bindings_oculus.json b/vr_actions/bindings_oculus.json new file mode 100644 index 00000000..71a697b7 --- /dev/null +++ b/vr_actions/bindings_oculus.json @@ -0,0 +1,118 @@ +{ + "controller_type": "oculus_touch", + "description": "Oculus Touch\u63a7\u5236\u5668\u7ed1\u5b9a", + "name": "EG VR Editor - Oculus Touch", + "bindings": { + "/actions/default": { + "sources": [ + { + "inputs": { + "click": { + "output": "/actions/default/in/Trigger" + } + }, + "mode": "button", + "path": "/user/hand/left/input/trigger" + }, + { + "inputs": { + "click": { + "output": "/actions/default/in/Trigger" + } + }, + "mode": "button", + "path": "/user/hand/right/input/trigger" + }, + { + "inputs": { + "value": { + "output": "/actions/default/in/Squeeze" + } + }, + "mode": "trigger", + "path": "/user/hand/left/input/grip" + }, + { + "inputs": { + "value": { + "output": "/actions/default/in/Squeeze" + } + }, + "mode": "trigger", + "path": "/user/hand/right/input/grip" + }, + { + "inputs": { + "click": { + "output": "/actions/default/in/AButton" + } + }, + "mode": "button", + "path": "/user/hand/right/input/a" + }, + { + "inputs": { + "click": { + "output": "/actions/default/in/BButton" + } + }, + "mode": "button", + "path": "/user/hand/right/input/b" + }, + { + "inputs": { + "position": { + "output": "/actions/default/in/Joystick" + }, + "click": { + "output": "/actions/default/in/TrackpadClick" + } + }, + "mode": "joystick", + "path": "/user/hand/left/input/thumbstick" + }, + { + "inputs": { + "position": { + "output": "/actions/default/in/Joystick" + }, + "click": { + "output": "/actions/default/in/TrackpadClick" + } + }, + "mode": "joystick", + "path": "/user/hand/right/input/thumbstick" + }, + { + "inputs": { + "click": { + "output": "/actions/default/in/Menu" + } + }, + "mode": "button", + "path": "/user/hand/left/input/menu" + } + ], + "poses": [ + { + "output": "/actions/default/in/Pose", + "path": "/user/hand/left/pose/raw" + }, + { + "output": "/actions/default/in/Pose", + "path": "/user/hand/right/pose/raw" + } + ], + "haptics": [ + { + "output": "/actions/default/out/Haptic", + "path": "/user/hand/left/output/haptic" + }, + { + "output": "/actions/default/out/Haptic", + "path": "/user/hand/right/output/haptic" + } + ] + } + } +} \ No newline at end of file diff --git a/vr_actions/bindings_vive.json b/vr_actions/bindings_vive.json new file mode 100644 index 00000000..2dd50b51 --- /dev/null +++ b/vr_actions/bindings_vive.json @@ -0,0 +1,106 @@ +{ + "controller_type": "vive_controller", + "description": "Vive\u63a7\u5236\u5668\u7ed1\u5b9a", + "name": "EG VR Editor - Vive", + "bindings": { + "/actions/default": { + "sources": [ + { + "inputs": { + "click": { + "output": "/actions/default/in/Trigger" + } + }, + "mode": "button", + "path": "/user/hand/left/input/trigger" + }, + { + "inputs": { + "click": { + "output": "/actions/default/in/Trigger" + } + }, + "mode": "button", + "path": "/user/hand/right/input/trigger" + }, + { + "inputs": { + "click": { + "output": "/actions/default/in/Grip" + } + }, + "mode": "button", + "path": "/user/hand/left/input/grip" + }, + { + "inputs": { + "click": { + "output": "/actions/default/in/Grip" + } + }, + "mode": "button", + "path": "/user/hand/right/input/grip" + }, + { + "inputs": { + "click": { + "output": "/actions/default/in/Menu" + } + }, + "mode": "button", + "path": "/user/hand/left/input/menu" + }, + { + "inputs": { + "position": { + "output": "/actions/default/in/Trackpad" + }, + "click": { + "output": "/actions/default/in/TrackpadClick" + }, + "touch": { + "output": "/actions/default/in/TrackpadTouch" + } + }, + "mode": "trackpad", + "path": "/user/hand/left/input/trackpad" + }, + { + "inputs": { + "position": { + "output": "/actions/default/in/Trackpad" + }, + "click": { + "output": "/actions/default/in/TrackpadClick" + }, + "touch": { + "output": "/actions/default/in/TrackpadTouch" + } + }, + "mode": "trackpad", + "path": "/user/hand/right/input/trackpad" + } + ], + "poses": [ + { + "output": "/actions/default/in/Pose", + "path": "/user/hand/left/pose/raw" + }, + { + "output": "/actions/default/in/Pose", + "path": "/user/hand/right/pose/raw" + } + ], + "haptics": [ + { + "output": "/actions/default/out/Haptic", + "path": "/user/hand/left/output/haptic" + }, + { + "output": "/actions/default/out/Haptic", + "path": "/user/hand/right/output/haptic" + } + ] + } + } +} \ No newline at end of file diff --git a/vr_test.py b/vr_test.py deleted file mode 100644 index b35a6ee3..00000000 --- a/vr_test.py +++ /dev/null @@ -1,468 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -VR功能测试脚本 -测试VR系统的各种功能,包括模拟模式和真实VR模式 -""" - -import sys -import os -from main import MyWorld - -def test_basic_vr_functionality(): - """测试基本VR功能""" - print("=== VR基本功能测试 ===") - - # 创建世界实例 - world = MyWorld() - - # 检查VR管理器是否正确初始化 - print("✓ VR管理器已创建") - - # 检查OpenVR库是否可用 - try: - import openvr - print("✓ OpenVR库已安装") - except ImportError: - print("⚠ OpenVR库未安装,将使用模拟模式") - - # 测试VR初始化(会自动回退到模拟模式) - print("\n正在测试VR初始化...") - vr_success = world.initializeVR() - - if vr_success: - print("✓ VR系统初始化成功") - - # 获取VR信息 - vr_info = world.getVRInfo() - print(f"\n📊 VR系统信息:") - print(f" - 模式: {vr_info['mode']}") - print(f" - 启用状态: {vr_info['enabled']}") - print(f" - 模拟模式: {vr_info['simulation_mode']}") - print(f" - 渲染尺寸: {vr_info['render_size']}") - print(f" - 控制器数量: {vr_info.get('controllers', 0)}") - - # 测试控制器输入 - print("\n🎮 测试控制器输入:") - for i in range(2): - controller_input = world.vr_manager.get_controller_input(i) - if controller_input and controller_input.get('connected'): - print(f" 控制器 {i}: 已连接") - print(f" - 扳机: {controller_input['trigger']:.2f}") - print(f" - 握把: {controller_input['grip']:.2f}") - print(f" - 触摸板: ({controller_input['touchpad']['x']:.2f}, {controller_input['touchpad']['y']:.2f})") - else: - print(f" 控制器 {i}: 未连接") - - # 测试VR状态 - print("\n📊 VR系统状态:") - status = world.getVRStatus() - print(f" - VR启用: {status['vr_enabled']}") - print(f" - 控制器总数: {len(status['controllers'])}") - - else: - print("✗ VR系统初始化失败") - return False - - # 关闭VR系统 - world.shutdownVR() - print("\n✓ VR系统已关闭") - - return True - -def test_simulation_mode(): - """测试VR模拟模式""" - print("=== VR模拟模式测试 ===") - - # 创建世界实例 - world = MyWorld() - - # 强制启用模拟模式 - print("强制启用VR模拟模式...") - vr_success = world.vr_manager.enable_simulation_mode() - - if vr_success: - print("✓ VR模拟模式启用成功") - - # 获取模拟数据 - sim_data = world.vr_manager.get_simulation_data() - if sim_data: - print(f"\n🎮 模拟数据:") - print(f" - 头部位置: {sim_data['head_pose']['position']}") - print(f" - 控制器 0 位置: {sim_data['controller_poses'][0]['position']}") - print(f" - 控制器 1 位置: {sim_data['controller_poses'][1]['position']}") - print(f" - 渲染尺寸: {sim_data['render_size']}") - - # 测试更新模拟数据 - print(f"\n🔧 测试模拟数据更新:") - new_head_pos = [0.1, 0.1, 1.7] - success = world.vr_manager.update_simulation_data('head_pose', { - 'position': new_head_pos, - 'rotation': [0, 0, 0, 1] - }) - if success: - print(f" ✓ 头部位置更新为: {new_head_pos}") - - # 测试控制器输入(模拟) - print(f"\n🎮 模拟控制器输入测试:") - for i in range(2): - controller_input = world.vr_manager.get_controller_input(i) - if controller_input: - print(f" 控制器 {i}:") - print(f" - 连接状态: {controller_input['connected']}") - print(f" - 扳机: {controller_input['trigger']}") - print(f" - 握把: {controller_input['grip']}") - print(f" - 菜单键: {controller_input['menu']}") - - else: - print("✗ VR模拟模式启用失败") - return False - - # 关闭VR系统 - world.vr_manager.shutdown_vr() - print("\n✓ VR模拟模式已关闭") - - return True - -def test_alvr_streaming(): - """测试ALVR串流功能""" - print("=== ALVR串流测试 ===") - - # 创建世界实例 - world = MyWorld() - - # 初始化VR系统 - print("初始化VR系统...") - if not world.initializeVR(): - print("✗ VR系统初始化失败") - return False - - # 测试ALVR初始化 - print("\n正在测试ALVR初始化...") - alvr_success = world.initializeALVR() - - if alvr_success: - print("✓ ALVR初始化成功") - - # 测试串流状态 - print("\n📊 ALVR串流状态:") - status = world.getALVRStreamingStatus() - print(f" - 连接状态: {world.isALVRConnected()}") - print(f" - 串流状态: {world.isALVRStreaming()}") - - # 测试串流质量设置 - print("\n🎥 测试串流质量设置:") - quality_success = world.setALVRStreamQuality(1920, 1080, 60, 100) - if quality_success: - print(" ✓ 串流质量设置成功: 1920x1080@60fps, 100Mbps") - else: - print(" ⚠ 串流质量设置失败") - - # 测试触觉反馈 - print("\n🔔 测试触觉反馈:") - for controller_id in range(2): - feedback_success = world.sendHapticFeedback(controller_id, 0.1, 0.5) - if feedback_success: - print(f" ✓ 控制器 {controller_id} 触觉反馈发送成功") - else: - print(f" ⚠ 控制器 {controller_id} 触觉反馈发送失败") - - # 测试串流控制 - print("\n🎮 测试串流控制:") - start_success = world.startALVRStreaming() - if start_success: - print(" ✓ 串流开始成功") - else: - print(" ⚠ 串流开始失败") - - # 等待一下然后停止 - import time - time.sleep(1) - - stop_success = world.stopALVRStreaming() - if stop_success: - print(" ✓ 串流停止成功") - else: - print(" ⚠ 串流停止失败") - - else: - print("⚠ ALVR初始化失败(这在没有ALVR服务器时是正常的)") - print(" 提示: 要使用ALVR功能,请:") - print(" 1. 下载并安装ALVR服务器") - print(" 2. 启动ALVR服务器") - print(" 3. 在Quest头盔上安装并启动ALVR客户端") - - # 关闭系统 - world.shutdownALVR() - world.shutdownVR() - print("\n✓ ALVR和VR系统已关闭") - - return True - -def test_vr_gui_control_panel(): - """测试VR GUI控制面板""" - print("=== VR GUI控制面板测试 ===") - - try: - from ui.vr_control_panel import VRControlPanel - from PyQt5.QtWidgets import QApplication - - # 创建Qt应用程序 - app = QApplication.instance() - if app is None: - app = QApplication(sys.argv) - - # 创建世界实例 - world = MyWorld() - - # 创建VR控制面板 - print("创建VR控制面板...") - control_panel = VRControlPanel(world) - - # 测试面板功能 - print("✓ VR控制面板创建成功") - print(" - 面板标题:", control_panel.windowTitle()) - print(" - 面板大小:", control_panel.size().width(), "x", control_panel.size().height()) - - # 显示面板(非阻塞) - control_panel.show() - print("✓ VR控制面板已显示") - - print("\n💡 GUI控制面板功能:") - print(" - VR系统开关控制") - print(" - ALVR串流控制") - print(" - 实时状态监控") - print(" - 质量设置调整") - print(" - 控制器状态显示") - print(" - 性能监控") - - # 短暂显示然后关闭 - import time - time.sleep(2) - control_panel.close() - - print("\n✓ VR控制面板测试完成") - - except Exception as e: - print(f"✗ VR控制面板测试失败: {str(e)}") - return False - - return True - -def test_vr_interaction(): - """测试VR交互功能""" - print("=== VR交互功能测试 ===") - - # 创建世界实例 - world = MyWorld() - - # 初始化VR系统 - print("初始化VR系统...") - if not world.initializeVR(): - print("✗ VR系统初始化失败") - return False - - # 测试VR输入处理 - print("\n🎮 测试VR输入处理:") - input_success = world.startVRInput() - if input_success: - print(" ✓ VR输入处理启动成功") - - # 测试控制器射线显示 - print("\n🌟 测试控制器射线:") - ray_success = world.showControllerRays(True) - if ray_success: - print(" ✓ 控制器射线显示启用") - else: - print(" ⚠ 控制器射线显示失败") - - # 测试手势识别 - print("\n✋ 测试手势识别:") - gesture_success = world.setVRGestureEnabled(True) - if gesture_success: - print(" ✓ VR手势识别启用") - else: - print(" ⚠ VR手势识别启用失败") - - # 测试VR交互 - print("\n🤏 测试VR交互:") - interaction_success = world.setVRInteractionEnabled(True) - if interaction_success: - print(" ✓ VR交互功能启用") - else: - print(" ⚠ VR交互功能启用失败") - - # 获取控制器状态 - print("\n🎮 控制器状态:") - controllers = world.getAllControllers() - for i, controller in enumerate(controllers): - if controller: - print(f" 控制器 {i}: 可用") - state = world.getControllerState(i) - if state: - print(f" - 位置: {state.get('position', 'N/A')}") - print(f" - 旋转: {state.get('rotation', 'N/A')}") - print(f" - 按钮: {state.get('buttons', 'N/A')}") - else: - print(f" 控制器 {i}: 不可用") - - # 停止VR输入 - world.stopVRInput() - print("\n✓ VR输入处理已停止") - - else: - print(" ⚠ VR输入处理启动失败") - - # 关闭VR系统 - world.shutdownVR() - print("\n✓ VR系统已关闭") - - return True - -def run_all_tests(): - """运行所有测试""" - print("=== 运行所有VR测试 ===") - - tests = [ - ("基本VR功能", test_basic_vr_functionality), - ("VR模拟模式", test_simulation_mode), - ("ALVR串流", test_alvr_streaming), - ("VR GUI控制面板", test_vr_gui_control_panel), - ("VR交互功能", test_vr_interaction) - ] - - results = [] - for test_name, test_func in tests: - print(f"\n{'='*50}") - print(f"正在运行: {test_name}") - print('='*50) - - try: - success = test_func() - results.append((test_name, success)) - if success: - print(f"✓ {test_name} 测试通过") - else: - print(f"✗ {test_name} 测试失败") - except Exception as e: - print(f"✗ {test_name} 测试出错: {str(e)}") - results.append((test_name, False)) - - # 打印总结 - print(f"\n{'='*50}") - print("测试总结") - print('='*50) - - passed = sum(1 for _, success in results if success) - total = len(results) - - for test_name, success in results: - status = "✓ 通过" if success else "✗ 失败" - print(f"{test_name}: {status}") - - print(f"\n总计: {passed}/{total} 个测试通过") - - if passed == total: - print("🎉 所有测试都通过了!") - else: - print("⚠ 部分测试失败,请检查上述输出") - - return passed == total - -def print_vr_requirements(): - """打印VR系统要求""" - print("📋 VR系统要求说明") - print("="*50) - print("🔧 软件要求:") - print(" - Python 3.8+") - print(" - Panda3D") - print(" - PyQt5") - print(" - OpenVR库 (pip install openvr)") - print(" - 其他依赖见 requirements/vr-requirements.txt") - - print("\n🎮 硬件要求(完整VR模式):") - print(" - 支持VR的显卡 (GTX 1060/RX 580 或更好)") - print(" - VR头盔 (Quest 2/3, Valve Index, HTC Vive等)") - print(" - 足够的USB端口或无线连接") - - print("\n🌐 连接方式:") - print(" 有线连接:") - print(" - Valve Index: DisplayPort + USB 3.0") - print(" - HTC Vive: HDMI + USB 3.0") - print(" - Quest 2/3: USB-C (Quest Link)") - print(" 无线连接:") - print(" - Quest 2/3: 通过ALVR串流") - print(" - 需要5GHz Wi-Fi网络") - print(" - 需要ALVR服务器运行") - - print("\n🔄 启动顺序(完整VR模式):") - print(" 1. 确保VR头盔已连接并识别") - print(" 2. 启动SteamVR") - print(" 3. (可选) 启动ALVR服务器(Quest无线)") - print(" 4. 运行VR测试脚本") - - print("\n🎮 模拟模式说明:") - print(" - 无需VR硬件") - print(" - 模拟头盔和控制器追踪") - print(" - 立体渲染到窗口") - print(" - 适用于开发和调试") - - print("\n💡 故障排除:") - print(" - 如果OpenVR初始化失败,系统会自动切换到模拟模式") - print(" - 检查SteamVR是否正在运行") - print(" - 确认VR头盔被系统识别") - print(" - 检查USB/DisplayPort连接") - print(" - 重启SteamVR和头盔") - -def main(): - """主函数""" - print("🎮 VR功能测试脚本") - print("="*50) - - # 显示要求说明 - print_vr_requirements() - - print("\n请选择测试类型:") - print("1. 基本VR功能测试") - print("2. VR模拟模式测试") - print("3. ALVR串流测试") - print("4. VR GUI控制面板") - print("5. VR交互功能测试") - print("6. 运行所有测试") - print("7. 查看VR系统要求") - - try: - choice = input("请输入选择 (1-7): ") - - if choice == "1": - success = test_basic_vr_functionality() - elif choice == "2": - success = test_simulation_mode() - elif choice == "3": - success = test_alvr_streaming() - elif choice == "4": - success = test_vr_gui_control_panel() - elif choice == "5": - success = test_vr_interaction() - elif choice == "6": - success = run_all_tests() - elif choice == "7": - print_vr_requirements() - return - else: - print("无效的选择") - return - - print("\n" + "="*50) - if success: - print("✓ 测试成功") - else: - print("✗ 测试失败") - - except KeyboardInterrupt: - print("\n用户中断测试") - except Exception as e: - print(f"测试过程中发生错误: {str(e)}") - -if __name__ == "__main__": - main() \ No newline at end of file