Compare commits

...

51 Commits

Author SHA1 Message Date
ayuan9957
5c0a61d253 保存、打开项目优化 2026-03-24 20:51:39 +08:00
ayuan9957
062bd4e720 Fix SSBO model and child selection in the editor 2026-03-24 14:11:03 +08:00
ayuan9957
a6986002a7 Fix material targeting and update MetaCore branding 2026-03-24 09:12:37 +08:00
ayuan9957
f17073160c Merge these animation changes from hu_migrate_on_geng into your branch: 2026-03-23 08:25:30 +08:00
ayuan9957
c14c2b8796 Refine MetaCore scene workflow and SSBO loading 2026-03-19 16:25:32 +08:00
ayuan9957
b9053b6a28 保存打包优化 2026-03-19 11:26:43 +08:00
ayuan9957
1aa21c2680 新建保存打包优化 2026-03-19 11:06:17 +08:00
ayuan9957
4ef6ce97d4 优化项目资源结构和保存项目 2026-03-19 10:16:03 +08:00
ayuan9957
776d3c01aa 更新保存和打包 2026-03-18 18:34:44 +08:00
ayuan9957
07359082cd 修复导入模型卡顿问题 2026-03-18 15:48:10 +08:00
ayuan9957
04cb0dc441 Merge branch 'hu_migrate_on_geng' into geng_migrate_on_imgui_hu
# Conflicts:
#	imgui.ini
#	ui/panels/editor_panels_top.py
2026-03-18 15:28:22 +08:00
ayuan9957
8a1a0d19ba Align packaged camera controls with editor behavior 2026-03-18 15:20:41 +08:00
Hector
bdd4d0f1b5 修复拖动按钮闪退,保存和加载LUI信息 2026-03-18 15:13:18 +08:00
Hector
74b6a3307c 保存模型子级模型位置移动 2026-03-18 14:43:18 +08:00
ayuan9957
da629e508d 打包功能 2026-03-18 13:58:53 +08:00
Hector
4635458300 修复保存材质模型位置,保存场景树,加载场景树, 2026-03-18 11:48:13 +08:00
ayuan9957
6524f8306b 工具栏优化 2026-03-18 08:59:03 +08:00
ayuan9957
c4a507c6ac Update Python env and requirements 2026-03-17 16:18:10 +08:00
ayuan9957
11ad2d5742 清理qt和旧版gizmo 2026-03-15 17:23:37 +08:00
ayuan9957
49d630f967 Fix transparency shader responses 2026-03-15 16:19:32 +08:00
ayuan9957
7e6b1d54d9 Use commands for reset transform 2026-03-13 14:44:00 +08:00
ayuan9957
8a80af9825 Fix light registration handling 2026-03-13 14:15:38 +08:00
ayuan9957
6e76b54ddb 撤销和重做优化 2026-03-13 12:33:23 +08:00
ayuan9957
1cfbeda77b 优化模型导入中英文输入 2026-03-13 09:53:12 +08:00
ayuan9957
b48616d5f2 Fix transparency visibility issue 2026-03-12 22:07:48 +08:00
ayuan9957
780536203e imgui优化 2026-03-12 15:13:05 +08:00
ayuan9957
c5dbc6be6f fix: unify py311 startup and restore LUI editor panel toggle 2026-03-04 12:39:02 +08:00
ayuan9957
718cf802be refactor(lui): complete batch split for interaction/editor/property flows 2026-03-04 12:05:34 +08:00
ayuan9957
22a9b5b64a 回补G任务:拆分LUI _update_drag 2026-03-04 12:04:47 +08:00
ayuan9957
c006b69613 重启N1资源管理器面板拆分(保留布局) 2026-03-04 12:04:32 +08:00
ayuan9957
4f70cc113b refactor: 拆分updateGizmoDrag并同步任务文档 2026-03-04 12:03:45 +08:00
ayuan9957
d32a604c2f refactor: 拆分loadScene流程并更新优化分析 2026-03-04 12:01:32 +08:00
ayuan9957
c2e528c55f refactor: 收敛非VR编辑上下文并更新优化索引 2026-03-04 12:00:00 +08:00
ayuan9957
381a478abf refactor: 拆分右侧属性面板为变换/材质/碰撞子mixin 2026-03-04 11:59:30 +08:00
ayuan9957
3a13025009 继续优化:拆分editor_panels并收敛旧接口 2026-03-04 11:59:13 +08:00
ayuan9957
e9784b3400 清理qt 2026-03-04 11:57:40 +08:00
Hector
37b3cc30dc 模型导入成黑色问题修复。打开项目使用SSBO导入模型逻辑流畅运行。 2026-02-28 17:39:04 +08:00
Hector
036b68ef41 合并更新 2026-02-28 11:10:43 +08:00
Hector
5752559cdd Merge remote-tracking branch 'origin/geng' into IMgui_hu 2026-02-28 11:06:14 +08:00
Your Name
7386d687da feat: 接入RenderPipeline选中描边后处理 2026-02-28 10:58:34 +08:00
Hector
28d2b124cc 合并更新 2026-02-28 10:49:03 +08:00
Hector
2ac08b0582 Merge remote-tracking branch 'origin/geng' into IMgui_hu
# Conflicts:
#	imgui.ini
#	ssbo_component/ssbo_controller.py
#	ssbo_component/ssbo_editor.py
2026-02-28 09:37:33 +08:00
Your Name
86aaa21ddd ssbo 视锥剔除优化提升效果一般 2026-02-27 16:57:53 +08:00
Hector
af898947b5 拆分 2026-02-27 16:52:00 +08:00
Your Name
f3f8da7b90 Optimize SSBO root selection to avoid full dynamic activation 2026-02-27 16:11:35 +08:00
Your Name
2183d3fc3e Fix SSBO root selection behavior and optimize group sync FPS 2026-02-27 16:02:40 +08:00
Your Name
756db5b010 Fix SSBO picking sync and deletion cleanup 2026-02-27 15:39:23 +08:00
Your Name
1fd7e1d7ac Update ssbo editor 2026-02-27 14:24:11 +08:00
Your Name
53e6a829e4 Fix gizmo selection: GPU picking, group proxy, parent-child transform, pick sync 2026-02-27 11:47:15 +08:00
Hector
e917f9019d 简单实现http通信 2026-02-27 11:14:08 +08:00
Hector
c93ab3edac 修复手柄选中以及移动异常,添加Imgui的web视图 2026-02-27 10:32:57 +08:00
152 changed files with 40114 additions and 40306 deletions

View File

@ -0,0 +1,11 @@
# THIS IS AUTOGENERATED. DO NOT EDIT MANUALLY
version = 1
name = "EG"
[setup]
script = ""
[[actions]]
name = "go"
icon = "tool"
command = "python ./main.py"

View File

@ -0,0 +1,11 @@
# THIS IS AUTOGENERATED. DO NOT EDIT MANUALLY
version = 1
name = "EG"
[setup]
script = ""
[[actions]]
name = "运行"
icon = "run"
command = "python ./main.py"

18
.gitignore vendored
View File

@ -1,7 +1,17 @@
__pycache__/
*.pyc
.idea/
【模板】模型及算法与功能对应清单.docx
__pycache__/
*.pyc
imgui.ini
build/
Builds/
Library/
**/Library/
**/Builds/
**/Scenes/*_gui.json
**/Scenes/*_lui.json
**/scenes/*_gui.json
**/scenes/*_lui.json
.idea/
【模板】模型及算法与功能对应清单.docx
【模板】GY知识-产品模块架构及功能清单和主要应用场景说明.docx
1.json
2.json

View File

@ -7,10 +7,10 @@ EG 是一个基于 Panda3D 引擎开发的 3D 编辑器和游戏引擎,集成
## 核心技术栈
- **渲染引擎**: Panda3D 1.10.15 + RenderPipeline (延迟渲染、PBR材质)
- **GUI框架**: PyQt5 + imgui_bundle (用于编辑器界面)
- **GUI框架**: imgui_bundle + p3dimgui (用于编辑器界面)
- **VR支持**: OpenVR 2.2.0
- **脚本系统**: Python 3.10.12
- **其他依赖**: PyQt5-WebEngine, assimp, pillow 等
- **脚本系统**: Python 3.11
- **其他依赖**: openvr, numpy, aiohttp, pyassimp, pillow 等
## 项目架构
@ -88,9 +88,8 @@ EG 是一个基于 Panda3D 引擎开发的 3D 编辑器和游戏引擎,集成
### 环境要求
- Python 3.10.12
- Python 3.11
- Panda3D 1.10.15
- PyQt5 5.15.9
- OpenVR 2.2.0 (VR功能)
### 运行方式
@ -216,4 +215,4 @@ pip install -r requirements/requirements.txt
---
*该文档由iFlow CLI自动生成最后更新时间: 2026年1月28日*
*该文档由iFlow CLI自动生成最后更新时间: 2026年3月17日*

1
EG

@ -1 +0,0 @@
Subproject commit 69e2bda47e9713705ad5c45a08b6fc643a2b51f6

62
IMGUI_MODULE_ANALYSIS.md Normal file
View File

@ -0,0 +1,62 @@
# EG 本地项目模块分析Qt 清理后)
更新时间2026-02-28
分析范围:`d:\IMGUI\EG` 本地文件,不包含任何远程仓库信息。
## 1. 当前结论
- 主编辑器路径已切换为 ImGui 方案Qt/PySide 在项目代码主路径中已清理完成。
- 本地扫描结果(排除 `RenderPipelineFile` 第三方目录):
- `PyQt/PySide/Qt` 关键引用:`0`
- Python 脚本数:`385`
- 当前主要技术债已从“Qt 运行时依赖”转为“旧接口命名和耦合残留”。
- 已完成基础稳定性优化:入口保护、兼容字段初始化、脚本恢复拼写错误修复。
- 已完成第一轮接口收敛:`event_handler/selection/runtime_actions/InfoPanelManager` 已改为 helper 访问模式。
## 2. 本轮 Qt 清理落点
已完成清理的关键文件:
- [core/InfoPanelManager.py](d:/IMGUI/EG/core/InfoPanelManager.py)
- [core/selection.py](d:/IMGUI/EG/core/selection.py)
- [scene/scene_manager_convert_tiles_mixin.py](d:/IMGUI/EG/scene/scene_manager_convert_tiles_mixin.py)
- [ui/widgets.py](d:/IMGUI/EG/ui/widgets.py)
- [ui/icon_manager.py](d:/IMGUI/EG/ui/icon_manager.py)
- [core/world.py](d:/IMGUI/EG/core/world.py)
- [core/vr_manager.py](d:/IMGUI/EG/core/vr_manager.py)
- [core/vr/testing/test_mode.py](d:/IMGUI/EG/core/vr/testing/test_mode.py)
- [scene/tree_roles.py](d:/IMGUI/EG/scene/tree_roles.py)
- [TransformGizmo/move_gizmo.py](d:/IMGUI/EG/TransformGizmo/move_gizmo.py)
- [TransformGizmo/rotate_gizmo.py](d:/IMGUI/EG/TransformGizmo/rotate_gizmo.py)
- [TransformGizmo/scale_gizmo.py](d:/IMGUI/EG/TransformGizmo/scale_gizmo.py)
- [requirements/requirements.txt](d:/IMGUI/EG/requirements/requirements.txt)
- [requirements/clean-requirements.txt](d:/IMGUI/EG/requirements/clean-requirements.txt)
- [requirements/environment.yml](d:/IMGUI/EG/requirements/environment.yml)
- [requirements/DEPLOYMENT_README.md](d:/IMGUI/EG/requirements/DEPLOYMENT_README.md)
## 3. 仍需优化的结构点(非 Qt 依赖问题)
- 旧接口命名残留仍较多:`interface_manager/treeWidget/gui_manager` 引用约 `92` 处。
- `main.py` 仍是脚本式入口(模块导入副作用风险仍在)。
- 场景树与 GUI 元素生命周期在多个模块分散维护,后续建议继续做单一上下文收敛。
## 4. 风险分级Qt 清理后)
- 高风险:
- 多处旧接口命名仍在调用链中,后续重构若不做适配层可能引发行为回归。
- 中风险:
- 场景保存/加载与树状态同步仍跨模块分散,维护成本高。
- 低风险:
- 文档和模板文件中仍有历史术语,易造成新成员理解偏差。
## 5. 后续建议顺序
1. 建立统一 EditorContext收敛 `interface_manager/gui_manager` 访问入口。
2. 将场景树操作抽象为 ImGui 专用接口,逐步移除 `treeWidget` 语义。
3. 统一入口规范(`if __name__ == "__main__":`)并补最小回归脚本。
## 6. 全量目录与脚本清单
为后续优化分析,完整目录树与全部脚本行数/用途清单见:
- [PROJECT_FULL_CATALOG.md](d:/IMGUI/EG/PROJECT_FULL_CATALOG.md)

2465
PROJECT_FULL_CATALOG.md Normal file

File diff suppressed because it is too large Load Diff

352
PROJECT_MODULE_INDEX.md Normal file
View File

@ -0,0 +1,352 @@
# PROJECT_MODULE_INDEX
> 本文档只基于本地工作区扫描生成,不依赖 Git 远程信息。
- 生成时间: 2026-02-28 16:41:46
- 目标: 为后续优化/重构提供 功能模块 -> 文件 的快速定位索引
## 0. 关联分析文档
- 优化分析(本轮): `PROJECT_OPTIMIZATION_ANALYSIS.md`
- Qt 清理迁移清单: `QT_TO_IMGUI_MIGRATION_CHECKLIST.md`
- 历史模块分析: `IMGUI_MODULE_ANALYSIS.md`
## 1. 一级目录规模Python
| 目录 | 文件数 | 代码行数 |
|---|---:|---:|
| core | 40 | 19892 |
| gui | 1 | 7 |
| project | 2 | 759 |
| root | 2 | 812 |
| scene | 10 | 3792 |
| scripts | 18 | 1378 |
| ssbo_component | 3 | 1907 |
| templates | 1 | 1282 |
| tools | 1 | 73 |
| TransformGizmo | 5 | 3705 |
| ui | 63 | 17403 |
## 2. 功能模块 -> 关键文件映射
### 应用启动与组装
- 模块职责: 程序入口、系统初始化、模块装配
- 对应文件:
- Start_Run.py
- main.py
- templates/main_template.py
### 世界与运行时核心
- 模块职责: 世界生命周期、输入事件、选择、命令、资源、碰撞、地形等
- 对应文件:
- core/world.py
- core/event_handler.py
- core/selection.py
- core/Command_System.py
- core/tool_manager.py
- core/script_system.py
- core/resource_manager.py
- core/collision_manager.py
- core/terrain_manager.py
- core/model_drag_drop.py
- core/InfoPanelManager.py
- core/CustomMouseController.py
- core/imgui_style_manager.py
- core/imgui_webview.py
- core/selection_outline.py
- core/render_pipeline_utils.py
### VR 系统
- 模块职责: VR 管理、渲染阶段、交互、追踪、性能、可视化、配置
- 对应文件:
- core/vr_manager.py
- core/vr/config/vr_config.py
- core/vr/config/joystick_config.py
- core/vr/config/shadow_stage.py
- core/vr/rendering/stages.py
- core/vr/interaction/actions.py
- core/vr/interaction/grab.py
- core/vr/interaction/joystick.py
- core/vr/interaction/teleport.py
- core/vr/tracking/controllers.py
- core/vr/visualization/controllers.py
- core/vr/visualization/effects.py
- core/vr/performance/monitoring.py
- core/vr/performance/optimization.py
- core/vr/testing/test_mode.py
### 场景管理
- 模块职责: 模型导入、灯光管理、序列化、IO、切片转换
- 对应文件:
- scene/scene_manager.py
- scene/scene_manager_impl.py
- scene/scene_manager_model_mixin.py
- scene/scene_manager_light_mixin.py
- scene/scene_manager_serialization_mixin.py
- scene/scene_manager_io_mixin.py
- scene/scene_manager_convert_tiles_mixin.py
- scene/util.py
- scene/tree_roles.py
### 项目管理
- 模块职责: 项目创建、打开、保存、资源清单
- 对应文件:
- project/project_manager.py
### 编辑器面板 (ImGui)
- 模块职责: 顶部/左侧/中心/右侧属性面板、弹窗、运行时动作、创建对象、动画工具
- 对应文件:
- ui/panels/editor_panels.py
- ui/panels/editor_panels_top.py
- ui/panels/editor_panels_left.py
- ui/panels/editor_panels_center.py
- ui/panels/editor_panels_right.py
- ui/panels/editor_panels_right_transform.py
- ui/panels/editor_panels_right_material.py
- ui/panels/editor_panels_right_collision.py
- ui/panels/panel_delegates.py
- ui/panels/property_helpers.py
- ui/panels/runtime_actions.py
- ui/panels/create_actions.py
- ui/panels/app_actions.py
- ui/panels/dialog_panels.py
- ui/panels/script_panels.py
- ui/panels/interaction_panels.py
- ui/panels/object_factory.py
- ui/panels/animation_tools.py
### LUI 编辑器与组件
- 模块职责: LUI 管理、属性编辑、交互编辑、组件定义
- 对应文件:
- ui/lui_manager.py
- ui/lui_function.py
- ui/LUI/lui_manager_editor.py
- ui/LUI/lui_manager_interaction.py
- ui/LUI/lui_function_properties.py
- ui/LUI/lui_function_components.py
- ui/LUI/lui_shared.py
- ui/widgets.py
- ui/icon_manager.py
### LUI 内建控件与皮肤
- 模块职责: LUI 基础控件实现与皮肤资源脚本
- 对应文件:
- ui/Builtin/Elements.py
- ui/Builtin/LUI*.py
- ui/Builtin/RectTransform.py
- ui/Skins/Metro/LUIMetroSkin.py
- ui/Skins/Metro/copy_frames.py
### Transform Gizmo
- 模块职责: 移动/旋转/缩放 gizmo 与事件
- 对应文件:
- TransformGizmo/transform_gizmo.py
- TransformGizmo/move_gizmo.py
- TransformGizmo/rotate_gizmo.py
- TransformGizmo/scale_gizmo.py
- TransformGizmo/events.py
### SSBO 选取与编辑
- 模块职责: 基于 SSBO 的对象选取与编辑器
- 对应文件:
- ssbo_component/ssbo_editor.py
- ssbo_component/ssbo_controller.py
- ssbo_component/demo_component.py
### 脚本样例与测试
- 模块职责: 内置脚本、旋转/移动/缩放测试脚本
- 对应文件:
- scripts/*.py
### 开发工具
- 模块职责: 辅助分析脚本
- 对应文件:
- tools/open_source_rate.py
## 3. 配置与运行关键文件
- config/vr_settings.json
- core/vr/config/vr_settings.json
- vr_actions/actions.json
- vr_actions/bindings_index.json
- vr_actions/bindings_oculus.json
- vr_actions/bindings_vive.json
- imgui.ini
- requirements/requirements.txt
- requirements/clean-requirements.txt
- requirements/conda-requirements.txt
- requirements/environment.yml
## 4. 第三方/外部子树说明
- RenderPipelineFile/: 第三方渲染管线源码与工具Python 文件 246 个,约 31629 行。
- 优化建议: 优先在本项目业务目录改动core/, ui/, scene/, project/, ssbo_component/, TransformGizmo/),避免直接修改第三方目录。
## 5. 完整 Python 文件索引(含行数)
| 文件 | 行数 |
|---|---:|
| core/__init__.py | 23 |
| core/collision_manager.py | 1050 |
| core/Command_System.py | 576 |
| core/CustomMouseController.py | 185 |
| core/event_handler.py | 576 |
| core/imgui_style_manager.py | 428 |
| core/imgui_webview.py | 189 |
| core/InfoPanelManager.py | 1460 |
| core/model_drag_drop.py | 485 |
| core/render_pipeline_utils.py | 17 |
| core/resource_manager.py | 471 |
| core/script_system.py | 817 |
| core/selection.py | 2461 |
| core/selection_outline.py | 263 |
| core/terrain_manager.py | 571 |
| core/tool_manager.py | 133 |
| core/vr/__init__.py | 31 |
| core/vr/config/__init__.py | 8 |
| core/vr/config/joystick_config.py | 226 |
| core/vr/config/shadow_stage.py | 145 |
| core/vr/config/vr_config.py | 216 |
| core/vr/interaction/__init__.py | 9 |
| core/vr/interaction/actions.py | 484 |
| core/vr/interaction/grab.py | 329 |
| core/vr/interaction/joystick.py | 561 |
| core/vr/interaction/teleport.py | 331 |
| core/vr/performance/__init__.py | 9 |
| core/vr/performance/monitoring.py | 972 |
| core/vr/performance/optimization.py | 287 |
| core/vr/rendering/__init__.py | 10 |
| core/vr/rendering/stages.py | 712 |
| core/vr/testing/__init__.py | 6 |
| core/vr/testing/test_mode.py | 609 |
| core/vr/tracking/__init__.py | 9 |
| core/vr/tracking/controllers.py | 391 |
| core/vr/visualization/__init__.py | 8 |
| core/vr/visualization/controllers.py | 631 |
| core/vr/visualization/effects.py | 174 |
| core/vr_manager.py | 2970 |
| core/world.py | 1059 |
| gui/__init__.py | 7 |
| main.py | 770 |
| project/__init__.py | 10 |
| project/project_manager.py | 749 |
| scene/__init__.py | 10 |
| scene/scene_manager.py | 17 |
| scene/scene_manager_convert_tiles_mixin.py | 473 |
| scene/scene_manager_impl.py | 15 |
| scene/scene_manager_io_mixin.py | 962 |
| scene/scene_manager_light_mixin.py | 403 |
| scene/scene_manager_model_mixin.py | 919 |
| scene/scene_manager_serialization_mixin.py | 749 |
| scene/tree_roles.py | 3 |
| scene/util.py | 241 |
| scripts/a.py | 25 |
| scripts/BouncerScript.py | 99 |
| scripts/ColorChangerScript.py | 157 |
| scripts/ComboAnimatorScript.py | 36 |
| scripts/example_script.py | 44 |
| scripts/FollowerScript.py | 66 |
| scripts/MoverScript.py | 88 |
| scripts/R_P.py | 110 |
| scripts/R_R.py | 110 |
| scripts/Rotate_H_Script.py | 181 |
| scripts/Rotate_P_Script.py | 181 |
| scripts/Rotate_R_Script.py | 44 |
| scripts/RotatorScript.py | 36 |
| scripts/ScalerScript.py | 88 |
| scripts/test_quick_script.py | 25 |
| scripts/TestMover.py | 38 |
| scripts/TestRotator.py | 25 |
| scripts/TestScaler.py | 25 |
| ssbo_component/demo_component.py | 70 |
| ssbo_component/ssbo_controller.py | 1055 |
| ssbo_component/ssbo_editor.py | 782 |
| Start_Run.py | 42 |
| templates/main_template.py | 1282 |
| tools/open_source_rate.py | 73 |
| TransformGizmo/events.py | 14 |
| TransformGizmo/move_gizmo.py | 950 |
| TransformGizmo/rotate_gizmo.py | 1412 |
| TransformGizmo/scale_gizmo.py | 860 |
| TransformGizmo/transform_gizmo.py | 469 |
| ui/Builtin/__init__.py | 0 |
| ui/Builtin/Elements.py | 242 |
| ui/Builtin/LUIBlockText.py | 74 |
| ui/Builtin/LUIButton.py | 169 |
| ui/Builtin/LUICanvas.py | 97 |
| ui/Builtin/LUICheckbox.py | 65 |
| ui/Builtin/LUIFormattedLabel.py | 38 |
| ui/Builtin/LUIFrame.py | 54 |
| ui/Builtin/LUIHorizontalLayout.py | 13 |
| ui/Builtin/LUIInitialState.py | 32 |
| ui/Builtin/LUIInputField.py | 180 |
| ui/Builtin/LUIInputHandler.py | 5 |
| ui/Builtin/LUILabel.py | 63 |
| ui/Builtin/LUILayouts.py | 80 |
| ui/Builtin/LUIObject.py | 12 |
| ui/Builtin/LUIProgressbar.py | 54 |
| ui/Builtin/LUIRadiobox.py | 73 |
| ui/Builtin/LUIRadioboxGroup.py | 31 |
| ui/Builtin/LUIRegion.py | 5 |
| ui/Builtin/LUIRoot.py | 5 |
| ui/Builtin/LUIScrollableRegion.py | 119 |
| ui/Builtin/LUISelectbox.py | 158 |
| ui/Builtin/LUISkin.py | 34 |
| ui/Builtin/LUISlider.py | 185 |
| ui/Builtin/LUISprite.py | 12 |
| ui/Builtin/LUISpriteButton.py | 23 |
| ui/Builtin/LUITabbedFrame.py | 77 |
| ui/Builtin/LUIVerticalLayout.py | 13 |
| ui/Builtin/RectTransform.py | 73 |
| ui/icon_manager.py | 123 |
| ui/LUI/__init__.py | 1 |
| ui/LUI/lui_function_components.py | 1149 |
| ui/LUI/lui_function_properties.py | 1601 |
| ui/LUI/lui_manager_editor.py | 1593 |
| ui/LUI/lui_manager_interaction.py | 1351 |
| ui/LUI/lui_shared.py | 62 |
| ui/lui_function.py | 31 |
| ui/lui_manager.py | 199 |
| ui/panels/__init__.py | 0 |
| ui/panels/animation_tools.py | 1390 |
| ui/panels/app_actions.py | 1097 |
| ui/panels/create_actions.py | 147 |
| ui/panels/dialog_panels.py | 992 |
| ui/panels/editor_panels.py | 20 |
| ui/panels/editor_panels_center.py | 187 |
| ui/panels/editor_panels_left.py | 543 |
| ui/panels/editor_panels_right.py | 742 |
| ui/panels/editor_panels_right_collision.py | 210 |
| ui/panels/editor_panels_right_material.py | 222 |
| ui/panels/editor_panels_right_transform.py | 89 |
| ui/panels/editor_panels_top.py | 319 |
| ui/panels/interaction_panels.py | 128 |
| ui/panels/object_factory.py | 390 |
| ui/panels/panel_delegates.py | 512 |
| ui/panels/property_helpers.py | 1583 |
| ui/panels/runtime_actions.py | 356 |
| ui/panels/script_panels.py | 287 |
| ui/Skins/__init__.py | 0 |
| ui/Skins/Default/__init__.py | 0 |
| ui/Skins/Metro/__init__.py | 0 |
| ui/Skins/Metro/copy_frames.py | 31 |
| ui/Skins/Metro/LUIMetroSkin.py | 24 |
| ui/widgets.py | 38 |
## 6. 使用方式(优化流程建议)
1. 先在 功能模块 -> 关键文件映射 中定位模块。
2. 再到 完整 Python 文件索引 按文件名快速跳转。
3. 优先处理高行数核心文件(如 core/selection.py, core/vr_manager.py, ui/panels/property_helpers.py

View File

@ -0,0 +1,270 @@
# PROJECT_OPTIMIZATION_ANALYSIS
> 基于本地代码静态扫描生成(不含远程仓库信息,不含 `RenderPipelineFile/` 第三方目录)。
- 分析时间: 2026-02-28
- 扫描范围: `main.py`, `Start_Run.py`, `core/`, `scene/`, `project/`, `ui/`, `ssbo_component/`, `TransformGizmo/`, `scripts/`, `tools/`, `templates/`
## 0. 执行进展(非 VR
- 已完成 `EditorContext` 适配层:`core/editor_context.py`
- 已接入文件:
- `core/event_handler.py`
- `core/selection.py`
- `core/InfoPanelManager.py`
- `ui/panels/runtime_actions.py`
- `core/terrain_manager.py`
- `scene/scene_manager_convert_tiles_mixin.py`
- `scene/scene_manager_serialization_mixin.py`
- `scene/scene_manager_model_mixin.py`
- 效果(本地静态检索):
- 直接访问 `world.interface_manager`: `0`
- 直接访问 `interface_manager.treeWidget`: `0`
- `app.gui_manager` 仅剩注释引用(`ui/panels/editor_panels_left.py`
- Task B 第一轮已落地(`scene_manager_io_mixin.loadScene`
- 已抽出流程 helper
- `_preflight_load_scene`
- `_cleanup_after_failed_load`
- `_clear_current_scene_for_load`
- `_load_scene_root_from_file`
- `_bootstrap_scene_tree_for_loaded_root`
- `_load_scene_gui_metadata`
- `_retry_load_scene`
- `loadScene` 行数:`556 -> 366`
- 已清理重复异常分支:`_rebuildParentChildRelationships` 内重复 `except` 已移除
## 1. 总体画像
- Python 文件: `146`
- 代码总行数: `58,371`
- `except Exception` / `except:` 总计: `950`
- 裸 `except:` 总计: `63`
- 旧上下文关键词引用总量:
- `interface_manager`: `35`
- `treeWidget`: `10`
- `gui_manager`: `77`
结论:
- Qt 依赖已清理后,当前主要技术债集中在三类:
- 过大函数(可维护性差)
- 异常处理过宽(问题可观测性差)
- 旧 GUI 上下文命名耦合(边界不清晰)
## 2. 热点文件(按规模/风险)
### 2.1 超大文件 Top
1. `core/vr_manager.py` (`3553` 行)
2. `core/selection.py` (`2942` 行)
3. `core/InfoPanelManager.py` (`1726` 行)
4. `ui/LUI/lui_manager_editor.py` (`1724` 行)
5. `ui/panels/property_helpers.py` (`1711` 行)
6. `ui/LUI/lui_function_properties.py` (`1707` 行)
7. `TransformGizmo/rotate_gizmo.py` (`1587` 行)
8. `ui/panels/animation_tools.py` (`1579` 行)
### 2.2 长函数 Top优先拆分
1. `ui/LUI/lui_function_properties.py::_draw_component_properties` (`1441` 行)
2. `scene/scene_manager_io_mixin.py::loadScene` (`556` 行)
3. `ui/panels/animation_tools.py::_getActor` (`510` 行)
4. `main.py::__init__` (`375` 行)
5. `ui/LUI/lui_manager_interaction.py::_update_drag` (`348` 行)
6. `ui/panels/editor_panels_left.py::_draw_resource_manager` (`310` 行)
7. `scene/scene_manager_io_mixin.py::processNode` (`281` 行)
8. `core/selection.py::updateGizmoDrag` (`278` 行)
### 2.3 异常处理密度高(可观测性风险)
1. `ui/panels/animation_tools.py` (`except* = 85`)
2. `core/vr_manager.py` (`71`)
3. `core/selection.py` (`57`)
4. `ui/panels/property_helpers.py` (`54`)
5. `ui/panels/app_actions.py` (`46`)
6. `scene/scene_manager_model_mixin.py` (`36`)
7. `scene/scene_manager_serialization_mixin.py` (`27`)
8. `scene/scene_manager_io_mixin.py` (`20`)
9. `project/project_manager.py` (`20`)
10. `ui/panels/runtime_actions.py` (`20`)
`except:` 集中区:
- `core/selection.py` (`7`)
- `scene/scene_manager_model_mixin.py` (`7`)
- `ui/panels/editor_panels_right_material.py` (`6`)
- `ui/panels/editor_panels_left.py` (`5`)
### 2.4 旧上下文耦合集中区历史基线Task A 前)
1. `ui/panels/runtime_actions.py` (`gui_manager=29`)
2. `core/event_handler.py` (`interface_manager=11`, `gui_manager=11`)
3. `ui/panels/editor_panels_right.py` (`gui_manager=18`)
4. `scene/scene_manager_serialization_mixin.py` (`interface_manager=6`, `treeWidget=2`, `gui_manager=5`)
5. `core/selection.py` (`interface_manager=4`, `treeWidget=1`)
6. `core/InfoPanelManager.py` (`interface_manager=4`, `treeWidget=1`)
7. `core/terrain_manager.py` (`interface_manager=3`, `treeWidget=2`)
## 3. 优化优先级(建议执行顺序)
## P0: 上下文收敛(先做)
目标: 统一 GUI/场景树访问边界,减少跨模块 `hasattr(..., 'interface_manager')``treeWidget` 语义残留。
建议动作:
1. 引入 `EditorContext`(或 `UIContext`)统一提供:
- `get_tree_adapter()`
- `get_gui_service()`
- `get_selection_service()`
2. 在以下文件先改为调用上下文接口:
- `core/event_handler.py`
- `core/selection.py`
- `core/InfoPanelManager.py`
- `core/terrain_manager.py`
- `scene/scene_manager_serialization_mixin.py`
- `scene/scene_manager_convert_tiles_mixin.py`
- `ui/panels/runtime_actions.py`
预期收益:
- 降低命名残留与多处 `hasattr` 防御代码。
- 后续模块拆分时边界更稳定。
## P1: 大函数拆分(第二阶段)
目标: 将核心长函数拆分成“流程编排 + 子步骤函数”,减少单函数认知负担。
建议拆分顺序:
1. `scene/scene_manager_io_mixin.py::loadScene`
2. `main.py::__init__`
3. `ui/panels/animation_tools.py::_getActor`
4. `core/selection.py::updateGizmoDrag`
5. `ui/LUI/lui_function_properties.py::_draw_component_properties`(可按“变换/布局/视觉/交互/脚本”分区)
预期收益:
- 回归问题定位更快。
- 面板和场景加载逻辑更易测试。
## P2: 异常处理治理(并行推进)
目标: 将“吞异常”改为“有边界的降级 + 可追踪日志”。
建议规则:
1. 禁止新增裸 `except:`
2. 高风险路径必须记录上下文:
- 节点名/资源路径/操作类型/当前工具状态
3. 对可恢复错误使用 `warning`,不可恢复错误返回显式失败值。
优先文件:
- `ui/panels/animation_tools.py`
- `core/vr_manager.py`
- `core/selection.py`
- `ui/panels/property_helpers.py`
- `scene/scene_manager_io_mixin.py`
## 4. 下一步可直接执行的任务包
### Task A推荐先做1-2 天)
- 建立 `core/editor_context.py`(或同级命名)
- 给 `event_handler/selection/InfoPanelManager/runtime_actions` 接入上下文
- 保持外部 API 不变,仅替换内部访问路径
### Task B1 天)
- 重构 `scene_manager_io_mixin.loadScene`
- `preflight`
- `clear_old_scene`
- `load_bam`
- `rebuild_scene_tree`
- `post_load_sync`
### Task C1 天)
- 统一异常日志工具(轻量封装)
- 首批替换 `animation_tools.py``property_helpers.py`
## 4.1 本轮深入分析(非 VRP1 准备)
### A) `scene/scene_manager_io_mixin.py::loadScene`(核心优先)
- 函数规模: `556`
- 近似圈复杂度: `114`
- 关键问题:
- 单函数同时承担 8 类职责(校验/清理/加载/树同步/节点递归处理/脚本恢复/材质恢复/重试)。
- 内嵌 `processNode` 递归函数长达 `281` 行,可测试性差。
- 调试输出密度高(`print` 与注释 `print` 很多),影响可读性和噪声控制。
- `scene/scene_manager_io_mixin.py:1026``scene/scene_manager_io_mixin.py:1032` 存在重复 `except Exception` 分支(可合并)。
- `scene/scene_manager_io_mixin.py:946` 的 GUI 重建入口目前仍注释,行为边界不清晰。
- 建议拆分(保持外部 API `loadScene` 不变):
- `_preflight_scene_file(filename) -> (ok, normalized_path, reason)`
- `_cleanup_before_load(tree_widget, retry_count)`
- `_load_bam_scene(filename) -> scene_or_none`
- `_bootstrap_tree_items(scene, tree_widget)`
- `_walk_loaded_scene(scene, tree_widget) -> loaded_nodes`
- `_restore_loaded_nodes_state(node_path, processed_lights, loaded_nodes)`(从 `processNode` 中抽)
- `_post_load_finalize(scene, loaded_nodes, filename)`
- `_retry_load_scene(filename, retry_count, error) -> bool`
- 验收标准:
- `loadScene` 主体压缩到 `120` 行以内,只保留流程编排。
- 节点恢复行为(位置/材质/脚本/可见性)与当前一致。
- 失败重试逻辑保持语义一致。
- 当前状态2026-02-28:
- 第一轮已完成(预检/清理/加载/树初始化/GUI元数据/重试)。
- 剩余主要体积来自内嵌 `processNode`,下一轮应继续提取为独立方法。
### B) `main.py::__init__`(第二优先)
- 函数规模: `375`
- 近似圈复杂度: `15`
- 问题性质:
- 复杂度不高,但“启动装配职责”过于集中,初始化顺序风险高。
- 建议拆分:
- `_init_legacy_compat_fields()`
- `_init_core_services_non_vr()`(不含 VR
- `_init_imgui_runtime()`
- `_init_panel_modules()`
- `_init_runtime_state_flags()`
- `_bind_input_shortcuts()`
- `_init_drag_drop_and_messages()`
- 约束:
- VR 初始化保持原样,不纳入本轮改造范围。
### C) `ui/panels/animation_tools.py::_getActor`(第三优先)
- 函数规模: `510`
- 近似圈复杂度: `143`
- 关键问题:
- 路径推断、缓存策略、Actor 构建、autoBind 回退、GLTF 特化混在一个函数中。
- 局部嵌套函数层级深,行为分支很难覆盖测试。
- 建议拆分:
- `_resolve_actor_owner_and_paths(origin_model)`
- `_load_actor_from_candidate_paths(owner_model, paths)`
- `_load_actor_via_memory_fallback(owner_model, origin_model)`
- `_load_actor_via_gltf_special(path)`
- `_validate_actor_playable(actor_or_proxy)`
- `_cache_actor(owner_model, actor)`
- 验收标准:
- `_getActor` 保留为流程入口,长度控制到 `150` 行以内。
- 保持当前“优先路径加载,失败回退内存/autoBind”的策略不变。
## 4.2 Task B 执行顺序建议(非 VR
1. 先拆 `loadScene`(收益最大,且与 VR 无关)。
2. 再整理 `main.__init__`(降低后续模块接入冲突)。
3. 最后处理 `_getActor`(风险最高,建议独立提交并做手工回归)。
## 5. 与现有文档关系
- 模块总索引: `PROJECT_MODULE_INDEX.md`
- Qt 迁移状态: `QT_TO_IMGUI_MIGRATION_CHECKLIST.md`
- 历史分析: `IMGUI_MODULE_ANALYSIS.md`
---
如果按此路线继续,建议下一轮直接从 **Task A** 开始,我可以先落地上下文适配层并改 4 个高耦合文件。

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1 @@
index-aa08c2.boo

View File

@ -0,0 +1,62 @@
# Qt -> ImGui 迁移清单Qt 清理后状态)
更新时间2026-02-28
适用范围:`d:\IMGUI\EG` 本地工作区
## 目标
- 编辑器主路径稳定运行在 ImGui + Panda3D。
- 项目主代码不再依赖 PyQt/PySide 运行。
- 后续把历史命名与耦合(`interface_manager/treeWidget/gui_manager`)继续收敛。
## 当前状态总览
- `PyQt/PySide/Qt` 在项目代码主路径(排除 `RenderPipelineFile`)扫描结果:`0`。
- 依赖清单已从 Qt 运行依赖切到 ImGui 运行依赖。
- `ui/widgets.py` 已改为 legacy 占位模块,避免误用 Qt 路径。
## 已完成Done
- [x] `core/InfoPanelManager.py`:去除 Qt 直接导入。
- [x] `core/selection.py`:光标逻辑改为 Panda3D 路径。
- [x] `scene/scene_manager_convert_tiles_mixin.py`:去掉 `QProgressDialog`,改为非 Qt 进度反馈。
- [x] `ui/widgets.py`:替换为 ImGui 迁移提示占位模块。
- [x] `ui/icon_manager.py`:替换为无 Qt 的兼容层实现。
- [x] `core/world.py` / `core/vr_manager.py` / `core/vr/testing/test_mode.py`:去除 `qtWidget` 语义依赖。
- [x] `requirements/*`:清理 Qt/PySide 依赖项并同步部署文档。
- [x] `main.py`:补齐兼容字段初始化(`gui_elements/gui_manager/interface_manager/guiEditMode/currentGUITool`)。
- [x] `scene/scene_manager_io_mixin.py`:修复 `_find_scrip_in_directory` 拼写错误。
- [x] `main.py`:入口改为 `if __name__ == "__main__":`,消除导入即运行副作用。
- [x] `core/event_handler.py`:新增场景树访问 helper替换 `interface_manager.treeWidget` 直连。
- [x] `core/selection.py`:统一树控件清空逻辑(`_get_tree_widget/_clear_tree_selection`)。
- [x] `ui/panels/runtime_actions.py`:统一 `gui_manager` 访问与 `gui_elements` 追加入口。
- [x] `core/InfoPanelManager.py`:统一场景树控件访问 helper。
- [x] `ui/panels/editor_panels.py`:按布局拆分为 4 个 mixin`top/left/right/center`),保留门面类组合。
## 待完成Next
### N1 场景树接口收敛(高优先级)
- 目标:移除 `treeWidget` 语义,统一走 ImGui 场景树接口。
- 影响路径:`core/event_handler.py`、`scene/scene_manager_*_mixin.py`、`core/terrain_manager.py` 等。
### N2 GUI 管理上下文收敛(高优先级)
- 目标:将 `interface_manager/gui_manager/gui_elements` 统一到单一上下文对象。
- 影响路径:`project/project_manager.py`、`scene/scene_manager_io_mixin.py`、`ui/panels/*`。
### N3 入口规范化与回归脚本(中优先级)
- 目标:减少脚本式副作用,建立最小回归检查。
- 建议覆盖:启动、导入模型、选择变换、保存加载、脚本挂载。
## 验收标准
- 不安装 PyQt/PySide 时,`python Start_Run.py` 能进入编辑器主界面。
- 场景导入/保存/加载流程不触发 Qt 相关异常。
- 关键交互(选择、变换、场景树)行为与清理前一致。
## 关联文档
- 模块分析: [IMGUI_MODULE_ANALYSIS.md](d:/IMGUI/EG/IMGUI_MODULE_ANALYSIS.md)
- 全量目录与脚本清单: [PROJECT_FULL_CATALOG.md](d:/IMGUI/EG/PROJECT_FULL_CATALOG.md)

View File

@ -13,6 +13,7 @@ enabled:
- scattering
- skin_shading
- sky_ao
- selection_outline
- smaa
- ssr
# - clouds

View File

@ -63,5 +63,6 @@ global_stage_order:
# Finishing stages, do not insert anything below
- UpscaleStage
- SelectionOutlineStage
- FinalStage
- UpdatePreviousPipesStage

View File

@ -10,17 +10,21 @@ vertex_shader: |
texcoord = p3d_MultiTexCoord0;
}
fragment_shader: |
#version 330
uniform sampler2D p3d_Texture0;
uniform float material_opacity = 1.0;
in vec2 texcoord;
out vec4 o_color;
void main() {
vec4 c = texture(p3d_Texture0, texcoord);
o_color = vec4(c.rgb, c.a * material_opacity);
}
render_states:
TransparencyAttrib: M_alpha
DepthWriteAttrib: 0
fragment_shader: |
#version 330
uniform sampler2D p3d_Texture0;
uniform vec4 material_base_color = vec4(1.0, 1.0, 1.0, 1.0);
uniform float material_opacity = 1.0;
in vec2 texcoord;
out vec4 o_color;
void main() {
vec4 c = texture(p3d_Texture0, texcoord);
o_color = vec4(
material_base_color.rgb * c.rgb,
material_base_color.a * c.a * material_opacity
);
}
render_states:
TransparencyAttrib: M_alpha
DepthWriteAttrib: 0

View File

@ -45,6 +45,7 @@
layout(location = 0) in VertexOutput vOutput;
uniform Panda3DMaterial p3d_Material;
uniform vec4 p3d_ColorScale;
#pragma include "includes/normal_mapping.inc.glsl"
#pragma include "includes/forward_shading.inc.glsl"
@ -141,9 +142,9 @@ void main() {
m.shading_model = mInput.shading_model;
#if DONT_FETCH_DEFAULT_TEXTURES
m.basecolor = mInput.color;
m.basecolor = mInput.color * p3d_ColorScale.xyz;
#else
m.basecolor = mInput.color * sampled_diffuse.xyz;
m.basecolor = mInput.color * sampled_diffuse.xyz * p3d_ColorScale.xyz;
#endif
m.normal = material_nrm;
m.metallic = mInput.metallic;
@ -160,14 +161,21 @@ void main() {
vec3 view_dir = normalize(m_out.position - MainSceneData.camera_pos);
vec3 color = vec3(0);
float alpha = m_out.shading_model_param0;
float alpha = m_out.shading_model_param0 * p3d_ColorScale.w;
AmbientResult ambient = get_full_forward_ambient(m_out, view_dir);
color += ambient.diffuse;
color += ambient.specular;
color += get_sun_shading(m_out, view_dir);
color += get_forward_light_shading(m_out);
// XXX: Apply shading from lights too
// Transparent forward materials end up noticeably darker than the same
// object in the deferred path in this editor build, which makes them look
// "gone" when artists first switch the material type. Apply a small
// preview-space lift so the result stays visually comparable.
if (m_out.shading_model == SHADING_MODEL_TRANSPARENT) {
color *= 1.55;
}
alpha = mix(alpha, 1.0, ambient.fresnel);

View File

@ -0,0 +1,2 @@
"""Selection outline plugin package."""

View File

@ -0,0 +1,3 @@
settings:
daytime_settings:

View File

@ -0,0 +1,15 @@
from rpcore.pluginbase.base_plugin import BasePlugin
from .selection_outline_stage import SelectionOutlineStage
class Plugin(BasePlugin):
name = "Selection Outline"
author = "EG Team"
description = "Adds Unity-style selected-object outline as a post-process stage."
version = "1.0"
def on_stage_setup(self):
self.stage = self.create_stage(SelectionOutlineStage)

View File

@ -0,0 +1,68 @@
from panda3d.core import PNMImage, SamplerState, Texture, Vec4
from rpcore.render_stage import RenderStage
class SelectionOutlineStage(RenderStage):
required_pipes = ["ShadedScene"]
def __init__(self, pipeline):
RenderStage.__init__(self, pipeline)
self._enabled = False
self._outline_color = Vec4(1.0, 0.55, 0.0, 1.0)
self._outline_width = 2.0
self._fill_alpha = 0.0
self._mask_tex = self._make_default_mask()
@property
def produced_pipes(self):
return {"ShadedScene": self.target.color_tex}
def create(self):
self.target = self.create_target("SelectionOutline")
self.target.add_color_attachment(bits=16)
self.target.prepare_buffer()
self._apply_inputs()
def reload_shaders(self):
self.target.shader = self.load_plugin_shader("selection_outline.frag.glsl")
self._apply_inputs()
def set_mask_texture(self, mask_texture):
self._mask_tex = mask_texture if mask_texture else self._make_default_mask()
self._apply_inputs()
def set_enabled_outline(self, enabled):
self._enabled = bool(enabled)
self._apply_inputs()
def set_outline_style(self, color=None, width_px=None, fill_alpha=None):
if color is not None:
self._outline_color = Vec4(color)
if width_px is not None:
self._outline_width = max(0.0, float(width_px))
if fill_alpha is not None:
self._fill_alpha = max(0.0, min(1.0, float(fill_alpha)))
self._apply_inputs()
def _apply_inputs(self):
enabled_val = 1.0 if self._enabled else 0.0
self.target.set_shader_input("SelectionMaskTex", self._mask_tex)
self.target.set_shader_input("SelectionOutlineEnabled", enabled_val)
self.target.set_shader_input("SelectionOutlineColor", self._outline_color)
self.target.set_shader_input("SelectionOutlineWidth", self._outline_width)
self.target.set_shader_input("SelectionFillAlpha", self._fill_alpha)
def _make_default_mask(self):
image = PNMImage(1, 1, 4)
image.fill(0.0, 0.0, 0.0)
image.alpha_fill(0.0)
texture = Texture("selection_outline_default_mask")
texture.load(image)
texture.set_minfilter(SamplerState.FT_nearest)
texture.set_magfilter(SamplerState.FT_nearest)
texture.set_wrap_u(SamplerState.WM_clamp)
texture.set_wrap_v(SamplerState.WM_clamp)
return texture

View File

@ -0,0 +1,59 @@
/**
* Selection outline post process stage.
*/
#version 430
#pragma include "render_pipeline_base.inc.glsl"
uniform sampler2D ShadedScene;
uniform sampler2D SelectionMaskTex;
uniform float SelectionOutlineEnabled;
uniform vec4 SelectionOutlineColor;
uniform float SelectionOutlineWidth; // pixels
uniform float SelectionFillAlpha; // 0..1
out vec4 result;
float sample_mask(vec2 uv) {
return textureLod(SelectionMaskTex, uv, 0).r;
}
void main() {
vec2 uv = get_texcoord();
vec3 scene_col = textureLod(ShadedScene, uv, 0).rgb;
if (SelectionOutlineEnabled < 0.5) {
result = vec4(scene_col, 1.0);
return;
}
vec2 texel = vec2(max(1.0, SelectionOutlineWidth)) / SCREEN_SIZE;
float center = sample_mask(uv);
float max_nei = 0.0;
max_nei = max(max_nei, sample_mask(uv + vec2( texel.x, 0.0)));
max_nei = max(max_nei, sample_mask(uv + vec2(-texel.x, 0.0)));
max_nei = max(max_nei, sample_mask(uv + vec2(0.0, texel.y)));
max_nei = max(max_nei, sample_mask(uv + vec2(0.0, -texel.y)));
max_nei = max(max_nei, sample_mask(uv + vec2( texel.x, texel.y)));
max_nei = max(max_nei, sample_mask(uv + vec2( texel.x, -texel.y)));
max_nei = max(max_nei, sample_mask(uv + vec2(-texel.x, texel.y)));
max_nei = max(max_nei, sample_mask(uv + vec2(-texel.x, -texel.y)));
// Outer contour only.
float edge = clamp(max_nei - center, 0.0, 1.0);
float fill = center * SelectionFillAlpha;
vec3 col = scene_col;
if (fill > 0.0) {
col = mix(col, SelectionOutlineColor.rgb, fill * SelectionOutlineColor.a);
}
if (edge > 0.0) {
col = mix(col, SelectionOutlineColor.rgb, edge * SelectionOutlineColor.a);
}
result = vec4(col, 1.0);
}

