Compare commits
51 Commits
main
...
geng_migra
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c0a61d253 | ||
|
|
062bd4e720 | ||
|
|
a6986002a7 | ||
|
|
f17073160c | ||
|
|
c14c2b8796 | ||
|
|
b9053b6a28 | ||
|
|
1aa21c2680 | ||
|
|
4ef6ce97d4 | ||
|
|
776d3c01aa | ||
|
|
07359082cd | ||
|
|
04cb0dc441 | ||
|
|
8a1a0d19ba | ||
|
|
bdd4d0f1b5 | ||
|
|
74b6a3307c | ||
|
|
da629e508d | ||
|
|
4635458300 | ||
|
|
6524f8306b | ||
|
|
c4a507c6ac | ||
|
|
11ad2d5742 | ||
|
|
49d630f967 | ||
|
|
7e6b1d54d9 | ||
|
|
8a80af9825 | ||
|
|
6e76b54ddb | ||
|
|
1cfbeda77b | ||
|
|
b48616d5f2 | ||
|
|
780536203e | ||
|
|
c5dbc6be6f | ||
|
|
718cf802be | ||
|
|
22a9b5b64a | ||
|
|
c006b69613 | ||
|
|
4f70cc113b | ||
|
|
d32a604c2f | ||
|
|
c2e528c55f | ||
|
|
381a478abf | ||
|
|
3a13025009 | ||
|
|
e9784b3400 | ||
|
|
37b3cc30dc | ||
|
|
036b68ef41 | ||
|
|
5752559cdd | ||
|
|
7386d687da | ||
|
|
28d2b124cc | ||
|
|
2ac08b0582 | ||
|
|
86aaa21ddd | ||
|
|
af898947b5 | ||
|
|
f3f8da7b90 | ||
|
|
2183d3fc3e | ||
|
|
756db5b010 | ||
|
|
1fd7e1d7ac | ||
|
|
53e6a829e4 | ||
|
|
e917f9019d | ||
|
|
c93ab3edac |
11
.codex/environments/environment-2.toml
Normal 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"
|
||||
11
.codex/environments/environment.toml
Normal 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
@ -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
|
||||
|
||||
11
AGENTS.md
@ -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
@ -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
352
PROJECT_MODULE_INDEX.md
Normal 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)。
|
||||
270
PROJECT_OPTIMIZATION_ANALYSIS.md
Normal 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 B(1 天)
|
||||
|
||||
- 重构 `scene_manager_io_mixin.loadScene`:
|
||||
- `preflight`
|
||||
- `clear_old_scene`
|
||||
- `load_bam`
|
||||
- `rebuild_scene_tree`
|
||||
- `post_load_sync`
|
||||
|
||||
### Task C(1 天)
|
||||
|
||||
- 统一异常日志工具(轻量封装)
|
||||
- 首批替换 `animation_tools.py` 与 `property_helpers.py`
|
||||
|
||||
## 4.1 本轮深入分析(非 VR,P1 准备)
|
||||
|
||||
### 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 个高耦合文件。
|
||||
BIN
Panda3D-1.10/618a39808b4eb20ba86a9f6cabe5b91e.bam
Normal file
BIN
Panda3D-1.10/index-aa08c2.boo
Normal file
1
Panda3D-1.10/index_name.txt
Normal file
@ -0,0 +1 @@
|
||||
index-aa08c2.boo
|
||||
62
QT_TO_IMGUI_MIGRATION_CHECKLIST.md
Normal 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)
|
||||
@ -13,6 +13,7 @@ enabled:
|
||||
- scattering
|
||||
- skin_shading
|
||||
- sky_ao
|
||||
- selection_outline
|
||||
- smaa
|
||||
- ssr
|
||||
# - clouds
|
||||
|
||||
@ -63,5 +63,6 @@ global_stage_order:
|
||||
|
||||
# Finishing stages, do not insert anything below
|
||||
- UpscaleStage
|
||||
- SelectionOutlineStage
|
||||
- FinalStage
|
||||
- UpdatePreviousPipesStage
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -0,0 +1,2 @@
|
||||
"""Selection outline plugin package."""
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
settings:
|
||||
daytime_settings:
|
||||
|
||||
15
RenderPipelineFile/rpplugins/selection_outline/plugin.py
Normal 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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 6.5 MiB |
42
Start_Run.py
@ -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.11,LUI 可能不可用。")
|
||||
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()
|
||||
|
||||
@ -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 top‑left, 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)
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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] 的标准化设备坐标
|
||||
"""
|
||||
|
||||
@ -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:
|
||||
|
||||
1
build/.111_staging/assets/gui/gui_elements.json
Normal file
@ -0,0 +1 @@
|
||||
[]
|
||||
BIN
build/.111_staging/assets/scene.bam
Normal file
102
build/.111_staging/assets/scripts/BouncerScript.py
Normal 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("跳跃脚本停止")
|
||||
160
build/.111_staging/assets/scripts/ColorChangerScript.py
Normal 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("颜色变化脚本停止")
|
||||
39
build/.111_staging/assets/scripts/ComboAnimatorScript.py
Normal 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("复合动画脚本停止")
|
||||
69
build/.111_staging/assets/scripts/FollowerScript.py
Normal 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("跟随脚本停止")
|
||||
91
build/.111_staging/assets/scripts/MoverScript.py
Normal 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("移动脚本停止")
|
||||
133
build/.111_staging/assets/scripts/R_P.py
Normal 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("无法获取旋转信息")
|
||||
133
build/.111_staging/assets/scripts/R_R.py
Normal 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("无法获取旋转信息")
|
||||
215
build/.111_staging/assets/scripts/Rotate_H_Script.py
Normal 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("已设置为平滑旋转模式")
|
||||
215
build/.111_staging/assets/scripts/Rotate_P_Script.py
Normal 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("已设置为平滑旋转模式")
|
||||
56
build/.111_staging/assets/scripts/Rotate_R_Script.py
Normal 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("旋转脚本停止")
|
||||
39
build/.111_staging/assets/scripts/RotatorScript.py
Normal 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("旋转脚本停止")
|
||||
91
build/.111_staging/assets/scripts/ScalerScript.py
Normal 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("缩放脚本停止")
|
||||
41
build/.111_staging/assets/scripts/TestMover.py
Normal 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("移动脚本被销毁")
|
||||
28
build/.111_staging/assets/scripts/TestRotator.py
Normal 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("脚本被销毁")
|
||||
28
build/.111_staging/assets/scripts/TestScaler.py
Normal 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("脚本被销毁")
|
||||
28
build/.111_staging/assets/scripts/a.py
Normal 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("脚本被销毁")
|
||||
47
build/.111_staging/assets/scripts/example_script.py
Normal 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("示例脚本被禁用")
|
||||
28
build/.111_staging/assets/scripts/test_quick_script.py
Normal 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("脚本被销毁")
|
||||
534
build/.111_staging/source/main.py
Normal 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()
|
||||
53
config/default_imgui_layout.ini
Normal 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
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
44
core/editor_context.py
Normal 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
|
||||
@ -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)
|
||||
|
||||
@ -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
@ -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}")
|
||||
@ -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("✓ 默认巡检路线已创建")
|
||||
|
||||
130
core/render_pipeline_tag_state.py
Normal 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
|
||||
@ -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}")
|
||||
|
||||
@ -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'
|
||||
]
|
||||
]
|
||||
|
||||
1001
core/selection.py
303
core/selection_outline.py
Normal 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
|
||||
@ -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)} 个平面地形")
|
||||
|
||||
|
||||
@ -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 == "选择":
|
||||
|
||||
@ -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():
|
||||
|
||||
@ -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模式已禁用,手柄模型已隐藏")
|
||||
|
||||
|
||||
198
core/world.py
@ -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)
|
||||
|
||||
4027
gui/gui_manager.py
92
imgui.ini
@ -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
@ -0,0 +1 @@
|
||||
index-b2d982.boo
|
||||
@ -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
@ -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)
|
||||
219
project/project_schema.py
Normal 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)
|
||||
655
project/scene_description.py
Normal 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 {},
|
||||
}
|
||||
@ -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。
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
523
scene/scene_manager_convert_tiles_mixin.py
Normal 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
|
||||
19
scene/scene_manager_impl.py
Normal 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
|
||||