View File

View File

View File

View File

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 MiB

View File

@ -1,9 +1,48 @@
import os
import shutil
import subprocess
import sys
def _maybe_relaunch_with_py311():
"""Use Python 3.11 to match ui/lui.pyd ABI on Windows."""
if sys.version_info >= (3, 11):
return
if os.environ.get("EG_RELAUNCHED_PY311") == "1":
return
if os.name != "nt":
return
py_launcher = shutil.which("py")
if not py_launcher:
print(f"⚠ 当前解释器是 Python {sys.version.split()[0]},未找到 py launcher无法自动切换到 3.11。")
return
try:
probe = subprocess.run(
[py_launcher, "-3.11", "-c", "import sys;print(sys.executable)"],
capture_output=True,
text=True,
check=False,
)
if probe.returncode != 0:
print("⚠ 未检测到可用的 Python 3.11LUI 可能不可用。")
return
except Exception as e:
print(f"⚠ 检测 Python 3.11 失败: {e}")
return
os.environ["EG_RELAUNCHED_PY311"] = "1"
relaunch_cmd = [py_launcher, "-3.11", os.path.abspath(__file__), *sys.argv[1:]]
print(f"✓ 检测到 Python {sys.version.split()[0]},自动切换到 Python 3.11: {probe.stdout.strip()}")
os.execv(py_launcher, relaunch_cmd)
# 添加项目根目录到 Python 路径
project_root = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, project_root)
third_party_path = os.path.join(project_root, "third_party")
if os.path.isdir(third_party_path):
sys.path.insert(0, third_party_path)
# 设置工作目录为项目根目录
os.chdir(project_root)
@ -22,6 +61,7 @@ sys.path.insert(0, icons_path)
# 现在可以导入并运行主程序
if __name__ == "__main__":
_maybe_relaunch_with_py311()
args = sys.argv[1:]
# args = "/home/tiger/桌面/Test1"
# args = "C:/Users/29381/Desktop/1"
@ -46,4 +86,4 @@ if __name__ == "__main__":
else:
# print(f"[DEBUG] 无项目路径,正常启动")
app = MyWorld()
app.run()
app.run()

View File

@ -496,7 +496,7 @@ class MoveGizmo(DirectObject):
def undo_last_move(self):
"""
Undo the last committed move.
Default hotkey (from Qt -> Panda3D translation): Ctrl+Z => 'control-z'.
Default hotkey: Ctrl+Z => 'control-z'.
"""
if not self._move_undo_stack:
self._log("undo_last_move: stack empty")
@ -530,7 +530,7 @@ class MoveGizmo(DirectObject):
# --- Mouse helpers -------------------------------------------------
def _get_normalized_mouse(self, extra) -> Optional[Point3]:
"""
Convert Qt pixel coordinates (from QPanda3DWidget) to Panda's
Convert external UI pixel coordinates to Panda's
normalized device coordinates (-1..1), or fall back to
mouseWatcherNode when available.
"""
@ -538,14 +538,14 @@ class MoveGizmo(DirectObject):
mouse = self.world.mouseWatcherNode.getMouse()
return Point3(mouse.x, mouse.y, 0)
# 1) Extra payload from QPanda3DWidget (pixels)
# 1) Extra payload from external UI (pixels)
if isinstance(extra, dict) and "x" in extra and "y" in extra:
parent = getattr(self.world, "parent", None)
if parent is not None:
w = max(parent.width(), 1)
h = max(parent.height(), 1)
nx = (extra["x"] / w) * 2.0 - 1.0
# Qt origin is topleft, Panda origin is center with +Y up
# Pixel origin is top-left; Panda origin is center with +Y up.
ny = 1.0 - (extra["y"] / h) * 2.0
return Point3(nx, ny, 0)

View File

@ -54,10 +54,10 @@ from .events import GizmoEvent
- 控件会根据摄像机距离自动缩放屏幕大小保持近似不变
集成方式示例
from QPanda3D.Panda3DWorld import Panda3DWorld
from QPanda3DExamples.rotate_gizmo import RotateGizmo
from direct.showbase.ShowBase import ShowBase
from TransformGizmo.rotate_gizmo import RotateGizmo
world = Panda3DWorld()
world = ShowBase()
model_np = world.render.attachNewNode("Box")
# ... 在 model_np 下加载模型
@ -72,7 +72,7 @@ from .events import GizmoEvent
鼠标事件要求
- 本类默认监听 Panda3D "mouse1" / "mouse1-up" / "mouse-move" 事件
- 如果从 Qt / 自定义 UI 传递鼠标像素坐标可以在发送事件时传入 extra 字典
- 如果从外部 UI 传递鼠标像素坐标可以在发送事件时传入 extra 字典
messenger.send("mouse1", [{"x": mouse_x, "y": mouse_y}])
本类会自动将像素坐标转换到 [-1, 1] 的标准化设备坐标
"""
@ -955,7 +955,7 @@ class RotateGizmo(DirectObject):
"""
将鼠标转换到 Panda3D 的标准化设备坐标 [-1, 1]
优先使用 Qt / 外部 UI 传入的像素坐标extra 字典
优先使用外部 UI 传入的像素坐标extra 字典
如果没有则回退到 Panda3D mouseWatcherNode
"""
if self.world.mouseWatcherNode.hasMouse():
@ -969,7 +969,7 @@ class RotateGizmo(DirectObject):
w = max(parent.width(), 1)
h = max(parent.height(), 1)
nx = (extra["x"] / w) * 2.0 - 1.0
# Qt 原点在左上Panda3D 原点在中心Y 轴向上
# 像素坐标原点在左上Panda3D 原点在中心Y 轴向上
ny = 1.0 - (extra["y"] / h) * 2.0
return Point3(nx, ny, 0.0)

View File

@ -48,10 +48,10 @@ from .events import GizmoEvent
- 控件会根据摄像机距离自动缩放屏幕大小保持近似不变
集成方式示例
from QPanda3D.Panda3DWorld import Panda3DWorld
from QPanda3DExamples.scale_gizmo import ScaleGizmo
from direct.showbase.ShowBase import ShowBase
from TransformGizmo.scale_gizmo import ScaleGizmo
world = Panda3DWorld()
world = ShowBase()
model_np = world.render.attachNewNode("Box")
# ... 在 model_np 下加载模型
@ -66,7 +66,7 @@ from .events import GizmoEvent
鼠标事件要求
- 本类默认监听 Panda3D "mouse1" / "mouse1-up" / "mouse-move" 事件
- 如果从 Qt / 自定义 UI 传递鼠标像素坐标可以在发送事件时传入 extra 字典
- 如果从外部 UI 传递鼠标像素坐标可以在发送事件时传入 extra 字典
messenger.send("mouse1", [{"x": mouse_x, "y": mouse_y}])
本类会自动将像素坐标转换到 [-1, 1] 的标准化设备坐标
"""

View File

@ -462,10 +462,189 @@ class TransformGizmo(DirectObject):
Callback used by MoveGizmo / RotateGizmo / ScaleGizmo to report that a
transform action (move/rotate/scale) has been committed.
"""
if self._record_action_with_command_manager(action):
return
self._history.append(action)
# New user action invalidates redo chain.
self._redo_history.clear()
def _coerce_vec3(self, value, fallback) -> p3d.Vec3:
if value is None:
return p3d.Vec3(fallback)
try:
return p3d.Vec3(value)
except Exception:
pass
if isinstance(value, (tuple, list)) and len(value) >= 3:
return p3d.Vec3(value[0], value[1], value[2])
return p3d.Vec3(fallback)
def _make_transform_mat(self, pos, hpr, scale) -> p3d.LMatrix4f:
try:
state = p3d.TransformState.make_pos_hpr_scale(pos, hpr, scale)
return p3d.LMatrix4f(state.get_mat())
except Exception:
pass
try:
state = p3d.TransformState.makePosHprScale(pos, hpr, scale)
return p3d.LMatrix4f(state.getMat())
except Exception:
temp = NodePath("tg_temp_transform")
temp.setPos(pos)
temp.setHpr(hpr)
temp.setScale(scale)
return p3d.LMatrix4f(temp.getMat())
def _invert_matrix(self, mat) -> Optional[p3d.LMatrix4f]:
inv = p3d.LMatrix4f(mat)
try:
inv.invertInPlace()
return inv
except Exception:
pass
try:
inv.invert_in_place()
return inv
except Exception:
return None
def _build_ssbo_group_snapshot_command(self, action: Dict[str, Any]):
node: NodePath = action.get("node")
if node is None or node.isEmpty() or (not node.hasTag("is_ssbo_proxy")):
return None
ssbo_editor = getattr(self.world, "ssbo_editor", None)
controller = getattr(ssbo_editor, "controller", None) if ssbo_editor else None
if not controller:
return None
selection_key = node.getTag("ssbo_selection_key") if node.hasTag("ssbo_selection_key") else None
selected_ids = list(controller.name_to_ids.get(selection_key, [])) if selection_key else []
if not selected_ids:
selected_ids = list(getattr(ssbo_editor, "selected_ids", []) or [])
targets = []
for gid in selected_ids:
obj_np = controller.id_to_object_np.get(gid)
if obj_np and not obj_np.is_empty():
targets.append(obj_np)
if not targets:
return None
current_pos = node.getPos(self.world.render)
current_hpr = node.getHpr(self.world.render)
current_scale = node.getScale(self.world.render)
old_pos = self._coerce_vec3(action.get("old_pos"), current_pos)
new_pos = self._coerce_vec3(action.get("new_pos"), current_pos)
old_hpr = self._coerce_vec3(action.get("old_hpr"), current_hpr)
new_hpr = self._coerce_vec3(action.get("new_hpr"), current_hpr)
old_scale = self._coerce_vec3(action.get("old_scale"), current_scale)
new_scale = self._coerce_vec3(action.get("new_scale"), current_scale)
old_proxy_mat = self._make_transform_mat(old_pos, old_hpr, old_scale)
new_proxy_mat = self._make_transform_mat(new_pos, new_hpr, new_scale)
new_proxy_inv = self._invert_matrix(new_proxy_mat)
if new_proxy_inv is None:
return None
before_state = []
after_state = []
for target in targets:
try:
current_world_mat = p3d.LMatrix4f(target.get_mat(self.world.render))
except Exception:
try:
current_world_mat = p3d.LMatrix4f(target.getMat(self.world.render))
except Exception:
continue
old_world_mat = p3d.LMatrix4f(current_world_mat * new_proxy_inv * old_proxy_mat)
before_state.append({"node": target, "mat": old_world_mat})
after_state.append({"node": target, "mat": current_world_mat})
if not before_state:
return None
def apply_state(state):
synced_nodes = []
for item in state:
target = item.get("node")
mat = item.get("mat")
if target is None or target.isEmpty() or mat is None:
continue
try:
target.set_mat(self.world.render, mat)
except Exception:
try:
target.setMat(self.world.render, mat)
except Exception:
continue
synced_nodes.append(target)
if ssbo_editor and hasattr(ssbo_editor, "sync_scene_nodes_to_pick"):
try:
ssbo_editor.sync_scene_nodes_to_pick(synced_nodes)
except Exception:
pass
from core.Command_System import SnapshotStateCommand
return SnapshotStateCommand(apply_state, before_state, after_state)
def _record_action_with_command_manager(self, action: Dict[str, Any]) -> bool:
"""Prefer routing transform actions into the global command manager."""
command_manager = getattr(self.world, "command_manager", None)
if not command_manager:
return False
try:
group_command = self._build_ssbo_group_snapshot_command(action)
if group_command is not None:
command_manager.execute_command(group_command)
return True
from core.Command_System import MoveNodeCommand, RotateNodeCommand, ScaleNodeCommand
kind = action.get("kind")
node: NodePath = action.get("node")
if node is None or node.isEmpty():
return False
command = None
if kind == "move":
command = MoveNodeCommand(
node,
action.get("old_pos"),
action.get("new_pos"),
reference_node=self.world.render,
world=self.world,
)
elif kind == "rotate":
command = RotateNodeCommand(
node,
action.get("old_hpr"),
action.get("new_hpr"),
reference_node=self.world.render,
world=self.world,
)
elif kind == "scale":
command = ScaleNodeCommand(
node,
action.get("old_scale"),
action.get("new_scale"),
world=self.world,
)
if command is None:
return False
command_manager.execute_command(command)
return True
except Exception:
return False
def _sync_light_position_if_needed(self, node: Optional[NodePath]) -> None:
"""When target node wraps an RP light, keep RP light position in sync."""
try:

View File

@ -0,0 +1 @@
[]

Binary file not shown.

View File

@ -0,0 +1,102 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
跳跃脚本 - 让对象产生上下跳跃效果
"""
from core.script_system import ScriptBase
import math
class BouncerScript(ScriptBase):
"""跳跃脚本类"""
def __init__(self):
super().__init__()
# 跳跃参数
self.jump_height = 2.0 # 跳跃高度
self.jump_speed = 3.0 # 跳跃速度 (跳跃/秒)
self.bounce_type = "sine" # 跳跃类型: "sine", "abs_sine", "square"
# 内部变量
self.time_accumulator = 0.0 # 时间累积器
self.original_y = None # 原始Y位置
self.is_bouncing = True # 是否正在跳跃
self.bounce_direction = 1 # 跳跃方向
def start(self):
"""脚本开始时调用"""
self.log("跳跃脚本启动!")
self.log(f"跳跃参数: 高度={self.jump_height}, 速度={self.jump_speed}, 类型={self.bounce_type}")
# 记录原始Y位置
self.original_y = self.gameObject.getZ() # Z轴是高度
self.log(f"原始高度: {self.original_y}")
def update(self, dt):
"""每帧更新"""
if not self.is_bouncing:
return
# 累积时间
self.time_accumulator += dt * self.bounce_direction
# 根据类型计算跳跃高度
if self.bounce_type == "sine":
# 标准正弦波跳跃
height_offset = math.sin(self.time_accumulator * self.jump_speed * 2 * math.pi) * self.jump_height
elif self.bounce_type == "abs_sine":
# 绝对值正弦波(始终向上)
height_offset = abs(math.sin(self.time_accumulator * self.jump_speed * 2 * math.pi)) * self.jump_height
elif self.bounce_type == "square":
# 方波跳跃(突然跳起落下)
sine_val = math.sin(self.time_accumulator * self.jump_speed * 2 * math.pi)
height_offset = self.jump_height if sine_val > 0 else 0
else:
height_offset = 0
# 应用跳跃
current_pos = self.gameObject.getPos()
new_z = self.original_y + height_offset
self.gameObject.setPos(current_pos.getX(), current_pos.getY(), new_z)
def set_bounce_parameters(self, height=None, speed=None, bounce_type=None):
"""设置跳跃参数"""
if height is not None:
self.jump_height = height
if speed is not None:
self.jump_speed = speed
if bounce_type is not None and bounce_type in ["sine", "abs_sine", "square"]:
self.bounce_type = bounce_type
self.log(f"跳跃参数更新: 高度={self.jump_height}, 速度={self.jump_speed}, 类型={self.bounce_type}")
def toggle_bouncing(self):
"""切换跳跃状态"""
self.is_bouncing = not self.is_bouncing
status = "恢复" if self.is_bouncing else "暂停"
self.log(f"跳跃{status}")
def reverse_direction(self):
"""反转跳跃方向"""
self.bounce_direction *= -1
direction = "正向" if self.bounce_direction > 0 else "反向"
self.log(f"跳跃方向改为{direction}")
def reset_position(self):
"""重置到原始高度"""
if self.original_y is not None:
current_pos = self.gameObject.getPos()
self.gameObject.setPos(current_pos.getX(), current_pos.getY(), self.original_y)
self.time_accumulator = 0.0
self.log("位置已重置到原始高度")
def jump_once(self):
"""执行一次跳跃"""
self.time_accumulator = 0.0
self.log("执行单次跳跃")
def on_destroy(self):
"""脚本销毁时调用"""
self.log("跳跃脚本停止")

View File

@ -0,0 +1,160 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
颜色变化脚本 - 让对象颜色产生循环变化
"""
from core.script_system import ScriptBase
from panda3d.core import Vec4
import math
class ColorChangerScript(ScriptBase):
"""颜色变化脚本类"""
def __init__(self):
super().__init__()
# 颜色参数
self.color_speed = 1.0 # 颜色变化速度 (周期/秒)
self.color_mode = "rainbow" # 颜色模式: "rainbow", "pulse", "fade", "strobe"
self.base_color = Vec4(1, 1, 1, 1) # 基础颜色
self.intensity = 1.0 # 颜色强度
# 内部变量
self.time_accumulator = 0.0 # 时间累积器
self.original_color = None # 原始颜色
self.is_changing = True # 是否正在变化
self.strobe_state = False # 闪烁状态
def start(self):
"""脚本开始时调用"""
self.log("颜色变化脚本启动!")
self.log(f"颜色参数: 速度={self.color_speed}, 模式={self.color_mode}, 强度={self.intensity}")
# 记录原始颜色
self.original_color = self.gameObject.getColor()
self.log(f"原始颜色: {self.original_color}")
def update(self, dt):
"""每帧更新"""
if not self.is_changing:
return
# 累积时间
self.time_accumulator += dt
# 根据模式计算新颜色
if self.color_mode == "rainbow":
new_color = self._calculate_rainbow_color()
elif self.color_mode == "pulse":
new_color = self._calculate_pulse_color()
elif self.color_mode == "fade":
new_color = self._calculate_fade_color()
elif self.color_mode == "strobe":
new_color = self._calculate_strobe_color()
else:
new_color = self.base_color
# 应用颜色
self.gameObject.setColor(new_color)
def _calculate_rainbow_color(self):
"""计算彩虹颜色"""
# 使用HSV到RGB的转换创建彩虹效果
hue = (self.time_accumulator * self.color_speed) % 1.0
# 简单的HSV到RGB转换
i = int(hue * 6.0)
f = (hue * 6.0) - i
p = 0.0
q = 1.0 - f
t = f
if i % 6 == 0:
r, g, b = 1.0, t, p
elif i % 6 == 1:
r, g, b = q, 1.0, p
elif i % 6 == 2:
r, g, b = p, 1.0, t
elif i % 6 == 3:
r, g, b = p, q, 1.0
elif i % 6 == 4:
r, g, b = t, p, 1.0
else:
r, g, b = 1.0, p, q
return Vec4(r * self.intensity, g * self.intensity, b * self.intensity, 1.0)
def _calculate_pulse_color(self):
"""计算脉冲颜色"""
pulse = (math.sin(self.time_accumulator * self.color_speed * 2 * math.pi) + 1.0) / 2.0
multiplier = pulse * self.intensity
return Vec4(
self.base_color.getX() * multiplier,
self.base_color.getY() * multiplier,
self.base_color.getZ() * multiplier,
self.base_color.getW()
)
def _calculate_fade_color(self):
"""计算淡入淡出颜色"""
fade = (math.sin(self.time_accumulator * self.color_speed * 2 * math.pi) + 1.0) / 2.0
alpha = fade * self.intensity
return Vec4(
self.base_color.getX(),
self.base_color.getY(),
self.base_color.getZ(),
alpha
)
def _calculate_strobe_color(self):
"""计算闪烁颜色"""
# 根据时间间隔切换状态
interval = 1.0 / (self.color_speed * 2) # 闪烁间隔
if int(self.time_accumulator / interval) % 2 == 0:
return Vec4(
self.base_color.getX() * self.intensity,
self.base_color.getY() * self.intensity,
self.base_color.getZ() * self.intensity,
self.base_color.getW()
)
else:
return Vec4(0.1, 0.1, 0.1, self.base_color.getW()) # 暗色状态
def set_color_parameters(self, speed=None, mode=None, base_color=None, intensity=None):
"""设置颜色参数"""
if speed is not None:
self.color_speed = speed
if mode is not None and mode in ["rainbow", "pulse", "fade", "strobe"]:
self.color_mode = mode
if base_color is not None:
self.base_color = base_color
if intensity is not None:
self.intensity = intensity
self.log(f"颜色参数更新: 速度={self.color_speed}, 模式={self.color_mode}, 强度={self.intensity}")
def toggle_color_change(self):
"""切换颜色变化状态"""
self.is_changing = not self.is_changing
status = "恢复" if self.is_changing else "暂停"
self.log(f"颜色变化{status}")
def reset_color(self):
"""重置到原始颜色"""
if self.original_color:
self.gameObject.setColor(self.original_color)
self.time_accumulator = 0.0
self.log("颜色已重置到原始值")
def set_solid_color(self, r=1.0, g=1.0, b=1.0, a=1.0):
"""设置固定颜色"""
color = Vec4(r, g, b, a)
self.gameObject.setColor(color)
self.base_color = color
self.log(f"设置固定颜色: {color}")
def on_destroy(self):
"""脚本销毁时调用"""
self.log("颜色变化脚本停止")

View File

@ -0,0 +1,39 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
复合动画脚本 - 结合旋转和跳跃效果
"""
from core.script_system import ScriptBase
import math
class ComboAnimatorScript(ScriptBase):
def __init__(self):
super().__init__()
self.time = 0.0
self.original_pos = None
self.is_active = True
def start(self):
self.log("复合动画脚本启动!")
self.original_pos = self.gameObject.getPos()
def update(self, dt):
if not self.is_active:
return
self.time += dt
# 旋转效果
current_hpr = self.gameObject.getHpr()
new_h = current_hpr.getX() + 45.0 * dt
self.gameObject.setHpr(new_h, current_hpr.getY(), current_hpr.getZ())
# 跳跃效果
if self.original_pos:
bounce_offset = abs(math.sin(self.time * 3.0)) * 1.0
self.gameObject.setZ(self.original_pos.getZ() + bounce_offset)
def on_destroy(self):
self.log("复合动画脚本停止")

View File

@ -0,0 +1,69 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
跟随脚本 - 让对象跟随指定的目标对象
"""
from core.script_system import ScriptBase
from panda3d.core import Vec3
class FollowerScript(ScriptBase):
"""跟随脚本类"""
def __init__(self):
super().__init__()
self.target = None # 跟随目标
self.follow_speed = 5.0 # 跟随速度
self.follow_distance = 2.0 # 跟随距离
self.is_following = True # 是否正在跟随
def start(self):
"""脚本开始时调用"""
self.log("跟随脚本启动!")
self.log(f"跟随参数: 速度={self.follow_speed}, 距离={self.follow_distance}")
def update(self, dt):
"""每帧更新"""
if not self.is_following or self.target is None:
return
target_pos = self.target.getPos()
current_pos = self.gameObject.getPos()
# 计算目标方向
direction = target_pos - current_pos
distance = direction.length()
# 如果距离大于跟随距离,则移动
if distance > self.follow_distance:
if distance > 0:
direction.normalize()
# 计算目标位置(保持跟随距离)
target_follow_pos = target_pos - direction * self.follow_distance
# 平滑移动到目标位置
move_direction = target_follow_pos - current_pos
move_distance = move_direction.length()
if move_distance > 0:
move_direction.normalize()
move_amount = min(self.follow_speed * dt, move_distance)
new_pos = current_pos + move_direction * move_amount
self.gameObject.setPos(new_pos)
# 朝向目标
self.gameObject.lookAt(target_pos)
def set_target(self, target):
"""设置跟随目标"""
self.target = target
if target:
self.log(f"设置跟随目标: {target.getName()}")
else:
self.log("清除跟随目标")
def on_destroy(self):
"""脚本销毁时调用"""
self.log("跟随脚本停止")

View File

@ -0,0 +1,91 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
移动脚本 - 让对象在指定方向上来回移动
"""
from core.script_system import ScriptBase
import math
class MoverScript(ScriptBase):
"""移动脚本类"""
def __init__(self):
super().__init__()
# 移动参数
self.move_distance = 5.0 # 移动距离
self.move_speed = 2.0 # 移动速度 (单位/秒)
self.move_axis = "x" # 移动轴: "x", "y", "z"
# 内部变量
self.start_position = None # 起始位置
self.current_direction = 1 # 当前移动方向: 1或-1
self.current_distance = 0.0 # 当前移动距离
self.is_moving = True # 是否正在移动
def start(self):
"""脚本开始时调用"""
self.log("移动脚本启动!")
self.log(f"移动参数: 距离={self.move_distance}, 速度={self.move_speed}, 轴={self.move_axis}")
# 记录起始位置
self.start_position = self.gameObject.getPos()
self.log(f"起始位置: {self.start_position}")
def update(self, dt):
"""每帧更新"""
if not self.is_moving or self.start_position is None:
return
# 计算移动增量
move_delta = self.move_speed * dt * self.current_direction
self.current_distance += abs(move_delta)
# 检查是否需要改变方向
if self.current_distance >= self.move_distance:
self.current_direction *= -1
self.current_distance = 0.0
# 应用移动
current_pos = self.gameObject.getPos()
new_pos = [current_pos.getX(), current_pos.getY(), current_pos.getZ()]
if self.move_axis == "x":
new_pos[0] += move_delta
elif self.move_axis == "y":
new_pos[1] += move_delta
elif self.move_axis == "z":
new_pos[2] += move_delta
self.gameObject.setPos(new_pos[0], new_pos[1], new_pos[2])
def set_move_parameters(self, distance=None, speed=None, axis=None):
"""设置移动参数"""
if distance is not None:
self.move_distance = distance
if speed is not None:
self.move_speed = speed
if axis is not None and axis in ["x", "y", "z"]:
self.move_axis = axis
self.log(f"移动参数更新: 距离={self.move_distance}, 速度={self.move_speed}, 轴={self.move_axis}")
def toggle_movement(self):
"""切换移动状态"""
self.is_moving = not self.is_moving
status = "恢复" if self.is_moving else "暂停"
self.log(f"移动{status}")
def reset_position(self):
"""重置到起始位置"""
if self.start_position:
self.gameObject.setPos(self.start_position)
self.current_distance = 0.0
self.current_direction = 1
self.log("位置已重置到起始点")
def on_destroy(self):
"""脚本销毁时调用"""
self.log("移动脚本停止")

View File

@ -0,0 +1,133 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
旋转脚本 - 让对象持续旋转
"""
from core.script_system import ScriptBase
class RotatorScript(ScriptBase):
"""旋转脚本类"""
def __init__(self):
super().__init__()
self.rotation_speed_y = 30.0 # Y轴旋转速度 (度/秒)
self.max_angle = 30.0 # 最大旋转角度(相对于初始角度)
self.direction = 1
self.current_offset = 0.0 # 当前相对于初始角度的偏移
self.initial_angle = None # 模型的初始角度
self.is_rotating = True # 是否正在旋转
def start(self):
"""脚本开始时调用"""
self.log("旋转脚本启动!")
self.log(f"旋转速度: {self.rotation_speed_y}度/秒")
self.log(f"最大旋转角度: ±{self.max_angle}")
# 记录模型的初始角度
if self.gameObject:
initial_hpr = self.gameObject.getHpr()
self.initial_angle = initial_hpr.getZ() # 记录Z轴的初始角度
self.log(f"模型初始角度: {self.initial_angle}")
self.log(f"旋转范围: {self.initial_angle - self.max_angle}° 到 {self.initial_angle + self.max_angle}°")
else:
self.log("⚠️ 无法获取游戏对象使用默认初始角度0")
self.initial_angle = 0.0
def update(self, dt):
"""每帧更新"""
if not self.is_rotating or self.initial_angle is None:
return
# 计算角度变化量
delta_angle = self.rotation_speed_y * dt * self.direction
self.current_offset += delta_angle
# 如果超出角度范围,则反向并限制在边界
if self.current_offset > self.max_angle:
self.current_offset = self.max_angle
self.direction *= -1
elif self.current_offset < -self.max_angle:
self.current_offset = -self.max_angle
self.direction *= -1
# 计算最终角度(初始角度 + 偏移量)
final_angle = self.initial_angle + self.current_offset
# 设置新的旋转只改变Z轴保持其他不变
current_hpr = self.gameObject.getHpr()
self.gameObject.setHpr(current_hpr.getX(), final_angle, current_hpr.getZ())
# if not self.is_rotating:
# return
#
# # 获取当前旋转并应用增量
# current_hpr = self.gameObject.getHpr()
# new_r = current_hpr.getZ() + self.rotation_speed_y * dt
# self.gameObject.setHpr(current_hpr.getX(), current_hpr.getY(), new_r)
def on_destroy(self):
"""脚本销毁时调用"""
self.log("旋转脚本停止")
# ==================== 控制方法 ====================
def set_max_angle(self, new_max_angle):
"""
设置新的最大旋转角度
Args:
new_max_angle: 新的最大角度值
"""
self.max_angle = new_max_angle
self.log(f"最大旋转角度已设置为: ±{self.max_angle}")
if self.initial_angle is not None:
self.log(f"新的旋转范围: {self.initial_angle - self.max_angle}° 到 {self.initial_angle + self.max_angle}°")
def set_rotation_speed(self, new_speed):
"""
设置新的旋转速度
Args:
new_speed: 新的旋转速度/
"""
self.rotation_speed_y = new_speed
self.log(f"旋转速度已设置为: {self.rotation_speed_y}度/秒")
def pause_rotation(self):
"""暂停旋转"""
self.is_rotating = False
self.log("旋转已暂停")
def resume_rotation(self):
"""恢复旋转"""
self.is_rotating = True
self.log("旋转已恢复")
def reset_to_initial_angle(self):
"""重置到初始角度"""
if self.initial_angle is not None and self.gameObject:
current_hpr = self.gameObject.getHpr()
self.gameObject.setHpr(current_hpr.getX(), current_hpr.getY(), self.initial_angle)
self.current_offset = 0.0
self.direction = 1
self.log(f"已重置到初始角度: {self.initial_angle}")
def get_current_info(self):
"""获取当前旋转信息"""
if self.gameObject and self.initial_angle is not None:
current_hpr = self.gameObject.getHpr()
current_angle = current_hpr.getZ()
self.log("=== 当前旋转信息 ===")
self.log(f"初始角度: {self.initial_angle}")
self.log(f"当前角度: {current_angle}")
self.log(f"偏移量: {self.current_offset}")
self.log(f"最大角度: ±{self.max_angle}")
self.log(f"旋转速度: {self.rotation_speed_y}度/秒")
self.log(f"旋转方向: {'正向' if self.direction > 0 else '反向'}")
self.log(f"旋转状态: {'运行中' if self.is_rotating else '已暂停'}")
self.log("=== 信息结束 ===")
else:
self.log("无法获取旋转信息")

View File

@ -0,0 +1,133 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
旋转脚本 - 让对象持续旋转
"""
from core.script_system import ScriptBase
class RotatorScript(ScriptBase):
"""旋转脚本类"""
def __init__(self):
super().__init__()
self.rotation_speed_y = 30.0 # Y轴旋转速度 (度/秒)
self.max_angle = 30.0 # 最大旋转角度(相对于初始角度)
self.direction = 1
self.current_offset = 0.0 # 当前相对于初始角度的偏移
self.initial_angle = None # 模型的初始角度
self.is_rotating = True # 是否正在旋转
def start(self):
"""脚本开始时调用"""
self.log("旋转脚本启动!")
self.log(f"旋转速度: {self.rotation_speed_y}度/秒")
self.log(f"最大旋转角度: ±{self.max_angle}")
# 记录模型的初始角度
if self.gameObject:
initial_hpr = self.gameObject.getHpr()
self.initial_angle = initial_hpr.getZ() # 记录Z轴的初始角度
self.log(f"模型初始角度: {self.initial_angle}")
self.log(f"旋转范围: {self.initial_angle - self.max_angle}° 到 {self.initial_angle + self.max_angle}°")
else:
self.log("⚠️ 无法获取游戏对象使用默认初始角度0")
self.initial_angle = 0.0
def update(self, dt):
"""每帧更新"""
if not self.is_rotating or self.initial_angle is None:
return
# 计算角度变化量
delta_angle = self.rotation_speed_y * dt * self.direction
self.current_offset += delta_angle
# 如果超出角度范围,则反向并限制在边界
if self.current_offset > self.max_angle:
self.current_offset = self.max_angle
self.direction *= -1
elif self.current_offset < -self.max_angle:
self.current_offset = -self.max_angle
self.direction *= -1
# 计算最终角度(初始角度 + 偏移量)
final_angle = self.initial_angle + self.current_offset
# 设置新的旋转只改变Z轴保持其他不变
current_hpr = self.gameObject.getHpr()
self.gameObject.setHpr(current_hpr.getX(), current_hpr.getY(), final_angle)
# if not self.is_rotating:
# return
#
# # 获取当前旋转并应用增量
# current_hpr = self.gameObject.getHpr()
# new_r = current_hpr.getZ() + self.rotation_speed_y * dt
# self.gameObject.setHpr(current_hpr.getX(), current_hpr.getY(), new_r)
def on_destroy(self):
"""脚本销毁时调用"""
self.log("旋转脚本停止")
# ==================== 控制方法 ====================
def set_max_angle(self, new_max_angle):
"""
设置新的最大旋转角度
Args:
new_max_angle: 新的最大角度值
"""
self.max_angle = new_max_angle
self.log(f"最大旋转角度已设置为: ±{self.max_angle}")
if self.initial_angle is not None:
self.log(f"新的旋转范围: {self.initial_angle - self.max_angle}° 到 {self.initial_angle + self.max_angle}°")
def set_rotation_speed(self, new_speed):
"""
设置新的旋转速度
Args:
new_speed: 新的旋转速度/
"""
self.rotation_speed_y = new_speed
self.log(f"旋转速度已设置为: {self.rotation_speed_y}度/秒")
def pause_rotation(self):
"""暂停旋转"""
self.is_rotating = False
self.log("旋转已暂停")
def resume_rotation(self):
"""恢复旋转"""
self.is_rotating = True
self.log("旋转已恢复")
def reset_to_initial_angle(self):
"""重置到初始角度"""
if self.initial_angle is not None and self.gameObject:
current_hpr = self.gameObject.getHpr()
self.gameObject.setHpr(current_hpr.getX(), current_hpr.getY(), self.initial_angle)
self.current_offset = 0.0
self.direction = 1
self.log(f"已重置到初始角度: {self.initial_angle}")
def get_current_info(self):
"""获取当前旋转信息"""
if self.gameObject and self.initial_angle is not None:
current_hpr = self.gameObject.getHpr()
current_angle = current_hpr.getZ()
self.log("=== 当前旋转信息 ===")
self.log(f"初始角度: {self.initial_angle}")
self.log(f"当前角度: {current_angle}")
self.log(f"偏移量: {self.current_offset}")
self.log(f"最大角度: ±{self.max_angle}")
self.log(f"旋转速度: {self.rotation_speed_y}度/秒")
self.log(f"旋转方向: {'正向' if self.direction > 0 else '反向'}")
self.log(f"旋转状态: {'运行中' if self.is_rotating else '已暂停'}")
self.log("=== 信息结束 ===")
else:
self.log("无法获取旋转信息")

View File

@ -0,0 +1,215 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
旋转脚本 - 让对象持续旋转
"""
from core.script_system import ScriptBase
class RotatorScript(ScriptBase):
"""旋转脚本类"""
def __init__(self):
super().__init__()
self.rotation_speed_y = 30.0 # Y轴旋转速度 (度/秒)
self.max_angle = 30.0 # 最大旋转角度(相对于初始角度)
self.direction = 1
self.current_offset = 0.0 # 当前相对于初始角度的偏移
self.initial_angle = None # 模型的初始角度
self.is_rotating = True # 是否正在旋转
# 机器人式停顿参数
self.pause_duration = 0.5 # 停顿时间(秒)
self.current_pause_time = 0.0 # 当前停顿计时
self.is_paused = False # 是否正在停顿
self.robot_mode = True # 是否启用机器人模式
def start(self):
"""脚本开始时调用"""
self.log("机器人旋转脚本启动!")
self.log(f"旋转速度: {self.rotation_speed_y}度/秒")
self.log(f"最大旋转角度: ±{self.max_angle}")
self.log(f"机器人模式: {'开启' if self.robot_mode else '关闭'}")
if self.robot_mode:
self.log(f"停顿时间: {self.pause_duration}")
# 记录模型的初始角度
if self.gameObject:
initial_hpr = self.gameObject.getHpr()
self.initial_angle = initial_hpr.getY() # 记录Y轴的初始角度Pitch
self.log(f"模型初始角度: {self.initial_angle}")
self.log(f"旋转范围: {self.initial_angle - self.max_angle}° 到 {self.initial_angle + self.max_angle}°")
else:
self.log("⚠️ 无法获取游戏对象使用默认初始角度0")
self.initial_angle = 0.0
def update(self, dt):
"""每帧更新 - 机器人式旋转"""
if not self.is_rotating or self.initial_angle is None:
return
# 如果正在停顿中
if self.is_paused:
self.current_pause_time += dt
if self.current_pause_time >= self.pause_duration:
# 停顿结束,继续旋转
self.is_paused = False
self.current_pause_time = 0.0
self.log(f"停顿结束,继续旋转,方向: {'正向' if self.direction > 0 else '反向'}")
return
# 计算角度变化量
delta_angle = self.rotation_speed_y * dt * self.direction
self.current_offset += delta_angle
# 检查是否到达边界
reached_boundary = False
if self.current_offset > self.max_angle:
self.current_offset = self.max_angle
self.direction *= -1
reached_boundary = True
elif self.current_offset < -self.max_angle:
self.current_offset = -self.max_angle
self.direction *= -1
reached_boundary = True
# 如果到达边界且启用机器人模式,开始停顿
if reached_boundary and self.robot_mode:
self.is_paused = True
self.current_pause_time = 0.0
self.log(f"到达边界 ({self.current_offset}°),开始停顿 {self.pause_duration}")
# 计算最终角度(初始角度 + 偏移量)
final_angle = self.initial_angle + self.current_offset
# 设置新的旋转只改变Y轴保持其他不变
current_hpr = self.gameObject.getHpr()
self.gameObject.setHpr(final_angle, current_hpr.getY(), current_hpr.getZ())
# if not self.is_rotating:
# return
#
# # 获取当前旋转并应用增量
# current_hpr = self.gameObject.getHpr()
# new_r = current_hpr.getZ() + self.rotation_speed_y * dt
# self.gameObject.setHpr(current_hpr.getX(), current_hpr.getY(), new_r)
def on_destroy(self):
"""脚本销毁时调用"""
self.log("机器人旋转脚本停止")
# ==================== 机器人模式控制方法 ====================
def set_robot_mode(self, enabled=True):
"""
启用或禁用机器人模式
Args:
enabled: 是否启用机器人模式
"""
self.robot_mode = enabled
self.log(f"机器人模式: {'开启' if enabled else '关闭'}")
if not enabled:
self.is_paused = False # 如果禁用机器人模式,立即结束停顿
def set_pause_duration(self, duration):
"""
设置停顿时间
Args:
duration: 停顿时间
"""
self.pause_duration = max(0.1, duration) # 最小0.1秒
self.log(f"停顿时间已设置为: {self.pause_duration}")
def set_max_angle(self, new_max_angle):
"""
设置新的最大旋转角度
Args:
new_max_angle: 新的最大角度值
"""
self.max_angle = new_max_angle
self.log(f"最大旋转角度已设置为: ±{self.max_angle}")
if self.initial_angle is not None:
self.log(f"新的旋转范围: {self.initial_angle - self.max_angle}° 到 {self.initial_angle + self.max_angle}°")
def set_rotation_speed(self, new_speed):
"""
设置新的旋转速度
Args:
new_speed: 新的旋转速度/
"""
self.rotation_speed_y = new_speed
self.log(f"旋转速度已设置为: {self.rotation_speed_y}度/秒")
def pause_rotation(self):
"""暂停旋转"""
self.is_rotating = False
self.log("旋转已暂停")
def resume_rotation(self):
"""恢复旋转"""
self.is_rotating = True
self.is_paused = False # 同时结束停顿状态
self.log("旋转已恢复")
def reset_to_initial_angle(self):
"""重置到初始角度"""
if self.initial_angle is not None and self.gameObject:
current_hpr = self.gameObject.getHpr()
self.gameObject.setHpr(current_hpr.getX(), self.initial_angle, current_hpr.getZ())
self.current_offset = 0.0
self.direction = 1
self.is_paused = False
self.current_pause_time = 0.0
self.log(f"已重置到初始角度: {self.initial_angle}")
def get_current_info(self):
"""获取当前旋转信息"""
if self.gameObject and self.initial_angle is not None:
current_hpr = self.gameObject.getHpr()
current_angle = current_hpr.getY()
self.log("=== 机器人旋转信息 ===")
self.log(f"初始角度: {self.initial_angle}")
self.log(f"当前角度: {current_angle}")
self.log(f"偏移量: {self.current_offset}")
self.log(f"最大角度: ±{self.max_angle}")
self.log(f"旋转速度: {self.rotation_speed_y}度/秒")
self.log(f"旋转方向: {'正向' if self.direction > 0 else '反向'}")
self.log(f"旋转状态: {'运行中' if self.is_rotating else '已暂停'}")
self.log(f"机器人模式: {'开启' if self.robot_mode else '关闭'}")
if self.robot_mode:
self.log(f"停顿时间: {self.pause_duration}")
self.log(f"当前状态: {'停顿中' if self.is_paused else '旋转中'}")
if self.is_paused:
remaining_time = self.pause_duration - self.current_pause_time
self.log(f"剩余停顿时间: {remaining_time:.1f}")
self.log("=== 信息结束 ===")
else:
self.log("无法获取旋转信息")
# ==================== 预设配置方法 ====================
def set_slow_robot_mode(self):
"""预设:慢速机器人模式"""
self.set_rotation_speed(15.0)
self.set_pause_duration(1.0)
self.set_robot_mode(True)
self.log("已设置为慢速机器人模式")
def set_fast_robot_mode(self):
"""预设:快速机器人模式"""
self.set_rotation_speed(45.0)
self.set_pause_duration(0.3)
self.set_robot_mode(True)
self.log("已设置为快速机器人模式")
def set_smooth_mode(self):
"""预设:平滑模式(非机器人)"""
self.set_robot_mode(False)
self.set_rotation_speed(30.0)
self.log("已设置为平滑旋转模式")

View File

@ -0,0 +1,215 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
旋转脚本 - 让对象持续旋转
"""
from core.script_system import ScriptBase
class RotatorScript(ScriptBase):
"""旋转脚本类"""
def __init__(self):
super().__init__()
self.rotation_speed_y = 30.0 # Y轴旋转速度 (度/秒)
self.max_angle = 25.0 # 最大旋转角度(相对于初始角度)
self.direction = 1
self.current_offset = 0.0 # 当前相对于初始角度的偏移
self.initial_angle = None # 模型的初始角度
self.is_rotating = True # 是否正在旋转
# 机器人式停顿参数
self.pause_duration = 0.5 # 停顿时间(秒)
self.current_pause_time = 0.0 # 当前停顿计时
self.is_paused = False # 是否正在停顿
self.robot_mode = True # 是否启用机器人模式
def start(self):
"""脚本开始时调用"""
self.log("机器人旋转脚本启动!")
self.log(f"旋转速度: {self.rotation_speed_y}度/秒")
self.log(f"最大旋转角度: ±{self.max_angle}")
self.log(f"机器人模式: {'开启' if self.robot_mode else '关闭'}")
if self.robot_mode:
self.log(f"停顿时间: {self.pause_duration}")
# 记录模型的初始角度
if self.gameObject:
initial_hpr = self.gameObject.getHpr()
self.initial_angle = initial_hpr.getY() # 记录Y轴的初始角度Pitch
self.log(f"模型初始角度: {self.initial_angle}")
self.log(f"旋转范围: {self.initial_angle - self.max_angle}° 到 {self.initial_angle + self.max_angle}°")
else:
self.log("⚠️ 无法获取游戏对象使用默认初始角度0")
self.initial_angle = 0.0
def update(self, dt):
"""每帧更新 - 机器人式旋转"""
if not self.is_rotating or self.initial_angle is None:
return
# 如果正在停顿中
if self.is_paused:
self.current_pause_time += dt
if self.current_pause_time >= self.pause_duration:
# 停顿结束,继续旋转
self.is_paused = False
self.current_pause_time = 0.0
self.log(f"停顿结束,继续旋转,方向: {'正向' if self.direction > 0 else '反向'}")
return
# 计算角度变化量
delta_angle = self.rotation_speed_y * dt * self.direction
self.current_offset += delta_angle
# 检查是否到达边界
reached_boundary = False
if self.current_offset > self.max_angle:
self.current_offset = self.max_angle
self.direction *= -1
reached_boundary = True
elif self.current_offset < -self.max_angle:
self.current_offset = -self.max_angle
self.direction *= -1
reached_boundary = True
# 如果到达边界且启用机器人模式,开始停顿
if reached_boundary and self.robot_mode:
self.is_paused = True
self.current_pause_time = 0.0
self.log(f"到达边界 ({self.current_offset}°),开始停顿 {self.pause_duration}")
# 计算最终角度(初始角度 + 偏移量)
final_angle = self.initial_angle + self.current_offset
# 设置新的旋转只改变Y轴保持其他不变
current_hpr = self.gameObject.getHpr()
self.gameObject.setHpr(current_hpr.getX(), final_angle, current_hpr.getZ())
# if not self.is_rotating:
# return
#
# # 获取当前旋转并应用增量
# current_hpr = self.gameObject.getHpr()
# new_r = current_hpr.getZ() + self.rotation_speed_y * dt
# self.gameObject.setHpr(current_hpr.getX(), current_hpr.getY(), new_r)
def on_destroy(self):
"""脚本销毁时调用"""
self.log("机器人旋转脚本停止")
# ==================== 机器人模式控制方法 ====================
def set_robot_mode(self, enabled=True):
"""
启用或禁用机器人模式
Args:
enabled: 是否启用机器人模式
"""
self.robot_mode = enabled
self.log(f"机器人模式: {'开启' if enabled else '关闭'}")
if not enabled:
self.is_paused = False # 如果禁用机器人模式,立即结束停顿
def set_pause_duration(self, duration):
"""
设置停顿时间
Args:
duration: 停顿时间
"""
self.pause_duration = max(0.1, duration) # 最小0.1秒
self.log(f"停顿时间已设置为: {self.pause_duration}")
def set_max_angle(self, new_max_angle):
"""
设置新的最大旋转角度
Args:
new_max_angle: 新的最大角度值
"""
self.max_angle = new_max_angle
self.log(f"最大旋转角度已设置为: ±{self.max_angle}")
if self.initial_angle is not None:
self.log(f"新的旋转范围: {self.initial_angle - self.max_angle}° 到 {self.initial_angle + self.max_angle}°")
def set_rotation_speed(self, new_speed):
"""
设置新的旋转速度
Args:
new_speed: 新的旋转速度/
"""
self.rotation_speed_y = new_speed
self.log(f"旋转速度已设置为: {self.rotation_speed_y}度/秒")
def pause_rotation(self):
"""暂停旋转"""
self.is_rotating = False
self.log("旋转已暂停")
def resume_rotation(self):
"""恢复旋转"""
self.is_rotating = True
self.is_paused = False # 同时结束停顿状态
self.log("旋转已恢复")
def reset_to_initial_angle(self):
"""重置到初始角度"""
if self.initial_angle is not None and self.gameObject:
current_hpr = self.gameObject.getHpr()
self.gameObject.setHpr(current_hpr.getX(), self.initial_angle, current_hpr.getZ())
self.current_offset = 0.0
self.direction = 1
self.is_paused = False
self.current_pause_time = 0.0
self.log(f"已重置到初始角度: {self.initial_angle}")
def get_current_info(self):
"""获取当前旋转信息"""
if self.gameObject and self.initial_angle is not None:
current_hpr = self.gameObject.getHpr()
current_angle = current_hpr.getY()
self.log("=== 机器人旋转信息 ===")
self.log(f"初始角度: {self.initial_angle}")
self.log(f"当前角度: {current_angle}")
self.log(f"偏移量: {self.current_offset}")
self.log(f"最大角度: ±{self.max_angle}")
self.log(f"旋转速度: {self.rotation_speed_y}度/秒")
self.log(f"旋转方向: {'正向' if self.direction > 0 else '反向'}")
self.log(f"旋转状态: {'运行中' if self.is_rotating else '已暂停'}")
self.log(f"机器人模式: {'开启' if self.robot_mode else '关闭'}")
if self.robot_mode:
self.log(f"停顿时间: {self.pause_duration}")
self.log(f"当前状态: {'停顿中' if self.is_paused else '旋转中'}")
if self.is_paused:
remaining_time = self.pause_duration - self.current_pause_time
self.log(f"剩余停顿时间: {remaining_time:.1f}")
self.log("=== 信息结束 ===")
else:
self.log("无法获取旋转信息")
# ==================== 预设配置方法 ====================
def set_slow_robot_mode(self):
"""预设:慢速机器人模式"""
self.set_rotation_speed(15.0)
self.set_pause_duration(1.0)
self.set_robot_mode(True)
self.log("已设置为慢速机器人模式")
def set_fast_robot_mode(self):
"""预设:快速机器人模式"""
self.set_rotation_speed(45.0)
self.set_pause_duration(0.3)
self.set_robot_mode(True)
self.log("已设置为快速机器人模式")
def set_smooth_mode(self):
"""预设:平滑模式(非机器人)"""
self.set_robot_mode(False)
self.set_rotation_speed(30.0)
self.log("已设置为平滑旋转模式")

View File

@ -0,0 +1,56 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
旋转脚本 - 让对象持续旋转
"""
from core.script_system import ScriptBase
class RotatorScript(ScriptBase):
"""旋转脚本类"""
def __init__(self):
super().__init__()
self.rotation_speed_y = 30.0 # Y轴旋转速度 (度/秒)
self.max_angle = 15.0
self.direction = 1
self.current_angle = 0.0
self.is_rotating = True # 是否正在旋转
def start(self):
"""脚本开始时调用"""
self.log("旋转脚本启动!")
self.log(f"旋转速度: {self.rotation_speed_y}度/秒")
def update(self, dt):
"""每帧更新"""
delta_angle = self.rotation_speed_y * dt * self.direction
self.current_angle += delta_angle
# 如果超出角度范围,则反向
if self.current_angle > self.max_angle:
self.current_angle = self.max_angle
self.direction *= -1
elif self.current_angle < -self.max_angle:
self.current_angle = -self.max_angle
self.direction *= -1
# 设置新的旋转只改变Z轴保持其他不变
base_hpr = self.gameObject.getHpr()
new_r = self.current_angle
self.gameObject.setHpr(base_hpr.getX(), base_hpr.getY(), new_r)
# if not self.is_rotating:
# return
#
# # 获取当前旋转并应用增量
# current_hpr = self.gameObject.getHpr()
# new_r = current_hpr.getZ() + self.rotation_speed_y * dt
# self.gameObject.setHpr(current_hpr.getX(), current_hpr.getY(), new_r)
def on_destroy(self):
"""脚本销毁时调用"""
self.log("旋转脚本停止")

View File

@ -0,0 +1,39 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
旋转脚本 - 让对象持续旋转
"""
from core.script_system import ScriptBase
class RotatorScript(ScriptBase):
"""旋转脚本类"""
def __init__(self):
super().__init__()
self.rotation_speed_y = 30.0 # Y轴旋转速度 (度/秒)
self.is_rotating = True # 是否正在旋转
def start(self):
"""脚本开始时调用"""
self.log("旋转脚本启动!")
self.log(f"旋转速度: {self.rotation_speed_y}度/秒")
def update(self, dt):
# 检查 gameObject 是否存在且不为空
if not self.gameObject or self.gameObject.isEmpty():
print("RotatorScript: gameObject is empty or None, skipping update")
return
"""每帧更新"""
if not self.is_rotating:
return
# 获取当前旋转并应用增量
current_hpr = self.gameObject.getHpr()
new_h = current_hpr.getX() + self.rotation_speed_y * dt
self.gameObject.setHpr(new_h, current_hpr.getY(), current_hpr.getZ())
def on_destroy(self):
"""脚本销毁时调用"""
self.log("旋转脚本停止")

View File

@ -0,0 +1,91 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
缩放脚本 - 让对象产生呼吸般的缩放效果
"""
from core.script_system import ScriptBase
import math
class ScalerScript(ScriptBase):
"""缩放脚本类"""
def __init__(self):
super().__init__()
# 缩放参数
self.base_scale = 1.0 # 基础缩放
self.scale_amplitude = 0.3 # 缩放幅度
self.scale_speed = 2.0 # 缩放速度 (周期/秒)
self.uniform_scale = True # 是否统一缩放(所有轴)
# 内部变量
self.time_accumulator = 0.0 # 时间累积器
self.original_scale = None # 原始缩放
self.is_scaling = True # 是否正在缩放
def start(self):
"""脚本开始时调用"""
self.log("缩放脚本启动!")
self.log(f"缩放参数: 基础={self.base_scale}, 幅度={self.scale_amplitude}, 速度={self.scale_speed}")
# 记录原始缩放
self.original_scale = self.gameObject.getScale()
self.log(f"原始缩放: {self.original_scale}")
def update(self, dt):
"""每帧更新"""
if not self.is_scaling:
return
# 累积时间
self.time_accumulator += dt
# 计算正弦波缩放值
sine_value = math.sin(self.time_accumulator * self.scale_speed * 2 * math.pi)
scale_factor = self.base_scale + (self.scale_amplitude * sine_value)
# 应用缩放
if self.uniform_scale:
# 统一缩放
self.gameObject.setScale(scale_factor)
else:
# 非统一缩放仅Z轴
current_scale = self.gameObject.getScale()
self.gameObject.setScale(current_scale.getX(), current_scale.getY(), scale_factor)
def set_scale_parameters(self, base=None, amplitude=None, speed=None, uniform=None):
"""设置缩放参数"""
if base is not None:
self.base_scale = base
if amplitude is not None:
self.scale_amplitude = amplitude
if speed is not None:
self.scale_speed = speed
if uniform is not None:
self.uniform_scale = uniform
self.log(f"缩放参数更新: 基础={self.base_scale}, 幅度={self.scale_amplitude}, 速度={self.scale_speed}")
def toggle_scaling(self):
"""切换缩放状态"""
self.is_scaling = not self.is_scaling
status = "恢复" if self.is_scaling else "暂停"
self.log(f"缩放{status}")
def reset_scale(self):
"""重置到原始缩放"""
if self.original_scale:
self.gameObject.setScale(self.original_scale)
self.time_accumulator = 0.0
self.log("缩放已重置到原始值")
def pulse_once(self):
"""执行一次脉冲缩放"""
self.time_accumulator = 0.0
self.log("执行脉冲缩放")
def on_destroy(self):
"""脚本销毁时调用"""
self.log("缩放脚本停止")

View File

@ -0,0 +1,41 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
TestMover - 移动脚本
"""
from core.script_system import ScriptBase
class Testmover(ScriptBase):
"""移动脚本类"""
def __init__(self):
super().__init__()
self.speed = 5.0 # 移动速度
self.direction = [1, 0, 0] # 移动方向
def start(self):
"""脚本开始时调用"""
self.log("移动脚本开始运行!")
def update(self, dt):
"""每帧更新"""
if self.transform:
# 计算移动偏移
offset_x = self.direction[0] * self.speed * dt
offset_y = self.direction[1] * self.speed * dt
offset_z = self.direction[2] * self.speed * dt
# 更新位置
current_pos = self.transform.getPos()
new_pos = (
current_pos.x + offset_x,
current_pos.y + offset_y,
current_pos.z + offset_z
)
self.transform.setPos(*new_pos)
def on_destroy(self):
"""脚本销毁时调用"""
self.log("移动脚本被销毁")

View File

@ -0,0 +1,28 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
TestRotator - 自定义脚本
"""
from core.script_system import ScriptBase
class Testrotator(ScriptBase):
"""自定义脚本类"""
def __init__(self):
super().__init__()
# 在这里初始化您的变量
def start(self):
"""脚本开始时调用"""
self.log("脚本开始运行!")
def update(self, dt):
"""每帧更新"""
# 在这里编写更新逻辑
pass
def on_destroy(self):
"""脚本销毁时调用"""
self.log("脚本被销毁")

View File

@ -0,0 +1,28 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
TestScaler - 自定义脚本
"""
from core.script_system import ScriptBase
class Testscaler(ScriptBase):
"""自定义脚本类"""
def __init__(self):
super().__init__()
# 在这里初始化您的变量
def start(self):
"""脚本开始时调用"""
self.log("脚本开始运行!")
def update(self, dt):
"""每帧更新"""
# 在这里编写更新逻辑
pass
def on_destroy(self):
"""脚本销毁时调用"""
self.log("脚本被销毁")

View File

@ -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("脚本被销毁")

View File

@ -0,0 +1,47 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
示例脚本 - 演示如何编写脚本
"""
from core.script_system import ScriptBase
class ExampleScript(ScriptBase):
"""示例脚本类"""
def __init__(self):
super().__init__()
self.counter = 0
self.rotation_speed = 30.0 # 度/秒
def start(self):
"""脚本开始时调用"""
self.log("示例脚本开始运行!")
self.log(f"挂载到对象: {self.gameObject.getName()}")
def update(self, dt):
"""每帧更新"""
self.counter += 1
# 每60帧输出一次信息
if self.counter % 60 == 0:
self.log(f"运行了 {self.counter}")
# 让对象旋转
if self.transform:
current_h = self.transform.getH()
new_h = current_h + self.rotation_speed * dt
self.transform.setH(new_h)
def on_destroy(self):
"""脚本销毁时调用"""
self.log("示例脚本被销毁")
def on_enable(self):
"""脚本启用时调用"""
self.log("示例脚本被启用")
def on_disable(self):
"""脚本禁用时调用"""
self.log("示例脚本被禁用")

View File

@ -0,0 +1,28 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
test_quick_script - 自定义脚本
"""
from core.script_system import ScriptBase
class TestQuickScript(ScriptBase):
"""自定义脚本类"""
def __init__(self):
super().__init__()
# 在这里初始化您的变量
def start(self):
"""脚本开始时调用"""
self.log("脚本开始运行!")
def update(self, dt):
"""每帧更新"""
# 在这里编写更新逻辑
pass
def on_destroy(self):
"""脚本销毁时调用"""
self.log("脚本被销毁")

View File

@ -0,0 +1,534 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""EG packaged project runtime template."""
from __future__ import annotations
import importlib
import json
import os
import sys
import traceback
from direct.actor.Actor import Actor
from direct.showbase.ShowBase import ShowBase
from panda3d.core import (
CardMaker,
Filename,
MovieTexture,
Point3,
TextNode,
Texture,
TransparencyAttrib,
Vec3,
load_prc_file_data,
)
PROJECT_NAME = "111"
def _bootstrap_paths():
if getattr(sys, "frozen", False):
project_root = os.path.dirname(sys.executable)
else:
project_root = os.path.dirname(os.path.abspath(__file__))
os.chdir(project_root)
search_paths = [
project_root,
os.path.join(project_root, "third_party"),
os.path.join(project_root, "RenderPipelineFile"),
]
for path in search_paths:
if os.path.isdir(path) and path not in sys.path:
sys.path.insert(0, path)
return project_root
PROJECT_ROOT = _bootstrap_paths()
class MainApp(ShowBase):
def __init__(self):
self.project_path = PROJECT_ROOT
self.gui_elements = []
self.chinese_font = None
self.script_manager = None
self.render_pipeline = None
load_prc_file_data(
"",
f"""
win-size 1380 750
window-title {PROJECT_NAME}
sync-video false
show-frame-rate-meter false
support-threads false
""",
)
from rpcore import RenderPipeline
self.render_pipeline = RenderPipeline()
self.render_pipeline.pre_showbase_init()
ShowBase.__init__(self)
self.render_pipeline.create(self)
self.render_pipeline._showbase.camera = self.render_pipeline._showbase.cam
self.disableMouse()
self._load_font()
self._init_script_manager()
self.load_full_scene()
self.load_gui_from_json()
def _init_script_manager(self):
script_module = importlib.import_module("core.script_system")
self.script_manager = script_module.ScriptManager(self)
self.script_manager.hot_reload_enabled = False
scripts_dir = self.get_resource_path("scripts")
if hasattr(self.script_manager, "set_scripts_directory"):
self.script_manager.set_scripts_directory(
scripts_dir,
create=False,
reload_scripts=False,
)
self.script_manager.start_system()
self.script_manager.set_hot_reload_enabled(False)
def _load_font(self):
font_candidates = [
"C:/Windows/Fonts/msyh.ttc",
"C:/Windows/Fonts/simhei.ttf",
]
for font_path in font_candidates:
if os.path.exists(font_path):
try:
self.chinese_font = self.loader.loadFont(font_path)
if self.chinese_font:
print(f"✓ 中文字体加载成功: {font_path}")
return
except Exception:
continue
print("⚠ 未找到可用中文字体,继续使用默认字体")
def get_chinese_font(self):
return self.chinese_font
def get_resource_path(self, relative_path):
return os.path.normpath(os.path.join(PROJECT_ROOT, relative_path))
def _resolve_media_path(self, relative_path):
if not relative_path:
return ""
if os.path.isabs(relative_path):
return relative_path
return self.get_resource_path(relative_path.replace("/", os.sep))
def load_full_scene(self):
scene_file = self.get_resource_path("scene.bam")
if not os.path.exists(scene_file):
print(f"⚠ 未找到场景文件: {scene_file}")
return
scene = self.loader.loadModel(Filename.fromOsSpecific(scene_file))
if not scene:
print("⚠ 场景文件加载失败")
return
scene.reparentTo(self.render)
self.render_pipeline.prepare_scene(scene)
self.process_scene_elements(scene)
print("✓ 场景加载完成")
def process_scene_elements(self, root_node):
processed_lights = set()
def walk(node_path):
self._apply_user_visibility(node_path)
if node_path.hasTag("scripts_info"):
try:
scripts_info = json.loads(node_path.getTag("scripts_info"))
self.process_scripts(node_path, scripts_info)
except Exception as e:
print(f"处理节点脚本失败 {node_path.getName()}: {e}")
if node_path.hasTag("light_type") and node_path not in processed_lights:
if node_path.hasTag("is_auxiliary_light") and node_path.getTag("is_auxiliary_light").lower() == "true":
return
light_type = node_path.getTag("light_type")
if light_type == "spot_light":
self._recreate_spot_light(node_path)
elif light_type == "point_light":
self._recreate_point_light(node_path)
processed_lights.add(node_path)
for child in node_path.getChildren():
walk(child)
walk(root_node)
def _apply_user_visibility(self, node_path):
if not node_path.hasTag("user_visible"):
return
user_visible = node_path.getTag("user_visible").lower() == "true"
node_path.setPythonTag("user_visible", user_visible)
if user_visible:
node_path.show()
else:
node_path.hide()
def _recreate_spot_light(self, light_node):
try:
from rpcore import SpotLight
light = SpotLight()
light.direction = Vec3(0, 0, -1)
light.fov = float(light_node.getTag("light_fov")) if light_node.hasTag("light_fov") else 70.0
light.energy = float(light_node.getTag("light_energy")) if light_node.hasTag("light_energy") else 5000.0
light.radius = float(light_node.getTag("light_radius")) if light_node.hasTag("light_radius") else 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()}: {e}")
def _recreate_point_light(self, light_node):
try:
from rpcore import PointLight
light = PointLight()
light.energy = float(light_node.getTag("light_energy")) if light_node.hasTag("light_energy") else 5000.0
light.radius = float(light_node.getTag("light_radius")) if light_node.hasTag("light_radius") else 1000.0
light.inner_radius = 0.4
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()}: {e}")
def process_scripts(self, node_path, script_info_list):
if not self.script_manager:
return
for script_info in script_info_list or []:
script_name = str(script_info.get("name", "") or "").strip()
if not script_name:
continue
try:
if script_name not in self.script_manager.loader.script_classes:
script_path = ""
if hasattr(self.script_manager, "resolve_script_path"):
script_path = self.script_manager.resolve_script_path(script_info)
if script_path:
self.script_manager.load_script_from_file(script_path)
script_component = self.script_manager.add_script_to_object(node_path, script_name)
if script_component:
print(f"✓ 脚本 {script_name} 已挂载到 {node_path.getName()}")
else:
print(f"⚠ 脚本 {script_name} 挂载失败")
except Exception as e:
print(f"挂载脚本失败 {script_name}: {e}")
def load_gui_from_json(self):
gui_json_path = self.get_resource_path(os.path.join("gui", "gui_elements.json"))
if not os.path.exists(gui_json_path):
return
with open(gui_json_path, "r", encoding="utf-8") as f:
content = f.read().strip()
if not content:
return
gui_data = json.loads(content)
self.create_gui_elements(gui_data)
def create_gui_elements(self, element_data):
processed_names = set()
element_original_data = {}
for index, gui_info in enumerate(element_data or []):
name = gui_info.get("name", f"gui_element_{index}")
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"),
}
for index, gui_info in enumerate(element_data or []):
try:
gui_type = gui_info.get("type", "unknown")
name = gui_info.get("name", f"gui_element_{index}")
if name in processed_names:
continue
processed_names.add(name)
position = list(gui_info.get("position", [0, 0, 0]))
scale = list(gui_info.get("scale", [1, 1, 1]))
parent_name = gui_info.get("parent_name")
if parent_name and parent_name in element_original_data:
parent_scale = element_original_data[parent_name]["scale"]
for i in range(min(len(position), len(parent_scale))):
position[i] *= parent_scale[i]
for i in range(min(len(scale), len(parent_scale))):
scale[i] *= parent_scale[i]
text = gui_info.get("text", "")
image_path = self._resolve_media_path(gui_info.get("image_path", ""))
video_path = self._resolve_media_path(gui_info.get("video_path", ""))
new_element = None
if gui_type == "3d_text":
new_element = self.create_gui_3d_text(tuple(position), text, scale[0] if scale else 1.0)
elif gui_type == "3d_image":
new_element = self.create_gui_3d_image(tuple(position), image_path, scale)
elif gui_type == "button":
new_element = self.create_gui_button(tuple(position), text, scale[0] if scale else 1.0)
elif gui_type == "label":
new_element = self.create_gui_label(tuple(position), text, scale[0] if scale else 1.0)
elif gui_type == "entry":
new_element = self.create_gui_entry(tuple(position), text, scale[0] if scale else 1.0)
elif gui_type == "2d_image":
new_element = self.create_gui_2d_image(tuple(position), image_path, scale)
elif gui_type == "video_screen":
new_element = self.create_gui_video_screen(tuple(position), scale, video_path)
elif gui_type == "2d_video_screen":
new_element = self.create_gui_2d_video_screen(tuple(position), scale, video_path)
if not new_element:
continue
self._apply_gui_metadata(new_element, gui_info, gui_type, text, image_path, video_path)
self.gui_elements.append(new_element)
if gui_info.get("scripts"):
self.process_scripts(new_element, gui_info["scripts"])
except Exception as e:
print(f"重建 GUI 元素失败: {e}")
traceback.print_exc()
def _apply_gui_metadata(self, node, gui_info, gui_type, text, image_path, video_path):
try:
name = gui_info.get("name")
if name and hasattr(node, "setName"):
node.setName(name)
except Exception:
pass
if hasattr(node, "setTag"):
node.setTag("gui_type", gui_type)
node.setTag("is_gui_element", "true")
if text:
node.setTag("gui_text", str(text))
if image_path:
node.setTag("gui_image_path", str(image_path))
if video_path:
node.setTag("video_path", str(video_path))
for key, value in (gui_info.get("tags") or {}).items():
try:
node.setTag(str(key), str(value))
except Exception:
continue
user_visible = bool(gui_info.get("user_visible", True))
if hasattr(node, "setPythonTag"):
node.setPythonTag("user_visible", user_visible)
if hasattr(node, "show") and hasattr(node, "hide"):
if user_visible:
node.show()
else:
node.hide()
def create_gui_button(self, pos=(0, 0, 0), text="按钮", size=0.1):
from direct.gui.DirectGui import DirectButton
return DirectButton(
text=text,
pos=(pos[0], pos[1], pos[2]),
scale=size,
frameColor=(0.2, 0.6, 0.8, 1),
text_font=self.get_chinese_font() or None,
rolloverSound=None,
clickSound=None,
parent=self.aspect2d,
command=None,
)
def create_gui_label(self, pos=(0, 0, 0), text="标签", size=0.08):
from direct.gui.DirectGui import DirectLabel
return 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.get_chinese_font() or None,
text_align=TextNode.ACenter,
text_mayChange=True,
parent=self.aspect2d,
)
def create_gui_entry(self, pos=(0, 0, 0), placeholder="输入文本...", size=0.08):
from direct.gui.DirectGui import DirectEntry
return DirectEntry(
text="",
pos=(pos[0], pos[1], pos[2]),
scale=size,
command=self.on_gui_entry_submit,
initialText=placeholder,
numLines=1,
width=12,
focus=0,
frameColor=(0, 0, 0, 0),
text_fg=(1, 1, 1, 1),
text_font=self.get_chinese_font() or None,
text_align=TextNode.ACenter,
text_mayChange=True,
parent=self.aspect2d,
rolloverSound=None,
clickSound=None,
suppressKeys=True,
suppressMouse=True,
)
def on_gui_entry_submit(self, text, *_args):
print(f"GUI 输入框提交: {text}")
def create_gui_2d_image(self, pos=(0, 0, 0), image_path="", size=(1, 1, 1)):
if isinstance(size, (list, tuple)) and len(size) >= 3:
width_scale = float(size[0]) * 0.2
height_scale = float(size[2]) * 0.2
else:
scalar = float(size) if isinstance(size, (int, float)) else 0.2
width_scale = scalar * 0.1
height_scale = width_scale
cm = CardMaker("gui_2d_image")
cm.setFrame(-width_scale, width_scale, -height_scale, height_scale)
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.setTransparency(TransparencyAttrib.MAlpha)
if image_path:
texture = self.loader.loadTexture(image_path)
if texture:
image_node.setTexture(texture, 1)
return image_node
def create_gui_3d_text(self, pos=(0, 0, 0), text="3D文本", size=0.5):
text_node = TextNode("gui_3d_text")
text_node.setText(text)
text_node.setAlign(TextNode.ACenter)
if self.get_chinese_font():
text_node.setFont(self.get_chinese_font())
text_np = self.render.attachNewNode(text_node)
text_np.setPos(Vec3(pos[0], pos[1], pos[2]))
text_np.setScale(size)
text_np.setBin("fixed", 40)
text_np.setDepthWrite(False)
return text_np
def create_gui_3d_image(self, pos=(0, 0, 0), image_path="", size=(1, 1, 1)):
if isinstance(size, (list, tuple)) and len(size) >= 3:
width_scale = float(size[0])
height_scale = float(size[2])
else:
width_scale = float(size) if isinstance(size, (int, float)) else 1.0
height_scale = width_scale
cm = CardMaker("gui_3d_image")
cm.setFrame(-width_scale, width_scale, -height_scale, height_scale)
image_node = self.render.attachNewNode(cm.generate())
image_node.setPos(pos)
image_node.setTransparency(TransparencyAttrib.MAlpha)
if image_path:
texture = self.loader.loadTexture(image_path)
if texture:
image_node.setTexture(texture, 1)
return image_node
def _load_movie_texture(self, name, video_path):
if not video_path:
return None
movie_texture = MovieTexture(name)
if not movie_texture.read(Filename.fromOsSpecific(video_path)):
print(f"⚠ 无法加载视频: {video_path}")
return None
movie_texture.play()
return movie_texture
def create_gui_video_screen(self, pos=(0, 0, 0), size=(1, 1, 1), video_path=""):
if isinstance(size, (list, tuple)) and len(size) >= 3:
width_scale = float(size[0])
height_scale = float(size[2])
else:
width_scale = float(size) if isinstance(size, (int, float)) else 1.0
height_scale = width_scale
cm = CardMaker("gui_video_screen")
cm.setFrame(-width_scale, width_scale, -height_scale, height_scale)
video_node = self.render.attachNewNode(cm.generate())
video_node.setPos(pos)
video_node.setTransparency(TransparencyAttrib.MAlpha)
movie_texture = self._load_movie_texture("gui_video_texture_3d", video_path)
if movie_texture:
video_node.setTexture(movie_texture, 1)
return video_node
def create_gui_2d_video_screen(self, pos=(0, 0, 0), size=(1, 1, 1), video_path=""):
if isinstance(size, (list, tuple)) and len(size) >= 3:
width_scale = float(size[0]) * 0.2
height_scale = float(size[2]) * 0.2
else:
scalar = float(size) if isinstance(size, (int, float)) else 0.2
width_scale = scalar * 0.1
height_scale = width_scale
cm = CardMaker("gui_2d_video_screen")
cm.setFrame(-width_scale, width_scale, -height_scale, height_scale)
video_node = self.aspect2d.attachNewNode(cm.generate())
video_node.setPos(pos)
video_node.setTransparency(TransparencyAttrib.MAlpha)
video_node.setDepthWrite(False)
video_node.setDepthTest(False)
movie_texture = self._load_movie_texture("gui_video_texture_2d", video_path)
if movie_texture:
video_node.setTexture(movie_texture, 1)
return video_node
def play_model_animation(self):
actors = self.render.findAllMatches("**/+ActorNode")
for actor_np in actors:
actor_node = actor_np.node()
if isinstance(actor_node, Actor):
anim_names = actor_node.getAnimNames()
if anim_names:
actor_node.loop(anim_names[0])
if __name__ == "__main__":
try:
app = MainApp()
app.run()
except Exception as e:
print(f"应用程序启动失败: {e}")
traceback.print_exc()

View File

@ -0,0 +1,53 @@
[Window][WindowOverViewport_11111111]
Pos=0,20
Size=2048,1084
Collapsed=0
[Window][工具栏]
Pos=453,20
Size=1326,32
Collapsed=0
DockId=0x0000000D,0
[Window][场景树]
Pos=0,20
Size=451,748
Collapsed=0
DockId=0x00000007,0
[Window][属性面板]
Pos=1781,20
Size=267,390
Collapsed=0
DockId=0x00000003,0
[Window][脚本管理]
Pos=1781,412
Size=267,356
Collapsed=0
DockId=0x00000004,0
[Window][资源管理器]
Pos=0,770
Size=2048,334
Collapsed=0
DockId=0x0000000A,0
[Window][控制台]
Pos=0,770
Size=2048,334
Collapsed=0
DockId=0x0000000A,1
[Docking][Data]
DockSpace ID=0x08BD597D Window=0x1BBC0F80 Pos=0,20 Size=2048,1084 Split=Y
DockNode ID=0x00000009 Parent=0x08BD597D SizeRef=2560,748 Split=X
DockNode ID=0x00000007 Parent=0x00000009 SizeRef=451,1084 Selected=0xE0015051
DockNode ID=0x00000008 Parent=0x00000009 SizeRef=1595,1084 Split=X
DockNode ID=0x00000001 Parent=0x00000008 SizeRef=1651,989 Split=Y
DockNode ID=0x0000000D Parent=0x00000001 SizeRef=1318,32 HiddenTabBar=1 Selected=0x43A39006
DockNode ID=0x0000000E Parent=0x00000001 SizeRef=1318,714 CentralNode=1
DockNode ID=0x00000002 Parent=0x00000008 SizeRef=267,989 Split=Y Selected=0x3188AB8D
DockNode ID=0x00000003 Parent=0x00000002 SizeRef=351,390 Selected=0x5DB6FF37
DockNode ID=0x00000004 Parent=0x00000002 SizeRef=351,597 Selected=0x3188AB8D
DockNode ID=0x0000000A Parent=0x08BD597D SizeRef=2560,334 Selected=0x3A2E05C3

View File

@ -4,6 +4,205 @@ from typing import List
from panda3d.core import NodePath, Point3
def _is_valid_node(node) -> bool:
return bool(node) and hasattr(node, "isEmpty") and (not node.isEmpty())
def _is_light_node(node: NodePath) -> bool:
return bool(node) and hasattr(node, "hasTag") and node.hasTag("light_type")
def _is_terrain_node(node: NodePath) -> bool:
return bool(node) and hasattr(node, "hasTag") and node.hasTag("tree_item_type") and node.getTag("tree_item_type") == "TERRAIN_NODE"
def _set_light_registration(world, node: NodePath, registered: bool):
if not world or not _is_valid_node(node) or not _is_light_node(node):
return
scene_manager = getattr(world, "scene_manager", None)
light_type = node.getTag("light_type")
light_lists = []
if scene_manager:
if light_type == "spot_light" and hasattr(scene_manager, "Spotlight"):
light_lists.append(scene_manager.Spotlight)
elif light_type == "point_light" and hasattr(scene_manager, "Pointlight"):
light_lists.append(scene_manager.Pointlight)
rp_light = node.getPythonTag("rp_light_object") if hasattr(node, "hasPythonTag") and node.hasPythonTag("rp_light_object") else None
current_registered = bool(node.getPythonTag("engine_light_registered")) if hasattr(node, "hasPythonTag") and node.hasPythonTag("engine_light_registered") else False
if registered:
for light_list in light_lists:
if node not in light_list:
light_list.append(node)
if not current_registered:
try:
if rp_light is not None and getattr(world, "render_pipeline", None):
world.render_pipeline.add_light(rp_light)
elif hasattr(world, "render") and world.render:
world.render.setLight(node)
except Exception:
pass
try:
node.setPythonTag("engine_light_registered", True)
except Exception:
pass
return
for light_list in light_lists:
try:
while node in light_list:
light_list.remove(node)
except Exception:
pass
if current_registered:
try:
if rp_light is not None and getattr(world, "render_pipeline", None):
world.render_pipeline.remove_light(rp_light)
elif hasattr(world, "render") and world.render:
world.render.clearLight(node)
except Exception:
pass
try:
node.setPythonTag("engine_light_registered", False)
except Exception:
pass
def _set_terrain_registration(world, node: NodePath, registered: bool):
if not world or not _is_valid_node(node) or not _is_terrain_node(node):
return
terrain_manager = getattr(world, "terrain_manager", None)
if not terrain_manager or not hasattr(terrain_manager, "terrains"):
return
terrain_info = None
if hasattr(node, "hasPythonTag") and node.hasPythonTag("terrain_info"):
terrain_info = node.getPythonTag("terrain_info")
else:
for info in getattr(terrain_manager, "terrains", []):
if info.get("node") == node:
terrain_info = info
break
if terrain_info is not None:
try:
node.setPythonTag("terrain_info", terrain_info)
except Exception:
pass
if registered:
if terrain_info is not None:
terrain_info["node"] = node
if all(info.get("node") != node for info in terrain_manager.terrains):
terrain_manager.terrains.append(terrain_info)
return
try:
terrain_manager.terrains = [info for info in terrain_manager.terrains if info.get("node") != node]
except Exception:
pass
def _register_scene_node(world, node: NodePath):
if not world or not _is_valid_node(node):
return
scene_manager = getattr(world, "scene_manager", None)
_set_light_registration(world, node, True)
_set_terrain_registration(world, node, True)
if scene_manager and hasattr(scene_manager, "models") and node not in scene_manager.models:
scene_manager.models.append(node)
try:
if hasattr(world, "updateSceneTree"):
world.updateSceneTree()
except Exception:
pass
def _unregister_scene_node(world, node: NodePath):
if not world or not node:
return
scene_manager = getattr(world, "scene_manager", None)
_set_light_registration(world, node, False)
_set_terrain_registration(world, node, False)
if scene_manager and hasattr(scene_manager, "models"):
try:
while node in scene_manager.models:
scene_manager.models.remove(node)
except Exception:
pass
try:
if hasattr(world, "updateSceneTree"):
world.updateSceneTree()
except Exception:
pass
def _refresh_scene_tree(world):
if not world:
return
try:
if hasattr(world, "updateSceneTree"):
world.updateSceneTree()
except Exception:
pass
def _resolve_world(world=None):
if world:
return world
try:
from direct.showbase.ShowBaseGlobal import base
return base
except Exception:
return None
def _sync_scene_node_side_effects(world, nodes):
world = _resolve_world(world)
if not world:
return
ssbo_editor = getattr(world, "ssbo_editor", None)
if ssbo_editor and hasattr(ssbo_editor, "sync_scene_nodes_to_pick"):
try:
ssbo_editor.sync_scene_nodes_to_pick(nodes or [])
except Exception:
pass
def _sync_transform_side_effects(world, nodes):
_sync_scene_node_side_effects(world, nodes)
def _apply_vec3_method(node, method_name: str, value, reference_node=None):
if not _is_valid_node(node):
return
if reference_node is not None and not _is_valid_node(reference_node):
reference_node = None
method = getattr(node, method_name)
if reference_node is not None:
try:
method(reference_node, value)
return
except Exception:
pass
if isinstance(value, (tuple, list)) and len(value) >= 3:
method(reference_node, value[0], value[1], value[2])
return
else:
try:
method(value)
return
except Exception:
pass
if isinstance(value, (tuple, list)) and len(value) >= 3:
method(value[0], value[1], value[2])
return
method(value)
class Command(ABC):
"""
抽象命令类所有具体命令都需要继承此类
@ -50,13 +249,22 @@ class CommandManager:
"""
try:
command.execute()
self._undo_stack.append(command)
# 清空重做栈,因为执行新命令后就无法重做之前的命令了
self._redo_stack.clear()
self.record_command(command)
except Exception as e:
print(f"执行命令时出错: {e}")
raise
def record_command(self, command: Command):
"""记录一个已经执行完成的命令。"""
self._undo_stack.append(command)
self._redo_stack.clear()
def pop_last_command(self):
"""弹出最后一个已执行命令,供复合操作合并历史使用。"""
if not self._undo_stack:
return None
return self._undo_stack.pop()
def undo(self) -> bool:
"""
撤销上一个命令
@ -130,31 +338,37 @@ class CommandManager:
# 示例命令实现
class MoveNodeCommand(Command):
"""
移动节点命令示例
Move node command.
"""
def __init__(self, node: NodePath, old_pos, new_pos):
def __init__(self, node: NodePath, old_pos, new_pos, reference_node=None, world=None):
self.node = node
self.old_pos = old_pos
self.new_pos = new_pos
self.reference_node = reference_node
self.world = world
def _apply(self, value):
_apply_vec3_method(self.node, "setPos", value, self.reference_node)
_sync_transform_side_effects(self.world, [self.node])
def execute(self):
"""
执行移动操作
Execute move operation.
"""
self.node.setPos(self.new_pos)
self._apply(self.new_pos)
def undo(self):
"""
撤销移动操作
Undo move operation.
"""
self.node.setPos(self.old_pos)
self._apply(self.old_pos)
def redo(self):
"""
重做移动操作
Redo move operation.
"""
self.node.setPos(self.new_pos)
self._apply(self.new_pos)
class DeleteNodeCommand(Command):
@ -286,8 +500,10 @@ class DeleteNodeCommand(Command):
for i in reversed(tilesets_to_remove):
del scene_manager.tilesets[i]
_unregister_scene_node(self.world, self.node)
# 从场景图中移除节点,使用 detachNode 而不是 removeNode 以便可以撤销
if self.node and not self.node.isEmpty():
if _is_valid_node(self.node):
self.node.detachNode()
def undo(self):
@ -295,9 +511,12 @@ class DeleteNodeCommand(Command):
撤销删除操作恢复旧节点
"""
try:
if self.node and not self.node.isEmpty():
if _is_valid_node(self.node):
# 直接将节点挂载回原父节点
self.node.reparentTo(self.parent_node)
if _is_valid_node(self.parent_node):
self.node.reparentTo(self.parent_node)
elif self.world and _is_valid_node(getattr(self.world, "render", None)):
self.node.reparentTo(self.world.render)
# 恢复到相应的管理器列表中
if self.world and hasattr(self.world, 'scene_manager'):
@ -319,6 +538,8 @@ class DeleteNodeCommand(Command):
# 简单恢复到 tilesets
if hasattr(scene_manager, 'tilesets'):
scene_manager.tilesets.append({'node': self.node, 'url': self.extra_data.get('tileset_url', '')})
_register_scene_node(self.world, self.node)
print(f"✅ 成功撤销删除操作,节点 {self.node_name} 已恢复")
else:
@ -338,60 +559,147 @@ class DeleteNodeCommand(Command):
class RotateNodeCommand(Command):
"""
旋转节点命令
Rotate node command.
"""
def __init__(self, node: NodePath, old_hpr, new_hpr):
def __init__(self, node: NodePath, old_hpr, new_hpr, reference_node=None, world=None):
self.node = node
self.old_hpr = old_hpr
self.new_hpr = new_hpr
self.reference_node = reference_node
self.world = world
def _apply(self, value):
_apply_vec3_method(self.node, "setHpr", value, self.reference_node)
_sync_transform_side_effects(self.world, [self.node])
def execute(self):
"""
执行旋转操作
Execute move operation.
"""
self.node.setHpr(self.new_hpr)
self._apply(self.new_hpr)
def undo(self):
"""
撤销旋转操作
Undo move operation.
"""
self.node.setHpr(self.old_hpr)
self._apply(self.old_hpr)
def redo(self):
"""
重做旋转操作
Redo move operation.
"""
self.node.setHpr(self.new_hpr)
self._apply(self.new_hpr)
class ScaleNodeCommand(Command):
"""
缩放节点命令
Scale node command.
"""
def __init__(self, node: NodePath, old_scale, new_scale):
def __init__(self, node: NodePath, old_scale, new_scale, world=None):
self.node = node
self.old_scale = old_scale
self.new_scale = new_scale
self.world = world
def _apply(self, value):
_apply_vec3_method(self.node, "setScale", value)
_sync_transform_side_effects(self.world, [self.node])
def execute(self):
"""
执行缩放操作
Execute rotate operation.
"""
self.node.setScale(self.new_scale)
self._apply(self.new_scale)
def undo(self):
"""
撤销缩放操作
Undo rotate operation.
"""
self.node.setScale(self.old_scale)
self._apply(self.old_scale)
def redo(self):
"""
重做缩放操作
Redo rotate operation.
"""
self.node.setScale(self.new_scale)
self._apply(self.new_scale)
class RenameNodeCommand(Command):
"""Rename a node and refresh scene tree bindings."""
def __init__(self, node: NodePath, old_name: str, new_name: str, world=None):
self.node = node
self.old_name = old_name
self.new_name = new_name
self.world = world
def execute(self):
if _is_valid_node(self.node):
self.node.setName(self.new_name)
_refresh_scene_tree(self.world)
def undo(self):
if _is_valid_node(self.node):
self.node.setName(self.old_name)
_refresh_scene_tree(self.world)
def redo(self):
self.execute()
class VisibilityNodeCommand(Command):
"""Toggle editor visibility state for a node."""
def __init__(self, node: NodePath, old_visible: bool, new_visible: bool, world=None):
self.node = node
self.old_visible = bool(old_visible)
self.new_visible = bool(new_visible)
self.world = world
def _apply(self, visible: bool):
if not _is_valid_node(self.node):
return
self.node.setPythonTag("user_visible", bool(visible))
if visible:
self.node.show()
else:
self.node.hide()
_sync_scene_node_side_effects(self.world, [self.node])
def execute(self):
self._apply(self.new_visible)
def undo(self):
self._apply(self.old_visible)
def redo(self):
self.execute()
class MaterialStateCommand(Command):
"""Replay a captured material snapshot for undo/redo."""
def __init__(self, apply_state_callback, before_state, after_state):
self.apply_state_callback = apply_state_callback
self.before_state = before_state
self.after_state = after_state
def execute(self):
if self.apply_state_callback:
self.apply_state_callback(self.after_state)
def undo(self):
if self.apply_state_callback:
self.apply_state_callback(self.before_state)
def redo(self):
self.execute()
class SnapshotStateCommand(MaterialStateCommand):
"""Generic callback-based snapshot command."""
class CreateNodeCommand(Command):
@ -399,49 +707,99 @@ class CreateNodeCommand(Command):
创建节点命令
"""
def __init__(self, node_creator_func,parent_node, *args, **kwargs):
def __init__(self, node_creator_func, parent_node, *args, world=None, **kwargs):
self.node_creator_func = node_creator_func
self.parent_node = parent_node
self.args = args
self.kwargs = kwargs
self.world = world
self.created_node = None
def execute(self):
"""
执行创建节点操作
"""
self.created_node = self.node_creator_func(self.parent_node,*self.args, **self.kwargs)
if _is_valid_node(self.created_node):
target_parent = self.parent_node
if (not _is_valid_node(target_parent)) and self.world:
target_parent = getattr(self.world, "render", None)
if _is_valid_node(target_parent):
self.created_node.wrtReparentTo(target_parent)
_register_scene_node(self.world, self.created_node)
_sync_scene_node_side_effects(self.world, [self.created_node])
return self.created_node
self.created_node = self.node_creator_func(self.parent_node, *self.args, **self.kwargs)
_register_scene_node(self.world, self.created_node)
_sync_scene_node_side_effects(self.world, [self.created_node])
return self.created_node
def undo(self):
"""
撤销创建节点操作
"""
if self.created_node:
if _is_valid_node(self.created_node):
_unregister_scene_node(self.world, self.created_node)
self.created_node.detachNode()
_sync_scene_node_side_effects(self.world, [self.created_node])
def redo(self):
"""
重做创建节点操作
"""
if _is_valid_node(self.created_node):
target_parent = self.parent_node
if (not _is_valid_node(target_parent)) and self.world:
target_parent = getattr(self.world, "render", None)
if _is_valid_node(target_parent):
self.created_node.wrtReparentTo(target_parent)
_register_scene_node(self.world, self.created_node)
_sync_scene_node_side_effects(self.world, [self.created_node])
return
self.execute()
class ReparentNodeCommand(Command):
"""
重新设置节点父子关系命令 - 增强版同时处理Panda3D和Qt树
"""
class AttachNodeCommand(Command):
"""Attach an existing detached node into a parent and make it undoable."""
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):
def __init__(self, node: NodePath, parent_node: NodePath, 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.parent_node = parent_node
self.world = world
def execute(self):
target_parent = self.parent_node
if (not target_parent or target_parent.isEmpty()) and self.world:
target_parent = getattr(self.world, "render", None)
if _is_valid_node(self.node) and _is_valid_node(target_parent):
self.node.wrtReparentTo(target_parent)
_register_scene_node(self.world, self.node)
_sync_scene_node_side_effects(self.world, [self.node])
return self.node
def undo(self):
if _is_valid_node(self.node):
_unregister_scene_node(self.world, self.node)
self.node.detachNode()
_sync_scene_node_side_effects(self.world, [self.node])
def redo(self):
self.execute()
class ReparentNodeCommand(Command):
"""
重新设置节点父子关系命令
"""
def __init__(self, node: NodePath, old_parent: NodePath, new_parent: NodePath,
is_2d_gui=False, world=None):
self.node = node
self.old_parent = old_parent
self.new_parent = new_parent
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())
@ -451,34 +809,16 @@ class ReparentNodeCommand(Command):
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):
"""
执行重新父化操作
"""
if not _is_valid_node(self.node):
return
# 更新Panda3D节点父子关系
if self.is_2d_gui and self.world:
# 2D GUI元素需要特殊处理
if self.new_parent and not self.new_parent.isEmpty():
if _is_valid_node(self.new_parent):
if hasattr(self.new_parent, 'getTag') and self.new_parent.getTag("is_gui_element") == "1":
# 目标是GUI元素直接重新父化
self.node.wrtReparentTo(self.new_parent)
@ -492,7 +832,7 @@ class ReparentNodeCommand(Command):
print(f"2D GUI元素重新父化到aspect2d")
else:
# 普通3D节点的处理
if self.new_parent and not self.new_parent.isEmpty():
if _is_valid_node(self.new_parent):
self.node.wrtReparentTo(self.new_parent)
else:
# 如果新父节点为空将其父化到render节点
@ -507,13 +847,15 @@ class ReparentNodeCommand(Command):
"""
撤销重新父化操作
"""
if not _is_valid_node(self.node):
return
# 在改变父节点前保存当前的缩放值
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 _is_valid_node(self.old_parent):
if hasattr(self.old_parent, 'getTag') and self.old_parent.getTag("is_gui_element") == "1":
# 原父节点是GUI元素直接重新父化
self.node.wrtReparentTo(self.old_parent)
@ -527,7 +869,7 @@ class ReparentNodeCommand(Command):
print(f"2D GUI元素恢复到aspect2d")
else:
# 普通3D节点的处理
if self.old_parent and not self.old_parent.isEmpty():
if _is_valid_node(self.old_parent):
self.node.wrtReparentTo(self.old_parent)
else:
# 如果原父节点为空将其父化到render节点
@ -549,13 +891,15 @@ class ReparentNodeCommand(Command):
"""
重做重新父化操作
"""
if not _is_valid_node(self.node):
return
# 在改变父节点前保存当前的缩放值
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 _is_valid_node(self.new_parent):
if hasattr(self.new_parent, 'getTag') and self.new_parent.getTag("is_gui_element") == "1":
# 目标是GUI元素直接重新父化
self.node.wrtReparentTo(self.new_parent)
@ -569,7 +913,7 @@ class ReparentNodeCommand(Command):
print(f"2D GUI元素重新父化到aspect2d")
else:
# 普通3D节点的处理
if self.new_parent and not self.new_parent.isEmpty():
if _is_valid_node(self.new_parent):
self.node.wrtReparentTo(self.new_parent)
else:
# 如果新父节点为空将其父化到render节点
@ -616,40 +960,40 @@ class CompositeCommand(Command):
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 _apply_light_position(self, pos):
if not self.light_object:
return
try:
self.light_object.setPos(pos)
return
except Exception:
pass
try:
self.light_object.setPos(pos.x, pos.y, pos.z)
return
except Exception:
pass
try:
self.light_object.pos = Point3(pos)
except Exception:
pass
def execute(self): # 将原来的 do() 改为 execute()
self._apply_light_position(self.new_pos)
if self.node:
self.node.setPos(self.new_pos)
def undo(self):
self._apply_light_position(self.old_pos)
if self.node:
self.node.setPos(self.old_pos)
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 _apply_light_position(self, pos):
if not self.light_object:
return
try:
self.light_object.setPos(pos)
return
except Exception:
pass
try:
self.light_object.setPos(pos.x, pos.y, pos.z)
return
except Exception:
pass
try:
self.light_object.pos = Point3(pos)
except Exception:
pass
def execute(self): # 将原来的 do() 改为 execute()
self._apply_light_position(self.new_pos)
if _is_valid_node(self.node):
self.node.setPos(self.new_pos)
def undo(self):
self._apply_light_position(self.old_pos)
if _is_valid_node(self.node):
self.node.setPos(self.old_pos)
def redo(self):
self.execute() # 调用 execute() 而不是 do()

View File

@ -64,10 +64,12 @@ class CustomMouseController:
self.keyMap[key] = value
# 只在右键按下时记录鼠标位置
if key == "mouse3" and value == True:
mouse_pos = self.showbase.mouseWatcherNode.getMouse()
if mouse_pos:
self.last_mouse_x = mouse_pos.get_x()
self.last_mouse_y = mouse_pos.get_y()
watcher = getattr(self.showbase, "mouseWatcherNode", None)
if watcher and watcher.hasMouse():
mouse_pos = watcher.getMouse()
if mouse_pos:
self.last_mouse_x = mouse_pos.get_x()
self.last_mouse_y = mouse_pos.get_y()
def move(self, task):
dt = self.showbase.clock.dt
@ -98,7 +100,10 @@ class CustomMouseController:
try:
# 检查是否应该处理鼠标事件避免与ImGui冲突
if self._should_handle_mouse():
mouse_pos = self.showbase.mouseWatcherNode.getMouse()
watcher = getattr(self.showbase, "mouseWatcherNode", None)
if not watcher or not watcher.hasMouse():
return task.cont
mouse_pos = watcher.getMouse()
if mouse_pos:
current_x = mouse_pos.get_x()
current_y = mouse_pos.get_y()
@ -160,7 +165,10 @@ class CustomMouseController:
pass
# 检查鼠标位置是否在ImGui窗口区域内
mouse_pos = self.showbase.mouseWatcherNode.getMouse()
watcher = getattr(self.showbase, "mouseWatcherNode", None)
if not watcher or not watcher.hasMouse():
return True
mouse_pos = watcher.getMouse()
if not mouse_pos:
return True
@ -185,4 +193,4 @@ class CustomMouseController:
return True
except Exception as e:
print(f"鼠标事件检测失败: {e}")
return True
return True

File diff suppressed because it is too large Load Diff

View File

@ -1,26 +1,40 @@
"""
Core package - 核心功能模块
包含引擎的核心功能
- world.py: 基础世界功能相机光照地板等
- selection.py: 选择和变换系统
- event_handler.py: 事件处理系统
- tool_manager.py: 工具管理系统
- script_system.py: 脚本系统
"""
from .world import CoreWorld
from .selection import SelectionSystem
from .event_handler import EventHandler
from .tool_manager import ToolManager
from .script_system import ScriptManager, ScriptBase, ScriptComponent
__all__ = [
'CoreWorld',
'SelectionSystem',
'EventHandler',
'ToolManager',
'ScriptManager',
'ScriptBase',
'ScriptComponent'
]
"""
Core package - 核心功能模块
Keep package imports lazy so lightweight runtime contexts can import
`core.script_system` without eagerly pulling in the editor stack.
"""
from importlib import import_module
__all__ = [
"CoreWorld",
"SelectionSystem",
"EventHandler",
"ToolManager",
"ScriptManager",
"ScriptBase",
"ScriptComponent",
]
_LAZY_IMPORTS = {
"CoreWorld": ("core.world", "CoreWorld"),
"SelectionSystem": ("core.selection", "SelectionSystem"),
"EventHandler": ("core.event_handler", "EventHandler"),
"ToolManager": ("core.tool_manager", "ToolManager"),
"ScriptManager": ("core.script_system", "ScriptManager"),
"ScriptBase": ("core.script_system", "ScriptBase"),
"ScriptComponent": ("core.script_system", "ScriptComponent"),
}
def __getattr__(name):
if name not in _LAZY_IMPORTS:
raise AttributeError(f"module 'core' has no attribute {name!r}")
module_name, attr_name = _LAZY_IMPORTS[name]
module = import_module(module_name)
value = getattr(module, attr_name)
globals()[name] = value
return value

File diff suppressed because it is too large Load Diff

44
core/editor_context.py Normal file
View File

@ -0,0 +1,44 @@
"""Editor context adapter for the ImGui editor runtime."""
class EditorContext:
"""Provide a stable access layer for editor-facing services."""
def __init__(self, world):
self.world = world
def get_tree_widget(self):
return None
def get_gui_manager(self):
return getattr(self.world, "gui_manager", None)
def ensure_gui_elements(self):
gui_manager = self.get_gui_manager()
if gui_manager is not None:
gui_elements = getattr(gui_manager, "gui_elements", None)
if gui_elements is None:
gui_elements = []
setattr(gui_manager, "gui_elements", gui_elements)
return gui_elements
gui_elements = getattr(self.world, "gui_elements", None)
if gui_elements is None:
gui_elements = []
setattr(self.world, "gui_elements", gui_elements)
return gui_elements
def append_gui_element(self, element):
gui_elements = self.ensure_gui_elements()
gui_elements.append(element)
return True
def get_editor_context(owner):
"""Get or create editor context from a world/owner object."""
world = getattr(owner, "world", owner)
context = getattr(world, "_editor_context", None)
if context is None or getattr(context, "world", None) is not world:
context = EditorContext(world)
setattr(world, "_editor_context", context)
return context

View File

@ -1,6 +1,7 @@
from panda3d.core import (Point3, Point2, CollisionTraverser, CollisionHandlerQueue,
CollisionNode, CollisionRay, GeomNode, LineSegs, RenderState,
DepthTestAttrib, ColorAttrib)
from core.editor_context import get_editor_context
class EventHandler:
@ -9,11 +10,29 @@ class EventHandler:
def __init__(self, world):
"""初始化事件处理器"""
self.world = world
self._editor_context = get_editor_context(world)
# 射线显示相关
self.showRay = False # 是否显示射线(默认关闭)
self.rayNode = None # 当前显示的射线节点
self.rayLifetime = 2.0 # 射线显示时间(秒)
def _get_editor_context(self):
if not getattr(self, "_editor_context", None):
self._editor_context = get_editor_context(self.world)
return self._editor_context
def _get_tree_widget(self):
"""Qt tree has been removed in the ImGui editor."""
return self._get_editor_context().get_tree_widget()
def _get_gui_manager(self):
"""安全获取 GUI 管理器。"""
return self._get_editor_context().get_gui_manager()
def _sync_tree_selection(self, selected_model):
"""Scene tree selection is rendered directly from editor state in ImGui."""
return False
def showClickRay(self, nearPoint, farPoint, hitPos=None):
"""显示鼠标点击的射线"""
@ -191,68 +210,10 @@ class EventHandler:
pickerNP.removeNode()
return
# 优先检查是否点击了坐标轴
#print(f"检查坐标轴点击: 坐标轴存在={bool(self.world.selection.gizmo)}")
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}")
# 开始坐标轴拖拽
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
traceback.print_exc()
print("继续处理模型选择...")
#print("继续处理碰撞结果...")
if hitPos and hitNode:
#print(f"✓ 检测到碰撞,开始处理点击事件")
#print(f"GUI编辑模式: {self.world.guiEditMode}")
#print(f"当前工具: {self.world.currentTool}")
# 处理GUI编辑模式
if self.world.guiEditMode:
#print("处理GUI编辑模式点击")
# 检查是否点击了GUI元素
clickedGUI = self.world.gui_manager.findClickedGUI(hitNode)
if clickedGUI:
# 选中GUI元素
self.world.selection.updateSelection(clickedGUI)
self.world.gui_manager.selectGUIInTree(clickedGUI)
print(f"选中GUI元素: {clickedGUI.getTag('gui_text')}")
elif hasattr(self.world, 'currentGUITool') and self.world.currentGUITool:
# 在点击位置创建新GUI元素
self.world.gui_manager.createGUIAtPosition(hitPos, self.world.currentGUITool)
pickerNP.removeNode()
return
# 根据当前工具处理点击事件
if self.world.currentTool in ("选择", "移动", "旋转", "缩放"):
print("✓ 使用选择工具处理点击")
@ -267,20 +228,7 @@ class EventHandler:
print(f"当前工具不是'选择',无法处理: {self.world.currentTool}")
else:
print("没有检测到碰撞")
# 如果不在GUI编辑模式清除选择
if not self.world.guiEditMode:
self.world.selection.updateSelection(None)
# 在GUI编辑模式下即使没有碰撞也可以在空白区域创建GUI
if (self.world.guiEditMode and
hasattr(self.world, 'currentGUITool') and
self.world.currentGUITool):
# 使用默认的地面高度创建GUI
default_height = 0.0
world_pos = Point3(mx * 10, 0, my * 10) # 简单的屏幕到世界坐标转换
world_pos.setZ(default_height)
self.world.gui_manager.createGUIAtPosition(world_pos, self.world.currentGUITool)
self.world.selection.updateSelection(None)
# 确保总是清理碰撞检测节点
try:
@ -441,89 +389,82 @@ class EventHandler:
def _handleSelectionClick(self, hitNode):
"""处理选择工具的点击事件"""
print(f"开始处理选择点击,碰撞节点: {hitNode.getName()}")
# 查找对应的实际模型节点
selectedModel = None
# 如果点击的是碰撞节点,找到它的父模型
if isinstance(hitNode.node(), CollisionNode):
print(f"点击的是碰撞节点: {hitNode.getName()}")
# 碰撞节点的父节点应该是模型
parent = hitNode.getParent()
if parent in self.world.models:
selectedModel = parent
print(f"找到对应的模型: {selectedModel.getName()}")
else:
print(f"碰撞节点的父节点不是模型: {parent.getName()}")
else:
# 查找可选择的节点(模型或其子节点)
current = hitNode
while current != self.world.render:
# 检查是否是模型
if current in self.world.models:
selectedModel = current
print(f"找到模型节点: {selectedModel.getName()}")
break
# 检查是否是模型的子节点
for model in self.world.models:
if current.getParent() == model or current.isAncestorOf(model):
selectedModel = model
print(f"找到父模型: {selectedModel.getName()}")
break
if selectedModel:
def _is_valid_node(node):
if not node:
return False
try:
return not node.isEmpty()
except Exception:
return False
def _is_helper_node(node):
if not _is_valid_node(node):
return True
try:
name = node.getName() or ""
except Exception:
name = ""
lowered = name.lower()
return (
isinstance(node.node(), CollisionNode)
or lowered.startswith("modelcollision_")
or lowered.startswith("collision_")
or lowered.startswith("gizmo")
)
def _is_selectable_scene_node(node):
if not _is_valid_node(node) or node == self.world.render:
return False
if _is_helper_node(node):
return False
try:
if node.hasTag("is_scene_element") or node.hasTag("is_model_root"):
return True
except Exception:
pass
return True
def _resolve_pick_target(node):
current = node
fallback_model_root = None
model_list = self.world.models if hasattr(self.world, "models") else []
while _is_valid_node(current) and current != self.world.render:
try:
if current in model_list or current.hasTag("is_model_root"):
fallback_model_root = current
if not _is_helper_node(current):
return current
if _is_selectable_scene_node(current):
return current
current = current.getParent()
except Exception:
break
current = current.getParent()
if selectedModel:
#print(f"✓ 最终选中模型: {selectedModel.getName()}")
self.world.selection.handleMouseClick(selectedModel)
return fallback_model_root
selected_node = _resolve_pick_target(hitNode)
if selected_node:
print(f"最终选中节点: {selected_node.getName()}")
self.world.selection.handleMouseClick(selected_node)
# 更新选择状态并显示选择框和坐标轴
self.world.selection.updateSelection(selectedModel)
self.world.selection.updateSelection(selected_node)
# 在树形控件中查找并选中对应的项
if hasattr(self.world, 'interface_manager') and self.world.interface_manager and hasattr(self.world.interface_manager, 'treeWidget') and self.world.interface_manager.treeWidget:
#print("查找树形控件中的对应项...")
root = self.world.interface_manager.treeWidget.invisibleRootItem()
foundItem = None
for i in range(root.childCount()):
sceneItem = root.child(i)
if sceneItem.text(0) == "场景":
#print(f"在场景节点下查找...")
foundItem = self.world.interface_manager.findTreeItem(selectedModel, sceneItem)
if foundItem:
print(f"✓ 在树形控件中找到对应项: {foundItem.text(0)}")
try:
self.world.interface_manager.treeWidget.itemClicked.disconnect()
except TypeError:
pass
self.world.interface_manager.treeWidget.setCurrentItem(foundItem)
self.world.interface_manager.treeWidget.itemClicked.connect(
self.world.interface_manager.onTreeItemClicked)
else:
print("× 在树形控件中没有找到对应项")
break
if not foundItem:
print("× 没有找到场景节点或对应的树形项")
if self._sync_tree_selection(selected_node):
tree_widget = self._get_tree_widget()
if tree_widget and tree_widget.currentItem():
print(f"✓ 在树形控件中找到对应项: {tree_widget.currentItem().text(0)}")
else:
print("× 树形控件不存在")
print("× 场景树未同步(接口不可用或未找到对应项)")
else:
print("× 没有找到可选择的模型节点")
self.world.selection.updateSelection(None)
def mouseReleaseEventLeft(self, evt):
"""处理鼠标左键释放事件"""
# 处理坐标轴拖拽结束
if self.world.selection.isDraggingGizmo:
self.world.selection.stopGizmoDrag()
return
return
def wheelForward(self, data=None):
"""处理滚轮向前滚动(前进)"""
@ -531,11 +472,12 @@ class EventHandler:
super(type(self.world), self.world).wheelForward(data)
# 更新属性面板
if (self.world.interface_manager.treeWidget and
self.world.interface_manager.treeWidget.currentItem() and
self.world.interface_manager.treeWidget.currentItem().text(0) == "相机"):
self.world.property_panel.updatePropertyPanel(
self.world.interface_manager.treeWidget.currentItem())
tree_widget = self._get_tree_widget()
current_item = tree_widget.currentItem() if tree_widget else None
if (current_item and
current_item.text(0) == "相机" and
hasattr(self.world, "property_panel")):
self.world.property_panel.updatePropertyPanel(current_item)
def wheelBackward(self, data=None):
"""处理滚轮向后滚动(后退)"""
@ -543,11 +485,12 @@ class EventHandler:
super(type(self.world), self.world).wheelBackward(data)
# 更新属性面板
if (self.world.interface_manager.treeWidget and
self.world.interface_manager.treeWidget.currentItem() and
self.world.interface_manager.treeWidget.currentItem().text(0) == "相机"):
self.world.property_panel.updatePropertyPanel(
self.world.interface_manager.treeWidget.currentItem())
tree_widget = self._get_tree_widget()
current_item = tree_widget.currentItem() if tree_widget else None
if (current_item and
current_item.text(0) == "相机" and
hasattr(self.world, "property_panel")):
self.world.property_panel.updatePropertyPanel(current_item)
def mousePressEventMiddle(self, evt):
"""处理鼠标中键按下事件"""
@ -563,30 +506,6 @@ class EventHandler:
"""处理鼠标移动事件"""
if not evt:
return
# 处理坐标轴拖拽
if self.world.selection.isDraggingGizmo:
x = evt.get('x', 0)
y = evt.get('y', 0)
# 获取准确的窗口尺寸
winWidth, winHeight = self.world.getWindowSize()
# 将屏幕坐标转换为世界坐标
mx = 2.0 * x / float(winWidth) - 1.0
my = 1.0 - 2.0 * y / float(winHeight)
# 更新坐标轴拖拽
self.world.selection.updateGizmoDrag(x, y)
return
# 更新坐标轴高亮(鼠标悬停效果)
if self.world.selection.gizmo and not self.world.selection.isDraggingGizmo:
x = evt.get('x', 0)
y = evt.get('y', 0)
# 减少高亮调试输出,只在需要时输出
# 已静默处理,避免控制台刷屏
self.world.selection.updateGizmoHighlight(x, y)
# 调用CoreWorld的父类方法处理基础的相机旋转
super(type(self.world), self.world).mouseMoveEvent(evt)

View File

@ -24,6 +24,8 @@ class ImGuiStyleManager:
self.world = world
self.io = imgui_backend.io
self.style = None # 延迟初始化在apply_style中设置
self._icon_cache = {}
self._missing_icons = set()
# 颜色定义 - 与Qt UI保持一致
self.colors = {
@ -339,6 +341,7 @@ class ImGuiStyleManager:
style.scrollbar_rounding = self.sizes['frame_rounding']
style.grab_min_size = 10.0
style.grab_rounding = self.sizes['frame_rounding']
style.window_menu_button_position = imgui.Dir_.none
# 禁用一些ImGui的默认效果使其更像Qt
style.window_border_size = 1.0
@ -377,6 +380,14 @@ class ImGuiStyleManager:
flags = self.get_window_flags(window_type)
return imgui_ctx.begin(name, open, flags)
def prepare_centered_dialog(self, width, height, cond=imgui.Cond_.appearing):
"""Place a modal/dialog in the center of the current main viewport."""
viewport = imgui.get_main_viewport()
center_x = viewport.pos.x + (viewport.size.x - width) / 2
center_y = viewport.pos.y + (viewport.size.y - height) / 2
imgui.set_next_window_size((width, height), cond)
imgui.set_next_window_pos((center_x, center_y), cond)
def styled_button(self, label, size=(0, 0)):
"""绘制带样式的按钮"""
@ -394,24 +405,40 @@ class ImGuiStyleManager:
def load_icon(self, icon_name):
"""加载图标纹理为ImGui可用的格式"""
if icon_name in self._icon_cache:
return self._icon_cache[icon_name]
if icon_name in self._missing_icons:
return None
try:
# 构建图标路径
project_root = Path(__file__).resolve().parent.parent
icon_path = project_root / "icons" / f"{icon_name}.png"
if icon_path.exists():
# 使用base.imgui.loadTexture方法
if hasattr(base, 'imgui') and hasattr(base.imgui, 'loadTexture'):
# 转换路径为Panda3D兼容格式 (Windows下: D:\... -> /d/...)
# 注意: p3dimgui.loadTexture 仅支持 str 或 Texture不支持 Filename 对象
fn = Filename.fromOsSpecific(str(icon_path))
return base.imgui.loadTexture(fn.getFullpath())
else:
print(f"⚠ ImGui后端未初始化")
return None
else:
if not icon_path.exists():
print(f"⚠ 图标文件不存在: {icon_path}")
self._missing_icons.add(icon_name)
return None
base_app = getattr(self.world, "base", None) if self.world else None
if base_app is None:
try:
from direct.showbase.ShowBaseGlobal import base as base_app
except Exception:
base_app = None
imgui_runtime = getattr(base_app, "imgui", None) if base_app else None
texture_loader = getattr(imgui_runtime, "loadTexture", None)
if not callable(texture_loader):
print("⚠ ImGui后端未初始化")
return None
# 转换路径为Panda3D兼容格式 (Windows下: D:\... -> /d/...)
# 注意: p3dimgui.loadTexture 仅支持 str 或 Texture不支持 Filename 对象
fn = Filename.fromOsSpecific(str(icon_path))
texture = texture_loader(fn.getFullpath())
self._icon_cache[icon_name] = texture
return texture
except Exception as e:
print(f"⚠ 加载图标失败: {e}")
return None
@ -419,6 +446,72 @@ class ImGuiStyleManager:
def image_button(self, texture_id, size=(32, 32), bg_col=(0, 0, 0, 0), tint_col=(1, 1, 1, 1)):
"""绘制图像按钮"""
return imgui.image_button(texture_id, size, bg_col, tint_col)
def draw_stat_chip(self, label, tint=None, text_color=None):
"""绘制不可交互的状态胶囊。"""
if tint is None:
tint = self.colors['button_bg']
if text_color is None:
text_color = self.colors['text']
text_size = imgui.calc_text_size(label)
horizontal_padding = 8.0
vertical_padding = 4.0
chip_width = float(text_size.x) + horizontal_padding * 2.0
chip_height = float(text_size.y) + vertical_padding * 2.0
cursor_pos = imgui.get_cursor_screen_pos()
draw_list = imgui.get_window_draw_list()
bg_color = imgui.color_convert_float4_to_u32(tint)
fg_color = imgui.color_convert_float4_to_u32(text_color)
draw_list.add_rect_filled(
cursor_pos,
(cursor_pos.x + chip_width, cursor_pos.y + chip_height),
bg_color,
12.0,
)
draw_list.add_text(
(cursor_pos.x + horizontal_padding, cursor_pos.y + vertical_padding),
fg_color,
label,
)
imgui.dummy((chip_width, chip_height))
def draw_toolbar_button(self, label, active=False, size=(56, 28), tooltip=None, enabled=True):
"""绘制统一风格的工具栏按钮。"""
if active:
button_color = self.colors['primary']
hovered_color = self.colors['primary_dark']
text_color = (1.0, 1.0, 1.0, 1.0)
border_color = self.colors['primary_dark']
else:
button_color = self.colors['button_bg']
hovered_color = self.colors['panel_bg']
text_color = self.colors['text']
border_color = self.colors['border_secondary']
imgui.push_style_color(imgui.Col_.button, button_color)
imgui.push_style_color(imgui.Col_.button_hovered, hovered_color)
imgui.push_style_color(imgui.Col_.button_active, hovered_color)
imgui.push_style_color(imgui.Col_.text, text_color)
imgui.push_style_color(imgui.Col_.border, border_color)
imgui.push_style_var(imgui.StyleVar_.frame_rounding, 8.0)
imgui.push_style_var(imgui.StyleVar_.frame_border_size, 1.0)
imgui.push_style_var(imgui.StyleVar_.frame_padding, (10.0, 6.0))
if not enabled:
imgui.begin_disabled()
clicked = imgui.button(label, size)
if not enabled:
imgui.end_disabled()
if tooltip and imgui.is_item_hovered():
imgui.set_tooltip(tooltip)
imgui.pop_style_var(3)
imgui.pop_style_color(5)
return clicked
def get_icon_text_button(self, icon_texture, text, size=(0, 0)):
"""绘制带图标的文本按钮"""
@ -428,4 +521,4 @@ class ImGuiStyleManager:
imgui.same_line()
# 再绘制文本按钮
return imgui.button(text, size)
return imgui.button(text, size)

225
core/imgui_webview.py Normal file
View File

@ -0,0 +1,225 @@
"""
imgui_webview.py
后台 playwright 无头浏览器 + 截图 Panda3D 纹理 ImGui 面板显示
"""
from __future__ import annotations
import threading
import io
import time
class ImGuiWebView:
"""
后台线程运行 playwright Chromium定期截图并转换为 Panda3D 纹理
ImGui 直接用 imgui.image() 显示纹理鼠标/滚轮事件转发给浏览器
"""
def __init__(self, width: int = 1280, height: int = 720):
self.view_width = width
self.view_height = height
# 截图数据bytes
self._screenshot: bytes | None = None
self._screenshot_lock = threading.Lock()
self.tex_dirty = False # 有新截图待上传 GPU
# 状态
self.current_url = ""
self.title = ""
self.is_loading = False
self.error: str | None = None
# 待处理指令(由 ImGui 线程写,浏览器线程读)
self._cmd_navigate: str | None = None
self._cmd_click: tuple | None = None # (x_ratio, y_ratio)
self._cmd_scroll: float | None = None # pixels
self._cmd_back = False
self._cmd_forward = False
self._cmd_reload = False
self._lock = threading.Lock()
self._running = False
self._thread: threading.Thread | None = None
# ------------------------------------------------------------------ #
# 公开控制 API由 ImGui 线程调用,线程安全)
# ------------------------------------------------------------------ #
def start(self, url: str):
if self._running:
return
self._running = True
self._cmd_navigate = url
self._thread = threading.Thread(target=self._run, daemon=True,
name="imgui-webview")
self._thread.start()
def stop(self):
self._running = False
def navigate(self, url: str):
if not url.startswith(('http://', 'https://', 'file://')):
url = 'https://' + url
with self._lock:
self._cmd_navigate = url
self.is_loading = True
def click(self, x_ratio: float, y_ratio: float):
with self._lock:
self._cmd_click = (x_ratio, y_ratio)
def scroll(self, delta_px: float):
with self._lock:
self._cmd_scroll = delta_px
def go_back(self):
with self._lock:
self._cmd_back = True
def go_forward(self):
with self._lock:
self._cmd_forward = True
def reload(self):
with self._lock:
self._cmd_reload = True
def get_screenshot_bytes(self) -> bytes | None:
with self._screenshot_lock:
return self._screenshot
# ------------------------------------------------------------------ #
# 内部线程
# ------------------------------------------------------------------ #
def _run(self):
import asyncio
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
loop.run_until_complete(self._async_run())
except Exception as exc:
self.error = f"WebView 线程异常: {exc}"
import traceback; traceback.print_exc()
finally:
loop.close()
async def _async_run(self):
try:
from playwright.async_api import async_playwright
except ImportError:
self.error = (
"playwright 未安装。\n"
"请运行: pip install playwright\n"
"然后运行: playwright install chromium"
)
self._running = False
return
try:
async with async_playwright() as pw:
browser = await pw.chromium.launch(headless=True)
ctx = await browser.new_context(
viewport={"width": self.view_width,
"height": self.view_height},
user_agent=(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/121.0.0.0 Safari/537.36"
),
)
page = await ctx.new_page()
# 初次导航
start_url = self._cmd_navigate or "about:blank"
self._cmd_navigate = None
await self._goto(page, start_url)
await self._snap(page)
# 主循环
import asyncio
while self._running:
await asyncio.sleep(0.05)
with self._lock:
nav_url = self._cmd_navigate; self._cmd_navigate = None
clk = self._cmd_click; self._cmd_click = None
scr = self._cmd_scroll; self._cmd_scroll = None
do_back = self._cmd_back; self._cmd_back = False
do_fwd = self._cmd_forward; self._cmd_forward = False
do_reload = self._cmd_reload; self._cmd_reload = False
changed = False
if nav_url:
self.is_loading = True
await self._goto(page, nav_url)
changed = True
self.is_loading = False
if do_back:
await page.go_back()
await asyncio.sleep(0.5)
changed = True
if do_fwd:
await page.go_forward()
await asyncio.sleep(0.5)
changed = True
if do_reload:
await page.reload(wait_until="domcontentloaded")
changed = True
if clk is not None:
xr, yr = clk
x = int(xr * self.view_width)
y = int(yr * self.view_height)
await page.mouse.click(x, y)
await asyncio.sleep(0.4)
changed = True
if scr is not None:
await page.evaluate(
f"window.scrollBy(0, {int(scr)})"
)
await asyncio.sleep(0.15)
changed = True
if changed:
self.current_url = page.url
try:
self.title = await page.title()
except Exception:
pass
await self._snap(page)
await browser.close()
except Exception as exc:
self.error = str(exc)
import traceback; traceback.print_exc()
finally:
self._running = False
async def _goto(self, page, url: str):
import asyncio
try:
await page.goto(url, wait_until="domcontentloaded", timeout=20_000)
self.current_url = page.url
try:
self.title = await page.title()
except Exception:
pass
except Exception as exc:
print(f"[WebView] 导航失败 {url}: {exc}")
async def _snap(self, page):
"""截图并更新 self._screenshot"""
try:
data = await page.screenshot(type="png", full_page=False)
with self._screenshot_lock:
self._screenshot = data
self.tex_dirty = True
except Exception as exc:
print(f"[WebView] 截图失败: {exc}")

File diff suppressed because it is too large Load Diff

View File

@ -1,495 +0,0 @@
from direct.showbase.ShowBaseGlobal import globalClock
from direct.task.TaskManagerGlobal import taskMgr
from panda3d.core import Point3, Vec3
import math
class PatrolSystem:
"""巡检系统类"""
def __init__(self, world):
"""初始化巡检系统
Args:
world: 核心世界对象引用
"""
self.world = world
# 巡检状态
self.is_patrolling = False
self.patrol_points = [] # 巡检点列表 [(pos, hpr, wait_time), ...]
self.current_patrol_index = 0
self.patrol_task = None
# 巡检参数
self.patrol_speed = 5.0 # 巡检移动速度(单位/秒)
self.patrol_turn_speed = 90.0 # 转向速度(度/秒)
self.patrol_wait_timer = 0.0
self.patrol_state = "moving" # "moving", "turning_to_target", "waiting", "turning_back"
# 相机状态保存
self.original_cam_pos = None
self.original_cam_hpr = None
print("✓ 巡检系统初始化完成")
def add_patrol_point(self, position, heading=None, wait_time=3.0):
if heading is None:
if self.patrol_points:
last_pos = self.patrol_points[-1][0]
direction_x = position[0] - last_pos.x
direction_y = position[1] - last_pos.y
direction_z = position[2] - last_pos.z
import math
h=math.degrees(math.atan2(-direction_x,-direction_y))
distance_xy = math.sqrt(direction_x**2+direction_y**2)
p = math.degrees(math.atan2(direction_z,distance_xy))
p = max(-89,min(89,p))
r=0
heading = (h,p,r)
else:
# 使用当前相机朝向
current_hpr = self.world.cam.getHpr()
heading = (current_hpr.x, current_hpr.y, current_hpr.z)
pos = Point3(position[0], position[1], position[2])
hpr = Vec3(heading[0], heading[1], heading[2])
self.patrol_points.append((pos, hpr, wait_time))
print(f"✓ 添加巡检点 {len(self.patrol_points)}: 位置{position}, 朝向{heading}, 停留{wait_time}")
# 在 PatrolSystem 类中添加以下方法
def add_auto_heading_patrol_point(self, position, wait_time=3.0):
"""添加自动计算朝向的巡检点(朝向路径前进方向)
Args:
position: 相机位置 (x, y, z)
wait_time: 在该点停留时间
"""
heading = None # 将自动计算朝向
# 复用原有的 add_patrol_point 方法
self.add_patrol_point(position, heading, wait_time)
def add_patrol_point_looking_at(self, position, look_at_position, wait_time=3.0):
"""添加朝向指定位置的巡检点
Args:
position: 相机位置 (x, y, z)
look_at_position: 相机朝向的目标位置 (x, y, z)
wait_time: 在该点停留时间
"""
import math
# 计算从当前位置到目标位置的方向向量
direction_x = look_at_position[0] - position[0]
direction_y = look_at_position[1] - position[1]
direction_z = look_at_position[2] - position[2]
# 计算HPR朝向
h = math.degrees(math.atan2(-direction_x, -direction_y))
distance_xy = math.sqrt(direction_x ** 2 + direction_y ** 2)
p = math.degrees(math.atan2(direction_z, distance_xy))
p = max(-89, min(89, p)) # 限制pitch角度在合理范围内
r = 0 # roll通常为0
heading = (h, p, r)
self.add_patrol_point(position, heading, wait_time)
def clear_patrol_points(self):
"""清空所有巡检点"""
self.patrol_points = []
print("✓ 巡检点已清空")
def set_patrol_speed(self, move_speed, turn_speed=None):
"""设置巡检速度
Args:
move_speed: 移动速度单位/
turn_speed: 转向速度/如果为None则保持当前值
"""
self.patrol_speed = move_speed
if turn_speed is not None:
self.patrol_turn_speed = turn_speed
print(f"✓ 巡检速度已设置: 移动{move_speed}, 转向{turn_speed or self.patrol_turn_speed}")
def start_patrol(self):
"""开始巡检"""
if not self.patrol_points:
print("✗ 没有设置巡检点,无法开始巡检")
return False
if self.is_patrolling:
print("⚠ 巡检已在进行中")
return True
# 保存当前相机状态
self.original_cam_pos = Point3(self.world.cam.getPos())
self.original_cam_hpr = Vec3(self.world.cam.getHpr())
# 重置巡检状态
self.current_patrol_index = 0
self.patrol_state = "moving"
self.patrol_wait_timer = 0.0
self.is_patrolling = True
# 启动巡检任务
if self.patrol_task:
taskMgr.remove(self.patrol_task)
self.patrol_task = taskMgr.add(self._patrol_task, "patrol_task")
print(f"✓ 开始巡检,共{len(self.patrol_points)}个巡检点")
return True
def stop_patrol(self):
"""停止巡检"""
if not self.is_patrolling:
print("⚠ 巡检未在进行中")
return False
# 停止巡检任务
if self.patrol_task:
taskMgr.remove(self.patrol_task)
self.patrol_task = None
self.is_patrolling = False
self.patrol_state = "moving"
self.patrol_wait_timer = 0.0
print("✓ 巡检已停止")
return True
def pause_patrol(self):
"""暂停巡检"""
if not self.is_patrolling:
print("⚠ 巡检未在进行中")
return False
if self.patrol_task:
taskMgr.remove(self.patrol_task)
self.patrol_task = None
print("✓ 巡检已暂停")
return True
def resume_patrol(self):
"""恢复巡检"""
if self.is_patrolling:
print("⚠ 巡检已在进行中")
return False
if not self.patrol_points:
print("✗ 没有设置巡检点")
return False
self.is_patrolling = True
self.patrol_task = taskMgr.add(self._patrol_task, "patrol_task")
print("✓ 巡检已恢复")
return True
def reset_to_original_position(self):
"""重置相机到原始位置"""
if self.original_cam_pos and self.original_cam_hpr:
self.world.cam.setPos(self.original_cam_pos)
self.world.cam.setHpr(self.original_cam_hpr)
print("✓ 相机已重置到原始位置")
return True
else:
print("✗ 没有保存的原始位置")
return False
def _patrol_task(self, task):
"""巡检主任务"""
try:
if not self.is_patrolling or not self.patrol_points:
return task.done
# 获取当前巡检点
current_point = self.patrol_points[self.current_patrol_index]
target_pos, target_hpr, wait_time = current_point
# 根据当前状态执行不同操作
if self.patrol_state == "moving":
self._handle_moving_state(target_pos)
elif self.patrol_state == "turning_to_target":
self._handle_turning_to_target_state(target_hpr)
elif self.patrol_state == "waiting":
self._handle_waiting_state(wait_time)
elif self.patrol_state == "turning_back":
self._handle_turning_back_state()
return task.cont
except Exception as e:
print(f"巡检任务出错: {e}")
import traceback
traceback.print_exc()
return task.done
def _handle_moving_state(self, target_pos):
"""处理移动状态"""
current_pos = self.world.cam.getPos()
distance = (target_pos - current_pos).length()
if distance < 0.1: # 到达目标点
print(f"✓ 到达巡检点 {self.current_patrol_index + 1}")
self.patrol_state = "turning_to_target"
return
# 计算移动方向和距离
direction = target_pos - current_pos
direction.normalize()
# 计算目标朝向(看向目标点)
target_hpr = self._look_at_to_hpr(direction)
current_hpr = self.world.cam.getHpr()
# 平滑转向到目标朝向
h_diff = self._normalize_angle(target_hpr.x - current_hpr.x)
p_diff = self._normalize_angle(target_hpr.y - current_hpr.y)
r_diff = self._normalize_angle(target_hpr.z - current_hpr.z)
# 计算本帧应转动的角度
dt = globalClock.getDt()
turn_amount = self.patrol_turn_speed * dt
# 逐步转向目标角度
new_hpr = Vec3(current_hpr)
if abs(h_diff) > turn_amount:
new_hpr.x += turn_amount if h_diff > 0 else -turn_amount
else:
new_hpr.x = target_hpr.x
if abs(p_diff) > turn_amount:
new_hpr.y += turn_amount if p_diff > 0 else -turn_amount
else:
new_hpr.y = target_hpr.y
if abs(r_diff) > turn_amount:
new_hpr.z += turn_amount if r_diff > 0 else -turn_amount
else:
new_hpr.z = target_hpr.z
self.world.cam.setHpr(new_hpr)
# 计算本帧应移动的距离
move_distance = self.patrol_speed * dt
# 如果移动距离大于剩余距离,则直接移动到目标点
if move_distance >= distance:
self.world.cam.setPos(target_pos)
else:
# 否则按方向移动
new_pos = current_pos + direction * move_distance
self.world.cam.setPos(new_pos)
def _handle_turning_to_target_state(self, target_hpr):
"""处理转向目标状态"""
# 检查是否需要朝向下一个点
if target_hpr == "look_next":
# 计算朝向下一个点的方向
next_index = (self.current_patrol_index + 1) % len(self.patrol_points)
next_point_pos = self.patrol_points[next_index][0]
current_pos = self.world.cam.getPos()
direction = next_point_pos - current_pos
direction.normalize()
# 计算目标朝向
target_hpr = self._look_at_to_hpr(direction)
current_hpr = self.world.cam.getHpr()
# 计算角度差
h_diff = self._normalize_angle(target_hpr.x - current_hpr.x)
p_diff = self._normalize_angle(target_hpr.y - current_hpr.y)
r_diff = self._normalize_angle(target_hpr.z - current_hpr.z)
# 检查是否已完成转向
if abs(h_diff) < 1.0 and abs(p_diff) < 1.0 and abs(r_diff) < 1.0:
print(f"✓ 完成转向,开始停留")
self.patrol_state = "waiting"
self.patrol_wait_timer = 0.0
return
# 计算本帧应转动的角度
dt = globalClock.getDt()
turn_amount = self.patrol_turn_speed * dt
# 逐步转向目标角度
new_hpr = Vec3(current_hpr)
if abs(h_diff) > turn_amount:
new_hpr.x += turn_amount if h_diff > 0 else -turn_amount
else:
new_hpr.x = target_hpr.x
if abs(p_diff) > turn_amount:
new_hpr.y += turn_amount if p_diff > 0 else -turn_amount
else:
new_hpr.y = target_hpr.y
if abs(r_diff) > turn_amount:
new_hpr.z += turn_amount if r_diff > 0 else -turn_amount
else:
new_hpr.z = target_hpr.z
self.world.cam.setHpr(new_hpr)
def _handle_waiting_state(self, wait_time):
"""处理等待状态"""
self.patrol_wait_timer += globalClock.getDt()
if self.patrol_wait_timer >= wait_time:
print(f"✓ 停留结束,准备转回原朝向")
self.patrol_state = "turning_back"
# 修改 core/patrol_system.py 中的 _handle_turning_back_state 方法
def _handle_turning_back_state(self):
"""处理转回原朝向状态"""
# 直接完成转向状态,进入移动状态
print(f"✓ 停留结束,开始移动到下一个点")
# 移动到下一个巡检点
next_index = (self.current_patrol_index + 1) % len(self.patrol_points)
self.current_patrol_index = next_index
self.patrol_state = "moving"
return
def _normalize_angle(self, angle):
"""规范化角度到-180到180度之间"""
while angle > 180:
angle -= 360
while angle < -180:
angle += 360
return angle
def _look_at_to_hpr(self, direction):
"""将方向向量转换为HPR角度"""
# 简化的转换,实际应用中可能需要更精确的计算
h = math.degrees(math.atan2(-direction.x, -direction.y))
p = math.degrees(math.asin(direction.z))
return Vec3(h, p, 0)
def get_patrol_status(self):
"""获取巡检状态信息"""
return {
"is_patrolling": self.is_patrolling,
"current_point": self.current_patrol_index,
"total_points": len(self.patrol_points),
"state": self.patrol_state,
"wait_timer": self.patrol_wait_timer
}
def list_patrol_points(self):
"""列出所有巡检点"""
if not self.patrol_points:
print("没有设置巡检点")
return
print(f"巡检点列表 (共{len(self.patrol_points)}个):")
for i, (pos, hpr, wait_time) in enumerate(self.patrol_points):
current_marker = " >>>" if i == self.current_patrol_index and self.is_patrolling else ""
print(f" {i + 1}. 位置:({pos.x:.1f}, {pos.y:.1f}, {pos.z:.1f}) "
f"朝向:({hpr.x:.1f}, {hpr.y:.1f}, {hpr.z:.1f}) "
f"停留:{wait_time}{current_marker}")
def remove_patrol_point(self, index):
"""移除指定索引的巡检点"""
if 0 <= index < len(self.patrol_points):
removed_point = self.patrol_points.pop(index)
print(
f"✓ 移除巡检点 {index + 1}: 位置({removed_point[0].x:.1f}, {removed_point[0].y:.1f}, {removed_point[0].z:.1f})")
# 调整当前索引
if self.current_patrol_index >= len(self.patrol_points) and self.patrol_points:
self.current_patrol_index = len(self.patrol_points) - 1
elif self.current_patrol_index >= len(self.patrol_points):
self.current_patrol_index = 0
else:
print(f"✗ 无效的巡检点索引: {index}")
def insert_patrol_point(self, index, position, heading=None, wait_time=3.0):
"""在指定位置插入巡检点"""
if index < 0 or index > len(self.patrol_points):
print(f"✗ 无效的插入位置: {index}")
return
if heading is None:
# 使用当前相机朝向
current_hpr = self.world.cam.getHpr()
heading = (current_hpr.x, current_hpr.y, current_hpr.z)
pos = Point3(position[0], position[1], position[2])
hpr = Vec3(heading[0], heading[1], heading[2])
self.patrol_points.insert(index, (pos, hpr, wait_time))
print(f"✓ 在位置 {index + 1} 插入巡检点: 位置{position}, 朝向{heading}, 停留{wait_time}")
def update_patrol_point(self, index, position=None, heading=None, wait_time=None):
"""更新指定巡检点的信息"""
if 0 <= index < len(self.patrol_points):
pos, hpr, wt = self.patrol_points[index]
if position is not None:
pos = Point3(position[0], position[1], position[2])
if heading is not None:
hpr = Vec3(heading[0], heading[1], heading[2])
if wait_time is not None:
wt = wait_time
self.patrol_points[index] = (pos, hpr, wt)
print(f"✓ 更新巡检点 {index + 1}")
else:
print(f"✗ 无效的巡检点索引: {index}")
def goto_patrol_point(self, index):
"""直接跳转到指定巡检点"""
if not self.patrol_points:
print("✗ 没有设置巡检点")
return False
if 0 <= index < len(self.patrol_points):
pos, hpr, _ = self.patrol_points[index]
self.world.cam.setPos(pos)
self.world.cam.setHpr(hpr)
self.current_patrol_index = index
print(f"✓ 跳转到巡检点 {index + 1}")
return True
else:
print(f"✗ 无效的巡检点索引: {index}")
return False
def cleanup(self):
"""清理巡检系统资源"""
self.stop_patrol()
self.clear_patrol_points()
self.original_cam_pos = None
self.original_cam_hpr = None
print("✓ 巡检系统资源已清理")
# 使用示例和便捷函数
def create_default_patrol_route(patrol_system):
"""创建默认的巡检路线(示例)"""
# 清空现有巡检点
patrol_system.clear_patrol_points()
# 添加一些示例巡检点
patrol_system.add_patrol_point((0, -20, 5), (0, -15, 0), 2.0) # 点1前方低位置
patrol_system.add_patrol_point((0, 0, 10), (0, -30, 0), 3.0) # 点2中央高位置
patrol_system.add_patrol_point((15, 10, 5), (-45, -10, 0), 2.5) # 点3右侧位置
patrol_system.add_patrol_point((-15, 10, 5), (45, -10, 0), 2.5) # 点4左侧位置
print("✓ 默认巡检路线已创建")

View File

@ -0,0 +1,130 @@
"""RenderPipeline tag state helpers for the editor runtime.
The stock native TagStateManager used by RenderPipeline creates pass-specific
tag states from an empty RenderState. That works for static effects, but it
drops material attributes when we edit materials live in the editor. For
transparent forward materials this means the forward pass can no longer see
the current MaterialAttrib.
This module installs a Python-side tag state manager that preserves the
NodePath state when building tag states, and re-registers the already-created
pipeline cameras.
"""
from panda3d.core import BitMask32, ColorWriteAttrib, RenderState, ShaderAttrib
class EditorTagStateManager:
"""Tag-state manager that preserves NodePath state when applying effects."""
class StateContainer:
def __init__(self, tag_name, mask, write_color):
self.cameras = []
self.tag_states = {}
self.tag_name = tag_name
self.mask = BitMask32.bit(mask)
self.write_color = write_color
def __init__(self, main_cam_node):
self._main_cam_node = main_cam_node
self._main_cam_node.node().set_camera_mask(BitMask32.bit(1))
self.containers = {
"shadow": self.StateContainer("Shadows", 2, False),
"voxelize": self.StateContainer("Voxelize", 3, False),
"envmap": self.StateContainer("Envmap", 4, True),
"forward": self.StateContainer("Forward", 5, True),
}
def get_mask(self, container_name):
if container_name == "gbuffer":
return BitMask32.bit(1)
return self.containers[container_name].mask
def apply_state(self, container_name, np, shader, name, sort):
container = self.containers[container_name]
state = np.get_state()
if not container.write_color:
state = state.set_attrib(
ColorWriteAttrib.make(ColorWriteAttrib.C_off), 10000
)
state = state.set_attrib(ShaderAttrib.make(shader, sort), sort)
container.tag_states[name] = state
np.set_tag(container.tag_name, name)
for camera in container.cameras:
camera.set_tag_state(name, state)
def cleanup_states(self):
self._main_cam_node.node().clear_tag_states()
for container in self.containers.values():
for camera in container.cameras:
camera.clear_tag_states()
container.tag_states = {}
def register_camera(self, container_name, source):
container = self.containers[container_name]
source.set_tag_state_key(container.tag_name)
source.set_camera_mask(container.mask)
state = RenderState.make_empty()
if not container.write_color:
state = state.set_attrib(
ColorWriteAttrib.make(ColorWriteAttrib.C_off), 10000
)
source.set_initial_state(state)
container.cameras.append(source)
def unregister_camera(self, container_name, source):
container = self.containers[container_name]
if source not in container.cameras:
return
container.cameras.remove(source)
source.clear_tag_states()
source.set_initial_state(RenderState.make_empty())
def install_editor_tag_state_manager(render_pipeline, base):
"""Replace the native tag manager with an editor-safe Python variant."""
if not render_pipeline or not base:
return None
if isinstance(render_pipeline.tag_mgr, EditorTagStateManager):
return render_pipeline.tag_mgr
try:
render_pipeline.tag_mgr.cleanup_states()
except Exception:
pass
tag_mgr = EditorTagStateManager(base.cam)
for stage in render_pipeline.stage_mgr.stages:
stage_name = type(stage).__name__
if hasattr(stage, "forward_cam"):
tag_mgr.register_camera("forward", stage.forward_cam)
if stage_name == "EnvironmentCaptureStage" and hasattr(stage, "cameras"):
for camera_np in stage.cameras:
tag_mgr.register_camera("envmap", camera_np.node())
if hasattr(stage, "voxel_cam"):
tag_mgr.register_camera("voxelize", stage.voxel_cam)
if stage_name in (
"PSSMDistShadowStage",
"PSSMSceneShadowStage",
"SkyAOCaptureStage",
) and hasattr(stage, "camera"):
tag_mgr.register_camera("shadow", stage.camera)
pssm_plugin = render_pipeline.plugin_mgr.instances.get("pssm")
if pssm_plugin and hasattr(pssm_plugin, "camera_rig"):
split_count = pssm_plugin.get_setting("split_count")
for index in range(split_count):
camera_np = pssm_plugin.camera_rig.get_camera(index)
tag_mgr.register_camera("shadow", camera_np.node())
render_pipeline.tag_mgr = tag_mgr
return tag_mgr

View File

@ -14,15 +14,23 @@ import time
class ResourceManager:
"""ImGui资源管理器类"""
DEFAULT_ASSET_SUBDIRS = (
"Models",
"Textures",
"Audio",
"Video",
"UI",
"Scripts",
)
def __init__(self, world):
self.world = world
self.project_root = Path(__file__).resolve().parent.parent
self.project_path: Optional[Path] = None
# 当前浏览路径默认从Resources目录开始
self.current_path = self.project_root / "Resources"
if not self.current_path.exists():
self.current_path = self.project_root
# 当前浏览路径,默认从统一的 Assets 结构开始
self.current_path = self._get_default_root_path()
# 历史记录,用于前进后退导航
self.navigation_history: List[Path] = [self.current_path]
@ -55,6 +63,45 @@ class ResourceManager:
# 文件图标映射Unicode Emoji
self._init_icon_map()
def _get_default_root_path(self) -> Path:
assets_root = None
project_manager = getattr(self.world, "project_manager", None)
current_project_path = getattr(project_manager, "current_project_path", None)
if current_project_path:
assets_root = Path(current_project_path) / "Assets"
elif self.project_path:
assets_root = self.project_path / "Assets"
else:
assets_root = self.project_root / "Assets"
if assets_root:
self._ensure_assets_structure(assets_root)
if assets_root.exists():
return assets_root.resolve()
legacy_resources = self.project_root / "Resources"
if legacy_resources.exists():
return legacy_resources.resolve()
return self.project_root.resolve()
def _ensure_assets_structure(self, assets_root: Path):
"""确保资源浏览器默认使用统一的 Assets 目录结构。"""
try:
assets_root.mkdir(parents=True, exist_ok=True)
for subdir in self.DEFAULT_ASSET_SUBDIRS:
(assets_root / subdir).mkdir(parents=True, exist_ok=True)
except OSError:
# 目录创建失败时,保留后续兼容回退逻辑。
pass
def set_project_path(self, project_path: str):
self.project_path = Path(project_path).resolve() if project_path else None
self.current_path = self._get_default_root_path()
self.navigation_history = [self.current_path]
self.history_index = 0
self.selected_files.clear()
self.focused_file = None
def _init_icon_map(self):
"""初始化文件图标映射使用PNG图标文件"""
@ -389,6 +436,12 @@ class ResourceManager:
shutil.copytree(src, dst)
else:
shutil.copy2(src, dst)
if src.suffix.lower() == '.fbx':
fbm_src = src.with_name(src.stem + '.fbm')
if fbm_src.exists() and fbm_src.is_dir():
fbm_dst = destination_root / fbm_src.name
if not fbm_dst.exists():
shutil.copytree(fbm_src, fbm_dst)
imported.append(dst)
except Exception as e:
errors.append(f"导入失败 {src}: {e}")

View File

@ -72,7 +72,7 @@ class ScriptBase(ABC):
class ScriptComponent:
"""脚本组件 - 挂载到游戏对象上的脚本实例"""
def __init__(self, script_instance: ScriptBase, game_object, script_manager):
def __init__(self, script_instance: ScriptBase, game_object, script_manager, script_key: Optional[str] = None):
self.script_instance = script_instance
self.game_object = game_object
self.script_manager = script_manager
@ -80,6 +80,7 @@ class ScriptComponent:
# 保存脚本名称便于UI显示
self.script_name = script_instance.__class__.__name__
self.script_key = script_key or self.script_name
# 设置脚本实例的引用
script_instance.gameObject = game_object
@ -248,7 +249,7 @@ class ScriptLoader:
# 移除所有使用此脚本的组件
components_to_remove = []
for component in self.script_manager.engine.script_components:
if component.script_instance.__class__.__name__ == script_name:
if self.script_manager._script_matches(component, script_name):
components_to_remove.append(component)
for component in components_to_remove:
@ -265,6 +266,24 @@ class ScriptLoader:
del self.script_classes[script_name]
print(f"✓ 脚本已卸载: {script_name}")
def clear(self, unload_components: bool = False):
"""清空当前加载的脚本缓存。"""
if unload_components:
for script_name in list(self.loaded_modules.keys()):
try:
self.unload_script(script_name)
except Exception as e:
print(f"卸载脚本失败 {script_name}: {e}")
else:
for module in list(self.loaded_modules.values()):
module_name = getattr(module, "__name__", "")
if module_name and module_name in sys.modules:
del sys.modules[module_name]
self.loaded_modules.clear()
self.script_classes.clear()
self.file_mtimes.clear()
def reload_script(self, script_path: str) -> Optional[type]:
"""重新加载脚本(热重载)"""
@ -312,7 +331,7 @@ class ScriptLoader:
scripts_dir = self.script_manager.scripts_directory
if os.path.exists(scripts_dir):
for file_name in os.listdir(scripts_dir):
if file_name.endswith('.py'):
if file_name.endswith(('.py', '.pyc')):
base_name = os.path.splitext(file_name)[0]
if base_name == script_name:
return os.path.join(scripts_dir, file_name)
@ -417,7 +436,7 @@ class ScriptManager:
self.script_templates: Dict[str, type] = {} # 脚本名 -> 脚本类
# 脚本目录
self.scripts_directory = "scripts"
self.scripts_directory = self._normalize_scripts_directory("scripts")
self._ensure_scripts_directory()
# 热重载监控
@ -425,6 +444,159 @@ class ScriptManager:
self.hot_reload_task = None
print("✓ 脚本管理系统初始化完成")
def _normalize_scripts_directory(self, directory: str) -> str:
directory = directory or "scripts"
return os.path.normpath(os.path.abspath(directory))
def get_project_path(self) -> Optional[str]:
project_manager = getattr(self.world, "project_manager", None)
project_path = getattr(project_manager, "current_project_path", None)
if project_path:
return os.path.normpath(project_path)
project_path = getattr(self.world, "project_path", None)
if project_path:
return os.path.normpath(project_path)
return None
def get_project_scripts_directory(self, project_path: Optional[str] = None) -> Optional[str]:
project_path = project_path or self.get_project_path()
if not project_path:
return None
return os.path.normpath(os.path.join(project_path, "Assets", "Scripts"))
def get_script_relative_path(self, script_path: str) -> str:
if not script_path:
return ""
script_path = os.path.normpath(os.path.abspath(script_path))
project_path = self.get_project_path()
if not project_path:
return ""
try:
relative_path = os.path.relpath(script_path, project_path)
except ValueError:
return ""
if relative_path.startswith(".."):
return ""
return relative_path.replace("\\", "/")
def resolve_script_path(self, script_info: Dict[str, Any]) -> str:
if not isinstance(script_info, dict):
return ""
project_path = self.get_project_path()
candidates = []
script_guid = str(script_info.get("script_guid", "") or "").strip()
if script_guid:
try:
project_manager = getattr(self.world, "project_manager", None)
asset_database = project_manager.get_asset_database() if project_manager and hasattr(project_manager, "get_asset_database") else None
asset_record = asset_database.get_asset(script_guid) if asset_database else {}
asset_path = str(asset_record.get("asset_path", "") or "")
if asset_path and project_path:
asset_abs_path = os.path.normpath(os.path.join(project_path, asset_path.replace("/", os.sep)))
candidates.append(asset_abs_path)
except Exception:
pass
for key in ("project_relative_path", "relative_path", "path", "file"):
raw_value = str(script_info.get(key, "") or "").strip()
if not raw_value:
continue
normalized_value = raw_value.replace("/", os.sep)
if os.path.isabs(normalized_value):
candidates.append(os.path.normpath(normalized_value))
continue
if project_path:
candidates.append(os.path.normpath(os.path.join(project_path, normalized_value)))
candidates.append(os.path.normpath(os.path.join(self.scripts_directory, normalized_value)))
lower_value = normalized_value.lower()
if not lower_value.endswith((".py", ".pyc")):
candidates.append(os.path.normpath(os.path.join(self.scripts_directory, f"{normalized_value}.py")))
candidates.append(os.path.normpath(os.path.join(self.scripts_directory, f"{normalized_value}.pyc")))
elif lower_value.endswith(".py"):
candidates.append(os.path.normpath(os.path.join(self.scripts_directory, f"{os.path.splitext(normalized_value)[0]}.pyc")))
elif lower_value.endswith(".pyc"):
candidates.append(os.path.normpath(os.path.join(self.scripts_directory, f"{os.path.splitext(normalized_value)[0]}.py")))
script_name = str(script_info.get("name", "") or "").strip()
if script_name:
candidates.append(os.path.normpath(os.path.join(self.scripts_directory, f"{script_name}.py")))
candidates.append(os.path.normpath(os.path.join(self.scripts_directory, f"{script_name}.pyc")))
seen = set()
for candidate in candidates:
if not candidate or candidate in seen:
continue
seen.add(candidate)
if os.path.exists(candidate):
return candidate
if script_name:
return self.loader.find_script_file(script_name) or ""
return ""
def build_script_reference(self, script_name: str, script_file: str = "") -> Dict[str, Any]:
reference = {"name": script_name}
resolved_script_path = script_file or self.loader.find_script_file(script_name) or ""
if resolved_script_path:
resolved_script_path = os.path.normpath(os.path.abspath(resolved_script_path))
relative_path = self.get_script_relative_path(resolved_script_path)
if relative_path:
reference["project_relative_path"] = relative_path
reference["file"] = relative_path
else:
reference["file"] = resolved_script_path
try:
project_manager = getattr(self.world, "project_manager", None)
if project_manager and hasattr(project_manager, "register_project_asset"):
asset_record = project_manager.register_project_asset(resolved_script_path)
if asset_record and asset_record.get("guid"):
reference["script_guid"] = asset_record["guid"]
except Exception:
pass
return reference
def set_scripts_directory(
self,
directory: str,
*,
create: bool = True,
reload_scripts: bool = True,
) -> str:
normalized_directory = self._normalize_scripts_directory(directory)
if normalized_directory == self.scripts_directory and (
os.path.exists(normalized_directory) or not create
):
return self.scripts_directory
self.scripts_directory = normalized_directory
if create:
self._ensure_scripts_directory()
self.loader.clear(unload_components=False)
self.script_templates.clear()
if reload_scripts and os.path.exists(self.scripts_directory):
self.load_all_scripts_from_directory()
print(f"✓ 当前脚本目录已切换到: {self.scripts_directory}")
return self.scripts_directory
def _ensure_scripts_directory(self):
"""确保脚本目录存在"""
@ -510,6 +682,19 @@ class ExampleScript(ScriptBase):
self.engine.stop_engine()
self.stop_hot_reload()
print("✓ 脚本系统已停止")
def reset_scene_state(self):
"""Clear all mounted script components before loading/replacing a scene."""
try:
for component in list(self.engine.script_components):
try:
self.engine.remove_script_component(component)
except Exception as e:
print(f"移除脚本组件失败: {e}")
self.object_scripts.clear()
print("✓ 脚本场景状态已清空")
except Exception as e:
print(f"清空脚本场景状态失败: {e}")
def start_hot_reload(self):
"""启动热重载监控"""
@ -534,7 +719,11 @@ class ExampleScript(ScriptBase):
def create_script_file(self, script_name: str, template: str = "basic") -> str:
"""创建新的脚本文件"""
script_path = os.path.join(self.scripts_directory, f"{script_name}.py")
script_base_name = os.path.splitext(script_name.strip())[0]
script_path = os.path.join(self.scripts_directory, f"{script_base_name}.py")
if not os.path.exists(self.scripts_directory):
os.makedirs(self.scripts_directory)
if os.path.exists(script_path):
print(f"脚本文件已存在: {script_path}")
@ -542,21 +731,33 @@ class ExampleScript(ScriptBase):
# 根据模板创建脚本
if template == "basic":
script_content = self._get_basic_script_template(script_name)
script_content = self._get_basic_script_template(script_base_name)
elif template == "movement":
script_content = self._get_movement_script_template(script_name)
script_content = self._get_movement_script_template(script_base_name)
else:
script_content = self._get_basic_script_template(script_name)
script_content = self._get_basic_script_template(script_base_name)
with open(script_path, 'w', encoding='utf-8') as f:
f.write(script_content)
print(f"✓ 创建脚本文件: {script_path}")
return script_path
def _build_script_class_name(self, script_name: str) -> str:
normalized_parts = []
for raw_part in script_name.replace('-', '_').split('_'):
part = ''.join(ch for ch in raw_part if ch.isalnum())
if part:
normalized_parts.append(part.capitalize())
class_name = ''.join(normalized_parts) or "GeneratedScript"
if class_name[0].isdigit():
class_name = f"Script{class_name}"
return class_name
def _get_basic_script_template(self, script_name: str) -> str:
"""获取基础脚本模板"""
class_name = ''.join(word.capitalize() for word in script_name.split('_'))
class_name = self._build_script_class_name(script_name)
return f'''#!/usr/bin/env python3
# -*- coding: utf-8 -*-
@ -590,7 +791,7 @@ class {class_name}(ScriptBase):
def _get_movement_script_template(self, script_name: str) -> str:
"""获取移动脚本模板"""
class_name = ''.join(word.capitalize() for word in script_name.split('_'))
class_name = self._build_script_class_name(script_name)
return f'''#!/usr/bin/env python3
# -*- coding: utf-8 -*-
@ -643,19 +844,32 @@ class {class_name}(ScriptBase):
"""从目录加载所有脚本"""
if directory is None:
directory = self.scripts_directory
else:
directory = self._normalize_scripts_directory(directory)
if not os.path.exists(directory):
print(f"脚本目录不存在: {directory}")
return []
loaded_scripts = []
for filename in os.listdir(directory):
if filename.endswith('.py') and not filename.startswith('__'):
script_path = os.path.join(directory, filename)
script_class = self.load_script_from_file(script_path)
if script_class:
script_name = os.path.splitext(filename)[0]
loaded_scripts.append(script_name)
seen_script_names = set()
for filename in sorted(os.listdir(directory)):
if filename.startswith('__') or not filename.endswith(('.py', '.pyc')):
continue
script_name = os.path.splitext(filename)[0]
if script_name in seen_script_names:
continue
preferred_path = os.path.join(directory, f"{script_name}.py")
script_path = preferred_path if os.path.exists(preferred_path) else os.path.join(directory, filename)
if not os.path.exists(script_path):
continue
script_class = self.load_script_from_file(script_path)
if script_class:
loaded_scripts.append(script_name)
seen_script_names.add(script_name)
print(f"✓ 从目录 {directory} 加载了 {len(loaded_scripts)} 个脚本")
return loaded_scripts
@ -675,7 +889,7 @@ class {class_name}(ScriptBase):
script_instance = script_class()
# 创建脚本组件
script_component = ScriptComponent(script_instance, game_object, self)
script_component = ScriptComponent(script_instance, game_object, self, script_key=script_name)
# 添加到对象的脚本列表
if game_object not in self.object_scripts:
@ -702,7 +916,7 @@ class {class_name}(ScriptBase):
removed = False
for component in script_components[:]: # 复制列表以避免修改时出错
if component.script_instance.__class__.__name__ == script_name:
if self._script_matches(component, script_name):
# 从引擎移除
self.engine.remove_script_component(component)
# 从对象脚本列表移除
@ -721,6 +935,13 @@ class {class_name}(ScriptBase):
return removed
def _script_matches(self, component: ScriptComponent, script_identifier: str) -> bool:
return script_identifier in {
getattr(component, "script_key", None),
getattr(component, "script_name", None),
component.script_instance.__class__.__name__,
}
def _update_node_script_tags_after_removal(self, game_object, removed_script_name):
"""在移除脚本后更新节点标签"""
try:
@ -739,13 +960,8 @@ class {class_name}(ScriptBase):
script_info_list = []
for script_component in remaining_scripts:
script_name = script_component.script_name
script_class = script_component.script_instance.__class__
script_file = self.loader.find_script_file(script_name) or ""
script_info_list.append({
"name": script_name,
"file": script_file
})
script_info_list.append(self.build_script_reference(script_name, script_file))
import json
game_object.setTag("has_scripts", "true")
@ -763,7 +979,7 @@ class {class_name}(ScriptBase):
"""获取对象上的特定脚本"""
scripts = self.get_scripts_on_object(game_object)
for script in scripts:
if script.script_instance.__class__.__name__ == script_name:
if self._script_matches(script, script_name):
return script
return None
@ -783,7 +999,7 @@ class {class_name}(ScriptBase):
"name": script_name,
"class": script_class,
"doc": script_class.__doc__,
"file": inspect.getfile(script_class) if hasattr(script_class, '__file__') else None,
"file": self.loader.find_script_file(script_name) or inspect.getsourcefile(script_class),
"methods": [method for method in dir(script_class) if not method.startswith('_')]
}
@ -793,6 +1009,18 @@ class {class_name}(ScriptBase):
if script_info and script_info["file"]:
return self.loader.reload_script(script_info["file"]) is not None
return False
def set_hot_reload_enabled(self, enabled: bool):
"""切换热重载并同步后台监控任务。"""
enabled = bool(enabled)
if self.hot_reload_enabled == enabled:
return
self.hot_reload_enabled = enabled
if enabled:
self.start_hot_reload()
else:
self.stop_hot_reload()
# ==================== 调试功能 ====================
@ -858,4 +1086,4 @@ def get_script_api():
__all__ = [
'ScriptBase', 'ScriptComponent', 'ScriptEngine',
'ScriptLoader', 'ScriptAPI', 'ScriptManager'
]
]

File diff suppressed because it is too large Load Diff

303
core/selection_outline.py Normal file
View File

@ -0,0 +1,303 @@
from direct.task.TaskManagerGlobal import taskMgr
from panda3d.core import (
BitMask32,
Camera,
FrameBufferProperties,
GraphicsOutput,
GraphicsPipe,
NodePath,
Shader,
Texture,
Vec4,
WindowProperties,
)
class SelectionOutlineManager:
"""Selection mask manager feeding RenderPipeline SelectionOutlineStage."""
OUTLINE_PREFIX = "selectionOutline"
def __init__(
self,
app,
enabled=True,
outline_color=Vec4(1.0, 0.55, 0.0, 1.0),
outline_width_px=2.0,
fill_alpha=0.0,
max_targets=128,
):
self.app = app
self.enabled = bool(enabled)
self.outline_color = Vec4(outline_color)
self.outline_width_px = max(0.0, float(outline_width_px))
self.fill_alpha = max(0.0, min(1.0, float(fill_alpha)))
self.max_targets = max(1, int(max_targets))
self._task_name = "selection_outline_mask_sync"
self._tracked = [] # [(source_np, clone_np), ...]
self._stage_missing_warned = False
self._mask_root = NodePath(f"{self.OUTLINE_PREFIX}_mask_root")
self._mask_buffer = None
self._mask_texture = None
self._mask_cam = None
self._mask_cam_np = None
self._mask_shader = self._build_mask_shader()
self._buffer_size = (0, 0)
@staticmethod
def _is_empty(np):
if not np:
return True
if hasattr(np, "isEmpty"):
return np.isEmpty()
if hasattr(np, "is_empty"):
return np.is_empty()
return False
@classmethod
def is_outline_node(cls, node_path):
if not node_path or cls._is_empty(node_path):
return False
name = node_path.getName() if hasattr(node_path, "getName") else ""
if name.startswith(cls.OUTLINE_PREFIX):
return True
try:
if node_path.hasPythonTag("selection_outline"):
return True
except Exception:
pass
return False
def set_enabled(self, enabled):
self.enabled = bool(enabled)
if not self.enabled:
self.clear()
self._apply_stage_inputs()
def set_targets(self, targets):
if not self.enabled:
self.clear()
self._apply_stage_inputs()
return
self._ensure_mask_resources()
self.clear()
if not targets:
self._apply_stage_inputs()
return
seen = set()
valid = []
for target in targets:
if self._is_empty(target) or self.is_outline_node(target):
continue
key = str(target)
if key in seen:
continue
seen.add(key)
valid.append(target)
if len(valid) >= self.max_targets:
break
for source in valid:
self._clone_target(source)
if self._tracked:
self._start_sync_task()
self._sync_once()
print(f"[SelectionOutline] targets={len(self._tracked)} active")
else:
print("[SelectionOutline] no valid targets for outline")
self._apply_stage_inputs()
def clear(self):
self._stop_sync_task()
for _, clone_np in self._tracked:
if not self._is_empty(clone_np):
clone_np.removeNode()
self._tracked = []
self._apply_stage_inputs()
def cleanup(self):
self.clear()
self._destroy_mask_resources()
def _build_mask_shader(self):
return Shader.make(
Shader.SL_GLSL,
"""
#version 430
in vec4 p3d_Vertex;
uniform mat4 p3d_ModelViewProjectionMatrix;
void main() {
gl_Position = p3d_ModelViewProjectionMatrix * p3d_Vertex;
}
""",
"""
#version 430
out vec4 result;
void main() {
result = vec4(1.0, 1.0, 1.0, 1.0);
}
""",
)
def _clone_target(self, source):
try:
clone_np = source.copyTo(self._mask_root)
if self._is_empty(clone_np):
return
if not self._node_has_geom(clone_np):
clone_np.removeNode()
return
clone_np.setName(f"{self.OUTLINE_PREFIX}_{source.getName()}")
clone_np.setPythonTag("selection_outline", True)
clone_np.setCollideMask(BitMask32.allOff())
clone_np.setMat(self.app.render, source.getMat(self.app.render))
self._tracked.append((source, clone_np))
except Exception as exc:
print(f"[SelectionOutline] clone failed: {exc}")
def _node_has_geom(self, np):
if self._is_empty(np):
return False
try:
node = np.node()
if node and hasattr(node, "isGeomNode") and node.isGeomNode():
return True
except Exception:
pass
try:
return not np.find("**/+GeomNode").isEmpty()
except Exception:
return False
def _start_sync_task(self):
taskMgr.remove(self._task_name)
taskMgr.add(self._sync_task, self._task_name)
def _stop_sync_task(self):
taskMgr.remove(self._task_name)
def _sync_task(self, task):
self._sync_once()
return task.cont
def _sync_once(self):
if not self.enabled:
self._apply_stage_inputs()
return
self._ensure_mask_resources()
alive = []
for source, clone_np in self._tracked:
if self._is_empty(source) or self._is_empty(clone_np):
if not self._is_empty(clone_np):
clone_np.removeNode()
continue
clone_np.setMat(self.app.render, source.getMat(self.app.render))
alive.append((source, clone_np))
self._tracked = alive
self._apply_stage_inputs()
def _get_stage(self):
rp = getattr(self.app, "render_pipeline", None)
if not rp or not getattr(rp, "stage_mgr", None):
return None
return rp.stage_mgr.get_stage("SelectionOutlineStage")
def _apply_stage_inputs(self):
stage = self._get_stage()
if not stage:
if not self._stage_missing_warned:
print("[SelectionOutline] SelectionOutlineStage not found; plugin may be disabled.")
self._stage_missing_warned = True
return
self._stage_missing_warned = False
stage.set_outline_style(
color=self.outline_color,
width_px=self.outline_width_px,
fill_alpha=self.fill_alpha,
)
stage.set_mask_texture(self._mask_texture)
stage.set_enabled_outline(self.enabled and bool(self._tracked))
def _get_window_size(self):
if not getattr(self.app, "win", None):
return 1, 1
return max(1, self.app.win.getXSize()), max(1, self.app.win.getYSize())
def _ensure_mask_resources(self):
size = self._get_window_size()
if size != self._buffer_size:
self._destroy_mask_resources()
self._buffer_size = size
if self._mask_buffer:
return
if not getattr(self.app, "graphicsEngine", None) or not getattr(self.app, "win", None):
return
w, h = self._buffer_size
win_props = WindowProperties()
win_props.setSize(w, h)
fb_props = FrameBufferProperties()
fb_props.setRgbaBits(8, 8, 8, 8)
fb_props.setDepthBits(24)
self._mask_buffer = self.app.graphicsEngine.make_output(
self.app.pipe,
"selection_outline_mask",
-80,
fb_props,
win_props,
GraphicsPipe.BFRefuseWindow,
self.app.win.getGsg(),
self.app.win,
)
if not self._mask_buffer:
print("[SelectionOutline] failed to create mask buffer")
return
self._mask_texture = Texture("selection_outline_mask_tex")
self._mask_texture.setMinfilter(Texture.FTNearest)
self._mask_texture.setMagfilter(Texture.FTNearest)
self._mask_buffer.addRenderTexture(self._mask_texture, GraphicsOutput.RTMBindOrCopy)
self._mask_buffer.setClearColor(Vec4(0, 0, 0, 0))
self._mask_buffer.setClearColorActive(True)
self._mask_buffer.setActive(True)
self._mask_cam = Camera("selection_outline_mask_camera")
self._mask_cam.setScene(self._mask_root)
self._mask_cam.setLens(self.app.camLens)
self._mask_cam_np = self.app.cam.attachNewNode(self._mask_cam)
dr = self._mask_buffer.makeDisplayRegion()
dr.setCamera(self._mask_cam_np)
state_np = NodePath("selection_outline_mask_state")
state_np.setShader(self._mask_shader, 10000)
state_np.setLightOff(1)
state_np.setMaterialOff(1)
state_np.setTextureOff(1)
state_np.setColorOff(1)
self._mask_cam.setInitialState(state_np.getState())
def _destroy_mask_resources(self):
if self._mask_cam_np and not self._is_empty(self._mask_cam_np):
self._mask_cam_np.removeNode()
self._mask_cam_np = None
self._mask_cam = None
if self._mask_buffer and getattr(self.app, "graphicsEngine", None):
try:
self.app.graphicsEngine.removeWindow(self._mask_buffer)
except Exception:
pass
self._mask_buffer = None
self._mask_texture = None

View File

@ -3,29 +3,25 @@ import time
import urllib
from panda3d.core import GeoMipTerrain, PNMImage, Texture, Vec3, NodePath
from panda3d.core import Filename, Material, ColorAttrib, AmbientLight, DirectionalLight
import os
from scene import util
from panda3d.core import Filename, Material, ColorAttrib, AmbientLight, DirectionalLight
import os
from scene import util
from core.editor_context import get_editor_context
class TerrainManager:
"""地形管理类"""
def __init__(self, world):
self.world = world
self.terrains = []
def __init__(self, world):
self.world = world
self._editor_context = get_editor_context(world)
self.terrains = []
# core/terrain_manager.py
def _get_tree_widget(self):
"""安全获取树形控件"""
try:
if (hasattr(self.world, 'interface_manager') and
hasattr(self.world.interface_manager, 'treeWidget')):
return self.world.interface_manager.treeWidget
except AttributeError:
pass
return None
def _get_tree_widget(self):
"""安全获取树形控件"""
return self._editor_context.get_tree_widget()
def createTerrainFromHeightMap(self, heightmap_path, scale=(1, 1, 1)):
"""从高度图创建地形"""
@ -133,30 +129,22 @@ class TerrainManager:
terrain_node.setPythonTag("selectable", True)
# 保存地形信息(包括高度图的副本)
terrain_info = {
'terrain': terrain,
'node': terrain_node,
'heightmap': heightmap_path,
'heightfield': height_image, # 保存高度图副本
terrain_info = {
'terrain': terrain,
'node': terrain_node,
'heightmap': heightmap_path,
'heightfield': height_image, # 保存高度图副本
'scale': scale,
'name': node_name
}
}
self.terrains.append(terrain_info)
terrain_node.setPythonTag("terrain_info", terrain_info)
parent_name = parent_item.text(0) if parent_item else "root"
print(f"✅ 为 {parent_name} 创建高度图地形: {terrain_name}")
self.terrains.append(terrain_info)
parent_name = parent_item.text(0) if parent_item else "root"
print(f"✅ 为 {parent_name} 创建高度图地形: {terrain_name}")
# 在Qt树形控件中添加对应节点
qt_item = None
if tree_widget and parent_item:
qt_item = tree_widget.add_node_to_tree_widget(terrain_node, parent_item, "TERRAIN_NODE")
if qt_item:
created_terrains.append((terrain_node, qt_item))
else:
created_terrains.append((terrain_node, None))
print("⚠️ Qt树节点添加跳过或失败但Panda3D对象已创建")
created_terrains.append((terrain_node, None))
# 添加到场景管理器ImGui环境使用
if hasattr(self.world, 'scene_manager') and self.world.scene_manager:
@ -179,13 +167,8 @@ class TerrainManager:
print("❌ 没有成功创建任何高度图地形")
return None
# 选中最后创建的光源
if created_terrains:
last_light_np, last_qt_item = created_terrains[-1]
if last_qt_item:
tree_widget.setCurrentItem(last_qt_item)
# 更新选择和属性面板
tree_widget.update_selection_and_properties(last_light_np, last_qt_item)
if created_terrains:
pass
print(f"🎉 总共创建了 {len(created_terrains)} 个高度图地形")
@ -282,30 +265,22 @@ class TerrainManager:
terrain_node.setPythonTag("selectable", True)
# 保存地形信息(包括高度图)
terrain_info = {
'terrain': terrain,
'node': terrain_node,
'heightmap': None,
'heightfield': height_image, # 保存高度图
terrain_info = {
'terrain': terrain,
'node': terrain_node,
'heightmap': None,
'heightfield': height_image, # 保存高度图
'scale': (size[0], size[1], 50),
'name': node_name
}
}
self.terrains.append(terrain_info)
terrain_node.setPythonTag("terrain_info", terrain_info)
parent_name = parent_item.text(0) if parent_item else "root"
print(f"✅ 为 {parent_name} 创建平面地形: {terrain_name}")
self.terrains.append(terrain_info)
parent_name = parent_item.text(0) if parent_item else "root"
print(f"✅ 为 {parent_name} 创建平面地形: {terrain_name}")
# 在Qt树形控件中添加对应节点
qt_item = None
if tree_widget and parent_item:
qt_item = tree_widget.add_node_to_tree_widget(terrain_node, parent_item, "TERRAIN_NODE")
if qt_item:
created_terrains.append((terrain_node, qt_item))
else:
created_terrains.append((terrain_node, None))
print("⚠️ Qt树节点添加跳过或失败但Panda3D对象已创建")
created_terrains.append((terrain_node, None))
# 添加到场景管理器ImGui环境使用
if hasattr(self.world, 'scene_manager') and self.world.scene_manager:
@ -328,13 +303,8 @@ class TerrainManager:
print("❌ 没有成功创建任何平面地形")
return None
# 选中最后创建的光源
if created_terrains:
last_light_np, last_qt_item = created_terrains[-1]
if last_qt_item:
tree_widget.setCurrentItem(last_qt_item)
# 更新选择和属性面板
tree_widget.update_selection_and_properties(last_light_np, last_qt_item)
if created_terrains:
pass
print(f"🎉 总共创建了 {len(created_terrains)} 个平面地形")

View File

@ -9,11 +9,12 @@ class ToolManager:
def setCurrentTool(self, tool):
"""设置当前工具"""
self.currentTool = tool
print(f"\n=== 工具切换 ===")
print(f"当前工具: {tool}")
print(f"选中节点: {self.world.selection.selectedNode.getName() if self.world.selection.selectedNode else ''}")
self.currentTool = tool
print(f"\n=== 工具切换 ===")
print(f"当前工具: {tool}")
selected_node = self.world._get_selection_node() if hasattr(self.world, "_get_selection_node") else self.world.selection.selectedNode
print(f"选中节点: {selected_node.getName() if selected_node else ''}")
# 根据工具类型启用对应的方法
if tool == "选择":

View File

@ -91,10 +91,10 @@ class VRTestMode:
self.vr_manager._disable_main_cam()
# 设置高帧率用于测试
if hasattr(self.vr_manager.world, 'qtWidget') and self.vr_manager.world.qtWidget:
if hasattr(self.vr_manager.world.qtWidget, 'synchronizer'):
self.vr_manager.world.qtWidget.synchronizer.setInterval(int(1000/144))
print("✓ 测试模式:Qt Timer设置为144Hz")
host_widget = getattr(self.vr_manager.world, "host_widget", None)
if host_widget and hasattr(host_widget, "synchronizer"):
host_widget.synchronizer.setInterval(int(1000 / 144))
print("✓ 测试模式:渲染同步器设置为144Hz")
# 初始化测试显示系统
if not self._initialize_test_display():

View File

@ -1893,12 +1893,11 @@ class VRManager(DirectObject):
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渲染节奏")
# 🚀 动态调整宿主同步器频率以支持VR
host_widget = getattr(self.world, "host_widget", None)
if host_widget and hasattr(host_widget, "synchronizer"):
host_widget.synchronizer.setInterval(int(1000 / 144))
print("✓ 渲染同步器调整为144Hz让OpenVR控制VR渲染节奏")
# 🔧 关键修复检测并重建缺失的手柄visualizer
# 当渲染模式切换时visualizer可能被清理但控制器对象仍存在
@ -1952,11 +1951,11 @@ class VRManager(DirectObject):
# 恢复主相机
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")
# 恢复渲染同步器到60FPS
host_widget = getattr(self.world, "host_widget", None)
if host_widget and hasattr(host_widget, "synchronizer"):
host_widget.synchronizer.setInterval(int(1000 / 60))
print("渲染同步器恢复为60Hz")
print("✅ VR模式已禁用手柄模型已隐藏")

View File

@ -9,7 +9,8 @@ from direct.actor.Actor import Actor
warnings.filterwarnings("ignore", category=DeprecationWarning)
from panda3d.core import (CardMaker, Vec4, Vec3, AmbientLight, DirectionalLight,
Point3, WindowProperties, Material, LColor, loadPrcFileData)
Point3, WindowProperties, Material, LColor, Shader,
TransparencyAttrib, loadPrcFileData, GraphicsPipeSelection)
from direct.showbase.ShowBase import ShowBase
from direct.showbase.ShowBaseGlobal import globalClock
from scene.scene_manager import SceneManager
@ -24,6 +25,7 @@ from ssbo_component.ssbo_editor import SSBOEditor
# 从渲染管线工具模块导入全局函数
from core.render_pipeline_utils import get_render_pipeline, set_render_pipeline
from core.render_pipeline_tag_state import install_editor_tag_state_manager
# 尝试导入插件管理器(如果存在)
try:
@ -40,11 +42,16 @@ class CoreWorld(ShowBase):
global _global_render_pipeline
# 初始化基础属性
self.qtWidget = None # Qt部件引用用于获取准确的渲染区域尺寸
self.host_widget = None # 外部宿主窗口引用(用于获取准确渲染区域尺寸)
desktop_size = self._get_desktop_window_size()
if not is_fullscreen and (width, height) == (1380, 750) and desktop_size:
width, height = desktop_size
print(f"✓ 使用桌面分辨率启动窗口: {width} x {height}")
# 设置基本配置
loadPrcFileData("", "show-frame-rate-meter 0")
loadPrcFileData("", "window-type onscreen")
loadPrcFileData("", "window-title MetaCore")
loadPrcFileData("", f"win-size {width} {height}")
loadPrcFileData("", "win-fixed-size #f") # 允许窗口调整大小
@ -75,8 +82,17 @@ class CoreWorld(ShowBase):
# 初始化 ShowBase
ShowBase.__init__(self)
try:
props = WindowProperties()
props.setTitle("MetaCore")
self.win.requestProperties(props)
except Exception:
pass
# 创建渲染管线
self.render_pipeline.create(self)
install_editor_tag_state_manager(self.render_pipeline, self)
self._setupForwardTransparencyOverlay()
set_render_pipeline(self.render_pipeline)
# 设置相机
@ -112,6 +128,50 @@ class CoreWorld(ShowBase):
self._setupGround()
self._loadFont()
@staticmethod
def _get_desktop_window_size():
"""Query the current desktop display mode for a better default window size."""
if os.name == "nt":
try:
import ctypes
from ctypes import wintypes
class RECT(ctypes.Structure):
_fields_ = [
("left", wintypes.LONG),
("top", wintypes.LONG),
("right", wintypes.LONG),
("bottom", wintypes.LONG),
]
work_area = RECT()
spi_get_work_area = 0x0030
if ctypes.windll.user32.SystemParametersInfoW(
spi_get_work_area, 0, ctypes.byref(work_area), 0
):
width = int(work_area.right - work_area.left)
height = int(work_area.bottom - work_area.top)
if width > 0 and height > 0:
return width, height
except Exception as e:
print(f"⚠ 获取 Windows 工作区尺寸失败,回退到显示模式尺寸: {e}")
try:
pipe = GraphicsPipeSelection.getGlobalPtr().makeDefaultPipe()
if not pipe:
return None
info = pipe.getDisplayInformation()
current_mode = info.getCurrentDisplayModeIndex()
if current_mode < 0:
return None
width = int(info.getDisplayModeWidth(current_mode))
height = int(info.getDisplayModeHeight(current_mode))
if width > 0 and height > 0:
return width, height
except Exception as e:
print(f"⚠ 获取桌面分辨率失败,继续使用默认窗口尺寸: {e}")
return None
def _handle_transform_error(self):
"""处理TransformState相关的错误"""
try:
@ -122,6 +182,87 @@ class CoreWorld(ShowBase):
except Exception as e:
print(f"清理缓存时出错: {e}")
def _setupForwardTransparencyOverlay(self):
"""Composite the forward transparency result back onto the final frame.
The bundled RP forward merge stage is unreliable in this editor build
during live material edits, but the forward color buffer itself is
correct. We therefore blend that buffer as a fullscreen overlay and use
the scene/forward depth textures to keep transparent objects behind
opaque geometry clipped.
"""
self.forward_transparency_overlay = None
try:
forward_stage = next(
(
stage for stage in self.render_pipeline.stage_mgr.stages
if type(stage).__name__ == "ForwardStage"
),
None,
)
gbuffer_stage = next(
(
stage for stage in self.render_pipeline.stage_mgr.stages
if type(stage).__name__ == "GBufferStage"
),
None,
)
if not forward_stage or not gbuffer_stage:
return
vert = """
#version 330
uniform mat4 p3d_ModelViewProjectionMatrix;
in vec4 p3d_Vertex;
in vec2 p3d_MultiTexCoord0;
out vec2 texcoord;
void main() {
gl_Position = p3d_ModelViewProjectionMatrix * p3d_Vertex;
texcoord = p3d_MultiTexCoord0;
}
"""
frag = """
#version 330
uniform sampler2D ForwardColor;
uniform sampler2D ForwardDepth;
uniform sampler2D SceneDepth;
in vec2 texcoord;
out vec4 o_color;
void main() {
vec4 forward_color = texture(ForwardColor, texcoord);
float alpha = clamp(forward_color.a, 0.0, 1.0);
if (alpha <= 1e-4) {
discard;
}
float forward_depth = texture(ForwardDepth, texcoord).x;
float scene_depth = texture(SceneDepth, texcoord).x;
if (scene_depth > 1e-6 && forward_depth > scene_depth + 1e-5) {
discard;
}
o_color = vec4(forward_color.rgb, alpha);
}
"""
cm = CardMaker("ForwardTransparencyOverlay")
cm.setFrameFullscreenQuad()
overlay = self.render2d.attachNewNode(cm.generate())
overlay.setShader(Shader.make(Shader.SL_GLSL, vert, frag))
overlay.setShaderInput("ForwardColor", forward_stage.target.color_tex)
overlay.setShaderInput("ForwardDepth", forward_stage.target.depth_tex)
overlay.setShaderInput("SceneDepth", gbuffer_stage.target.depth_tex)
overlay.setTransparency(TransparencyAttrib.M_alpha)
overlay.setDepthTest(False)
overlay.setDepthWrite(False)
overlay.setBin("fixed", 1)
self.forward_transparency_overlay = overlay
print("✓ 前向透明叠加层初始化完成")
except Exception as e:
print(f"⚠ 初始化前向透明叠加层失败: {e}")
def _setupResourcePaths(self):
"""设置Panda3D资源搜索路径确保能正确找到Resources文件夹中的模型和贴图"""
try:
@ -184,6 +325,7 @@ class CoreWorld(ShowBase):
print(f"✓ 创建并添加子目录: {subdir}")
# 设置纹理搜索路径
texture_path = None
try:
from panda3d.core import getTexturePath
texture_path = getTexturePath()
@ -193,12 +335,13 @@ class CoreWorld(ShowBase):
# 新版本 Panda3D 中 getTexturePath 可能不可用
print(" 注意: getTexturePath 不可用,使用默认纹理路径")
for subdir in ['textures', 'materials', 'icons']:
subdir_path = os.path.join(resources_dir, subdir)
if os.path.exists(subdir_path):
subdir_filename = Filename.from_os_specific(subdir_path)
if not texture_path.findFile(subdir_filename):
texture_path.appendDirectory(subdir_filename)
if texture_path is not None:
for subdir in ['textures', 'materials', 'icons']:
subdir_path = os.path.join(resources_dir, subdir)
if os.path.exists(subdir_path):
subdir_filename = Filename.from_os_specific(subdir_path)
if not texture_path.findFile(subdir_filename):
texture_path.appendDirectory(subdir_filename)
print(f"✓ 资源路径设置完成")
print(f" 项目根目录: {project_root}")
@ -686,16 +829,17 @@ class CoreWorld(ShowBase):
print(f"警告: 加载中文字体时发生错误: {e}")
self.chinese_font = None
def setQtWidget(self, widget):
"""设置Qt部件引用"""
self.qtWidget = widget
print(f"✓ 设置Qt部件引用: {widget}")
def setHostWidget(self, widget):
"""设置外部宿主窗口引用。"""
self.host_widget = widget
print(f"✓ 设置宿主窗口引用: {widget}")
def getWindowSize(self):
"""获取准确的窗口尺寸"""
if self.qtWidget:
# 优先使用Qt部件的实际尺寸
width, height = self.qtWidget.getActualSize()
widget = self.host_widget
if widget and hasattr(widget, "getActualSize"):
# 优先使用宿主窗口的实际尺寸
width, height = widget.getActualSize()
if width > 0 and height > 0:
return width, height
@ -754,30 +898,28 @@ class CoreWorld(ShowBase):
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)
# if hasattr(self, 'host_widget') and self.host_widget:
# self.host_widget.setCursorHidden(True)
# # 捕获鼠标,使其无法离开窗口
# self.qtWidget.grabMouse()
# self.host_widget.grabMouse()
# except Exception as e:
# print(f"通过 Qt 隐藏光标时出错: {e}")
# print(f"隐藏光标时出错: {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() # 恢复默认光标
# if hasattr(self, 'host_widget') and self.host_widget:
# self.host_widget.setCursorHidden(False) # 恢复默认光标
# # 释放鼠标捕获
# self.qtWidget.releaseMouse()
# self.host_widget.releaseMouse()
# except Exception as e:
# print(f"恢复 Qt 光标时出错: {e}")
# print(f"恢复光标时出错: {e}")
def mouseMoveEvent(self, evt):
"""处理鼠标移动事件 - 只处理相机旋转"""
@ -966,7 +1108,7 @@ class CoreWorld(ShowBase):
return None
# 创建材质编辑器组件
# 使用纯 Panda3D 实现,不依赖 PyQt5
# 使用纯 Panda3D 实现,不依赖外部 GUI 框架
print("材质编辑器组件已创建(使用 Panda3D 原生实现)")
material_widget.setLayout(layout)

File diff suppressed because it is too large Load Diff

View File

@ -24,34 +24,33 @@ Size=832,45
Collapsed=0
[Window][工具栏]
Pos=241,20
Size=908,74
Pos=255,20
Size=1398,383
Collapsed=0
DockId=0x0000000D,0
[Window][场景树]
Pos=0,20
Size=239,468
Size=339,1008
Collapsed=0
DockId=0x00000007,0
[Window][属性面板]
Pos=1151,20
Size=229,730
Pos=1506,20
Size=346,1008
Collapsed=0
DockId=0x00000003,0
DockId=0x00000002,0
[Window][控制台]
Pos=0,490
Size=239,260
Pos=341,629
Size=1163,399
Collapsed=0
DockId=0x00000008,0
DockId=0x00000006,1
[Window][脚本管理]
Pos=1540,20
Size=380,390
Pos=1950,20
Size=610,995
Collapsed=0
DockId=0x00000003,1
[Window][中文显示测试]
Pos=60,60
@ -60,7 +59,7 @@ Collapsed=0
[Window][WindowOverViewport_11111111]
Pos=0,20
Size=1380,730
Size=1852,1008
Collapsed=0
[Window][测试窗口1]
@ -79,28 +78,28 @@ Size=93,65
Collapsed=0
[Window][新建项目]
Pos=760,354
Pos=824,402
Size=400,300
Collapsed=0
[Window][选择路径]
Pos=660,254
Pos=626,264
Size=600,500
Collapsed=0
[Window][打开项目]
Pos=710,304
Pos=676,314
Size=500,400
Collapsed=0
[Window][导入模型]
Pos=660,254
Pos=660,245
Size=600,500
Collapsed=0
[Window][资源管理器]
Pos=241,464
Size=908,286
Pos=341,629
Size=1163,399
Collapsed=0
DockId=0x00000006,0
@ -135,7 +134,7 @@ Size=89,250
Collapsed=0
[Window][颜色选择器]
Pos=810,304
Pos=878,354
Size=300,400
Collapsed=0
@ -150,10 +149,10 @@ Size=101,226
Collapsed=0
[Window][LUI编辑器]
Pos=1628,412
Size=292,597
Pos=2214,20
Size=346,1331
Collapsed=0
DockId=0x00000004,0
DockId=0x00000002,1
[Window][LUI测试控制面板]
Pos=6,10
@ -201,22 +200,39 @@ Size=600,400
Collapsed=0
[Window][Web面板]
Pos=373,98
Size=942,580
Pos=1438,20
Size=610,748
Collapsed=0
DockId=0x00000005,2
[Window][项目另存为]
Pos=794,432
Size=460,240
Collapsed=0
[Window][打包项目]
Pos=754,392
Size=540,320
Collapsed=0
[Window][关于 EG]
Pos=794,422
Size=460,260
Collapsed=0
[Window][关于 MetaCore]
Pos=790,422
Size=460,260
Collapsed=0
[Docking][Data]
DockSpace ID=0x08BD597D Window=0x1BBC0F80 Pos=0,20 Size=1380,730 Split=X
DockNode ID=0x00000001 Parent=0x08BD597D SizeRef=1689,989 Split=X
DockNode ID=0x00000009 Parent=0x00000001 SizeRef=239,989 Split=Y Selected=0xE0015051
DockNode ID=0x00000007 Parent=0x00000009 SizeRef=271,634 Selected=0xE0015051
DockNode ID=0x00000008 Parent=0x00000009 SizeRef=271,353 Selected=0x5428E753
DockNode ID=0x0000000A Parent=0x00000001 SizeRef=1448,989 Split=Y
DockNode ID=0x0000000D Parent=0x0000000A SizeRef=1318,74 HiddenTabBar=1 Selected=0x43A39006
DockNode ID=0x0000000E Parent=0x0000000A SizeRef=1318,913 Split=Y
DockNode ID=0x00000005 Parent=0x0000000E SizeRef=1341,625 CentralNode=1
DockNode ID=0x00000006 Parent=0x0000000E SizeRef=1341,286 Selected=0x3A2E05C3
DockNode ID=0x00000002 Parent=0x08BD597D SizeRef=229,989 Split=Y Selected=0x3188AB8D
DockNode ID=0x00000003 Parent=0x00000002 SizeRef=351,390 Selected=0x5DB6FF37
DockNode ID=0x00000004 Parent=0x00000002 SizeRef=351,597 Selected=0x1EB923B7
DockSpace ID=0x08BD597D Window=0x1BBC0F80 Pos=0,20 Size=1852,1008 Split=X
DockNode ID=0x00000001 Parent=0x08BD597D SizeRef=2212,1012 Split=X
DockNode ID=0x00000007 Parent=0x00000001 SizeRef=339,1084 Selected=0xE0015051
DockNode ID=0x00000008 Parent=0x00000001 SizeRef=1871,1084 Split=Y
DockNode ID=0x00000005 Parent=0x00000008 SizeRef=2048,683 Split=Y
DockNode ID=0x0000000D Parent=0x00000005 SizeRef=1318,383 HiddenTabBar=1 Selected=0x43A39006
DockNode ID=0x0000000E Parent=0x00000005 SizeRef=1318,363 CentralNode=1 Selected=0xE0015051
DockNode ID=0x00000006 Parent=0x00000008 SizeRef=2048,399 Selected=0x3A2E05C3
DockNode ID=0x00000002 Parent=0x08BD597D SizeRef=346,1012 Selected=0x5DB6FF37

1
index_name.txt Normal file
View File

@ -0,0 +1 @@
index-b2d982.boo

1560
main.py

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +0,0 @@
{
"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"
}

354
project/asset_database.py Normal file
View File

@ -0,0 +1,354 @@
"""Asset database and import cache helpers for MetaCore project v2."""
from __future__ import annotations
import hashlib
import json
import os
import shutil
from datetime import datetime
from panda3d.core import Filename
from project.project_schema import (
ASSET_DB_SCHEMA_VERSION,
ProjectLayout,
detect_asset_type,
generate_guid,
get_asset_subdir_for_type,
normalize_path,
relative_project_path,
)
class AssetDatabase:
def __init__(self, project_root: str, world=None):
self.layout = ProjectLayout(project_root)
self.world = world
self.data = {
"schema_version": ASSET_DB_SCHEMA_VERSION,
"assets": {},
"path_to_guid": {},
}
self.ensure_database()
def _get_all_meta_files(self) -> list[str]:
meta_files = []
if not os.path.exists(self.layout.assets_root):
return meta_files
for root, _, files in os.walk(self.layout.assets_root):
for file_name in files:
if file_name.endswith(".meta"):
meta_files.append(os.path.join(root, file_name))
return meta_files
def _rebuild_path_index(self):
rebuilt = {}
for asset_guid, asset_record in (self.data.get("assets", {}) or {}).items():
asset_path = str((asset_record or {}).get("asset_path", "") or "").replace("\\", "/")
if asset_path:
rebuilt[asset_path] = asset_guid
self.data["path_to_guid"] = rebuilt
def _sync_assets_from_meta_scan(self):
assets = self.data.setdefault("assets", {})
changed = False
for meta_path in self._get_all_meta_files():
meta_payload = self._read_meta(meta_path)
asset_guid = str(meta_payload.get("guid", "") or "").strip()
if not asset_guid:
continue
asset_path = meta_path[:-5]
if not os.path.exists(asset_path):
continue
relative_asset_path = relative_project_path(self.layout.project_root, asset_path)
if not relative_asset_path:
continue
asset_type = str(meta_payload.get("asset_type") or detect_asset_type(asset_path))
source_hash = self._hash_file(asset_path)
record = dict(assets.get(asset_guid, {}) or {})
previous_asset_path = str(record.get("asset_path", "") or "")
record.update(
{
"guid": asset_guid,
"asset_path": relative_asset_path,
"asset_type": asset_type,
"meta_path": relative_project_path(self.layout.project_root, meta_path),
"source_hash": source_hash,
"importer": str(meta_payload.get("importer") or f"{asset_type}_importer"),
"import_settings": meta_payload.get("import_settings", {}) or {},
"dependency_guids": meta_payload.get("dependency_guids", []) or [],
"updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
}
)
if asset_type == "model":
self._build_model_import_cache(record)
assets[asset_guid] = record
if previous_asset_path != relative_asset_path:
changed = True
self._rebuild_path_index()
return changed
def ensure_database(self):
os.makedirs(self.layout.library_root, exist_ok=True)
if os.path.exists(self.layout.asset_db_path):
try:
with open(self.layout.asset_db_path, "r", encoding="utf-8") as f:
payload = json.load(f)
if isinstance(payload, dict):
self.data["schema_version"] = payload.get("schema_version", ASSET_DB_SCHEMA_VERSION)
self.data["assets"] = payload.get("assets", {}) or {}
self.data["path_to_guid"] = payload.get("path_to_guid", {}) or {}
except Exception as e:
print(f"⚠️ 读取 AssetDB 失败,已重建为空数据库: {e}")
self._sync_assets_from_meta_scan()
self.save()
def save(self):
os.makedirs(os.path.dirname(self.layout.asset_db_path), exist_ok=True)
with open(self.layout.asset_db_path, "w", encoding="utf-8") as f:
json.dump(self.data, f, ensure_ascii=False, indent=4)
def get_asset(self, asset_guid: str) -> dict:
record = dict(self.data.get("assets", {}).get(asset_guid, {}) or {})
if not record:
self._sync_assets_from_meta_scan()
record = dict(self.data.get("assets", {}).get(asset_guid, {}) or {})
if not record:
return {}
asset_path = record.get("asset_path", "")
asset_abs_path = os.path.join(self.layout.project_root, asset_path.replace("/", os.sep)) if asset_path else ""
if asset_abs_path and not os.path.exists(asset_abs_path):
if self._sync_assets_from_meta_scan():
record = dict(self.data.get("assets", {}).get(asset_guid, {}) or {})
return record
def find_by_relative_path(self, relative_path: str) -> dict:
relative_path = str(relative_path or "").replace("\\", "/")
asset_guid = self.data.get("path_to_guid", {}).get(relative_path)
if not asset_guid:
self._sync_assets_from_meta_scan()
asset_guid = self.data.get("path_to_guid", {}).get(relative_path)
if not asset_guid:
return {}
return self.get_asset(asset_guid)
def find_by_absolute_path(self, asset_path: str) -> dict:
relative_path = relative_project_path(self.layout.project_root, asset_path)
if not relative_path:
return {}
return self.find_by_relative_path(relative_path)
def _hash_file(self, file_path: str) -> str:
file_path = normalize_path(file_path)
digest = hashlib.sha256()
with open(file_path, "rb") as f:
for chunk in iter(lambda: f.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def _read_meta(self, meta_path: str) -> dict:
if not os.path.exists(meta_path):
return {}
try:
with open(meta_path, "r", encoding="utf-8") as f:
payload = json.load(f)
if isinstance(payload, dict):
return payload
except Exception as e:
print(f"⚠️ 读取资源 meta 失败 {meta_path}: {e}")
return {}
def _write_meta(self, meta_path: str, payload: dict):
os.makedirs(os.path.dirname(meta_path), exist_ok=True)
with open(meta_path, "w", encoding="utf-8") as f:
json.dump(payload, f, ensure_ascii=False, indent=4)
def _copy_into_assets(self, source_path: str, preferred_subdir: str = "") -> str:
source_path = normalize_path(source_path)
asset_type = detect_asset_type(source_path)
subdir = preferred_subdir or get_asset_subdir_for_type(asset_type)
target_dir = os.path.join(self.layout.assets_root, subdir)
os.makedirs(target_dir, exist_ok=True)
file_name = os.path.basename(source_path)
target_path = os.path.join(target_dir, file_name)
name_root, name_ext = os.path.splitext(file_name)
index = 1
while os.path.exists(target_path) and self._hash_file(target_path) != self._hash_file(source_path):
target_path = os.path.join(target_dir, f"{name_root}_{index}{name_ext}")
index += 1
if not os.path.exists(target_path):
shutil.copy2(source_path, target_path)
return target_path
def _build_model_import_cache(self, asset_record: dict):
if not self.world or not getattr(self.world, "loader", None):
return
asset_path = os.path.join(self.layout.project_root, asset_record["asset_path"].replace("/", os.sep))
if not os.path.exists(asset_path):
return
cache_dir = self.layout.imported_asset_dir(asset_record["guid"])
os.makedirs(cache_dir, exist_ok=True)
model_bam_path = os.path.join(cache_dir, "model.bam")
hierarchy_path = os.path.join(cache_dir, "hierarchy.json")
materials_path = os.path.join(cache_dir, "materials.json")
import_info_path = os.path.join(cache_dir, "import_info.json")
try:
model_np = self.world.loader.loadModel(Filename.fromOsSpecific(asset_path))
except Exception as e:
print(f"⚠️ 生成模型导入缓存失败 {asset_path}: {e}")
return
if not model_np or model_np.isEmpty():
return
try:
model_np.writeBamFile(Filename.fromOsSpecific(model_bam_path))
except Exception as e:
print(f"⚠️ 写入模型缓存 BAM 失败 {asset_path}: {e}")
return
hierarchy = []
materials = []
def _walk(node, parent_key=""):
try:
node_name = node.getName()
except Exception:
node_name = "node"
try:
children = list(node.getChildren())
except Exception:
children = []
node_key = parent_key
hierarchy.append(
{
"key": node_key,
"name": node_name,
"child_count": len(children),
}
)
for index, child in enumerate(children):
_walk(child, f"{node_key}/{index}" if node_key else str(index))
def _collect_materials(root_np):
for geom_np in root_np.findAllMatches("**/+GeomNode"):
try:
geom_node = geom_np.node()
except Exception:
continue
for geom_index in range(geom_node.getNumGeoms()):
materials.append(
{
"node_name": geom_np.getName(),
"geom_index": geom_index,
"material_name": "",
}
)
_walk(model_np)
_collect_materials(model_np)
with open(hierarchy_path, "w", encoding="utf-8") as f:
json.dump(hierarchy, f, ensure_ascii=False, indent=4)
with open(materials_path, "w", encoding="utf-8") as f:
json.dump(materials, f, ensure_ascii=False, indent=4)
with open(import_info_path, "w", encoding="utf-8") as f:
json.dump(
{
"asset_guid": asset_record["guid"],
"asset_path": asset_record["asset_path"],
"asset_type": asset_record["asset_type"],
"source_hash": asset_record["source_hash"],
"generated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
},
f,
ensure_ascii=False,
indent=4,
)
asset_record["imported_cache"] = {
"root": relative_project_path(self.layout.project_root, cache_dir),
"model_bam": relative_project_path(self.layout.project_root, model_bam_path),
"hierarchy": relative_project_path(self.layout.project_root, hierarchy_path),
"materials": relative_project_path(self.layout.project_root, materials_path),
"import_info": relative_project_path(self.layout.project_root, import_info_path),
}
def register_asset(self, asset_path: str, preferred_subdir: str = "", copy_into_assets: bool = False) -> dict:
asset_path = normalize_path(asset_path)
if not os.path.exists(asset_path):
return {}
if copy_into_assets or not relative_project_path(self.layout.project_root, asset_path).startswith("Assets/"):
asset_path = self._copy_into_assets(asset_path, preferred_subdir=preferred_subdir)
relative_asset_path = relative_project_path(self.layout.project_root, asset_path)
if not relative_asset_path:
return {}
meta_path = self.layout.meta_path_for_asset(asset_path)
meta_payload = self._read_meta(meta_path)
asset_guid = str(meta_payload.get("guid") or generate_guid())
asset_type = str(meta_payload.get("asset_type") or detect_asset_type(asset_path))
source_hash = self._hash_file(asset_path)
record = self.data.get("assets", {}).get(asset_guid, {}) or {}
record.update(
{
"guid": asset_guid,
"asset_path": relative_asset_path,
"asset_type": asset_type,
"meta_path": relative_project_path(self.layout.project_root, meta_path),
"source_hash": source_hash,
"importer": f"{asset_type}_importer",
"import_settings": meta_payload.get("import_settings", {}) or {},
"dependency_guids": meta_payload.get("dependency_guids", []) or [],
"updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
}
)
if asset_type == "model":
self._build_model_import_cache(record)
meta_payload = {
"guid": asset_guid,
"asset_type": asset_type,
"source_hash": source_hash,
"importer": record["importer"],
"import_settings": record["import_settings"],
"dependency_guids": record["dependency_guids"],
}
self._write_meta(meta_path, meta_payload)
self.data.setdefault("assets", {})[asset_guid] = record
self._rebuild_path_index()
self.save()
return dict(record)
def import_asset(self, source_path: str, preferred_subdir: str = "") -> dict:
return self.register_asset(source_path, preferred_subdir=preferred_subdir, copy_into_assets=True)
def ensure_project_assets_registered(self):
self._sync_assets_from_meta_scan()
for root, _, files in os.walk(self.layout.assets_root):
for file_name in files:
if file_name.endswith(".meta"):
continue
asset_path = os.path.join(root, file_name)
self.register_asset(asset_path, copy_into_assets=False)

File diff suppressed because it is too large Load Diff

219
project/project_schema.py Normal file
View File

@ -0,0 +1,219 @@
"""Project layout and schema helpers for MetaCore project v2."""
from __future__ import annotations
import os
import uuid
from dataclasses import dataclass
ENGINE_NAME = "MetaCore"
PROJECT_SCHEMA_VERSION = 2
ASSET_DB_SCHEMA_VERSION = 1
METASCENE_SCHEMA_VERSION = 2
RUNTIME_SCENE_SCHEMA_VERSION = 2
MANIFEST_SCHEMA_VERSION = 1
SCENE_DESCRIPTION_EXTENSION = ".metascene"
MODEL_EXTENSIONS = {
".bam",
".egg",
".obj",
".fbx",
".gltf",
".glb",
".ply",
".stl",
}
TEXTURE_EXTENSIONS = {
".png",
".jpg",
".jpeg",
".bmp",
".tga",
".tif",
".tiff",
".dds",
".hdr",
".exr",
}
AUDIO_EXTENSIONS = {".wav", ".mp3", ".ogg", ".flac"}
VIDEO_EXTENSIONS = {".mp4", ".avi", ".mov", ".mkv", ".wmv", ".webm"}
SCRIPT_EXTENSIONS = {".py", ".pyc"}
UI_EXTENSIONS = {".json", ".ttf", ".otf", ".woff", ".woff2"}
def generate_guid() -> str:
return uuid.uuid4().hex
def normalize_path(path: str) -> str:
return os.path.normpath(os.path.abspath(path))
def relative_project_path(project_root: str, target_path: str) -> str:
project_root = normalize_path(project_root)
target_path = normalize_path(target_path)
try:
relative_path = os.path.relpath(target_path, project_root)
except ValueError:
return ""
if relative_path.startswith(".."):
return ""
return relative_path.replace("\\", "/")
def detect_asset_type(file_path: str) -> str:
extension = os.path.splitext(str(file_path or ""))[1].lower()
if extension in MODEL_EXTENSIONS:
return "model"
if extension in TEXTURE_EXTENSIONS:
return "texture"
if extension in AUDIO_EXTENSIONS:
return "audio"
if extension in VIDEO_EXTENSIONS:
return "video"
if extension in SCRIPT_EXTENSIONS:
return "script"
if extension in UI_EXTENSIONS:
return "ui"
return "other"
def get_asset_subdir_for_type(asset_type: str) -> str:
return {
"model": "Models",
"texture": "Textures",
"audio": "Audio",
"video": "Video",
"ui": "UI",
"script": "Scripts",
"other": "Misc",
}.get(asset_type, "Misc")
def default_build_settings() -> dict:
return {
"enabled_scene_guids": [],
"output_format": "directory",
"windows_player": True,
"active_profile": "default",
"profiles": {
"default": {
"name": "Default",
"target_platform": "windows",
"output_format": "directory",
"output_dir": "Builds",
"windows": {
"enabled": True,
"exe_name": "",
"windowed": True,
},
"runtime": {
"startup_scene_guid": "",
"enabled_scene_guids": [],
"script_mode": "pyc",
},
}
},
}
@dataclass(frozen=True)
class ProjectLayout:
project_root: str
def __post_init__(self):
object.__setattr__(self, "project_root", normalize_path(self.project_root))
@property
def project_file(self) -> str:
return os.path.join(self.project_root, "project.json")
@property
def assets_root(self) -> str:
return os.path.join(self.project_root, "Assets")
@property
def scenes_root(self) -> str:
return os.path.join(self.project_root, "Scenes")
@property
def library_root(self) -> str:
return os.path.join(self.project_root, "Library")
@property
def builds_root(self) -> str:
return os.path.join(self.project_root, "Builds")
@property
def asset_db_path(self) -> str:
return os.path.join(self.library_root, "AssetDB.json")
@property
def imported_root(self) -> str:
return os.path.join(self.library_root, "Imported")
@property
def build_cache_root(self) -> str:
return os.path.join(self.library_root, "BuildCache")
@property
def scripts_root(self) -> str:
return os.path.join(self.assets_root, "Scripts")
@property
def models_root(self) -> str:
return os.path.join(self.assets_root, "Models")
@property
def textures_root(self) -> str:
return os.path.join(self.assets_root, "Textures")
@property
def audio_root(self) -> str:
return os.path.join(self.assets_root, "Audio")
@property
def video_root(self) -> str:
return os.path.join(self.assets_root, "Video")
@property
def ui_root(self) -> str:
return os.path.join(self.assets_root, "UI")
@property
def misc_root(self) -> str:
return os.path.join(self.assets_root, "Misc")
def meta_path_for_asset(self, asset_abs_path: str) -> str:
return f"{normalize_path(asset_abs_path)}.meta"
def imported_asset_dir(self, asset_guid: str) -> str:
return os.path.join(self.imported_root, asset_guid)
def build_cache_dir(self, scene_guid: str) -> str:
return os.path.join(self.build_cache_root, scene_guid)
def scene_file(self, scene_name: str) -> str:
return os.path.join(self.scenes_root, f"{scene_name}{SCENE_DESCRIPTION_EXTENSION}")
def ensure_project_directories(layout: ProjectLayout) -> None:
for path in (
layout.assets_root,
layout.models_root,
layout.textures_root,
layout.audio_root,
layout.video_root,
layout.ui_root,
layout.scripts_root,
layout.misc_root,
layout.scenes_root,
layout.library_root,
layout.imported_root,
layout.build_cache_root,
layout.builds_root,
):
os.makedirs(path, exist_ok=True)

View File

@ -0,0 +1,655 @@
"""Scene description and runtime scene helpers for MetaCore project v2."""
from __future__ import annotations
import json
import os
from datetime import datetime
from project.project_schema import (
ENGINE_NAME,
MANIFEST_SCHEMA_VERSION,
METASCENE_SCHEMA_VERSION,
RUNTIME_SCENE_SCHEMA_VERSION,
relative_project_path,
)
def load_json(file_path: str, default_value):
if not file_path or not os.path.exists(file_path):
return default_value
try:
with open(file_path, "r", encoding="utf-8-sig") as f:
return json.load(f)
except Exception as e:
print(f"⚠️ 读取 JSON 失败 {file_path}: {e}")
return default_value
def save_json(file_path: str, payload):
os.makedirs(os.path.dirname(file_path), exist_ok=True)
with open(file_path, "w", encoding="utf-8") as f:
json.dump(payload, f, ensure_ascii=False, indent=4)
def _migrate_scene_description_v1_to_v2(payload: dict) -> dict:
payload = dict(payload or {})
scene_components = dict(payload.get("scene_components", {}) or {})
camera_component = dict(scene_components.get("camera", {}) or payload.get("camera", {}) or {})
gui_component = dict(scene_components.get("gui", {}) or {})
gui_elements = list(gui_component.get("elements", []) or payload.get("gui", []) or [])
lui_component = dict(scene_components.get("lui", {}) or payload.get("lui", {}) or {})
scene_components["camera"] = camera_component
scene_components["gui"] = {"elements": gui_elements}
scene_components["lui"] = lui_component
payload["scene_components"] = scene_components
payload["camera"] = camera_component
payload["gui"] = gui_elements
payload["lui"] = lui_component
normalized_nodes = []
for raw_node in list(payload.get("nodes", []) or []):
node = dict(raw_node or {})
components = dict(node.get("components", {}) or {})
runtime_interactive = bool(
(components.get("metadata", {}) or {}).get("runtime_interactive", node.get("runtime_interactive", False))
)
scripts = list((components.get("scripts", {}) or {}).get("entries", []) or node.get("scripts", []) or [])
model_component = dict(components.get("model", {}) or {})
if not model_component and (node.get("asset_guid") or node.get("asset_path") or node.get("imported_node_key")):
model_component = {
"asset_guid": node.get("asset_guid", ""),
"asset_path": node.get("asset_path", ""),
"imported_node_key": node.get("imported_node_key", ""),
"is_model_root": bool((node.get("tags", {}) or {}).get("is_model_root")),
}
components["model"] = model_component
components["scripts"] = {"entries": scripts} if scripts else {}
components["metadata"] = dict(components.get("metadata", {}) or {})
components["metadata"]["runtime_interactive"] = runtime_interactive
components["metadata"].setdefault("node_class", node.get("node_class", ""))
node["components"] = components
node["runtime_interactive"] = runtime_interactive
node["scripts"] = scripts
if model_component:
node["asset_guid"] = str(model_component.get("asset_guid", "") or node.get("asset_guid", "") or "")
node["asset_path"] = str(model_component.get("asset_path", "") or node.get("asset_path", "") or "")
node["imported_node_key"] = str(model_component.get("imported_node_key", "") or node.get("imported_node_key", "") or "")
normalized_nodes.append(node)
payload["nodes"] = normalized_nodes
payload["schema_version"] = 2
return payload
def normalize_scene_description(payload: dict) -> dict:
payload = dict(payload or {})
version = int(payload.get("schema_version", 1) or 1)
original_version = version
while version < METASCENE_SCHEMA_VERSION:
if version == 1:
payload = _migrate_scene_description_v1_to_v2(payload)
version = 2
continue
break
payload["schema_version"] = METASCENE_SCHEMA_VERSION
if original_version != METASCENE_SCHEMA_VERSION:
print(f" 场景描述已从 schema v{original_version} 迁移到 v{METASCENE_SCHEMA_VERSION}")
return payload
def _migrate_runtime_scene_v1_to_v2(payload: dict) -> dict:
payload = dict(payload or {})
scene_components = dict(payload.get("scene_components", {}) or {})
camera_component = dict(scene_components.get("camera", {}) or payload.get("camera", {}) or {})
gui_component = dict(scene_components.get("gui", {}) or {})
gui_elements = list(gui_component.get("elements", []) or payload.get("gui", []) or [])
lui_component = dict(scene_components.get("lui", {}) or payload.get("lui", {}) or {})
payload["scene_components"] = {
"camera": camera_component,
"gui": {"elements": gui_elements},
"lui": lui_component,
}
payload["camera"] = camera_component
payload["gui"] = gui_elements
payload["lui"] = lui_component
payload["nodes"] = normalize_scene_description({"nodes": payload.get("nodes", []) or []}).get("nodes", [])
payload["schema_version"] = 2
return payload
def normalize_runtime_scene(payload: dict) -> dict:
payload = dict(payload or {})
version = int(payload.get("schema_version", 1) or 1)
original_version = version
while version < RUNTIME_SCENE_SCHEMA_VERSION:
if version == 1:
payload = _migrate_runtime_scene_v1_to_v2(payload)
version = 2
continue
break
payload["schema_version"] = RUNTIME_SCENE_SCHEMA_VERSION
if original_version != RUNTIME_SCENE_SCHEMA_VERSION:
print(f" 运行时场景已从 schema v{original_version} 迁移到 v{RUNTIME_SCENE_SCHEMA_VERSION}")
return payload
def _node_path_id(parent_id: str, index: int) -> str:
return f"{parent_id}/{index}" if parent_id else str(index)
def _collect_light_component(node):
if not node.hasTag("light_type"):
return {}
component = {"type": node.getTag("light_type")}
for tag_name, output_name in (
("light_energy", "energy"),
("light_radius", "radius"),
("light_fov", "fov"),
("stored_energy", "stored_energy"),
):
if node.hasTag(tag_name):
component[output_name] = node.getTag(tag_name)
return component
def _collect_model_component(node, asset_guid: str, asset_path: str, imported_node_key: str) -> dict:
if not asset_guid and not node.hasTag("is_model_root"):
return {}
return {
"asset_guid": asset_guid,
"asset_path": asset_path,
"imported_node_key": imported_node_key,
"is_model_root": bool(node.hasTag("is_model_root")),
}
def _collect_script_component(scripts: list[dict]) -> dict:
return {"entries": list(scripts or [])} if scripts else {}
def _collect_node_metadata_component(node, runtime_interactive: bool) -> dict:
metadata = {
"node_class": node.node().getClassType().getName() if node.node() else "",
"runtime_interactive": runtime_interactive,
}
for tag_name in (
"has_animations",
"has_animations_checked",
"can_create_actor_from_memory",
"saved_has_animations",
"saved_can_create_actor_from_memory",
):
if node.hasTag(tag_name):
metadata[tag_name] = node.getTag(tag_name)
return metadata
def _collect_material_override_component(node):
material_tags = {}
for tag_name in (
"material_ambient",
"material_diffuse",
"material_specular",
"material_emission",
"material_shininess",
"material_basecolor",
"material_roughness",
"material_metallic",
"material_ior",
"material_effect_metallic_enabled",
"material_effect_default_texture_enabled",
"material_effect_parallax_enabled",
"material_render_effect_signature",
"material_texture_diffuse",
"material_texture_normal",
"material_texture_ior",
"material_texture_roughness",
"material_texture_parallax",
"material_texture_metallic",
"material_texture_emission",
"material_texture_ao",
"material_texture_alpha",
"material_texture_detail",
"material_texture_gloss",
):
if node.hasTag(tag_name):
material_tags[tag_name] = node.getTag(tag_name)
return material_tags
def _resolve_imported_node_key(node, fallback_key: str) -> str:
if node.hasTag("is_model_root"):
for tag_name in ("imported_node_key", "source_model_node_key", "ssbo_tree_key", "tree_item_key"):
if node.hasTag(tag_name):
tag_value = str(node.getTag(tag_name) or "").strip()
if tag_value:
return tag_value
return ""
for tag_name in ("imported_node_key", "source_model_node_key", "ssbo_tree_key", "tree_item_key"):
if node.hasTag(tag_name):
tag_value = str(node.getTag(tag_name) or "").strip()
if tag_value:
return tag_value
return fallback_key
def _should_serialize_child_nodes(node, asset_guid: str, scripts: list[dict], runtime_interactive: bool, light_component: dict, material_component: dict) -> bool:
if not node:
return False
if asset_guid:
return True
if scripts:
return True
if runtime_interactive:
return True
if light_component:
return True
if material_component:
return True
if node.hasTag("scene_transform_dirty") and node.getTag("scene_transform_dirty").lower() == "true":
return True
if node.hasTag("scene_material_dirty") and node.getTag("scene_material_dirty").lower() == "true":
return True
if node.hasTag("user_visible") and node.getTag("user_visible").lower() != "true":
return True
return False
def _build_scene_components(camera_state: dict, gui_elements: list, lui_snapshot: dict) -> dict:
return {
"camera": dict(camera_state or {}),
"gui": {"elements": list(gui_elements or [])},
"lui": dict(lui_snapshot or {}),
}
def _build_scene_description_payload(
*,
asset_database,
project_root: str,
scene_guid: str,
scene_name: str,
scene_file_rel: str,
root_nodes,
cache_bam_path: str = "",
cache_gui_path: str = "",
cache_lui_path: str = "",
gui_elements: list | None = None,
lui_snapshot: dict | None = None,
camera_state: dict | None = None,
):
gui_elements = list(gui_elements or [])
lui_snapshot = dict(lui_snapshot or {})
camera_state = dict(camera_state or {})
node_entries = []
referenced_asset_guids = set()
referenced_script_guids = set()
subtree_serializable_cache = {}
def _node_is_serializable(node) -> bool:
if not node or node.isEmpty():
return False
cache_key = id(node)
if cache_key in subtree_serializable_cache:
return subtree_serializable_cache[cache_key]
runtime_interactive = (
node.hasTag("runtime_interactive")
and node.getTag("runtime_interactive").lower() == "true"
)
scripts = []
if node.hasTag("scripts_info"):
try:
scripts = json.loads(node.getTag("scripts_info"))
except Exception:
scripts = []
asset_guid = ""
if node.hasTag("is_model_root"):
asset_guid = "root"
light_component = _collect_light_component(node)
material_component = _collect_material_override_component(node)
serializable_here = _should_serialize_child_nodes(
node,
asset_guid,
scripts,
runtime_interactive,
light_component,
material_component,
)
if serializable_here:
subtree_serializable_cache[cache_key] = True
return True
try:
children = list(node.getChildren())
except Exception:
children = []
result = any(_node_is_serializable(child) for child in children if child and not child.isEmpty())
subtree_serializable_cache[cache_key] = result
return result
def walk_nodes(children, parent_id=""):
for index, child in enumerate(list(children or [])):
if not child or child.isEmpty():
continue
node_id = _node_path_id(parent_id, index)
if not _node_is_serializable(child):
continue
node_name = child.getName()
imported_node_key = _resolve_imported_node_key(child, node_id)
runtime_interactive = (
child.hasTag("runtime_interactive")
and child.getTag("runtime_interactive").lower() == "true"
)
scripts = []
if child.hasTag("scripts_info"):
try:
scripts = json.loads(child.getTag("scripts_info"))
except Exception:
scripts = []
asset_guid = ""
asset_path = ""
if child.hasTag("is_model_root"):
for tag_name in ("model_path", "saved_model_path", "original_path"):
if child.hasTag(tag_name):
candidate = child.getTag(tag_name)
if candidate:
asset_record = asset_database.register_asset(candidate, copy_into_assets=False)
if asset_record:
asset_guid = asset_record.get("guid", "")
asset_path = asset_record.get("asset_path", "")
referenced_asset_guids.add(asset_guid)
break
for script_info in scripts:
script_guid = str(script_info.get("script_guid", "") or "").strip()
asset_record = asset_database.get_asset(script_guid) if script_guid else {}
if not asset_record:
project_relative_path = str(script_info.get("project_relative_path", "") or "")
if project_relative_path:
script_abs_path = os.path.join(project_root, project_relative_path.replace("/", os.sep))
asset_record = asset_database.register_asset(script_abs_path, copy_into_assets=False)
if asset_record:
resolved_guid = asset_record.get("guid", "")
if resolved_guid:
script_info["script_guid"] = resolved_guid
referenced_script_guids.add(resolved_guid)
asset_path_for_script = str(asset_record.get("asset_path", "") or "")
if asset_path_for_script:
script_info["project_relative_path"] = asset_path_for_script
script_info["file"] = asset_path_for_script
light_component = _collect_light_component(child)
material_component = _collect_material_override_component(child)
node_entries.append(
{
"node_id": node_id,
"parent_id": parent_id or None,
"name": node_name,
"imported_node_key": imported_node_key,
"asset_guid": asset_guid,
"asset_path": asset_path,
"node_class": child.node().getClassType().getName() if child.node() else "",
"transform": {
"position": [child.getX(), child.getY(), child.getZ()],
"rotation": [child.getH(), child.getP(), child.getR()],
"scale": [child.getSx(), child.getSy(), child.getSz()],
},
"visibility": {
"user_visible": (
child.getTag("user_visible").lower() == "true"
if child.hasTag("user_visible")
else True
)
},
"runtime_interactive": runtime_interactive,
"scripts": scripts,
"components": {
"model": _collect_model_component(child, asset_guid, asset_path, imported_node_key),
"scripts": _collect_script_component(scripts),
"light": light_component,
"material_overrides": material_component,
"metadata": _collect_node_metadata_component(child, runtime_interactive),
},
"tags": {
tag_name: child.getTag(tag_name)
for tag_name in child.getTagKeys()
},
}
)
if _should_serialize_child_nodes(
child,
asset_guid,
scripts,
runtime_interactive,
light_component,
material_component,
) or any(
_node_is_serializable(grandchild)
for grandchild in list(child.getChildren()) if grandchild and not grandchild.isEmpty()
):
try:
child_nodes = list(child.getChildren())
except Exception:
child_nodes = []
walk_nodes(child_nodes, node_id)
walk_nodes(root_nodes)
description = {
"schema_version": METASCENE_SCHEMA_VERSION,
"scene_guid": scene_guid,
"scene_name": scene_name,
"scene_file": scene_file_rel,
"saved_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"cache": {
"scene_bam": relative_project_path(project_root, cache_bam_path),
"gui_json": relative_project_path(project_root, cache_gui_path),
"lui_json": relative_project_path(project_root, cache_lui_path),
},
"referenced_asset_guids": sorted(guid for guid in referenced_asset_guids if guid),
"referenced_script_guids": sorted(guid for guid in referenced_script_guids if guid),
"scene_components": _build_scene_components(camera_state, gui_elements, lui_snapshot),
"camera": camera_state,
"gui": gui_elements,
"lui": lui_snapshot,
"nodes": node_entries,
}
return description
def build_scene_description_from_world(
*,
asset_database,
project_root: str,
scene_guid: str,
scene_name: str,
scene_file_rel: str,
root_nodes,
cache_bam_path: str = "",
cache_gui_path: str = "",
cache_lui_path: str = "",
gui_elements: list | None = None,
lui_snapshot: dict | None = None,
camera_state: dict | None = None,
):
return _build_scene_description_payload(
asset_database=asset_database,
project_root=project_root,
scene_guid=scene_guid,
scene_name=scene_name,
scene_file_rel=scene_file_rel,
root_nodes=root_nodes,
cache_bam_path=cache_bam_path,
cache_gui_path=cache_gui_path,
cache_lui_path=cache_lui_path,
gui_elements=gui_elements,
lui_snapshot=lui_snapshot,
camera_state=camera_state,
)
def build_runtime_scene(scene_description: dict):
nodes = list(scene_description.get("nodes", []) or [])
scene_components = dict(scene_description.get("scene_components", {}) or {})
camera_component = dict(scene_components.get("camera", {}) or scene_description.get("camera", {}) or {})
gui_component = dict(scene_components.get("gui", {}) or {})
gui_elements = list(gui_component.get("elements", []) or scene_description.get("gui", []) or [])
lui_component = dict(scene_components.get("lui", {}) or scene_description.get("lui", {}) or {})
interactive_model_names = []
interactive_node_ids = []
static_model_names = []
static_node_ids = []
for node in nodes:
if not node.get("asset_guid"):
continue
metadata_component = dict((node.get("components", {}) or {}).get("metadata", {}) or {})
has_animations = str(
metadata_component.get(
"has_animations",
metadata_component.get(
"saved_has_animations",
(node.get("tags", {}) or {}).get(
"has_animations",
(node.get("tags", {}) or {}).get("saved_has_animations", ""),
),
),
)
or ""
).lower() == "true"
can_create_actor = str(
metadata_component.get(
"can_create_actor_from_memory",
metadata_component.get(
"saved_can_create_actor_from_memory",
(node.get("tags", {}) or {}).get(
"can_create_actor_from_memory",
(node.get("tags", {}) or {}).get("saved_can_create_actor_from_memory", ""),
),
),
)
or ""
).lower() == "true"
if has_animations or can_create_actor:
interactive_node_ids.append(node.get("node_id", ""))
interactive_model_names.append(node.get("name", ""))
continue
if node.get("runtime_interactive"):
interactive_node_ids.append(node.get("node_id", ""))
interactive_model_names.append(node.get("name", ""))
continue
if node.get("scripts"):
interactive_node_ids.append(node.get("node_id", ""))
interactive_model_names.append(node.get("name", ""))
continue
static_node_ids.append(node.get("node_id", ""))
static_model_names.append(node.get("name", ""))
return {
"schema_version": RUNTIME_SCENE_SCHEMA_VERSION,
"scene_guid": scene_description.get("scene_guid", ""),
"scene_name": scene_description.get("scene_name", ""),
"cook": {
"interactive_node_ids": sorted({node_id for node_id in interactive_node_ids if node_id}),
"interactive_model_names": sorted({name for name in interactive_model_names if name}),
"static_node_ids": sorted({node_id for node_id in static_node_ids if node_id}),
"static_model_names": sorted({name for name in static_model_names if name}),
},
"interactive_model_names": sorted({name for name in interactive_model_names if name}),
"scene_components": {
"camera": camera_component,
"gui": {"elements": gui_elements},
"lui": lui_component,
},
"camera": camera_component,
"gui": gui_elements,
"lui": lui_component,
"nodes": nodes,
"referenced_asset_guids": scene_description.get("referenced_asset_guids", []) or [],
"referenced_script_guids": scene_description.get("referenced_script_guids", []) or [],
}
def build_scene_cook_manifest(scene_description: dict, runtime_scene: dict):
runtime_cook = dict(runtime_scene.get("cook", {}) or {})
return {
"schema_version": 1,
"scene_guid": scene_description.get("scene_guid", ""),
"scene_name": scene_description.get("scene_name", ""),
"source_scene_file": scene_description.get("scene_file", ""),
"node_count": len(scene_description.get("nodes", []) or []),
"interactive_node_ids": list(runtime_cook.get("interactive_node_ids", []) or []),
"interactive_model_names": list(runtime_cook.get("interactive_model_names", []) or []),
"static_node_ids": list(runtime_cook.get("static_node_ids", []) or []),
"static_model_names": list(runtime_cook.get("static_model_names", []) or []),
"referenced_asset_guids": list(scene_description.get("referenced_asset_guids", []) or []),
"referenced_script_guids": list(scene_description.get("referenced_script_guids", []) or []),
"scene_components": dict(scene_description.get("scene_components", {}) or {}),
"cache": dict(scene_description.get("cache", {}) or {}),
"saved_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
}
def build_runtime_manifest(project_config: dict, runtime_scenes: list[dict], asset_records: list[dict]):
scene_guids = [scene.get("scene_guid", "") for scene in runtime_scenes]
startup_scene_guid = project_config.get("startup_scene_guid", "")
if startup_scene_guid not in scene_guids:
startup_scene_guid = scene_guids[0] if scene_guids else ""
total_interactive_nodes = 0
total_static_nodes = 0
total_interactive_models = 0
total_static_models = 0
for scene in runtime_scenes:
cook = dict(scene.get("cook", {}) or {})
total_interactive_nodes += len(list(cook.get("interactive_node_ids", []) or []))
total_static_nodes += len(list(cook.get("static_node_ids", []) or []))
total_interactive_models += len(list(cook.get("interactive_model_names", []) or []))
total_static_models += len(list(cook.get("static_model_names", []) or []))
return {
"schema_version": MANIFEST_SCHEMA_VERSION,
"project_name": project_config.get("name", f"{ENGINE_NAME}Project"),
"startup_scene_guid": startup_scene_guid,
"scene_guids": scene_guids,
"scenes": [
{
"guid": scene.get("scene_guid", ""),
"name": scene.get("scene_name", ""),
"runtime_path": f"scenes/{scene.get('scene_guid', '')}.runtime.json",
}
for scene in runtime_scenes
],
"assets": [
{
"guid": asset.get("guid", ""),
"asset_type": asset.get("asset_type", ""),
"asset_path": asset.get("asset_path", ""),
"imported_cache": asset.get("imported_cache", {}) or {},
}
for asset in asset_records
if asset
],
"cook_summary": {
"scene_count": len(runtime_scenes),
"asset_count": len(asset_records),
"interactive_node_count": total_interactive_nodes,
"static_node_count": total_static_nodes,
"interactive_model_count": total_interactive_models,
"static_model_count": total_static_models,
},
"build_settings": project_config.get("build_settings", {}) or {},
}

View File

@ -2,7 +2,7 @@
## 🚀 在新电脑上运行此项目
### 方法1使用Conda推荐
### 方法1使用Conda推荐
```bash
# 1. 安装Miniconda
@ -19,8 +19,8 @@ conda env create -f environment.yml
# 4. 激活环境
conda activate eg-project
# 5. 运行项目
python main.py
# 5. 运行项目
python Start_Run.py
```
### 方法2使用virtualenv + pip
@ -35,26 +35,24 @@ python -m venv venv
source venv/bin/activate # Linux/Mac
# 或 venv\Scripts\activate # Windows
# 3. 安装依赖
pip install -r clean-requirements.txt
# 3. 安装依赖
pip install -r requirements/requirements.txt
# 4. 运行项目
python main.py
# 4. 运行项目
python Start_Run.py
```
## 📁 需要复制的文件
**必须复制:**
- `main.py` - 主程序
- `gui_preview_window.py` - GUI窗口
- `demo/` - 演示文件夹
- `environment.yml` - Conda环境配置
- `clean-requirements.txt` - 核心依赖
- `environment.yml` - Conda环境配置
- `requirements.txt` - 核心依赖
**不要复制:**
- `.conda/` - 虚拟环境文件夹
- `__pycache__/` - Python缓存
- `requirements.txt` - 包含系统包的混乱依赖
- `conda-requirements.txt` - 仅用于最小 Conda 环境说明
## 🔧 Cursor IDE 设置
@ -64,13 +62,15 @@ python main.py
- 选择虚拟环境中的Python路径
3. **验证环境**检查状态栏显示正确的Python版本
## 📦 项目依赖说明
- **Panda3D**: 3D图形引擎
- **PyQt5/PySide6**: GUI框架
- **Pillow**: 图像处理
- **python-dotenv**: 环境变量管理
- **pyassimp**: 3D模型加载
## 📦 项目依赖说明
- **Panda3D**: 3D图形引擎
- **imgui-bundle / p3dimgui**: ImGui 编辑器界面与 Panda3D 桥接
- **Pillow**: 图像处理与网页截图纹理转换
- **numpy**: VR / SSBO 数值处理
- **aiohttp**: 异步资源与场景 IO
- **openvr**: VR 支持
- **pyassimp**: 模型转换辅助
## 🌍 跨平台注意事项
@ -83,8 +83,8 @@ python main.py
**Q: 无法安装Panda3D**
A: 确保系统有OpenGL支持或使用conda安装
**Q: PyQt5无法运行**
A: 可能需要安装系统级GUI依赖
**Q: ImGui界面显示异常**
A: 检查显卡驱动/OpenGL支持并确认已安装 `imgui-bundle`
**Q: 导入错误?**
A: 检查Python版本需要3.10+
**Q: 导入错误?**
A: 检查 Python 版本,当前项目统一使用 Python 3.11。

View File

@ -1,6 +1,7 @@
Panda3D>=1.10.15
PyQt5>=5.15.9
PySide6>=6.8.1
Pillow>=9.0.1
python-dotenv>=1.0.1
pyassimp>=5.2.5
Panda3D==1.10.15
imgui-bundle
Pillow>=9.0.1
numpy>=1.24
aiohttp>=3.9
openvr==2.2.0
pyassimp>=5.2.5

View File

@ -1,34 +1,3 @@
# This file may be used to create an environment using:
# $ conda create --name <env> --file <this file>
# platform: linux-64
# created-by: conda 25.5.1
@EXPLICIT
https://repo.anaconda.com/pkgs/main/linux-64/_libgcc_mutex-0.1-main.conda
https://repo.anaconda.com/pkgs/main/linux-64/ca-certificates-2025.2.25-h06a4308_0.conda
https://repo.anaconda.com/pkgs/main/linux-64/ld_impl_linux-64-2.40-h12ee557_0.conda
https://repo.anaconda.com/pkgs/main/linux-64/libstdcxx-ng-11.2.0-h1234567_1.conda
https://repo.anaconda.com/pkgs/main/noarch/tzdata-2025b-h04d1e81_0.conda
https://repo.anaconda.com/pkgs/main/linux-64/libgomp-11.2.0-h1234567_1.conda
https://repo.anaconda.com/pkgs/main/linux-64/_openmp_mutex-5.1-1_gnu.conda
https://repo.anaconda.com/pkgs/main/linux-64/libgcc-ng-11.2.0-h1234567_1.conda
https://repo.anaconda.com/pkgs/main/linux-64/bzip2-1.0.8-h5eee18b_6.conda
https://repo.anaconda.com/pkgs/main/linux-64/expat-2.7.1-h6a678d5_0.conda
https://repo.anaconda.com/pkgs/main/linux-64/libffi-3.4.4-h6a678d5_1.conda
https://repo.anaconda.com/pkgs/main/linux-64/libuuid-1.41.5-h5eee18b_0.conda
https://repo.anaconda.com/pkgs/main/linux-64/ncurses-6.4-h6a678d5_0.conda
https://repo.anaconda.com/pkgs/main/linux-64/openssl-3.0.16-h5eee18b_0.conda
https://repo.anaconda.com/pkgs/main/linux-64/pthread-stubs-0.3-h0ce48e5_1.conda
https://repo.anaconda.com/pkgs/main/linux-64/xorg-libxau-1.0.12-h9b100fa_0.conda
https://repo.anaconda.com/pkgs/main/linux-64/xorg-libxdmcp-1.1.5-h9b100fa_0.conda
https://repo.anaconda.com/pkgs/main/linux-64/xorg-xorgproto-2024.1-h5eee18b_1.conda
https://repo.anaconda.com/pkgs/main/linux-64/xz-5.6.4-h5eee18b_1.conda
https://repo.anaconda.com/pkgs/main/linux-64/zlib-1.2.13-h5eee18b_1.conda
https://repo.anaconda.com/pkgs/main/linux-64/libxcb-1.17.0-h9b100fa_0.conda
https://repo.anaconda.com/pkgs/main/linux-64/readline-8.2-h5eee18b_0.conda
https://repo.anaconda.com/pkgs/main/linux-64/sqlite-3.45.3-h5eee18b_0.conda
https://repo.anaconda.com/pkgs/main/linux-64/xorg-libx11-1.8.12-h9b100fa_1.conda
https://repo.anaconda.com/pkgs/main/linux-64/tk-8.6.14-h993c535_1.conda
https://repo.anaconda.com/pkgs/main/linux-64/python-3.10.18-h1a3bd86_0.conda
https://repo.anaconda.com/pkgs/main/linux-64/setuptools-78.1.1-py310h06a4308_0.conda
https://repo.anaconda.com/pkgs/main/linux-64/wheel-0.45.1-py310h06a4308_0.conda
https://repo.anaconda.com/pkgs/main/noarch/pip-25.1-pyhc872135_2.conda
# Minimal conda spec for the current project environment.
python=3.11
pip

View File

@ -1,15 +1,15 @@
name: eg-project
channels:
- conda-forge
- defaults
dependencies:
- python=3.10
- pip
- pip:
- Panda3D>=1.10.15
- PyQt5>=5.15.9
- PySide6>=6.8.1
- Pillow>=9.0.1
- python-dotenv>=1.0.1
- pyassimp>=5.2.5
- six
name: eg-project
channels:
- conda-forge
- defaults
dependencies:
- python=3.11
- pip
- pip:
- Panda3D==1.10.15
- imgui-bundle
- Pillow>=9.0.1
- numpy>=1.24
- aiohttp>=3.9
- openvr==2.2.0
- pyassimp>=5.2.5

View File

@ -1,102 +1,14 @@
apturl==0.5.2
bcrypt==3.2.0
beautifulsoup4==4.10.0
blinker==1.4
Brlapi==0.8.3
certifi==2020.6.20
chardet==4.0.0
click==8.0.3
colorama==0.4.4
command-not-found==0.3
cryptography==3.4.8
cupshelpers==1.0
dbus-python==1.2.18
defer==1.0.6
DirectFolderBrowser==22.1
DirectGuiDesigner==22.4.1
DirectGuiExtension==22.9
distro==1.7.0
distro-info==1.1+ubuntu0.2
duplicity==0.8.21
fasteners==0.14.1
future==0.18.2
html5lib==1.1
httplib2==0.20.2
idna==3.3
importlib-metadata==4.6.4
jeepney==0.7.1
keyring==23.5.0
language-selector==0.1
launchpadlib==1.10.16
lazr.restfulclient==0.14.4
lazr.uri==1.0.6
lockfile==0.12.2
louis==3.20.0
lxml==4.8.0
macaroonbakery==1.3.1
Mako==1.1.3
MarkupSafe==2.0.1
monotonic==1.6
more-itertools==8.10.0
netifaces==0.11.0
oauthlib==3.2.0
olefile==0.46
Panda3D==1.10.15
panda3d-frame==22.10.post1
Panda3DNodeEditor==22.5
paramiko==2.9.3
pexpect==4.8.0
Pillow==9.0.1
platformdirs==4.3.6
protobuf==3.12.4
ptyprocess==0.7.0
pyassimp==5.2.5
pycairo==1.20.1
pycups==2.0.1
Pygments==2.11.2
PyGObject==3.42.1
PyJWT==2.3.0
pymacaroons==0.13.0
PyNaCl==1.5.0
pyparsing==2.4.7
PyQt5==5.15.9
pyqt5-plugins==5.15.9.2.3
PyQt5-Qt5==5.15.2
pyqt5-tools==5.15.9.3.3
PyQt5_sip==12.16.1
pyRFC3339==1.1
PySide6==6.8.1
PySide6_Addons==6.8.1
PySide6_Essentials==6.8.1
python-apt==2.4.0+ubuntu4
python-dateutil==2.8.1
python-debian==0.1.43+ubuntu1.1
python-dotenv==1.0.1
pytz==2022.1
pyxdg==0.27
PyYAML==5.4.1
QPanda3D==0.2.10
qt5-applications==5.15.2.2.3
qt5-tools==5.15.2.1.3
reportlab==3.6.8
requests==2.25.1
SceneEditor==22.5
screen-resolution-extra==0.0.0
SecretStorage==3.3.1
shiboken6==6.8.1
six==1.16.0
soupsieve==2.3.1
systemd-python==234
ubuntu-drivers-common==0.0.0
ubuntu-pro-client==8001
ufw==0.36.1
unattended-upgrades==0.1
urllib3==1.26.5
usb-creator==0.3.7
wadllib==1.3.6
webencodings==0.5.1
xdg==5
xkit==0.0.0
zipp==1.0.0
openvr==2.2.0
imgui-bundle
# Core runtime dependencies for the current ImGui editor stack.
Panda3D==1.10.15
imgui-bundle
Pillow>=9.0.1
numpy>=1.24
aiohttp>=3.9
openvr==2.2.0
# Asset import / conversion helpers.
pyassimp>=5.2.5
# Optional features:
# playwright>=1.40 # Required by core/imgui_webview.py
# bpy # Required only when using Blender-based conversion flows

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,523 @@
"""Scene manager conversion and tileset operations."""
import os
import shutil
import time
import json
import aiohttp
import asyncio
import inspect
from pathlib import Path
from panda3d.core import (
ModelPool, ModelRoot, Filename, NodePath, GeomNode, Material, Vec4, Vec3,
MaterialAttrib, ColorAttrib, Point3, CollisionNode, CollisionSphere, CollisionBox,
BitMask32, TransparencyAttrib, LColor, TransformState, RenderModeAttrib
)
from panda3d.egg import EggData, EggVertexPool
from direct.actor.Actor import Actor
from RenderPipelineFile.rpplugins.smaa.jitters import halton_seq
from scene import util
from core.editor_context import get_editor_context
class SceneManagerConvertTilesMixin:
def _get_tree_widget(self):
"""安全获取树形控件"""
return get_editor_context(self.world).get_tree_widget()
def _shouldConvertToGLB(self, filepath):
"""判断是否应该转换为GLB格式"""
ext = os.path.splitext(filepath)[1].lower()
# 需要转换的格式FBX, OBJ, DAE等但不转换已经是GLB/GLTF的
convert_formats = ['.fbx', '.obj', '.dae', '.3ds', '.blend']
return ext in convert_formats
def _convertToGLBWithProgress(self, filepath):
"""带进度显示的GLB转换"""
class _ConsoleProgress:
def __init__(self, label):
self._value = 0
self._label = label
def setWindowTitle(self, title):
print(f"[GLB转换] {title}")
def setWindowModality(self, _):
return None
def show(self):
print(f"[GLB转换] {self._label}")
def hide(self):
print("[GLB转换] 完成")
def setValue(self, value):
value = int(value)
if value != self._value:
self._value = value
print(f"[GLB转换] 进度: {self._value}%")
def setLabelText(self, text):
if text != self._label:
self._label = text
print(f"[GLB转换] {self._label}")
progress = _ConsoleProgress("正在转换模型格式以获得更好的动画支持...")
progress.setWindowTitle("模型格式转换")
progress.show()
try:
result = self._convertToGLB(filepath, progress)
progress.hide()
return result
except Exception as e:
progress.hide()
print(f"转换过程出错: {e}")
return self._convertToGLB(filepath)
def _convertToGLB(self, filepath, progress=None):
"""将模型文件转换为GLB格式"""
try:
print(f"[GLB转换] 开始转换: {filepath}")
if progress:
progress.setValue(10)
progress.setLabelText("准备转换...")
# 准备输出路径
base_name = os.path.splitext(os.path.basename(filepath))[0]
output_dir = os.path.dirname(filepath)
glb_path = os.path.join(output_dir, f"{base_name}_auto_converted.glb")
# 如果已经存在转换后的文件,直接使用
if os.path.exists(glb_path):
# 检查文件时间,如果原文件更新则重新转换
original_time = os.path.getmtime(filepath)
converted_time = os.path.getmtime(glb_path)
if converted_time > original_time:
print(f"[GLB转换] 使用现有转换文件: {glb_path}")
if progress:
progress.setValue(100)
return glb_path
if progress:
progress.setValue(20)
progress.setLabelText("尝试 Blender 转换...")
# 方法1: 使用 Blender 进行转换
if self._convertWithBlender(filepath, glb_path, progress):
return glb_path
if progress:
progress.setValue(60)
progress.setLabelText("尝试 FBX2glTF 转换...")
# 方法2: 使用 FBX2glTF (如果可用)
if self._convertWithFBX2glTF(filepath, glb_path, progress):
return glb_path
if progress:
progress.setValue(80)
progress.setLabelText("尝试 Assimp 转换...")
# 方法3: 使用 Assimp
if self._convertWithAssimp(filepath, glb_path, progress):
return glb_path
#print(f"[GLB转换] 所有转换方法都失败,既然没有可以转换格式的工具和环境那么就用原始文件,不一定非要转换")
return None
except Exception as e:
print(f"[GLB转换] 转换过程出错: {e}")
return None
def _convertWithBlender(self, input_path, output_path, progress=None):
"""使用 Blender 进行转换"""
try:
import subprocess
import tempfile
print(f"[Blender转换] {input_path}{output_path}")
# 创建 Blender 脚本
script_content = f'''
import bpy
import sys
import os
# 清理默认场景
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete(use_global=False)
print("开始导入文件...")
# 根据文件类型选择导入方法
input_file = "{input_path}"
output_file = "{output_path}"
try:
ext = os.path.splitext(input_file)[1].lower()
if ext == '.fbx':
bpy.ops.import_scene.fbx(filepath=input_file)
elif ext == '.obj':
bpy.ops.import_scene.obj(filepath=input_file)
elif ext == '.dae':
bpy.ops.wm.collada_import(filepath=input_file)
elif ext == '.blend':
bpy.ops.wm.open_mainfile(filepath=input_file)
else:
print(f"不支持的格式: {{ext}}")
sys.exit(1)
print("导入成功开始导出GLB...")
# 导出为 GLB保留动画
bpy.ops.export_scene.gltf(
filepath=output_file,
export_format='GLB',
export_animations=True,
export_force_sampling=True,
export_frame_range=True,
export_current_frame=False,
export_skins=True,
export_morph=True,
export_lights=True,
export_cameras=False
)
print("GLB导出成功")
except Exception as e:
print(f"转换失败: {{e}}")
sys.exit(1)
'''
# 写入临时脚本文件
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as temp_file:
temp_file.write(script_content)
script_path = temp_file.name
try:
# 执行 Blender 转换
result = subprocess.run([
'blender', '--background', '--python', script_path
], capture_output=True, text=True, timeout=180)
# 清理临时文件
os.unlink(script_path)
if result.returncode == 0 and os.path.exists(output_path):
print(f"[Blender转换] 转换成功")
return True
else:
print(f"[Blender转换] 转换失败: {result.stderr}")
return False
except subprocess.TimeoutExpired:
print(f"[Blender转换] 转换超时")
return False
except FileNotFoundError:
print(f"[Blender转换] Blender 未安装")
return False
except Exception as e:
print(f"[Blender转换] 转换过程出错: {e}")
return False
def _convertWithFBX2glTF(self, input_path, output_path, progress=None):
"""使用 FBX2glTF 进行转换仅支持FBX"""
try:
import subprocess
if not input_path.lower().endswith('.fbx'):
return False
print(f"[FBX2glTF转换] {input_path}{output_path}")
# 使用 FBX2glTF 转换
result = subprocess.run([
'FBX2glTF', input_path, '--output', output_path, '--binary'
], capture_output=True, text=True, timeout=120)
if result.returncode == 0 and os.path.exists(output_path):
print(f"[FBX2glTF转换] 转换成功")
return True
else:
print(f"[FBX2glTF转换] 转换失败: {result.stderr}")
return False
except subprocess.TimeoutExpired:
print(f"[FBX2glTF转换] 转换超时")
return False
except FileNotFoundError:
print(f"[FBX2glTF转换] FBX2glTF 未安装")
return False
except Exception as e:
print(f"[FBX2glTF转换] 转换过程出错: {e}")
return False
def _convertWithAssimp(self, input_path, output_path, progress=None):
"""使用 PyAssimp 进行转换"""
try:
import pyassimp
print(f"[PyAssimp转换] {input_path}{output_path}")
# 加载模型
scene = pyassimp.load(input_path)
if not scene:
print(f"[PyAssimp转换] 加载模型失败")
return False
if progress:
progress.setValue(30)
# 导出为GLB格式
pyassimp.export(scene, output_path, "glb2")
if progress:
progress.setValue(80)
# 释放资源
pyassimp.release(scene)
if os.path.exists(output_path):
print(f"[PyAssimp转换] 转换成功")
return True
else:
print(f"[PyAssimp转换] 转换失败: 输出文件未生成")
return False
except ImportError:
print(f"[PyAssimp转换] PyAssimp 未安装")
return False
except Exception as e:
print(f"[PyAssimp转换] 转换过程出错: {e}")
return False
def _check_async_task(self, panda3d_task):
# 检查 asyncio 任务是否完成
if hasattr(self, '_current_asyncio_task'):
if self._current_asyncio_task.done():
try:
self._current_asyncio_task.result()
except Exception as e:
print(f"异步任务出错:{e}")
# 返回 Panda3D 任务管理器的完成状态
return panda3d_task.done # 注意是 done 而不是 DONE
# 返回 Panda3D 任务管理器的继续状态
return panda3d_task.cont # 注意是 cont 而不是 CONTINUE
def _parse_tileset(self,tileset_data,tileset_info):
try:
root = tileset_data.get('root',{})
self._parse_tile(root,tileset_info['node'],tileset_info)
print("✓ Tileset 解析完成")
except Exception as e:
print(f"✗ Tileset 解析出错: {e}")
def _parse_tile(self, tile_data, parent_node, tileset_info):
try:
# 获取tileID
tile_id = f"tile_{len(tileset_info['tiles'])}"
print(f"创建tile节点: {tile_id}")
# 创建tile节点
tile_node = parent_node.attachNewNode(tile_id)
tileset_info['tiles'][tile_id] = {
'node': tile_node,
'data': tile_data,
'loaded': False
}
# 如果有内容,创建占位几何体
if 'content' in tile_data:
print(f"为tile {tile_id} 创建几何体")
self._create_tile_geometry(tile_node)
# 递归解析子tiles
children = tile_data.get('children', [])
print(f"Tile {tile_id}{len(children)} 个子节点")
for child_data in children:
self._parse_tile(child_data, tile_node, tileset_info)
except Exception as e:
print(f"✗ Tile 解析出错: {e}")
import traceback
traceback.print_exc()
def _create_tile_geometry(self,parent_node):
"""为 tile 创建占位几何体"""
try:
# 创建一个简单的立方体作为占位符
from panda3d.core import GeomVertexFormat, GeomVertexData, GeomVertexWriter
from panda3d.core import Geom, GeomTriangles, GeomNode
format = GeomVertexFormat.getV3n3c4()
vdata = GeomVertexData('tile_cube', format, Geom.UHStatic)
vertex = GeomVertexWriter(vdata, 'vertex')
normal = GeomVertexWriter(vdata, 'normal')
color = GeomVertexWriter(vdata, 'color')
# 定义立方体顶点
vertices = [
(-0.5, -0.5, -0.5), (0.5, -0.5, -0.5), (0.5, 0.5, -0.5), (-0.5, 0.5, -0.5),
(-0.5, -0.5, 0.5), (0.5, -0.5, 0.5), (0.5, 0.5, 0.5), (-0.5, 0.5, 0.5)
]
for vert in vertices:
vertex.addData3f(*vert)
normal.addData3f(0, 0, 1)
color.addData4f(0.2, 0.6, 0.8, 1.0)
# 创建几何体
geom = Geom(vdata)
# 创建面
prim = GeomTriangles(Geom.UHStatic)
# 底面
prim.addVertices(0, 1, 2)
prim.addVertices(0, 2, 3)
# 顶面
prim.addVertices(4, 7, 6)
prim.addVertices(4, 6, 5)
# 前面
prim.addVertices(0, 4, 5)
prim.addVertices(0, 5, 1)
# 后面
prim.addVertices(2, 6, 7)
prim.addVertices(2, 7, 3)
# 左面
prim.addVertices(0, 3, 7)
prim.addVertices(0, 7, 4)
# 右面
prim.addVertices(1, 5, 6)
prim.addVertices(1, 6, 2)
prim.closePrimitive()
geom.addPrimitive(prim)
# 创建几何节点
geom_node = GeomNode('tile_geometry')
geom_node.addGeom(geom)
# 添加到场景
cube_node = parent_node.attachNewNode(geom_node)
cube_node.setScale(1000) # 放大以便观察
# 添加材质
material = Material()
material.setBaseColor((0.2, 0.6, 0.8, 1.0))
material.setSpecular((0.1, 0.1, 0.1, 1.0))
material.setShininess(10.0)
cube_node.setMaterial(material)
except Exception as e:
print(f"✗ 创建 tile 几何体出错: {e}")
def _create_placeholder_geometry(self, parent_node):
"""创建一个简单的占位符几何体,让用户能看到节点"""
try:
from panda3d.core import GeomVertexFormat, GeomVertexData, GeomVertexWriter
from panda3d.core import Geom, GeomTriangles, GeomNode
# 创建简单的立方体作为占位符
format = GeomVertexFormat.getV3n3c4()
vdata = GeomVertexData('placeholder_cube', format, Geom.UHStatic)
vertex = GeomVertexWriter(vdata, 'vertex')
normal = GeomVertexWriter(vdata, 'normal')
color = GeomVertexWriter(vdata, 'color')
# 定义立方体顶点
size = 1.0
vertices = [
# 前面 (Z+)
(-size, -size, size), (size, -size, size), (size, size, size), (-size, size, size),
# 后面 (Z-)
(-size, -size, -size), (-size, size, -size), (size, size, -size), (size, -size, -size),
# 左面 (X-)
(-size, -size, -size), (-size, -size, size), (-size, size, size), (-size, size, -size),
# 右面 (X+)
(size, -size, -size), (size, size, -size), (size, size, size), (size, -size, size),
# 上面 (Y+)
(-size, size, -size), (-size, size, size), (size, size, size), (size, size, -size),
# 下面 (Y-)
(-size, -size, -size), (size, -size, -size), (size, -size, size), (-size, -size, size)
]
normals = [
# 前面法线
(0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1),
# 后面法线
(0, 0, -1), (0, 0, -1), (0, 0, -1), (0, 0, -1),
# 左面法线
(-1, 0, 0), (-1, 0, 0), (-1, 0, 0), (-1, 0, 0),
# 右面法线
(1, 0, 0), (1, 0, 0), (1, 0, 0), (1, 0, 0),
# 上面法线
(0, 1, 0), (0, 1, 0), (0, 1, 0), (0, 1, 0),
# 下面法线
(0, -1, 0), (0, -1, 0), (0, -1, 0), (0, -1, 0)
]
# 青色
face_colors = [
(0.0, 1.0, 1.0, 1.0), (0.0, 1.0, 1.0, 1.0), (0.0, 1.0, 1.0, 1.0), (0.0, 1.0, 1.0, 1.0), # 前面 - 青色
(0.0, 0.8, 0.8, 1.0), (0.0, 0.8, 0.8, 1.0), (0.0, 0.8, 0.8, 1.0), (0.0, 0.8, 0.8, 1.0), # 后面 - 稍暗青色
(0.0, 0.9, 0.9, 1.0), (0.0, 0.9, 0.9, 1.0), (0.0, 0.9, 0.9, 1.0), (0.0, 0.9, 0.9, 1.0), # 左面 - 中等青色
(0.0, 0.7, 0.7, 1.0), (0.0, 0.7, 0.7, 1.0), (0.0, 0.7, 0.7, 1.0), (0.0, 0.7, 0.7, 1.0), # 右面 - 稍暗青色
(0.0, 1.0, 1.0, 1.0), (0.0, 1.0, 1.0, 1.0), (0.0, 1.0, 1.0, 1.0), (0.0, 1.0, 1.0, 1.0), # 上面 - 青色
(0.0, 0.6, 0.6, 1.0), (0.0, 0.6, 0.6, 1.0), (0.0, 0.6, 0.6, 1.0), (0.0, 0.6, 0.6, 1.0) # 下面 - 更暗青色
]
for i, vert in enumerate(vertices):
vertex.addData3f(*vert)
normal.addData3f(*normals[i])
color.addData4f(*face_colors[i])
# 创建几何体
geom = Geom(vdata)
# 创建面(每个面两个三角形)
prim = GeomTriangles(Geom.UHStatic)
# 每个面4个顶点生成2个三角形
for face in range(6): # 6个面
base_index = face * 4
# 第一个三角形
prim.addVertices(base_index, base_index + 1, base_index + 2)
# 第二个三角形
prim.addVertices(base_index, base_index + 2, base_index + 3)
prim.closePrimitive()
geom.addPrimitive(prim)
# 创建几何节点
geom_node = GeomNode('tileset_placeholder')
geom_node.addGeom(geom)
# 添加到场景
cube_node = parent_node.attachNewNode(geom_node)
cube_node.setScale(5) # 设置合适的大小
# 设置双面渲染
cube_node.setTwoSided(True)
# 添加材质
material = Material()
material.setBaseColor((0.0, 1.0, 1.0, 1.0)) # 青色
material.setSpecular((0.5, 0.5, 0.5, 1.0))
material.setShininess(32.0)
cube_node.setMaterial(material)
# 添加标识标签
cube_node.setTag("element_type", "cesium_placeholder")
print("✓ 占位符几何体创建完成")
return cube_node
except Exception as e:
print(f"✗ 创建占位符几何体出错: {e}")
import traceback
traceback.print_exc()
return None

View File

@ -0,0 +1,19 @@
"""Scene manager implementation composed from focused mixins."""
from scene.scene_manager_convert_tiles_mixin import SceneManagerConvertTilesMixin
from scene.scene_manager_io_mixin import SceneManagerIOMixin
from scene.scene_manager_light_mixin import SceneManagerLightMixin
from scene.scene_manager_model_mixin import SceneManagerModelMixin
from scene.scene_manager_serialization_mixin import SceneManagerSerializationMixin
class SceneManagerImpl(
SceneManagerSerializationMixin,
SceneManagerConvertTilesMixin,
SceneManagerLightMixin,
SceneManagerIOMixin,
SceneManagerModelMixin,
):
"""Composed scene manager implementation."""
pass

Some files were not shown because too many files have changed in this diff Show More