Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8eb4cef4d | ||
|
|
a1f6fe6097 | ||
|
|
ea6ff140e9 | ||
|
|
2c9f512996 | ||
|
|
280d6f8b2a | ||
|
|
d140552779 | ||
|
|
ffdad6f099 | ||
|
|
4f2c545b88 | ||
|
|
e2544ea440 | ||
|
|
3f8617a1b6 | ||
|
|
036acfeaed | ||
|
|
5a9e31a195 | ||
|
|
036b68ef41 | ||
|
|
5752559cdd | ||
|
|
7386d687da | ||
|
|
28d2b124cc | ||
|
|
2ac08b0582 | ||
|
|
86aaa21ddd | ||
|
|
af898947b5 | ||
|
|
f3f8da7b90 | ||
|
|
2183d3fc3e | ||
|
|
756db5b010 | ||
|
|
1fd7e1d7ac | ||
|
|
53e6a829e4 | ||
|
|
e917f9019d | ||
|
|
c93ab3edac |
62
IMGUI_MODULE_ANALYSIS.md
Normal file
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
2465
PROJECT_FULL_CATALOG.md
Normal file
File diff suppressed because it is too large
Load Diff
352
PROJECT_MODULE_INDEX.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`
|
||||
- 项目任务清单: `PROJECT_TASK_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)。
|
||||
493
PROJECT_OPTIMIZATION_ANALYSIS.md
Normal file
493
PROJECT_OPTIMIZATION_ANALYSIS.md
Normal file
@ -0,0 +1,493 @@
|
||||
# PROJECT_OPTIMIZATION_ANALYSIS
|
||||
|
||||
> 基于本地代码静态扫描生成(不含远程仓库信息,不含 `RenderPipelineFile/` 第三方目录)。
|
||||
|
||||
- 分析时间: 2026-03-01
|
||||
- 扫描范围: `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`
|
||||
- 旧上下文引用已从“接口访问路径”转为“上下文适配层/兼容字段”模式
|
||||
- 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`
|
||||
- `_walk_loaded_scene`
|
||||
- `_process_loaded_scene_node`
|
||||
- `_restore_node_scripts`
|
||||
- `_parse_vec3_string`
|
||||
- `_parse_color_string`
|
||||
- `loadScene` 行数:`556 -> 74`
|
||||
- 已清理重复异常分支:`_rebuildParentChildRelationships` 内重复 `except` 已移除
|
||||
- `main.py::__init__` 第一轮拆分已落地(非 VR):
|
||||
- `__init__` 行数:`375 -> 27`
|
||||
- 已抽出初始化阶段方法:
|
||||
- `_configure_main_window`
|
||||
- `_init_legacy_compat_fields`
|
||||
- `_init_core_services_non_vr`
|
||||
- `_init_runtime_features_non_vr`
|
||||
- `_init_imgui_runtime`
|
||||
- `_init_panel_modules`
|
||||
- `_init_startup_fonts`
|
||||
- `_init_ui_runtime_state`
|
||||
- `_bind_runtime_events`
|
||||
- `_finalize_startup`
|
||||
- 本轮新增深入分析结论(非 VR):
|
||||
- `ui/panels/animation_tools.py::_getActor` 第一轮拆分已完成
|
||||
- 实测指标:`510 -> 39` 行,近似圈复杂度 `191 -> 15`,内嵌局部函数 `9 -> 0`
|
||||
- `core/selection.py::updateGizmoDrag` 第一轮拆分已完成:
|
||||
- 实测指标:`278 -> 32` 行,近似圈复杂度 `65 -> 10`
|
||||
- 缩放/旋转/平移逻辑已拆到独立 helper
|
||||
- `ui/LUI/lui_manager_interaction.py::_update_drag` 第一轮拆分已完成:
|
||||
- 实测指标:`348 -> 28` 行,近似圈复杂度 `79 -> 7`
|
||||
- Canvas/组件拖拽流程已拆到独立 helper
|
||||
- `ui/panels/editor_panels_left.py::_draw_resource_manager` 保留布局版拆分已完成:
|
||||
- 实测指标:`310 -> 23` 行(第一轮 `310 -> 163`,第二轮 `163 -> 23`)
|
||||
- 目录/文件项渲染、展开内容、右键菜单流程已拆到独立 helper
|
||||
- `ui/LUI/lui_function_properties.py::_draw_component_properties` 第一轮编排拆分已完成:
|
||||
- 实测指标:`1441 -> 3` 行(入口)
|
||||
- 新增编排/子流程 helper:
|
||||
- `_draw_component_properties_core`(`34`)
|
||||
- `_draw_text_and_button_properties`(`216`)
|
||||
- `_draw_layout_transform_properties`(`200`)
|
||||
- `_draw_type_specific_properties`(`835`)
|
||||
- `_draw_anchor_and_hierarchy_properties`(`168`)
|
||||
- `ui/LUI/lui_manager_interaction.py::_handle_resize_drag` 第二轮拆分已完成:
|
||||
- 实测指标:`257 -> 3` 行(入口)
|
||||
- 新增编排/子流程 helper:
|
||||
- `_handle_resize_drag_core`(`44`)
|
||||
- `_finish_resize_drag_if_released`(`19`)
|
||||
- `_compute_resize_drag_bounds`(`109`)
|
||||
- `_apply_resize_drag_updates`(`110`)
|
||||
- `ui/LUI/lui_manager_interaction.py::_compute_resize_drag_bounds` / `_apply_resize_drag_updates` 第三轮细拆已完成:
|
||||
- 实测指标:`109 -> 27` / `110 -> 21`
|
||||
- 新增细粒度 helper(边界计算与组件应用分层):
|
||||
- `_get_resize_start_values`(`7`)
|
||||
- `_apply_resize_handle_delta`(`36`)
|
||||
- `_apply_resize_min_size_clamp`(`15`)
|
||||
- `_apply_shift_keep_ratio_resize`(`17`)
|
||||
- `_apply_alt_center_resize`(`22`)
|
||||
- `_resolve_resize_parent_context`(`13`)
|
||||
- `_apply_resize_component_size_by_type`(`24`)
|
||||
- `_sync_input_field_layout_stretch`(`22`)
|
||||
- `ui/LUI/lui_function_properties.py::_draw_type_specific_properties` 第二轮拆分已完成:
|
||||
- 实测指标:`835 -> 25` 行(类型分发入口)
|
||||
- 新增类型 helper:
|
||||
- `_draw_type_props_input_field`(`91`)
|
||||
- `_draw_type_props_slider`(`11`)
|
||||
- `_draw_type_props_checkbox`(`19`)
|
||||
- `_draw_type_props_plane_image`(`152`)
|
||||
- `_draw_type_props_frame`(`14`)
|
||||
- `_draw_type_props_selectbox`(`84`)
|
||||
- `_draw_type_props_http_text`(`72`)
|
||||
- `_draw_type_props_video`(`10`)
|
||||
- `_draw_video_source_and_audio_controls`(`4`,第三轮后为编排入口)
|
||||
- `_draw_video_playback_controls`(`147`)
|
||||
- `ui/LUI/lui_function_properties.py::_draw_video_source_and_audio_controls` 第三轮拆分已完成:
|
||||
- 实测指标:`231 -> 4` 行(入口)
|
||||
- 新增子流程 helper:
|
||||
- `_draw_video_audio_controls`(`26`)
|
||||
- `_draw_video_source_controls`(`19`)
|
||||
- `_process_pending_video_source`(`22`)
|
||||
- `_load_video_texture_from_source`(`21`)
|
||||
- `_apply_loaded_video_texture`(`14`)
|
||||
- `_sync_video_size_and_sprite`(`28`)
|
||||
- `_replace_video_sprite`(`23`)
|
||||
- `_refresh_video_keep_alive_node`(`22`)
|
||||
- `_load_audio_for_component`(`22`)
|
||||
- `_start_video_texture_playback`(`7`)
|
||||
- `ui/LUI/lui_function_properties.py::_draw_text_and_button_properties` 第四轮拆分已完成:
|
||||
- 实测指标:`216 -> 14` 行(入口)
|
||||
- 新增子流程 helper(文本编辑/颜色同步/按钮贴图):
|
||||
- `_draw_text_content_editor`(`13`)
|
||||
- `_draw_text_font_size_editor`(`18`)
|
||||
- `_draw_text_color_editor`(`11`)
|
||||
- `_sync_text_or_button_color`(`10`)
|
||||
- `_draw_button_texture_controls`(`19`)
|
||||
- `_apply_button_texture_variant`(`28`)
|
||||
- `ui/LUI/lui_manager_editor.py::_set_parent_child_relationship` 第四轮拆分已完成:
|
||||
- 实测指标:`234 -> 43` 行(入口)
|
||||
- 新增子流程 helper(关系校验/索引同步/可见层级):
|
||||
- `_is_valid_relationship_index`(`5`)
|
||||
- `_prepare_parent_child_link`(`8`)
|
||||
- `_remove_child_from_old_parent`(`8`)
|
||||
- `_attach_child_to_new_parent`(`6`)
|
||||
- `_compute_new_child_local_position`(`7`)
|
||||
- `_ensure_child_visibility_and_layer`(`19`)
|
||||
- `ui/LUI/lui_function_properties.py::_draw_layout_transform_properties` 第四轮拆分已完成:
|
||||
- 实测指标:`200 -> 10` 行(入口)
|
||||
- 新增子流程 helper(填充模式/尺寸联动/Layout Group/Z-Offset):
|
||||
- `_draw_fill_layout_controls`(`37`)
|
||||
- `_draw_position_controls`(`18`)
|
||||
- `_draw_size_controls`(`17`)
|
||||
- `_draw_layout_group_controls`(`53`)
|
||||
- `_draw_z_offset_controls`(`27`)
|
||||
|
||||
## 1. 总体画像
|
||||
|
||||
- Python 文件: `147`
|
||||
- 代码总行数: `58,479`
|
||||
- `except Exception` / `except:` 总计: `993`
|
||||
- 裸 `except:` 总计: `52`
|
||||
- 旧上下文关键词引用总量:
|
||||
- `interface_manager`: `20`
|
||||
- `treeWidget`: `2`
|
||||
- `gui_manager`: `90`
|
||||
|
||||
结论:
|
||||
|
||||
- Qt 依赖已清理后,当前主要技术债集中在三类:
|
||||
- 过大函数(可维护性差)
|
||||
- 异常处理过宽(问题可观测性差)
|
||||
- 旧 GUI 上下文命名耦合(边界不清晰)
|
||||
|
||||
## 2. 热点文件(按规模/风险)
|
||||
|
||||
### 2.1 超大文件 Top(非 VR)
|
||||
|
||||
1. `core/selection.py` (`2933` 行)
|
||||
2. `ui/LUI/lui_function_properties.py` (`1731` 行)
|
||||
3. `core/InfoPanelManager.py` (`1726` 行)
|
||||
4. `ui/LUI/lui_manager_editor.py` (`1725` 行)
|
||||
5. `ui/panels/property_helpers.py` (`1712` 行)
|
||||
6. `TransformGizmo/rotate_gizmo.py` (`1588` 行)
|
||||
7. `templates/main_template.py` (`1572` 行)
|
||||
8. `ui/panels/animation_tools.py` (`1563` 行)
|
||||
|
||||
### 2.2 长函数 Top(优先拆分,非 VR 当前状态)
|
||||
|
||||
1. `ui/panels/editor_panels_top.py::draw_menu_bar` (`227` 行)
|
||||
2. `scene/scene_manager_io_mixin.py::saveScene` (`222` 行)
|
||||
3. `TransformGizmo/move_gizmo.py::_on_mouse_down` (`222` 行)
|
||||
4. `ui/LUI/lui_manager_editor.py::draw_editor` (`217` 行)
|
||||
5. `ui/LUI/lui_manager_editor.py::draw_component_tree` (`217` 行)
|
||||
6. `core/InfoPanelManager.py::add_methods_to_property_panel` (`209` 行)
|
||||
7. `ui/panels/editor_panels_right.py::_draw_animation_properties` (`201` 行)
|
||||
8. `ui/panels/editor_panels_right.py::_draw_gui_properties` (`191` 行)
|
||||
9. `core/event_handler.py::mousePressEventLeft` (`186` 行)
|
||||
10. `scene/scene_manager_model_mixin.py::importModel` (`177` 行)
|
||||
|
||||
### 2.3 异常处理密度高(可观测性风险)
|
||||
|
||||
1. `ui/panels/animation_tools.py` (`except* = 85`)
|
||||
2. `core/selection.py` (`56`)
|
||||
3. `ui/panels/property_helpers.py` (`54`)
|
||||
4. `templates/main_template.py` (`39`)
|
||||
5. `scene/scene_manager_model_mixin.py` (`36`)
|
||||
6. `ui/LUI/lui_function_properties.py` (`36`)
|
||||
7. `ui/LUI/lui_manager_editor.py` (`33`)
|
||||
8. `core/world.py` (`28`)
|
||||
9. `scene/scene_manager_serialization_mixin.py` (`27`)
|
||||
|
||||
裸 `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 旧上下文耦合集中区(当前分布,非 VR)
|
||||
|
||||
1. `ui/panels/runtime_actions.py` (`gui_manager=25`)
|
||||
2. `core/event_handler.py` (`interface_manager=7`, `gui_manager=13`)
|
||||
3. `ui/panels/editor_panels_right.py` (`gui_manager=18`)
|
||||
4. `scene/scene_manager_serialization_mixin.py` (`gui_manager=13`)
|
||||
5. `scene/scene_manager_model_mixin.py` (`interface_manager=6`)
|
||||
6. `ui/panels/editor_panels_left.py` (`gui_manager=5`)
|
||||
7. `ui/panels/interaction_panels.py` (`gui_manager=5`)
|
||||
|
||||
## 3. 优化优先级(建议执行顺序)
|
||||
|
||||
## P0: 上下文收敛(已完成主干,继续收尾)
|
||||
|
||||
目标: 统一 GUI/场景树访问边界,减少跨模块 `hasattr(..., 'interface_manager')` 与 `treeWidget` 语义残留。
|
||||
|
||||
当前状态:
|
||||
|
||||
1. `EditorContext` 已落地并完成第一批接入。
|
||||
2. 仍有 `gui_manager/interface_manager` 兼容字段引用集中在少量模块。
|
||||
|
||||
建议动作(收尾):
|
||||
|
||||
1. 将 `runtime_actions` 与 `editor_panels_right` 中 `gui_manager` 读写统一迁移到 `editor_context` 访问方法。
|
||||
2. 给 `event_handler` 与 `scene_manager_serialization_mixin` 增加“兼容字段缺失”单点兜底函数,移除重复 `hasattr` 防御分支。
|
||||
3. 文档层明确“兼容字段仅保留读,不再新增写路径”。
|
||||
|
||||
预期收益:
|
||||
|
||||
- 降低命名残留与多处 `hasattr` 防御代码。
|
||||
- 后续模块拆分时边界更稳定。
|
||||
|
||||
## P1: 大函数拆分(第二阶段)
|
||||
|
||||
目标: 将核心长函数拆分成“流程编排 + 子步骤函数”,减少单函数认知负担。
|
||||
|
||||
建议拆分顺序(非 VR 当前阶段):
|
||||
|
||||
1. `ui/panels/editor_panels_top.py::draw_menu_bar`
|
||||
2. `scene/scene_manager_io_mixin.py::saveScene`
|
||||
3. `core/InfoPanelManager.py::add_methods_to_property_panel`
|
||||
|
||||
预期收益:
|
||||
|
||||
- 回归问题定位更快。
|
||||
- 面板和场景加载逻辑更易测试。
|
||||
|
||||
## P2: 异常处理治理(并行推进)
|
||||
|
||||
目标: 将“吞异常”改为“有边界的降级 + 可追踪日志”。
|
||||
|
||||
建议规则:
|
||||
|
||||
1. 禁止新增裸 `except:`。
|
||||
2. 高风险路径必须记录上下文:
|
||||
- 节点名/资源路径/操作类型/当前工具状态
|
||||
3. 对可恢复错误使用 `warning`,不可恢复错误返回显式失败值。
|
||||
|
||||
优先文件:
|
||||
|
||||
- `ui/panels/animation_tools.py`
|
||||
- `core/selection.py`
|
||||
- `ui/panels/property_helpers.py`
|
||||
- `scene/scene_manager_io_mixin.py`
|
||||
|
||||
## 4. 下一步可直接执行的任务包
|
||||
|
||||
### Task A(已完成)
|
||||
|
||||
- 建立 `core/editor_context.py`(或同级命名)
|
||||
- 给 `event_handler/selection/InfoPanelManager/runtime_actions` 接入上下文
|
||||
- 保持外部 API 不变,仅替换内部访问路径
|
||||
|
||||
### Task B(已完成)
|
||||
|
||||
- 重构 `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`
|
||||
|
||||
### Task D(已完成,第一轮)
|
||||
|
||||
- 重构 `ui/panels/animation_tools.py::_getActor`:
|
||||
- `resolve_owner_and_model_path_tags`
|
||||
- `validate_or_evict_cached_actor`
|
||||
- `build_candidate_paths`
|
||||
- `try_actor_from_path`
|
||||
- `try_actor_from_memory`
|
||||
- `cache_and_tag_actor`
|
||||
- 验收:
|
||||
- `_getActor` 长度降到 `<= 170` 行(实际 `39` 行)
|
||||
- 保持“文件路径优先,内存 autoBind 兜底”语义一致
|
||||
- 不改 VR 逻辑
|
||||
|
||||
### Task E(已完成,第一轮)
|
||||
|
||||
- 拆分 `core/selection.py::updateGizmoDrag`:
|
||||
- `_apply_scale_drag`
|
||||
- `_apply_rotation_drag`
|
||||
- `_compute_axis_movement_distance`
|
||||
- `_apply_translation_drag`
|
||||
- 验收:
|
||||
- 主函数长度降到 `<= 120` 行(实际 `32` 行)
|
||||
- 保持当前 gizmo 拖拽行为一致
|
||||
|
||||
### Task F(已完成,保留布局版两轮)
|
||||
|
||||
- 拆分 `ui/panels/editor_panels_left.py::_draw_resource_manager`
|
||||
- 验收:
|
||||
- 主函数长度降到 `<= 160` 行(第一轮 `163` 行,第二轮 `23` 行)
|
||||
- 保持原有布局/样式/文案/交互顺序不变
|
||||
|
||||
### Task G(已完成,第一轮)
|
||||
|
||||
- 拆分 `ui/LUI/lui_manager_interaction.py::_update_drag`
|
||||
- 验收:
|
||||
- 主函数长度降到 `<= 140` 行(实际 `28` 行)
|
||||
- 拖拽与缩放交互行为保持一致
|
||||
|
||||
### Task H(已完成,第一轮)
|
||||
|
||||
- 拆分 `ui/LUI/lui_function_properties.py::_draw_component_properties`
|
||||
- 验收:
|
||||
- 主函数长度降到 `<= 260` 行(实际 `3` 行,编排核心 `34` 行)
|
||||
- 已完成第一轮分区拆分并保持渲染逻辑一致
|
||||
|
||||
### Task I(已完成,第二轮)
|
||||
|
||||
- 拆分 `ui/LUI/lui_manager_interaction.py::_handle_resize_drag`
|
||||
- 验收:
|
||||
- 入口流程收敛到 `<= 120` 行(实际 `3` 行,编排核心 `44` 行)
|
||||
- 缩放手柄行为与边界语义保持一致
|
||||
- 第三轮细拆补充(已完成):
|
||||
- `_compute_resize_drag_bounds`: `109 -> 27`
|
||||
- `_apply_resize_drag_updates`: `110 -> 21`
|
||||
- 继续保持缩放语义一致
|
||||
|
||||
### Task J(已完成,第二轮)
|
||||
|
||||
- 继续拆分 `ui/LUI/lui_function_properties.py::_draw_type_specific_properties`
|
||||
- 验收:
|
||||
- 单函数长度降到 `<= 320` 行(实际 `25` 行)
|
||||
- 按组件类型拆分为独立 helper 并保持行为一致
|
||||
|
||||
### Task K(已完成,第三轮)
|
||||
|
||||
- 继续拆分 `ui/LUI/lui_function_properties.py::_draw_video_source_and_audio_controls`(已完成)
|
||||
- 验收:
|
||||
- 单函数长度降到 `<= 160` 行(实际 `4` 行)
|
||||
- 行为保持一致(本地视频/URL加载/音频联动)
|
||||
|
||||
### Task L(已完成,第四轮)
|
||||
|
||||
- 继续拆分 `ui/LUI/lui_function_properties.py::_draw_text_and_button_properties`
|
||||
- 验收:
|
||||
- 单函数长度降到 `<= 160` 行(实际 `14` 行)
|
||||
- 文本/按钮属性编辑行为保持一致
|
||||
|
||||
### Task M(已完成,第四轮)
|
||||
|
||||
- 继续拆分 `ui/LUI/lui_manager_editor.py::_set_parent_child_relationship`
|
||||
- 验收:
|
||||
- 单函数长度降到 `<= 180` 行(实际 `43` 行)
|
||||
- 父子关系编辑行为保持一致
|
||||
|
||||
### Task N(推荐下一步,1-2 天)
|
||||
|
||||
- 拆分 `ui/panels/editor_panels_top.py::draw_menu_bar`
|
||||
- 验收:
|
||||
- 单函数长度降到 `<= 170` 行
|
||||
- 菜单项顺序与行为保持一致
|
||||
|
||||
### Task O(已完成,第四轮)
|
||||
|
||||
- 继续拆分 `ui/LUI/lui_function_properties.py::_draw_layout_transform_properties`
|
||||
- 验收:
|
||||
- 单函数长度降到 `<= 160` 行(实际 `10` 行)
|
||||
- 布局与尺寸联动行为保持一致
|
||||
|
||||
## 4.1 本轮深入分析(非 VR,P1 准备)
|
||||
|
||||
### A) `scene/scene_manager_io_mixin.py::loadScene`(核心优先,已完成)
|
||||
|
||||
- 函数规模: `74` 行(重构前 `556` 行)
|
||||
- 近似圈复杂度(重构前): `114`
|
||||
- 关键问题:
|
||||
- `processNode` 内联逻辑已外提为 `_walk_loaded_scene/_process_loaded_scene_node`。
|
||||
- 重复异常分支已清理。
|
||||
- 仍存在较多调试输出(`print` 与注释 `print`),后续可做日志分级。
|
||||
- 建议拆分(保持外部 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-03-01):
|
||||
- 第二轮已完成(`loadScene` 已达验收目标:`74` 行)。
|
||||
- 后续建议转入回归观察,优先推进 `ui/panels/editor_panels_top.py::draw_menu_bar`。
|
||||
|
||||
### B) `main.py::__init__`(第二优先,已完成第一轮)
|
||||
|
||||
- 函数规模: `27` 行(重构前 `375` 行)
|
||||
- 状态:
|
||||
- 初始化编排已拆分为阶段方法,VR 初始化逻辑保持原样。
|
||||
- 约束:
|
||||
- 后续仅建议做微调,不再进行结构性拆分。
|
||||
|
||||
### C) `ui/panels/animation_tools.py::_getActor`(已完成第一轮)
|
||||
|
||||
- 函数规模: `39` 行(重构前 `510` 行)
|
||||
- 近似圈复杂度: `15`(重构前 `191`)
|
||||
- 已落地 helper:
|
||||
- `_sync_owner_model_path_tags`
|
||||
- `_get_valid_cached_actor`
|
||||
- `_detect_animation_nodes`
|
||||
- `_collect_autobind_source_candidates`
|
||||
- `_try_memory_fallback_actor`
|
||||
- `_collect_actor_candidate_paths`
|
||||
- `_try_actor_from_path`
|
||||
- 状态:
|
||||
- 入口函数已收敛为流程编排。
|
||||
- 已通过 `python -m compileall ui/panels/animation_tools.py`。
|
||||
|
||||
### D) `core/selection.py::updateGizmoDrag`(已完成第一轮)
|
||||
|
||||
- 函数规模: `32` 行(重构前 `278` 行)
|
||||
- 近似圈复杂度: `10`(重构前 `65`)
|
||||
- 已落地 helper:
|
||||
- `_validate_gizmo_drag_state`
|
||||
- `_apply_scale_drag`
|
||||
- `_apply_rotate_drag`
|
||||
- `_compute_axis_movement_distance`
|
||||
- `_apply_translate_drag`
|
||||
- 状态:
|
||||
- 主流程已收敛为编排入口。
|
||||
- 已通过 `python -m compileall core/selection.py`。
|
||||
|
||||
### E) `ui/panels/editor_panels_left.py::_draw_resource_manager`(已完成两轮)
|
||||
|
||||
- 函数规模: `23` 行(重构前 `310` 行)
|
||||
- 近似圈复杂度: 已由主函数分支链路转移到子流程函数(入口仅保留编排)
|
||||
- 已落地 helper:
|
||||
- `_update_resource_manager_window_rect`
|
||||
- `_draw_resource_toolbar_and_filters`
|
||||
- `_draw_resource_directory_entries`
|
||||
- `_draw_resource_directory_entry`
|
||||
- `_draw_resource_directory_children`
|
||||
- `_draw_resource_subdir_entry`
|
||||
- `_draw_resource_subfile_entry`
|
||||
- `_draw_resource_file_entries`
|
||||
- `_draw_resource_file_entry`
|
||||
- `_draw_resource_context_menu`
|
||||
- 状态:
|
||||
- 主流程已收敛为编排入口,且保持原布局与交互顺序。
|
||||
- 已通过 `python -m compileall ui/panels/editor_panels_left.py`。
|
||||
|
||||
## 4.2 执行顺序建议(非 VR)
|
||||
|
||||
1. `loadScene` 已完成两轮拆分(当前建议冻结并转入回归观察)。
|
||||
2. `main.__init__` 已完成第一轮拆分(建议转入回归观察)。
|
||||
3. `_update_drag` 已完成第一轮拆分,建议转入回归观察。
|
||||
4. `_draw_component_properties` 第一轮已完成,`_draw_type_specific_properties` 二轮与视频段三轮已完成。
|
||||
5. `_handle_resize_drag` 第三轮细拆已完成,`_draw_text_and_button_properties`、`_set_parent_child_relationship`、`_draw_layout_transform_properties` 第四轮已完成。
|
||||
|
||||
## 5. 与现有文档关系
|
||||
|
||||
- 模块总索引: `PROJECT_MODULE_INDEX.md`
|
||||
- 项目任务清单: `PROJECT_TASK_CHECKLIST.md`
|
||||
- 历史分析: `IMGUI_MODULE_ANALYSIS.md`
|
||||
|
||||
---
|
||||
|
||||
如果按此路线继续,建议下一轮直接从 **Task N** 开始,优先落地 `draw_menu_bar` 的流程拆分并保持行为不变。
|
||||
277
PROJECT_TASK_CHECKLIST.md
Normal file
277
PROJECT_TASK_CHECKLIST.md
Normal file
@ -0,0 +1,277 @@
|
||||
# 项目任务清单(Project Task Checklist)
|
||||
|
||||
更新时间:2026-03-01
|
||||
适用范围:`d:\IMGUI\EG` 本地工作区(不含远程仓库内容)
|
||||
|
||||
## 目标
|
||||
|
||||
- 保持编辑器主路径稳定运行在 ImGui + Panda3D。
|
||||
- 持续收敛历史兼容耦合(`interface_manager/treeWidget/gui_manager`)。
|
||||
- 基于可执行任务包推进非 VR 优化重构。
|
||||
|
||||
## 当前状态总览
|
||||
|
||||
- Qt 运行依赖主路径已清理完成(排除 `RenderPipelineFile`)。
|
||||
- 项目已进入“非 VR 结构优化阶段”。
|
||||
- `_getActor`、`updateGizmoDrag`、`_update_drag` 第一轮拆分已完成。
|
||||
- `资源管理器面板` 已完成“保留原布局/样式/交互”的第二轮拆分收敛。
|
||||
- `_draw_component_properties` 已完成第一轮“编排入口 + 子流程”拆分(入口 `1441 -> 3` 行)。
|
||||
- `_handle_resize_drag` 已完成第三轮“边界计算 / 更新应用”细拆(核心 `44` 行)。
|
||||
- `_draw_video_source_and_audio_controls` 已完成第三轮“音频/视频源/加载应用”拆分(`231 -> 4` 行)。
|
||||
- `_draw_text_and_button_properties` 已完成第四轮“文本/颜色/按钮贴图”拆分(`216 -> 14` 行)。
|
||||
- `_set_parent_child_relationship` 已完成第四轮“关系编排 + 可见层级”拆分(`234 -> 43` 行)。
|
||||
- `_draw_layout_transform_properties` 已完成第四轮“布局/尺寸/Z-Offset”拆分(`200 -> 10` 行)。
|
||||
|
||||
## 已完成(Done)
|
||||
|
||||
### A. Qt 清理与迁移完成
|
||||
|
||||
- [x] 清理主路径 Qt 直接依赖:`PyQt/PySide/Qt` 扫描主路径为 `0`。
|
||||
- [x] `core/InfoPanelManager.py`:去除 Qt 直接导入。
|
||||
- [x] `core/selection.py`:光标逻辑改为 Panda3D 路径。
|
||||
- [x] `scene/scene_manager_convert_tiles_mixin.py`:移除 `QProgressDialog`。
|
||||
- [x] `ui/widgets.py`:替换为 legacy 占位模块。
|
||||
- [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 运行依赖。
|
||||
|
||||
### B. 上下文收敛与基础重构完成
|
||||
|
||||
- [x] 建立 `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`
|
||||
- [x] `scene/scene_manager_io_mixin.py::loadScene` 完成两轮拆分(`556 -> 74` 行)。
|
||||
- [x] `main.py::__init__` 第一轮拆分完成(`375 -> 27` 行,VR 初始化逻辑保持原样)。
|
||||
|
||||
### C. 面板模块化完成
|
||||
|
||||
- [x] `ui/panels/editor_panels.py` 按布局拆分为 4 个模块:
|
||||
- `editor_panels_top.py`
|
||||
- `editor_panels_left.py`
|
||||
- `editor_panels_center.py`
|
||||
- `editor_panels_right.py`
|
||||
- [x] `editor_panels_right.py` 再拆分为 3 个子 mixin:
|
||||
- `editor_panels_right_transform.py`
|
||||
- `editor_panels_right_material.py`
|
||||
- `editor_panels_right_collision.py`
|
||||
|
||||
### D. 动画加载入口拆分完成(第一轮)
|
||||
|
||||
- [x] `ui/panels/animation_tools.py::_getActor` 第一轮拆分完成:
|
||||
- 行数:`510 -> 39`
|
||||
- 新增 helper:`_sync_owner_model_path_tags`、`_get_valid_cached_actor`、`_try_memory_fallback_actor`、`_collect_actor_candidate_paths`、`_try_actor_from_path` 等
|
||||
- 保持策略:文件路径优先,失败后内存/autoBind 兜底
|
||||
|
||||
### E. Gizmo 拖拽拆分完成(第一轮)
|
||||
|
||||
- [x] `core/selection.py::updateGizmoDrag` 第一轮拆分完成:
|
||||
- 行数:`278 -> 32`
|
||||
- 近似圈复杂度:`65 -> 10`
|
||||
- 新增 helper:`_validate_gizmo_drag_state`、`_apply_scale_drag`、`_apply_rotate_drag`、`_compute_axis_movement_distance`、`_apply_translate_drag`
|
||||
- 保持目标:gizmo 缩放/旋转/平移行为不变
|
||||
|
||||
### F. LUI 拖拽链路拆分完成(第一轮)
|
||||
|
||||
- [x] `ui/LUI/lui_manager_interaction.py::_update_drag` 第一轮拆分完成:
|
||||
- 行数:`348 -> 28`
|
||||
- 近似圈复杂度:`79 -> 7`
|
||||
- 新增 helper:`_process_canvas_drag`、`_try_start_pending_component_drag`、`_finish_component_drag_if_released`、`_update_active_component_drag`
|
||||
- 保持目标:Canvas/组件拖拽、吸附与释放语义不变
|
||||
|
||||
### G. 资源管理器面板拆分完成(保留布局版)
|
||||
|
||||
- [x] 第一轮低侵入优化:
|
||||
- 行数:`310 -> 163`
|
||||
- 近似圈复杂度:`88 -> 41`
|
||||
- 新增 helper:`_draw_resource_toolbar_and_filters`、`_load_resource_icon`、`_handle_resource_item_selection`、`_open_resource_item_context_menu`、`_handle_resource_file_double_click`、`_draw_resource_context_menu`
|
||||
- [x] 第二轮(按当前标准重启,保持布局/样式/文案/交互顺序):
|
||||
- 入口函数行数:`163 -> 23`(相对原始 `310 -> 23`)
|
||||
- 新增 helper:`_update_resource_manager_window_rect`、`_draw_resource_directory_entries`、`_draw_resource_directory_entry`、`_draw_resource_directory_children`、`_draw_resource_subdir_entry`、`_draw_resource_subfile_entry`、`_draw_resource_file_entries`、`_draw_resource_file_entry`
|
||||
- 保持目标:不改视觉布局,不改交互语义,仅做结构分解
|
||||
|
||||
### H. LUI 属性面板拆分完成(第一轮编排)
|
||||
|
||||
- [x] `ui/LUI/lui_function_properties.py::_draw_component_properties` 第一轮拆分完成:
|
||||
- 入口函数行数:`1441 -> 3`
|
||||
- 编排主流程函数:`_draw_component_properties_core = 34` 行
|
||||
- 新增 helper:
|
||||
- `_draw_text_and_button_properties`(`216` 行)
|
||||
- `_draw_layout_transform_properties`(`200` 行)
|
||||
- `_draw_type_specific_properties`(`835` 行)
|
||||
- `_draw_anchor_and_hierarchy_properties`(`168` 行)
|
||||
- 保持目标:属性面板行为不变,先完成入口和流程层收敛
|
||||
|
||||
### I. LUI 缩放链路拆分完成(第二轮)
|
||||
|
||||
- [x] `ui/LUI/lui_manager_interaction.py::_handle_resize_drag` 第二轮拆分完成:
|
||||
- 入口函数行数:`257 -> 3`
|
||||
- 编排主流程函数:`_handle_resize_drag_core = 44` 行
|
||||
- 新增 helper:
|
||||
- `_finish_resize_drag_if_released`(`19` 行)
|
||||
- `_compute_resize_drag_bounds`(`109` 行)
|
||||
- `_apply_resize_drag_updates`(`110` 行)
|
||||
- 保持目标:缩放手柄行为与边界语义保持一致
|
||||
|
||||
### J. LUI 属性类型分支拆分完成(第二轮)
|
||||
|
||||
- [x] `ui/LUI/lui_function_properties.py::_draw_type_specific_properties` 第二轮拆分完成:
|
||||
- 主函数行数:`835 -> 25`(类型分发入口)
|
||||
- 新增类型 helper:
|
||||
- `_draw_type_props_input_field`(`91` 行)
|
||||
- `_draw_type_props_slider`(`11` 行)
|
||||
- `_draw_type_props_checkbox`(`19` 行)
|
||||
- `_draw_type_props_plane_image`(`152` 行)
|
||||
- `_draw_type_props_frame`(`14` 行)
|
||||
- `_draw_type_props_selectbox`(`84` 行)
|
||||
- `_draw_type_props_http_text`(`72` 行)
|
||||
- `_draw_type_props_video`(`10` 行)
|
||||
- `_draw_video_source_and_audio_controls`(`231` 行)
|
||||
- `_draw_video_playback_controls`(`147` 行)
|
||||
- 保持目标:各组件类型交互语义保持一致,先完成类型分发层收敛
|
||||
|
||||
### K. LUI 视频属性段拆分完成(第三轮)
|
||||
|
||||
- [x] `ui/LUI/lui_function_properties.py::_draw_video_source_and_audio_controls` 第三轮拆分完成:
|
||||
- 主函数行数:`231 -> 4`(编排入口)
|
||||
- 新增 helper:
|
||||
- `_draw_video_audio_controls`(`26` 行)
|
||||
- `_draw_video_source_controls`(`19` 行)
|
||||
- `_process_pending_video_source`(`22` 行)
|
||||
- `_load_video_texture_from_source`(`21` 行)
|
||||
- `_apply_loaded_video_texture`(`14` 行)
|
||||
- `_sync_video_size_and_sprite`(`28` 行)
|
||||
- `_replace_video_sprite`(`23` 行)
|
||||
- `_refresh_video_keep_alive_node`(`22` 行)
|
||||
- `_load_audio_for_component`(`22` 行)
|
||||
- `_start_video_texture_playback`(`7` 行)
|
||||
- 保持目标:本地视频加载/URL加载回退/音频联动语义保持一致
|
||||
|
||||
### L. LUI 缩放链路细拆完成(第三轮)
|
||||
|
||||
- [x] `ui/LUI/lui_manager_interaction.py::_compute_resize_drag_bounds` / `_apply_resize_drag_updates` 第三轮细拆完成:
|
||||
- 主函数行数:`109 -> 27` / `110 -> 21`
|
||||
- 新增 helper:
|
||||
- `_get_resize_start_values`(`7` 行)
|
||||
- `_apply_resize_handle_delta`(`36` 行)
|
||||
- `_apply_resize_min_size_clamp`(`15` 行)
|
||||
- `_apply_shift_keep_ratio_resize`(`17` 行)
|
||||
- `_apply_alt_center_resize`(`22` 行)
|
||||
- `_cache_resize_canvas_bounds`(`8` 行)
|
||||
- `_sanitize_resize_dimensions`(`6` 行)
|
||||
- `_resolve_resize_parent_context`(`13` 行)
|
||||
- `_write_resize_component_data`(`5` 行)
|
||||
- `_apply_resize_component_position`(`9` 行)
|
||||
- `_apply_resize_component_size_by_type`(`24` 行)
|
||||
- `_apply_button_resize_size`(`7` 行)
|
||||
- `_apply_input_field_resize_size`(`10` 行)
|
||||
- `_sync_input_field_layout_stretch`(`22` 行)
|
||||
- `_post_resize_component_sync`(`7` 行)
|
||||
- `_get_resize_modifier_info`(`7` 行)
|
||||
- 保持目标:缩放手柄行为与边界语义保持一致
|
||||
|
||||
### M. LUI 文本按钮属性段拆分完成(第四轮)
|
||||
|
||||
- [x] `ui/LUI/lui_function_properties.py::_draw_text_and_button_properties` 第四轮拆分完成:
|
||||
- 主函数行数:`216 -> 14`
|
||||
- 新增 helper:
|
||||
- `_draw_text_content_editor`(`13` 行)
|
||||
- `_draw_text_font_size_editor`(`18` 行)
|
||||
- `_draw_text_color_editor`(`11` 行)
|
||||
- `_sync_text_or_button_color`(`10` 行)
|
||||
- `_apply_button_layout_color`(`12` 行)
|
||||
- `_apply_text_color`(`3` 行)
|
||||
- `_draw_button_texture_controls`(`19` 行)
|
||||
- `_draw_button_texture_pick_button`(`22` 行)
|
||||
- `_apply_button_texture_variant`(`28` 行)
|
||||
- `_unpack_button_atlas_result`(`9` 行)
|
||||
- `_apply_button_custom_textures`(`11` 行)
|
||||
- `_fit_button_to_default_texture_size`(`22` 行)
|
||||
- `_clear_button_custom_textures`(`13` 行)
|
||||
- `_draw_button_texture_path_labels`(`7` 行)
|
||||
- 保持目标:文本/按钮编辑行为与贴图切换语义保持一致
|
||||
|
||||
### N. LUI 层级编辑父子关系拆分完成(第四轮)
|
||||
|
||||
- [x] `ui/LUI/lui_manager_editor.py::_set_parent_child_relationship` 第四轮拆分完成:
|
||||
- 主函数行数:`234 -> 43`
|
||||
- 新增 helper:
|
||||
- `_is_valid_relationship_index`(`5` 行)
|
||||
- `_prepare_parent_child_link`(`8` 行)
|
||||
- `_remove_child_from_old_parent`(`8` 行)
|
||||
- `_attach_child_to_new_parent`(`6` 行)
|
||||
- `_compute_new_child_local_position`(`7` 行)
|
||||
- `_resolve_parent_visual_object`(`10` 行)
|
||||
- `_set_child_visual_parent_policy`(`12` 行)
|
||||
- `_clamp_child_local_within_parent`(`27` 行)
|
||||
- `_sync_child_canvas_index_with_parent`(`7` 行)
|
||||
- `_apply_child_absolute_position`(`10` 行)
|
||||
- `_ensure_child_visibility_and_layer`(`19` 行)
|
||||
- `_ensure_button_child_visibility`(`15` 行)
|
||||
- `_ensure_frame_child_visibility`(`9` 行)
|
||||
- `_ensure_media_child_visibility`(`13` 行)
|
||||
- `_ensure_child_sprite_visibility_and_layer`(`24` 行)
|
||||
- `_finalize_parent_child_link`(`9` 行)
|
||||
- 保持目标:逻辑父子关系/可见层级/画布索引同步语义保持一致
|
||||
|
||||
### O. LUI 布局变换属性段拆分完成(第四轮)
|
||||
|
||||
- [x] `ui/LUI/lui_function_properties.py::_draw_layout_transform_properties` 第四轮拆分完成:
|
||||
- 主函数行数:`200 -> 10`
|
||||
- 新增 helper:
|
||||
- `_draw_fill_layout_controls`(`37` 行)
|
||||
- `_draw_position_controls`(`18` 行)
|
||||
- `_draw_size_controls`(`17` 行)
|
||||
- `_draw_width_control`(`23` 行)
|
||||
- `_draw_height_control`(`19` 行)
|
||||
- `_post_size_control_sync`(`9` 行)
|
||||
- `_draw_layout_group_controls`(`53` 行)
|
||||
- `_draw_z_offset_controls`(`27` 行)
|
||||
- 保持目标:填充模式/尺寸联动/Layout Group/Z-Offset 行为一致
|
||||
|
||||
## 下一步(Next)
|
||||
|
||||
### N1 `顶部菜单栏` 拆分(最高优先级,非 VR)
|
||||
|
||||
- 目标文件:`ui/panels/editor_panels_top.py::draw_menu_bar`
|
||||
- 当前规模:`227` 行
|
||||
- 验收目标:
|
||||
- 拆分为“菜单组渲染 / 行为分发 / 状态同步”子流程
|
||||
- 单函数规模收敛到 `<= 170` 行
|
||||
- 保持现有菜单项顺序与功能一致
|
||||
|
||||
### N2 `场景保存链路` 拆分(高优先级,非 VR)
|
||||
|
||||
- 目标文件:`scene/scene_manager_io_mixin.py::saveScene`
|
||||
- 当前规模:`222` 行
|
||||
- 验收目标:
|
||||
- 拆分为“保存前校验 / 节点序列化 / 后处理”子流程
|
||||
- 单函数规模收敛到 `<= 170` 行
|
||||
- 保持保存行为与输出格式一致
|
||||
|
||||
### N3 `属性面板方法注入` 拆分(中优先级,非 VR)
|
||||
|
||||
- 目标文件:`core/InfoPanelManager.py::add_methods_to_property_panel`
|
||||
- 当前规模:`209` 行
|
||||
- 验收目标:
|
||||
- 拆分为“能力绑定 / 兼容兜底 / 日志分层”子流程
|
||||
- 单函数规模收敛到 `<= 170` 行
|
||||
- 保持属性面板行为一致
|
||||
|
||||
## 验收标准(阶段)
|
||||
|
||||
- 不安装 PyQt/PySide 时,`python main.py` 可进入编辑器主界面。
|
||||
- 非 VR 主流程(导入/选择/变换/保存/加载)无 Qt 残余异常。
|
||||
- 每轮重构后可通过最小回归(启动、导入、播放动画、保存加载)。
|
||||
|
||||
## 关联文档
|
||||
|
||||
- 优化分析:`PROJECT_OPTIMIZATION_ANALYSIS.md`
|
||||
- 模块索引:`PROJECT_MODULE_INDEX.md`
|
||||
- 全量目录:`PROJECT_FULL_CATALOG.md`
|
||||
- 历史分析:`IMGUI_MODULE_ANALYSIS.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
|
||||
|
||||
@ -0,0 +1,2 @@
|
||||
"""Selection outline plugin package."""
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
settings:
|
||||
daytime_settings:
|
||||
|
||||
15
RenderPipelineFile/rpplugins/selection_outline/plugin.py
Normal file
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);
|
||||
}
|
||||
|
||||
Binary file not shown.
@ -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] 的标准化设备坐标
|
||||
"""
|
||||
|
||||
@ -1,18 +1,17 @@
|
||||
# 修改后的 InfoPanelManager.py
|
||||
from xml.sax.handler import property_encoding
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtWidgets import QMessageBox
|
||||
from direct.gui.DirectGui import DirectFrame, DirectLabel
|
||||
from direct.showbase.ShowBaseGlobal import aspect2d
|
||||
from panda3d.core import TextNode, Vec4, NodePath
|
||||
from direct.showbase.DirectObject import DirectObject
|
||||
import threading
|
||||
# 修改后的 InfoPanelManager.py
|
||||
from direct.gui.DirectGui import DirectFrame, DirectLabel
|
||||
from direct.showbase.ShowBaseGlobal import aspect2d
|
||||
from panda3d.core import TextNode, Vec4, NodePath
|
||||
from direct.showbase.DirectObject import DirectObject
|
||||
import threading
|
||||
import json
|
||||
import time
|
||||
from urllib.parse import urlparse
|
||||
import requests
|
||||
|
||||
import time
|
||||
from urllib.parse import urlparse
|
||||
import requests
|
||||
|
||||
from scene.tree_roles import TREE_USER_ROLE
|
||||
from core.editor_context import get_editor_context
|
||||
|
||||
|
||||
class InfoPanelManager(DirectObject):
|
||||
"""信息面板管理器,用于创建和管理2D信息面板"""
|
||||
@ -22,12 +21,17 @@ class InfoPanelManager(DirectObject):
|
||||
初始化信息面板管理器
|
||||
parent: 父节点,默认为aspect2d
|
||||
"""
|
||||
DirectObject.__init__(self)
|
||||
self.world= world
|
||||
self.parent = aspect2d
|
||||
self.panels = {} # 存储所有创建的面板
|
||||
self.data_sources = {} # 存储数据源
|
||||
self.data_callbacks = {} # 存储数据更新回调
|
||||
DirectObject.__init__(self)
|
||||
self.world= world
|
||||
self._editor_context = get_editor_context(world)
|
||||
self.parent = aspect2d
|
||||
self.panels = {} # 存储所有创建的面板
|
||||
self.data_sources = {} # 存储数据源
|
||||
self.data_callbacks = {} # 存储数据更新回调
|
||||
|
||||
def _get_tree_widget(self):
|
||||
"""统一获取场景树控件。"""
|
||||
return self._editor_context.get_tree_widget()
|
||||
|
||||
def setParent(self, parent):
|
||||
"""设置父节点"""
|
||||
@ -206,35 +210,40 @@ class InfoPanelManager(DirectObject):
|
||||
"""
|
||||
将信息面板添加到场景树中
|
||||
"""
|
||||
try:
|
||||
# 获取树形控件
|
||||
if hasattr(self.world, 'interface_manager') and hasattr(self.world.interface_manager, 'treeWidget'):
|
||||
tree_widget = self.world.interface_manager.treeWidget
|
||||
if tree_widget:
|
||||
# 查找根节点项
|
||||
root_item = None
|
||||
for i in range(tree_widget.topLevelItemCount()):
|
||||
item = tree_widget.topLevelItem(i)
|
||||
if item.text(0) == "render" or item.data(0, Qt.UserRole) == self.world.render:
|
||||
root_item = item
|
||||
break
|
||||
|
||||
if root_item:
|
||||
# 使用现有的 add_node_to_tree_widget 方法添加节点
|
||||
qt_item = tree_widget.add_node_to_tree_widget(panel_node, root_item, "INFO_PANEL")
|
||||
if qt_item:
|
||||
print(f"✅ 信息面板 {panel_id} 已添加到场景树")
|
||||
# 选中创建的节点
|
||||
tree_widget.setCurrentItem(qt_item)
|
||||
# 更新选择和属性面板
|
||||
tree_widget.update_selection_and_properties(panel_node, qt_item)
|
||||
else:
|
||||
print(f"⚠️ 信息面板 {panel_id} 添加到场景树失败")
|
||||
else:
|
||||
print("⚠️ 未找到场景树根节点,无法添加信息面板")
|
||||
else:
|
||||
print("⚠️ 无法访问场景树控件,信息面板未添加到场景树")
|
||||
except Exception as e:
|
||||
try:
|
||||
# 获取树形控件
|
||||
tree_widget = self._get_tree_widget()
|
||||
if tree_widget:
|
||||
# 查找根节点项
|
||||
root_item = None
|
||||
for i in range(tree_widget.topLevelItemCount()):
|
||||
item = tree_widget.topLevelItem(i)
|
||||
item_node = None
|
||||
if hasattr(item, "data"):
|
||||
try:
|
||||
item_node = item.data(0, TREE_USER_ROLE)
|
||||
except Exception:
|
||||
item_node = None
|
||||
if item.text(0) == "render" or item_node == self.world.render:
|
||||
root_item = item
|
||||
break
|
||||
|
||||
if root_item:
|
||||
# 使用现有的 add_node_to_tree_widget 方法添加节点
|
||||
qt_item = tree_widget.add_node_to_tree_widget(panel_node, root_item, "INFO_PANEL")
|
||||
if qt_item:
|
||||
print(f"✅ 信息面板 {panel_id} 已添加到场景树")
|
||||
# 选中创建的节点
|
||||
tree_widget.setCurrentItem(qt_item)
|
||||
# 更新选择和属性面板
|
||||
tree_widget.update_selection_and_properties(panel_node, qt_item)
|
||||
else:
|
||||
print(f"⚠️ 信息面板 {panel_id} 添加到场景树失败")
|
||||
else:
|
||||
print("⚠️ 未找到场景树根节点,无法添加信息面板")
|
||||
else:
|
||||
print("⚠️ 无法访问场景树控件,信息面板未添加到场景树")
|
||||
except Exception as e:
|
||||
print(f"❌ 添加信息面板到场景树时出错: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
@ -1306,52 +1315,75 @@ class InfoPanelManager(DirectObject):
|
||||
|
||||
print("✓ 示例天气信息面板已创建")
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ 创建示例天气信息面板失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
QMessageBox.critical(self, "错误", f"创建示例天气信息面板时出错: {str(e)}")
|
||||
except Exception as e:
|
||||
print(f"✗ 创建示例天气信息面板失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
if hasattr(self.world, "add_error_message"):
|
||||
self.world.add_error_message(f"创建示例天气信息面板时出错: {str(e)}")
|
||||
|
||||
def getRealWeatherData(self):
|
||||
"""获取真实天气数据"""
|
||||
try:
|
||||
import requests
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
# 请求天气数据
|
||||
url = "https://wttr.in/Beijing?format=j1"
|
||||
response = requests.get(url, timeout=10)
|
||||
response.raise_for_status()
|
||||
|
||||
# 解析JSON数据
|
||||
weather_data = response.json()
|
||||
|
||||
# 提取当前天气信息
|
||||
current_condition = weather_data['current_condition'][0]
|
||||
weather_desc = current_condition['weatherDesc'][0]['value']
|
||||
temp_c = current_condition['temp_C']
|
||||
feels_like = current_condition['FeelsLikeC']
|
||||
humidity = current_condition['humidity']
|
||||
pressure = current_condition['pressure']
|
||||
visibility = current_condition['visibility']
|
||||
wind_speed = current_condition['windspeedKmph']
|
||||
wind_dir = current_condition['winddir16Point']
|
||||
|
||||
# 提取空气质量(如果可用)
|
||||
air_quality = "N/A"
|
||||
if 'air_quality' in weather_data and weather_data['air_quality']:
|
||||
if 'us-epa-index' in current_condition:
|
||||
air_quality_index = current_condition['air_quality_index']
|
||||
air_quality = f"指数: {air_quality_index}"
|
||||
|
||||
# 获取更新时间
|
||||
update_time = datetime.now().strftime("%Y-%m-%d %H:%M")
|
||||
|
||||
# 格式化显示内容
|
||||
content = f"天气状况: {weather_desc}\n温度: {temp_c}°C (体感 {feels_like}°C)\n湿度: {humidity}%\n气压: {pressure} hPa\n能见度: {visibility} km\n风速: {wind_speed} km/h ({wind_dir})\n空气质量: {air_quality}\n更新时间: {update_time}"
|
||||
|
||||
return content
|
||||
def getRealWeatherData(self):
|
||||
"""获取真实天气数据"""
|
||||
try:
|
||||
import requests
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
# 请求天气数据
|
||||
url = "https://api.open-meteo.com/v1/forecast?latitude=39.9042&longitude=116.4074¤t=temperature_2m,apparent_temperature,relative_humidity_2m,surface_pressure,wind_speed_10m,wind_direction_10m,weather_code&timezone=Asia%2FShanghai"
|
||||
response = requests.get(url, timeout=10)
|
||||
response.raise_for_status()
|
||||
|
||||
# 解析JSON数据
|
||||
weather_data = response.json()
|
||||
|
||||
current_condition = weather_data.get('current', {})
|
||||
if not current_condition:
|
||||
return "错误: 天气数据格式不正确 (缺少 current 字段)"
|
||||
|
||||
weather_code = current_condition.get('weather_code')
|
||||
weather_desc_map = {
|
||||
0: "晴朗",
|
||||
1: "大部晴朗",
|
||||
2: "局部多云",
|
||||
3: "阴天",
|
||||
45: "雾",
|
||||
48: "冻雾",
|
||||
51: "毛毛雨",
|
||||
53: "小雨",
|
||||
55: "中雨",
|
||||
61: "小雨",
|
||||
63: "中雨",
|
||||
65: "大雨",
|
||||
71: "小雪",
|
||||
73: "中雪",
|
||||
75: "大雪",
|
||||
80: "阵雨",
|
||||
81: "较强阵雨",
|
||||
82: "强阵雨",
|
||||
95: "雷暴",
|
||||
96: "雷暴夹小冰雹",
|
||||
99: "雷暴夹大冰雹",
|
||||
}
|
||||
weather_desc = weather_desc_map.get(weather_code, f"天气代码 {weather_code}")
|
||||
|
||||
temp_c = current_condition.get('temperature_2m', 'N/A')
|
||||
feels_like = current_condition.get('apparent_temperature', 'N/A')
|
||||
humidity = current_condition.get('relative_humidity_2m', 'N/A')
|
||||
pressure = current_condition.get('surface_pressure', 'N/A')
|
||||
wind_speed = current_condition.get('wind_speed_10m', 'N/A')
|
||||
wind_dir = current_condition.get('wind_direction_10m', 'N/A')
|
||||
visibility = "N/A"
|
||||
air_quality = "N/A"
|
||||
|
||||
update_time = current_condition.get('time')
|
||||
if not update_time:
|
||||
update_time = datetime.now().strftime("%Y-%m-%d %H:%M")
|
||||
|
||||
# 格式化显示内容
|
||||
content = f"天气状况: {weather_desc}\n温度: {temp_c}°C (体感 {feels_like}°C)\n湿度: {humidity}%\n气压: {pressure} hPa\n能见度: {visibility}\n风速: {wind_speed} km/h (风向 {wind_dir}°)\n空气质量: {air_quality}\n更新时间: {update_time}"
|
||||
|
||||
return content
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
return "错误: 获取天气数据超时"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
50
core/editor_context.py
Normal file
50
core/editor_context.py
Normal file
@ -0,0 +1,50 @@
|
||||
"""Editor context adapter for legacy bridge access."""
|
||||
|
||||
|
||||
class EditorContext:
|
||||
"""Provide a stable access layer for editor-facing services."""
|
||||
|
||||
def __init__(self, world):
|
||||
self.world = world
|
||||
|
||||
def get_interface_manager(self):
|
||||
return getattr(self.world, "interface_manager", None)
|
||||
|
||||
def get_tree_widget(self):
|
||||
interface_manager = self.get_interface_manager()
|
||||
if not interface_manager:
|
||||
return None
|
||||
return getattr(interface_manager, "treeWidget", 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,55 @@ 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_interface_manager(self):
|
||||
"""获取兼容层中的场景树管理器(若存在)。"""
|
||||
return self._get_editor_context().get_interface_manager()
|
||||
|
||||
def _get_tree_widget(self):
|
||||
"""安全获取场景树控件。"""
|
||||
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):
|
||||
"""将当前选中模型同步到场景树。"""
|
||||
interface_manager = self._get_interface_manager()
|
||||
tree_widget = self._get_tree_widget()
|
||||
if not interface_manager or not tree_widget:
|
||||
return False
|
||||
|
||||
root = tree_widget.invisibleRootItem()
|
||||
found_item = None
|
||||
|
||||
for i in range(root.childCount()):
|
||||
scene_item = root.child(i)
|
||||
if scene_item.text(0) != "场景":
|
||||
continue
|
||||
found_item = interface_manager.findTreeItem(selected_model, scene_item)
|
||||
if found_item:
|
||||
try:
|
||||
tree_widget.itemClicked.disconnect()
|
||||
except TypeError:
|
||||
pass
|
||||
tree_widget.setCurrentItem(found_item)
|
||||
tree_widget.itemClicked.connect(interface_manager.onTreeItemClicked)
|
||||
return True
|
||||
break
|
||||
return False
|
||||
|
||||
def showClickRay(self, nearPoint, farPoint, hitPos=None):
|
||||
"""显示鼠标点击的射线"""
|
||||
@ -239,17 +284,22 @@ class EventHandler:
|
||||
|
||||
# 处理GUI编辑模式
|
||||
if self.world.guiEditMode:
|
||||
gui_manager = self._get_gui_manager()
|
||||
if not gui_manager:
|
||||
print("GUI编辑模式已开启,但 gui_manager 不可用")
|
||||
pickerNP.removeNode()
|
||||
return
|
||||
#print("处理GUI编辑模式点击")
|
||||
# 检查是否点击了GUI元素
|
||||
clickedGUI = self.world.gui_manager.findClickedGUI(hitNode)
|
||||
clickedGUI = gui_manager.findClickedGUI(hitNode)
|
||||
if clickedGUI:
|
||||
# 选中GUI元素
|
||||
self.world.selection.updateSelection(clickedGUI)
|
||||
self.world.gui_manager.selectGUIInTree(clickedGUI)
|
||||
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)
|
||||
gui_manager.createGUIAtPosition(hitPos, self.world.currentGUITool)
|
||||
pickerNP.removeNode()
|
||||
return
|
||||
|
||||
@ -280,7 +330,9 @@ class EventHandler:
|
||||
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)
|
||||
gui_manager = self._get_gui_manager()
|
||||
if gui_manager:
|
||||
gui_manager.createGUIAtPosition(world_pos, self.world.currentGUITool)
|
||||
|
||||
# 确保总是清理碰撞检测节点
|
||||
try:
|
||||
@ -450,7 +502,8 @@ class EventHandler:
|
||||
print(f"点击的是碰撞节点: {hitNode.getName()}")
|
||||
# 碰撞节点的父节点应该是模型
|
||||
parent = hitNode.getParent()
|
||||
if parent in self.world.models:
|
||||
model_list = self.world.models if hasattr(self.world, 'models') else []
|
||||
if parent in model_list or parent.hasTag('is_model_root'):
|
||||
selectedModel = parent
|
||||
print(f"找到对应的模型: {selectedModel.getName()}")
|
||||
else:
|
||||
@ -460,13 +513,15 @@ class EventHandler:
|
||||
current = hitNode
|
||||
while current != self.world.render:
|
||||
# 检查是否是模型
|
||||
if current in self.world.models:
|
||||
model_list = self.world.models if hasattr(self.world, 'models') else []
|
||||
if current in model_list or current.hasTag('is_model_root'):
|
||||
selectedModel = current
|
||||
print(f"找到模型节点: {selectedModel.getName()}")
|
||||
break
|
||||
|
||||
# 检查是否是模型的子节点
|
||||
for model in self.world.models:
|
||||
model_list = self.world.models if hasattr(self.world, 'models') else []
|
||||
for model in model_list:
|
||||
if current.getParent() == model or current.isAncestorOf(model):
|
||||
selectedModel = model
|
||||
print(f"找到父模型: {selectedModel.getName()}")
|
||||
@ -485,35 +540,12 @@ class EventHandler:
|
||||
self.world.selection.updateSelection(selectedModel)
|
||||
|
||||
# 在树形控件中查找并选中对应的项
|
||||
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(selectedModel):
|
||||
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)
|
||||
@ -531,11 +563,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 +576,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):
|
||||
"""处理鼠标中键按下事件"""
|
||||
|
||||
225
core/imgui_webview.py
Normal file
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}")
|
||||
File diff suppressed because it is too large
Load Diff
@ -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("✓ 默认巡检路线已创建")
|
||||
|
||||
@ -11,9 +11,11 @@ from direct.showbase.ShowBaseGlobal import globalClock
|
||||
from panda3d.core import (Vec3, Point3, Point2, LineSegs, ColorAttrib, RenderState,
|
||||
DepthTestAttrib, CollisionTraverser, CollisionHandlerQueue,
|
||||
CollisionNode, CollisionRay, GeomNode, BitMask32, Material, LColor, DepthWriteAttrib,
|
||||
TransparencyAttrib, Vec4, CollisionCapsule)
|
||||
TransparencyAttrib, Vec4, CollisionCapsule, WindowProperties)
|
||||
from direct.task.TaskManagerGlobal import taskMgr
|
||||
import math
|
||||
from core.selection_outline import SelectionOutlineManager
|
||||
from core.editor_context import get_editor_context
|
||||
|
||||
class SelectionSystem:
|
||||
"""选择和变换系统类"""
|
||||
@ -25,11 +27,23 @@ class SelectionSystem:
|
||||
world: 核心世界对象引用
|
||||
"""
|
||||
self.world = world
|
||||
self._editor_context = get_editor_context(world)
|
||||
|
||||
# 选择相关状态
|
||||
self.selectedNode = None
|
||||
self.selectionBox = None # 选择框
|
||||
self.selectionBoxTarget = None # 选择框跟踪的目标节点
|
||||
self.show_selection_box = False
|
||||
self.enable_unity_outline = True
|
||||
self.outline_manager = getattr(self.world, "_selection_outline_manager", None)
|
||||
if not self.outline_manager:
|
||||
self.outline_manager = SelectionOutlineManager(
|
||||
self.world,
|
||||
enabled=self.enable_unity_outline,
|
||||
)
|
||||
setattr(self.world, "_selection_outline_manager", self.outline_manager)
|
||||
else:
|
||||
self.outline_manager.set_enabled(self.enable_unity_outline)
|
||||
|
||||
# 坐标轴工具(Gizmo)相关
|
||||
self.gizmo = None # 坐标轴
|
||||
@ -78,41 +92,32 @@ class SelectionSystem:
|
||||
self._double_click_task = None
|
||||
|
||||
print("✓ 选择和变换系统初始化完成")
|
||||
|
||||
def _get_tree_widget(self):
|
||||
"""统一获取场景树控件。"""
|
||||
return self._editor_context.get_tree_widget()
|
||||
|
||||
def _clear_tree_selection(self):
|
||||
"""清空场景树选中项。"""
|
||||
tree_widget = self._get_tree_widget()
|
||||
if not tree_widget:
|
||||
return False
|
||||
tree_widget.setCurrentItem(None)
|
||||
return True
|
||||
|
||||
# ==================== 光标设置 ====================
|
||||
def _setCursor(self,cursor_type):
|
||||
try:
|
||||
from PyQt5.QtCore import Qt
|
||||
if self._current_cursor == cursor_type:
|
||||
return
|
||||
if hasattr(self.world,'main_window') and self.world.main_window:
|
||||
main_window = self.world.main_window
|
||||
else:
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
main_window = QApplication.activeWindow()
|
||||
if not main_window:
|
||||
windows = QApplication.topLevelWindows()
|
||||
for window in windows:
|
||||
if hasattr(window,'isVisible') and window.isVisible():
|
||||
main_window = window
|
||||
break
|
||||
if main_window:
|
||||
if cursor_type == "crosshair":
|
||||
main_window.setCursor(Qt.CrossCursor)
|
||||
elif cursor_type == "size_hor":
|
||||
main_window.setCursor(Qt.SizeHorCursor)
|
||||
elif cursor_type == "size_ver":
|
||||
main_window.setCursor(Qt.SizeVerCursor)
|
||||
elif cursor_type == "size_all":
|
||||
main_window.setCursor(Qt.SizeAllCursor)
|
||||
elif cursor_type == "pointing_hand":
|
||||
main_window.setCursor(Qt.PointingHandCursor)
|
||||
else:
|
||||
main_window.unsetCursor()
|
||||
self._current_cursor = cursor_type
|
||||
#print(f"光标已设置:{cursor_type}")
|
||||
self._current_cursor = cursor_type
|
||||
else:
|
||||
print("警告:无法获取主窗口,光标设置失败")
|
||||
if not hasattr(self.world, "win") or not self.world.win:
|
||||
return
|
||||
|
||||
# Panda3D 原生接口不支持直接切换系统光标形状,这里保留状态并确保光标可见。
|
||||
props = WindowProperties()
|
||||
props.setCursorHidden(False)
|
||||
self.world.win.requestProperties(props)
|
||||
self._current_cursor = cursor_type
|
||||
except Exception as e:
|
||||
print(f"设置光标失败{e}")
|
||||
def _resetCursor(self):
|
||||
@ -207,6 +212,21 @@ class SelectionSystem:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
def _updateSelectionOutline(self, nodePath):
|
||||
"""Update Unity-like selection outline visuals."""
|
||||
try:
|
||||
if not getattr(self, "outline_manager", None):
|
||||
return
|
||||
if not self.enable_unity_outline:
|
||||
self.outline_manager.clear()
|
||||
return
|
||||
if nodePath and not nodePath.isEmpty():
|
||||
self.outline_manager.set_targets([nodePath])
|
||||
else:
|
||||
self.outline_manager.clear()
|
||||
except Exception as e:
|
||||
print(f"update selection outline failed: {e}")
|
||||
|
||||
def updateSelectionBoxGeometry(self):
|
||||
"""更新选择框的几何形状和位置"""
|
||||
try:
|
||||
@ -1619,279 +1639,270 @@ class SelectionSystem:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
def _validate_gizmo_drag_state(self):
|
||||
if not self.isDraggingGizmo:
|
||||
print("拖拽更新失败: 不在拖拽状态")
|
||||
return False
|
||||
if not self.gizmoTarget:
|
||||
print("拖拽更新失败: 没有拖拽目标")
|
||||
return False
|
||||
if not hasattr(self, 'dragStartMousePos') or not self.dragStartMousePos:
|
||||
print("拖拽更新失败: 没有拖拽起始位置")
|
||||
return False
|
||||
if not hasattr(self, 'gizmoTargetStartPos') or not self.gizmoTargetStartPos:
|
||||
print("拖拽更新失败: 没有目标起始位置")
|
||||
return False
|
||||
if not hasattr(self, 'gizmoStartPos') or not self.gizmoStartPos:
|
||||
print("拖拽更新失败: 没有坐标轴起始位置")
|
||||
return False
|
||||
return True
|
||||
|
||||
def _refresh_gizmo_target_panel(self):
|
||||
if hasattr(self.world, 'property_panel') and self.world.property_panel:
|
||||
self.world.property_panel.refreshModelValues(self.gizmoTarget)
|
||||
|
||||
def _apply_scale_drag(self, mouse_delta_x, mouse_delta_y, is_gui_element):
|
||||
scale_factor = 1.0 + (mouse_delta_x + mouse_delta_y) * 0.01
|
||||
scale_factor = max(0.001, scale_factor)
|
||||
start_scale = getattr(self, 'gizmoTargetStartScale', Vec3(1, 1, 1))
|
||||
|
||||
if is_gui_element:
|
||||
if self.dragGizmoAxis == "x":
|
||||
new_scale = Vec3(start_scale.x * scale_factor, start_scale.y, start_scale.z)
|
||||
elif self.dragGizmoAxis == "y":
|
||||
new_scale = Vec3(start_scale.x, start_scale.y * scale_factor, start_scale.z)
|
||||
elif self.dragGizmoAxis == "z":
|
||||
new_scale = Vec3(start_scale.x, start_scale.y, start_scale.z * scale_factor)
|
||||
else:
|
||||
new_scale = Vec3(
|
||||
start_scale.x * scale_factor,
|
||||
start_scale.y * scale_factor,
|
||||
start_scale.z * scale_factor,
|
||||
)
|
||||
else:
|
||||
if self.dragGizmoAxis == "x":
|
||||
new_scale = Vec3(start_scale.x * scale_factor, start_scale.y, start_scale.z)
|
||||
elif self.dragGizmoAxis == "y":
|
||||
new_scale = Vec3(start_scale.x, start_scale.y * scale_factor, start_scale.z)
|
||||
elif self.dragGizmoAxis == "z":
|
||||
z_scale_factor = 1.0 - (mouse_delta_x + mouse_delta_y) * 0.01
|
||||
new_scale = Vec3(start_scale.x, start_scale.y, start_scale.z * z_scale_factor)
|
||||
else:
|
||||
new_scale = Vec3(
|
||||
start_scale.x * scale_factor,
|
||||
start_scale.y * scale_factor,
|
||||
start_scale.z * scale_factor,
|
||||
)
|
||||
|
||||
new_scale = Vec3(
|
||||
max(0.001, new_scale.x),
|
||||
max(0.001, new_scale.y),
|
||||
max(0.001, new_scale.z),
|
||||
)
|
||||
self.gizmoTarget.setScale(new_scale)
|
||||
self._refresh_gizmo_target_panel()
|
||||
|
||||
def _apply_rotate_drag(self, mouse_delta_x, mouse_delta_y):
|
||||
rotation_speed = 0.5
|
||||
rotation_amount = (mouse_delta_x + mouse_delta_y) * rotation_speed
|
||||
start_hpr = getattr(self, 'gizmoTargetStartHpr', self.gizmoTarget.getHpr())
|
||||
|
||||
if self.dragGizmoAxis == "x":
|
||||
new_hpr = Vec3(start_hpr.x + rotation_amount, start_hpr.y, start_hpr.z)
|
||||
elif self.dragGizmoAxis == "y":
|
||||
new_hpr = Vec3(start_hpr.x, start_hpr.y - rotation_amount, start_hpr.z)
|
||||
elif self.dragGizmoAxis == "z":
|
||||
new_hpr = Vec3(start_hpr.x, start_hpr.y, start_hpr.z + rotation_amount)
|
||||
else:
|
||||
new_hpr = Vec3(
|
||||
start_hpr.x + rotation_amount,
|
||||
start_hpr.y + rotation_amount,
|
||||
start_hpr.z + rotation_amount,
|
||||
)
|
||||
|
||||
self.gizmoTarget.setHpr(new_hpr)
|
||||
self._refresh_gizmo_target_panel()
|
||||
|
||||
def _get_local_axis_vector(self):
|
||||
if self.dragGizmoAxis == "x":
|
||||
return Vec3(1, 0, 0)
|
||||
if self.dragGizmoAxis == "y":
|
||||
return Vec3(0, 1, 0)
|
||||
if self.dragGizmoAxis == "z":
|
||||
return Vec3(0, 0, 1)
|
||||
print(f"拖拽更新失败: 未知轴类型 {self.dragGizmoAxis}")
|
||||
return None
|
||||
|
||||
def _compute_world_axis_vector(self, local_axis_vector):
|
||||
world_axis_vector = local_axis_vector
|
||||
parent_node = self.gizmoTarget.getParent()
|
||||
|
||||
if parent_node and parent_node != self.world.render:
|
||||
try:
|
||||
transfrom_mat = parent_node.getMat(self.world.render)
|
||||
if transfrom_mat.is_identity() or self._isMatrixValid(transfrom_mat):
|
||||
world_axis_vector = transfrom_mat.xformVec(local_axis_vector)
|
||||
else:
|
||||
print("警告: 检测到无效变换矩阵,使用默认轴向量")
|
||||
except Exception as e:
|
||||
print(f"变换计算出错: {e},使用默认轴向量")
|
||||
|
||||
return world_axis_vector
|
||||
|
||||
def _world_to_screen(self, world_pos):
|
||||
try:
|
||||
cam_pos = self.world.cam.getRelativePoint(self.world.render, world_pos)
|
||||
if cam_pos.getY() <= 0:
|
||||
return None
|
||||
|
||||
screen_pos = Point2()
|
||||
if self.world.cam.node().getLens().project(cam_pos, screen_pos):
|
||||
win_width, win_height = self.world.getWindowSize()
|
||||
win_x = (screen_pos.x + 1) * 0.5 * win_width
|
||||
win_y = (1 - screen_pos.y) * 0.5 * win_height
|
||||
return win_x, win_y
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"世界坐标转屏幕坐标失败: {e}")
|
||||
return None
|
||||
|
||||
def _compute_parent_scale_factor(self):
|
||||
total_scale_factor = 1.0
|
||||
current_node = self.gizmoTarget.getParent()
|
||||
|
||||
while current_node and current_node != self.world.render:
|
||||
try:
|
||||
if not current_node.isEmpty():
|
||||
node_scale = current_node.getScale()
|
||||
if node_scale.x > 0 and node_scale.y > 0 and node_scale.z > 0:
|
||||
avg_scale = (node_scale.x + node_scale.y + node_scale.z) / 3.0
|
||||
total_scale_factor *= avg_scale
|
||||
current_node = current_node.getParent()
|
||||
else:
|
||||
break
|
||||
except Exception:
|
||||
break
|
||||
return total_scale_factor
|
||||
|
||||
def _compute_axis_movement_distance(self, mouse_delta_x, mouse_delta_y):
|
||||
gizmo_world_pos = self.gizmoStartPos
|
||||
local_axis_vector = self._get_local_axis_vector()
|
||||
if local_axis_vector is None:
|
||||
return None
|
||||
|
||||
world_axis_vector = self._compute_world_axis_vector(local_axis_vector)
|
||||
axis_start_screen = self._world_to_screen(gizmo_world_pos)
|
||||
axis_end_world = gizmo_world_pos + world_axis_vector
|
||||
axis_end_screen = self._world_to_screen(axis_end_world)
|
||||
|
||||
if not axis_start_screen or not axis_end_screen:
|
||||
print("拖拽更新失败: 无法获取轴线屏幕坐标")
|
||||
return None
|
||||
|
||||
screen_axis_dir = (
|
||||
axis_end_screen[0] - axis_start_screen[0],
|
||||
axis_end_screen[1] - axis_start_screen[1],
|
||||
)
|
||||
length = math.sqrt(screen_axis_dir[0] ** 2 + screen_axis_dir[1] ** 2)
|
||||
if length <= 0:
|
||||
print("拖拽更新失败: 屏幕轴方向长度为0")
|
||||
return None
|
||||
|
||||
screen_axis_dir = (
|
||||
screen_axis_dir[0] / length,
|
||||
screen_axis_dir[1] / length,
|
||||
)
|
||||
projected_distance = (
|
||||
mouse_delta_x * screen_axis_dir[0] +
|
||||
mouse_delta_y * screen_axis_dir[1]
|
||||
)
|
||||
|
||||
cam_pos = self.world.cam.getPos(self.world.render)
|
||||
distance_to_object = (cam_pos - gizmo_world_pos).length()
|
||||
lens = self.world.cam.node().getLens()
|
||||
fov = lens.getFov()[0]
|
||||
win_width, _ = self.world.getWindowSize()
|
||||
|
||||
pixels_to_world_units = (2 * distance_to_object * math.tan(math.radians(fov / 2))) / win_width
|
||||
movement_distance = projected_distance * pixels_to_world_units
|
||||
|
||||
total_scale_factor = self._compute_parent_scale_factor()
|
||||
if total_scale_factor > 0:
|
||||
movement_distance = movement_distance / total_scale_factor
|
||||
|
||||
return movement_distance
|
||||
|
||||
def _apply_translate_drag(self, movement_distance):
|
||||
current_pos = self.gizmoTargetStartPos
|
||||
|
||||
if self.dragGizmoAxis == "x":
|
||||
new_pos = Vec3(current_pos.x + movement_distance, current_pos.y, current_pos.z)
|
||||
elif self.dragGizmoAxis == "y":
|
||||
new_pos = Vec3(current_pos.x, current_pos.y + movement_distance, current_pos.z)
|
||||
elif self.dragGizmoAxis == "z":
|
||||
new_pos = Vec3(current_pos.x, current_pos.y, current_pos.z + movement_distance)
|
||||
else:
|
||||
print(f"未知轴: {self.dragGizmoAxis}")
|
||||
return
|
||||
|
||||
light_object = self.gizmoTarget.getPythonTag("rp_light_object")
|
||||
if light_object:
|
||||
self.gizmoTarget.setPos(new_pos)
|
||||
self._sync_rp_light_position(self.gizmoTarget, light_object)
|
||||
print(f"🔄 光源拖拽移动: {current_pos} -> {new_pos}")
|
||||
else:
|
||||
self.gizmoTarget.setPos(new_pos)
|
||||
print(f"🔄 节点拖拽移动: {current_pos} -> {new_pos} (轴: {self.dragGizmoAxis}, 距离: {movement_distance:.3f})")
|
||||
|
||||
self._refresh_gizmo_target_panel()
|
||||
|
||||
min_point = Point3()
|
||||
max_point = Point3()
|
||||
if self.gizmoTarget.calcTightBounds(min_point, max_point, self.world.render):
|
||||
center = Point3(
|
||||
(min_point.x + max_point.x) * 0.5,
|
||||
(min_point.y + max_point.y) * 0.5,
|
||||
(min_point.z + max_point.z) * 0.5,
|
||||
)
|
||||
self.gizmo.setPos(center)
|
||||
|
||||
self._refresh_gizmo_target_panel()
|
||||
|
||||
if not hasattr(self, '_last_drag_debug_time'):
|
||||
self._last_drag_debug_time = 0
|
||||
|
||||
import time
|
||||
current_time = time.time()
|
||||
if current_time - self._last_drag_debug_time > 0.1:
|
||||
self._last_drag_debug_time = current_time
|
||||
|
||||
def updateGizmoDrag(self, mouseX, mouseY):
|
||||
"""更新坐标轴拖拽 - 使用正确的坐标系变换,支持旋转后的子节点拖拽"""
|
||||
try:
|
||||
# 添加详细的状态检查和调试信息
|
||||
if not self.isDraggingGizmo:
|
||||
print("拖拽更新失败: 不在拖拽状态")
|
||||
return
|
||||
if not self.gizmoTarget:
|
||||
print("拖拽更新失败: 没有拖拽目标")
|
||||
return
|
||||
if not hasattr(self, 'dragStartMousePos') or not self.dragStartMousePos:
|
||||
print("拖拽更新失败: 没有拖拽起始位置")
|
||||
return
|
||||
if not hasattr(self, 'gizmoTargetStartPos') or not self.gizmoTargetStartPos:
|
||||
print("拖拽更新失败: 没有目标起始位置")
|
||||
return
|
||||
if not hasattr(self, 'gizmoStartPos') or not self.gizmoStartPos:
|
||||
print("拖拽更新失败: 没有坐标轴起始位置")
|
||||
if not self._validate_gizmo_drag_state():
|
||||
return
|
||||
|
||||
is_scale_tool = self.world.tool_manager.isScaleTool() if self.world.tool_manager else False
|
||||
is_rotate_tool = self.world.tool_manager.isRotateTool() if self.world.tool_manager else False
|
||||
|
||||
is_gui_element = (hasattr(self.gizmoTarget,'getTag') and
|
||||
self.gizmoTarget.getTag("is_gui_element") == "1")
|
||||
|
||||
|
||||
# 计算鼠标移动距离(屏幕像素)
|
||||
mouseDeltaX = mouseX - self.dragStartMousePos[0]
|
||||
mouseDeltaY = mouseY - self.dragStartMousePos[1]
|
||||
|
||||
if is_scale_tool:
|
||||
scale_factor = 1.0 + (mouseDeltaX + mouseDeltaY) * 0.01
|
||||
|
||||
scale_factor = max(0.001, scale_factor)
|
||||
|
||||
start_scale = getattr(self,'gizmoTargetStartScale',Vec3(1,1,1))
|
||||
|
||||
if is_gui_element:
|
||||
if self.dragGizmoAxis == "x":
|
||||
new_scale = Vec3(start_scale.x * scale_factor,start_scale.y,start_scale.z)
|
||||
elif self.dragGizmoAxis == "y":
|
||||
new_scale = Vec3(start_scale.x, start_scale.y * scale_factor, start_scale.z)
|
||||
elif self.dragGizmoAxis == "z":
|
||||
new_scale = Vec3(start_scale.x, start_scale.y, start_scale.z * scale_factor)
|
||||
else:
|
||||
new_scale = Vec3(start_scale.x * scale_factor,
|
||||
start_scale.y * scale_factor,
|
||||
start_scale.z * scale_factor)
|
||||
else:
|
||||
# 普通3D模型的缩放处理
|
||||
if self.dragGizmoAxis == "x":
|
||||
new_scale = Vec3(start_scale.x * scale_factor, start_scale.y, start_scale.z)
|
||||
elif self.dragGizmoAxis == "y":
|
||||
new_scale = Vec3(start_scale.x, start_scale.y * scale_factor, start_scale.z)
|
||||
elif self.dragGizmoAxis == "z":
|
||||
z_scale_factor = 1.0 - (mouseDeltaX + mouseDeltaY) * 0.01
|
||||
new_scale = Vec3(start_scale.x, start_scale.y, start_scale.z * z_scale_factor)
|
||||
else:
|
||||
new_scale = Vec3(start_scale.x * scale_factor,
|
||||
start_scale.y * scale_factor,
|
||||
start_scale.z * scale_factor)
|
||||
|
||||
new_scale = Vec3(
|
||||
max(0.001,new_scale.x),
|
||||
max(0.001,new_scale.y),
|
||||
max(0.001,new_scale.z)
|
||||
)
|
||||
|
||||
# 应用新缩放值
|
||||
self.gizmoTarget.setScale(new_scale)
|
||||
if hasattr(self.world, 'property_panel') and self.world.property_panel:
|
||||
self.world.property_panel.refreshModelValues(self.gizmoTarget)
|
||||
return
|
||||
elif is_rotate_tool:
|
||||
rotation_speed = 0.5
|
||||
rotation_amount = (mouseDeltaX + mouseDeltaY) * rotation_speed
|
||||
start_hpr = getattr(self,'gizmoTargetStartHpr',self.gizmoTarget.getHpr())
|
||||
|
||||
if self.dragGizmoAxis == "x":
|
||||
new_hpr = Vec3(start_hpr.x+rotation_amount,start_hpr.y,start_hpr.z)
|
||||
elif self.dragGizmoAxis == "y":
|
||||
new_hpr = Vec3(start_hpr.x,start_hpr.y-rotation_amount,start_hpr.z)
|
||||
elif self.dragGizmoAxis == "z":
|
||||
new_hpr = Vec3(start_hpr.x,start_hpr.y,start_hpr.z+rotation_amount)
|
||||
else:
|
||||
# 默认绕所有轴旋转
|
||||
new_hpr = Vec3(start_hpr.x + rotation_amount,
|
||||
start_hpr.y + rotation_amount,
|
||||
start_hpr.z + rotation_amount)
|
||||
self.gizmoTarget.setHpr(new_hpr)
|
||||
if hasattr(self.world, 'property_panel') and self.world.property_panel:
|
||||
self.world.property_panel.refreshModelValues(self.gizmoTarget)
|
||||
return
|
||||
|
||||
# 使用坐标轴的实际位置而不是目标节点位置来计算屏幕投影
|
||||
gizmo_world_pos = self.gizmoStartPos
|
||||
|
||||
# 【关键修复】:获取正确的轴向量,考虑父节点的旋转
|
||||
# 检查目标节点是否有父节点
|
||||
parent_node = self.gizmoTarget.getParent()
|
||||
|
||||
# 计算轴向量在正确坐标系中的方向
|
||||
if self.dragGizmoAxis == "x":
|
||||
# 在局部坐标系中的X轴方向
|
||||
local_axis_vector = Vec3(1, 0, 0)
|
||||
elif self.dragGizmoAxis == "y":
|
||||
# 在局部坐标系中的Y轴方向
|
||||
local_axis_vector = Vec3(0, 1, 0)
|
||||
elif self.dragGizmoAxis == "z":
|
||||
# 在局部坐标系中的Z轴方向
|
||||
local_axis_vector = Vec3(0, 0, 1)
|
||||
else:
|
||||
print(f"拖拽更新失败: 未知轴类型 {self.dragGizmoAxis}")
|
||||
return
|
||||
|
||||
world_axis_vector = local_axis_vector
|
||||
|
||||
if parent_node and parent_node != self.world.render:
|
||||
try:
|
||||
#获取变换矩阵
|
||||
transfrom_mat = parent_node.getMat(self.world.render)
|
||||
if transfrom_mat.is_identity() or self._isMatrixValid(transfrom_mat):
|
||||
world_axis_vector = transfrom_mat.xformVec(local_axis_vector)
|
||||
else:
|
||||
print("警告: 检测到无效变换矩阵,使用默认轴向量")
|
||||
except Exception as e:
|
||||
print(f"变换计算出错: {e},使用默认轴向量")
|
||||
else:
|
||||
world_axis_vector = local_axis_vector
|
||||
|
||||
# 确定轴向量的变换上下文
|
||||
# if parent_node and parent_node != self.world.render:
|
||||
# transform_mat = parent_node.getMat(self.world.render)
|
||||
# world_axis_vector = transform_mat.xformVec(local_axis_vector)
|
||||
# else:
|
||||
# world_axis_vector = local_axis_vector
|
||||
|
||||
#axis_end = gizmo_world_pos + world_axis_vector
|
||||
|
||||
# 投影到屏幕空间
|
||||
def worldToScreen(worldPos):
|
||||
try:
|
||||
camPos = self.world.cam.getRelativePoint(self.world.render, worldPos)
|
||||
if camPos.getY() <= 0:
|
||||
return None
|
||||
|
||||
screenPos = Point2()
|
||||
if self.world.cam.node().getLens().project(camPos, screenPos):
|
||||
winWidth, winHeight = self.world.getWindowSize()
|
||||
winX = (screenPos.x + 1) * 0.5 * winWidth
|
||||
winY = (1 - screenPos.y) * 0.5 * winHeight
|
||||
return (winX, winY)
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"世界坐标转屏幕坐标失败: {e}")
|
||||
return None
|
||||
axis_start_screen = worldToScreen(gizmo_world_pos)
|
||||
axis_end_world = gizmo_world_pos + world_axis_vector
|
||||
axis_end_screen = worldToScreen(axis_end_world)
|
||||
|
||||
if not axis_start_screen or not axis_end_screen:
|
||||
print("拖拽更新失败: 无法获取轴线屏幕坐标")
|
||||
return
|
||||
|
||||
screen_axis_dir = (
|
||||
axis_end_screen[0] - axis_start_screen[0],
|
||||
axis_end_screen[1] - axis_start_screen[1]
|
||||
is_gui_element = (
|
||||
hasattr(self.gizmoTarget, 'getTag') and
|
||||
self.gizmoTarget.getTag("is_gui_element") == "1"
|
||||
)
|
||||
|
||||
# 归一化屏幕轴方向
|
||||
import math
|
||||
length = math.sqrt(screen_axis_dir[0]**2 + screen_axis_dir[1]**2)
|
||||
if length > 0:
|
||||
#screen_axis_dir = (screen_axis_dir[0] / length, screen_axis_dir[1] / length)
|
||||
screen_axis_dir = (
|
||||
screen_axis_dir[0] / length,
|
||||
screen_axis_dir[1] / length
|
||||
)
|
||||
mouse_delta_x = mouseX - self.dragStartMousePos[0]
|
||||
mouse_delta_y = mouseY - self.dragStartMousePos[1]
|
||||
|
||||
else:
|
||||
print("拖拽更新失败: 屏幕轴方向长度为0")
|
||||
if is_scale_tool:
|
||||
self._apply_scale_drag(mouse_delta_x, mouse_delta_y, is_gui_element)
|
||||
return
|
||||
if is_rotate_tool:
|
||||
self._apply_rotate_drag(mouse_delta_x, mouse_delta_y)
|
||||
return
|
||||
|
||||
# 将鼠标移动投影到轴方向上
|
||||
projected_distance = (mouseDeltaX * screen_axis_dir[0] +
|
||||
mouseDeltaY * screen_axis_dir[1])
|
||||
|
||||
cam_pos = self.world.cam.getPos(self.world.render)
|
||||
distance_to_object = (cam_pos - gizmo_world_pos).length()
|
||||
|
||||
lens = self.world.cam.node().getLens()
|
||||
fov = lens.getFov()[0]
|
||||
winWidth,winHeight = self.world.getWindowSize()
|
||||
|
||||
pixels_to_world_units = (2*distance_to_object*math.tan(math.radians(fov/2)))/winWidth
|
||||
|
||||
movement_distance = projected_distance * pixels_to_world_units
|
||||
|
||||
total_scale_factor = 1.0
|
||||
current_node = self.gizmoTarget.getParent()
|
||||
|
||||
while current_node and current_node != self.world.render:
|
||||
try:
|
||||
if not current_node.isEmpty():
|
||||
node_scale = current_node.getScale()
|
||||
if node_scale.x > 0 and node_scale.y >0 and node_scale.z >0 :
|
||||
avg_scale = (node_scale.x + node_scale.y + node_scale.z)/3.0
|
||||
total_scale_factor *= avg_scale
|
||||
#avg_scale = (node_scale.x+node_scale.y + node_scale.z) / 3.0
|
||||
#total_scale_factor *= avg_scale
|
||||
current_node = current_node.getParent()
|
||||
else:
|
||||
break
|
||||
except:
|
||||
break
|
||||
|
||||
|
||||
if total_scale_factor > 0:
|
||||
movement_distance = movement_distance / total_scale_factor
|
||||
|
||||
currentPos = self.gizmoTargetStartPos
|
||||
|
||||
# 根据拖拽的轴,只修改对应的坐标分量
|
||||
if self.dragGizmoAxis == "x":
|
||||
newPos = Vec3(currentPos.x + movement_distance, currentPos.y, currentPos.z)
|
||||
#print(f"X轴移动:{currentPos.x} -> {newPos.x}")
|
||||
elif self.dragGizmoAxis == "y":
|
||||
newPos = Vec3(currentPos.x, currentPos.y + movement_distance, currentPos.z)
|
||||
#print(f"Y轴移动:{currentPos.y} -> {newPos.y}")
|
||||
elif self.dragGizmoAxis == "z":
|
||||
newPos = Vec3(currentPos.x, currentPos.y, currentPos.z + movement_distance)
|
||||
#print(f"Z轴移动:{currentPos.z} -> {newPos.z}")
|
||||
else:
|
||||
print(f"未知轴: {self.dragGizmoAxis}")
|
||||
movement_distance = self._compute_axis_movement_distance(mouse_delta_x, mouse_delta_y)
|
||||
if movement_distance is None:
|
||||
return
|
||||
|
||||
# 应用新位置到目标节点
|
||||
light_object = self.gizmoTarget.getPythonTag("rp_light_object")
|
||||
if light_object:
|
||||
self.gizmoTarget.setPos(newPos)
|
||||
self._sync_rp_light_position(self.gizmoTarget, light_object)
|
||||
print(f"🔄 光源拖拽移动: {currentPos} -> {newPos}")
|
||||
else:
|
||||
self.gizmoTarget.setPos(newPos)
|
||||
print(f"🔄 节点拖拽移动: {currentPos} -> {newPos} (轴: {self.dragGizmoAxis}, 距离: {movement_distance:.3f})")
|
||||
|
||||
# 更新属性面板
|
||||
if hasattr(self.world, 'property_panel') and self.world.property_panel:
|
||||
self.world.property_panel.refreshModelValues(self.gizmoTarget)
|
||||
|
||||
# 更新坐标轴位置 - 计算新的中心位置
|
||||
minPoint = Point3()
|
||||
maxPoint = Point3()
|
||||
if self.gizmoTarget.calcTightBounds(minPoint, maxPoint, self.world.render):
|
||||
center = Point3((minPoint.x + maxPoint.x) * 0.5,
|
||||
(minPoint.y + maxPoint.y) * 0.5,
|
||||
(minPoint.z + maxPoint.z) * 0.5)
|
||||
self.gizmo.setPos(center)
|
||||
|
||||
# 实时更新属性面板
|
||||
if hasattr(self.world, 'property_panel') and self.world.property_panel:
|
||||
self.world.property_panel.refreshModelValues(self.gizmoTarget)
|
||||
|
||||
# 每次拖拽都输出调试信息(但限制频率)
|
||||
if not hasattr(self, '_last_drag_debug_time'):
|
||||
self._last_drag_debug_time = 0
|
||||
|
||||
import time
|
||||
current_time = time.time()
|
||||
if current_time - self._last_drag_debug_time > 0.1: # 每0.1秒最多输出一次
|
||||
#print(f"拖拽更新成功 - 轴:{self.dragGizmoAxis}, 比例:{scale_factor:.6f}, 投影:{projected_distance:.2f}")
|
||||
self._last_drag_debug_time = current_time
|
||||
self._apply_translate_drag(movement_distance)
|
||||
|
||||
except Exception as e:
|
||||
print(f"更新坐标轴拖拽失败: {str(e)}")
|
||||
@ -1963,10 +1974,30 @@ class SelectionSystem:
|
||||
command = RotateNodeCommand(self.gizmoTarget, self.gizmoTargetStartHpr, current_hpr)
|
||||
self.world.command_manager.execute_command(command)
|
||||
print(f"创建旋转命令: {self.gizmoTargetStartHpr} -> {current_hpr}")
|
||||
print(f"创建旋转命令: {self.gizmoTargetStartHpr} -> {current_hpr}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"创建撤销命令时出错: {e}")
|
||||
|
||||
# 同步碰撞体
|
||||
try:
|
||||
target = self.gizmoTarget
|
||||
if target:
|
||||
# 寻找它的所属模型根节点
|
||||
root_model = target
|
||||
while root_model and root_model != self.world.render:
|
||||
model_list = self.world.models if hasattr(self.world, 'models') else []
|
||||
if root_model in model_list or root_model.hasTag('is_model_root'):
|
||||
break
|
||||
root_model = root_model.getParent()
|
||||
|
||||
# 如果这个节点属于某个模型,或者是模型自己,更新该模型的碰撞边界
|
||||
if root_model and hasattr(self.world, 'models') and root_model in self.world.models:
|
||||
if hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, 'refreshCollisionBounds'):
|
||||
self.world.scene_manager.refreshCollisionBounds(root_model)
|
||||
except Exception as e:
|
||||
print(f"同步模型碰撞体失败: {e}")
|
||||
|
||||
# 恢复所有轴的颜色
|
||||
for axis_name in ["x", "y", "z"]:
|
||||
self.setGizmoAxisColor(axis_name, self.gizmo_colors[axis_name])
|
||||
@ -2045,8 +2076,12 @@ class SelectionSystem:
|
||||
|
||||
# 创建选择框
|
||||
#print("创建选择框...")
|
||||
self.createSelectionBox(nodePath)
|
||||
if self.selectionBox:
|
||||
if self.show_selection_box:
|
||||
self.createSelectionBox(nodePath)
|
||||
else:
|
||||
self.clearSelectionBox()
|
||||
|
||||
if (not self.show_selection_box) or self.selectionBox:
|
||||
box_name = "Unknown"
|
||||
if self.selectionBox and not self.selectionBox.isEmpty():
|
||||
box_name = self.selectionBox.getName()
|
||||
@ -2056,6 +2091,7 @@ class SelectionSystem:
|
||||
|
||||
# 创建坐标轴
|
||||
#print("创建坐标轴...")
|
||||
self._updateSelectionOutline(nodePath)
|
||||
self.createGizmo(nodePath)
|
||||
if self.gizmo:
|
||||
gizmo_name = "Unknown"
|
||||
@ -2068,14 +2104,12 @@ class SelectionSystem:
|
||||
else:
|
||||
print("清除选择...")
|
||||
self.clearSelectionBox()
|
||||
self._updateSelectionOutline(None)
|
||||
self.clearGizmo()
|
||||
print("✓ 取消选择")
|
||||
|
||||
#当取消选择时,同步清空树形控件的选中状态
|
||||
if (hasattr(self.world,'interface_manager')and
|
||||
self.world.interface_manager and
|
||||
self.world.interface_manager.treeWidget):
|
||||
self.world.interface_manager.treeWidget.setCurrentItem(None)
|
||||
if self._clear_tree_selection():
|
||||
print("✓ 树形控件选中状态已清空")
|
||||
|
||||
#print("=== 选择状态更新完成 ===\n")
|
||||
@ -2156,6 +2190,9 @@ class SelectionSystem:
|
||||
if (self.selectionBoxTarget and self.selectionBoxTarget.isEmpty()):
|
||||
self.clearSelectionBox()
|
||||
|
||||
if self.selectedNode and self.selectedNode.isEmpty():
|
||||
self._updateSelectionOutline(None)
|
||||
|
||||
def setupGizmoCollision(self):
|
||||
if not self.gizmo or not self.gizmoXAxis or not self.gizmoYAxis or not self.gizmoZAxis:
|
||||
return
|
||||
@ -2875,6 +2912,7 @@ class SelectionSystem:
|
||||
|
||||
# 清理其他资源
|
||||
self.clearSelectionBox()
|
||||
self._updateSelectionOutline(None)
|
||||
self.clearGizmo()
|
||||
def clearSelection(self):
|
||||
"""清除当前选择"""
|
||||
@ -2882,14 +2920,11 @@ class SelectionSystem:
|
||||
self.selectedNode = None
|
||||
self.selectedObject = None
|
||||
self.clearSelectionBox()
|
||||
self._updateSelectionOutline(None)
|
||||
self.clearGizmo()
|
||||
|
||||
# 清除树形控件中的选择
|
||||
if (hasattr(self.world, 'interface_manager') and
|
||||
self.world.interface_manager and
|
||||
hasattr(self.world.interface_manager, 'treeWidget') and
|
||||
self.world.interface_manager.treeWidget):
|
||||
self.world.interface_manager.treeWidget.setCurrentItem(None)
|
||||
self._clear_tree_selection()
|
||||
|
||||
print("已清除选择")
|
||||
except Exception as e:
|
||||
|
||||
303
core/selection_outline.py
Normal file
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)):
|
||||
"""从高度图创建地形"""
|
||||
|
||||
@ -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模式已禁用,手柄模型已隐藏")
|
||||
|
||||
|
||||
@ -40,7 +40,7 @@ class CoreWorld(ShowBase):
|
||||
global _global_render_pipeline
|
||||
|
||||
# 初始化基础属性
|
||||
self.qtWidget = None # Qt部件引用(用于获取准确的渲染区域尺寸)
|
||||
self.host_widget = None # 外部宿主窗口引用(用于获取准确渲染区域尺寸)
|
||||
|
||||
# 设置基本配置
|
||||
loadPrcFileData("", "show-frame-rate-meter 0")
|
||||
@ -686,16 +686,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 +755,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 +965,7 @@ class CoreWorld(ShowBase):
|
||||
return None
|
||||
|
||||
# 创建材质编辑器组件
|
||||
# 使用纯 Panda3D 实现,不依赖 PyQt5
|
||||
# 使用纯 Panda3D 实现,不依赖外部 GUI 框架
|
||||
print("材质编辑器组件已创建(使用 Panda3D 原生实现)")
|
||||
|
||||
material_widget.setLayout(layout)
|
||||
|
||||
4027
gui/gui_manager.py
4027
gui/gui_manager.py
File diff suppressed because it is too large
Load Diff
47
imgui.ini
47
imgui.ini
@ -24,26 +24,26 @@ Size=832,45
|
||||
Collapsed=0
|
||||
|
||||
[Window][工具栏]
|
||||
Pos=241,20
|
||||
Size=908,74
|
||||
Pos=278,20
|
||||
Size=1373,32
|
||||
Collapsed=0
|
||||
DockId=0x0000000D,0
|
||||
|
||||
[Window][场景树]
|
||||
Pos=0,20
|
||||
Size=239,468
|
||||
Size=276,634
|
||||
Collapsed=0
|
||||
DockId=0x00000007,0
|
||||
|
||||
[Window][属性面板]
|
||||
Pos=1151,20
|
||||
Size=229,730
|
||||
Pos=1653,20
|
||||
Size=267,989
|
||||
Collapsed=0
|
||||
DockId=0x00000003,0
|
||||
|
||||
[Window][控制台]
|
||||
Pos=0,490
|
||||
Size=239,260
|
||||
Pos=0,656
|
||||
Size=276,353
|
||||
Collapsed=0
|
||||
DockId=0x00000008,0
|
||||
|
||||
@ -60,7 +60,7 @@ Collapsed=0
|
||||
|
||||
[Window][WindowOverViewport_11111111]
|
||||
Pos=0,20
|
||||
Size=1380,730
|
||||
Size=1920,989
|
||||
Collapsed=0
|
||||
|
||||
[Window][测试窗口1]
|
||||
@ -84,7 +84,7 @@ Size=400,300
|
||||
Collapsed=0
|
||||
|
||||
[Window][选择路径]
|
||||
Pos=660,254
|
||||
Pos=660,245
|
||||
Size=600,500
|
||||
Collapsed=0
|
||||
|
||||
@ -94,13 +94,13 @@ 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=278,675
|
||||
Size=1373,334
|
||||
Collapsed=0
|
||||
DockId=0x00000006,0
|
||||
|
||||
@ -200,23 +200,18 @@ Pos=660,304
|
||||
Size=600,400
|
||||
Collapsed=0
|
||||
|
||||
[Window][Web面板]
|
||||
Pos=373,98
|
||||
Size=942,580
|
||||
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
|
||||
DockSpace ID=0x08BD597D Window=0x1BBC0F80 Pos=0,20 Size=1920,989 Split=X
|
||||
DockNode ID=0x00000001 Parent=0x08BD597D SizeRef=1651,989 Split=X
|
||||
DockNode ID=0x00000009 Parent=0x00000001 SizeRef=276,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=0x0000000A Parent=0x00000001 SizeRef=1373,989 Split=Y
|
||||
DockNode ID=0x0000000D Parent=0x0000000A SizeRef=1318,32 HiddenTabBar=1 Selected=0x43A39006
|
||||
DockNode ID=0x0000000E Parent=0x0000000A SizeRef=1318,937 Split=Y
|
||||
DockNode ID=0x00000005 Parent=0x0000000E SizeRef=1341,601 CentralNode=1
|
||||
DockNode ID=0x00000006 Parent=0x0000000E SizeRef=1341,334 Selected=0x3A2E05C3
|
||||
DockNode ID=0x00000002 Parent=0x08BD597D 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=0x1EB923B7
|
||||
|
||||
|
||||
@ -64,13 +64,13 @@ python main.py
|
||||
- 选择虚拟环境中的Python路径
|
||||
3. **验证环境**:检查状态栏显示正确的Python版本
|
||||
|
||||
## 📦 项目依赖说明
|
||||
|
||||
- **Panda3D**: 3D图形引擎
|
||||
- **PyQt5/PySide6**: GUI框架
|
||||
- **Pillow**: 图像处理
|
||||
- **python-dotenv**: 环境变量管理
|
||||
- **pyassimp**: 3D模型加载
|
||||
## 📦 项目依赖说明
|
||||
|
||||
- **Panda3D**: 3D图形引擎
|
||||
- **imgui-bundle / p3dimgui**: ImGui GUI框架
|
||||
- **Pillow**: 图像处理
|
||||
- **python-dotenv**: 环境变量管理
|
||||
- **pyassimp**: 3D模型加载
|
||||
|
||||
## 🌍 跨平台注意事项
|
||||
|
||||
@ -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+)
|
||||
A: 检查Python版本(需要3.10+)
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
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
|
||||
python-dotenv>=1.0.1
|
||||
pyassimp>=5.2.5
|
||||
|
||||
@ -5,11 +5,10 @@ channels:
|
||||
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
|
||||
- pip:
|
||||
- Panda3D>=1.10.15
|
||||
- imgui-bundle
|
||||
- Pillow>=9.0.1
|
||||
- python-dotenv>=1.0.1
|
||||
- pyassimp>=5.2.5
|
||||
- six
|
||||
|
||||
@ -58,33 +58,21 @@ 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
|
||||
pyparsing==2.4.7
|
||||
pyRFC3339==1.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
|
||||
PyYAML==5.4.1
|
||||
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
|
||||
SecretStorage==3.3.1
|
||||
six==1.16.0
|
||||
soupsieve==2.3.1
|
||||
systemd-python==234
|
||||
ubuntu-drivers-common==0.0.0
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
523
scene/scene_manager_convert_tiles_mixin.py
Normal file
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
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
|
||||
1000
scene/scene_manager_io_mixin.py
Normal file
1000
scene/scene_manager_io_mixin.py
Normal file
File diff suppressed because it is too large
Load Diff
455
scene/scene_manager_light_mixin.py
Normal file
455
scene/scene_manager_light_mixin.py
Normal file
@ -0,0 +1,455 @@
|
||||
"""Scene manager light 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
|
||||
|
||||
class SceneManagerLightMixin:
|
||||
def _recreateSpotLight(self, light_node):
|
||||
"""重新创建聚光灯"""
|
||||
try:
|
||||
from RenderPipelineFile.rpcore import SpotLight
|
||||
from panda3d.core import Vec3
|
||||
from core.render_pipeline_utils import get_render_pipeline
|
||||
|
||||
# 创建聚光灯对象
|
||||
light = SpotLight()
|
||||
light.direction = Vec3(0, 0, -1)
|
||||
light.fov = 70
|
||||
light.set_color_from_temperature(5 * 1000.0)
|
||||
|
||||
# 恢复保存的属性
|
||||
if light_node.hasTag("light_energy"):
|
||||
light.energy = float(light_node.getTag("light_energy"))
|
||||
else:
|
||||
light.energy = 5000
|
||||
|
||||
light.radius = 1000
|
||||
light.casts_shadows = True
|
||||
light.shadow_map_resolution = 256
|
||||
|
||||
light_pos = light_node.getPos()
|
||||
light.setPos(light_pos)
|
||||
|
||||
# 添加到渲染管线
|
||||
render_pipeline = get_render_pipeline()
|
||||
render_pipeline.add_light(light)
|
||||
|
||||
# 保存光源对象引用
|
||||
light_node.setPythonTag("rp_light_object", light)
|
||||
|
||||
# 添加到管理列表(去重)
|
||||
if light_node not in self.Spotlight:
|
||||
self.Spotlight.append(light_node)
|
||||
|
||||
# 确保灯光节点有正确的标签,以便在场景树更新时被识别
|
||||
if not light_node.hasTag("is_scene_element"):
|
||||
light_node.setTag("is_scene_element", "1")
|
||||
light_node.setTag("is_scene_element", "1")
|
||||
light_node.setTag("element_type", "spotlight")
|
||||
light_node.setTag("tree_item_type", "LIGHT_NODE")
|
||||
|
||||
if light_node.hasTag("stored_energy"):
|
||||
stored_energy = float(light_node.getTag("stored_energy"))
|
||||
if stored_energy > 0:
|
||||
light_node.setTag("stored_energy", str(stored_energy))
|
||||
|
||||
user_visible = True
|
||||
if light_node.hasTag("user_visible"):
|
||||
user_visible = light_node.getTag("user_visible").lower() == "true"
|
||||
|
||||
light_node.setPythonTag("user_visible",user_visible)
|
||||
if not user_visible:
|
||||
self.toggleLightVisibility(light_node,False)
|
||||
except Exception as e:
|
||||
print(f"重新创建聚光灯失败: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
def _recreatePointLight(self, light_node):
|
||||
"""重新创建点光源"""
|
||||
try:
|
||||
from RenderPipelineFile.rpcore import PointLight
|
||||
from core.render_pipeline_utils import get_render_pipeline
|
||||
|
||||
# 创建点光源对象
|
||||
light = PointLight()
|
||||
|
||||
# 恢复保存的属性
|
||||
if light_node.hasTag("light_energy"):
|
||||
light.energy = float(light_node.getTag("light_energy"))
|
||||
else:
|
||||
light.energy = 5000
|
||||
|
||||
light.radius = 1000
|
||||
light.inner_radius = 0.4
|
||||
light.set_color_from_temperature(5 * 1000.0)
|
||||
light.casts_shadows = True
|
||||
light.shadow_map_resolution = 256
|
||||
|
||||
# 设置位置
|
||||
light.setPos(light_node.getPos())
|
||||
|
||||
# 添加到渲染管线
|
||||
render_pipeline = get_render_pipeline()
|
||||
render_pipeline.add_light(light)
|
||||
|
||||
# 保存光源对象引用
|
||||
light_node.setPythonTag("rp_light_object", light)
|
||||
|
||||
# 添加到管理列表(去重)
|
||||
if light_node not in self.Pointlight:
|
||||
self.Pointlight.append(light_node)
|
||||
|
||||
# 确保灯光节点有正确的标签,以便在场景树更新时被识别
|
||||
if not light_node.hasTag("is_scene_element"):
|
||||
light_node.setTag("is_scene_element", "1")
|
||||
|
||||
light_node.setTag("is_scene_element", "1")
|
||||
light_node.setTag("element_type", "pointlight")
|
||||
light_node.setTag("tree_item_type", "LIGHT_NODE")
|
||||
|
||||
if light_node.hasTag("stored_energy"):
|
||||
stored_energy = float(light_node.getTag("stored_energy"))
|
||||
if stored_energy > 0:
|
||||
light_node.setTag("stored_energy", str(stored_energy))
|
||||
|
||||
user_visible = True
|
||||
if light_node.hasTag("user_visible"):
|
||||
user_visible = light_node.getTag("user_visible").lower()=="true"
|
||||
|
||||
light_node.setPythonTag("user_visible",user_visible)
|
||||
|
||||
if not user_visible:
|
||||
self.toggleLightVisibility(light_node,False)
|
||||
except Exception as e:
|
||||
print(f"重新创建点光源失败: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
def isLightObject(self, nodePath):
|
||||
"""检查是否为灯光对象"""
|
||||
try:
|
||||
if not nodePath:
|
||||
return False
|
||||
|
||||
|
||||
# 方法1: 检查PythonTag
|
||||
if nodePath.hasPythonTag("rp_light_object"):
|
||||
rp_light = nodePath.getPythonTag("rp_light_object")
|
||||
if rp_light is not None:
|
||||
return True
|
||||
|
||||
# 方法2: 检查element_type标签
|
||||
if nodePath.hasTag("element_type"):
|
||||
element_type = nodePath.getTag("element_type")
|
||||
if element_type in ["spotlight", "pointlight"]:
|
||||
return True
|
||||
|
||||
# 方法3: 检查tree_item_type标签
|
||||
if nodePath.hasTag("tree_item_type"):
|
||||
tree_item_type = nodePath.getTag("tree_item_type")
|
||||
if tree_item_type == "LIGHT_NODE":
|
||||
return True
|
||||
|
||||
# 方法4: 通过名称模式匹配(作为后备方案)
|
||||
node_name = nodePath.getName().lower()
|
||||
if "spotlight" in node_name or "pointlight" in node_name:
|
||||
return True
|
||||
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"检查灯光对象时出错: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
def toggleLightVisibility(self, light_node, visible):
|
||||
"""切换灯光可见性"""
|
||||
try:
|
||||
print(f"切换灯光可见性: {light_node.getName()}, 可见={visible}")
|
||||
|
||||
# 保存用户可见性状态到该特定节点
|
||||
light_node.setPythonTag("user_visible", visible)
|
||||
|
||||
# 获取该特定灯光对象
|
||||
rp_light_object = light_node.getPythonTag("rp_light_object")
|
||||
if not rp_light_object:
|
||||
print(f"错误: {light_node.getName()} 未找到RP灯光对象引用")
|
||||
return
|
||||
|
||||
# 获取RenderPipeline实例
|
||||
from core.render_pipeline_utils import get_render_pipeline
|
||||
render_pipeline = get_render_pipeline()
|
||||
|
||||
if not render_pipeline:
|
||||
print("错误: 无法获取RenderPipeline实例")
|
||||
return
|
||||
|
||||
try:
|
||||
if visible:
|
||||
if light_node.hasTag("stored_energy"):
|
||||
stored_energy = float(light_node.getTag("stored_energy"))
|
||||
rp_light_object.energy=stored_energy
|
||||
print(f"已恢复灯光强度: {light_node.getName()}, 能量={stored_energy}")
|
||||
# 启用特定灯光
|
||||
# render_pipeline.add_light(rp_light_object)
|
||||
# print(f"已添加灯光到渲染管线: {light_node.getName()}")
|
||||
else:
|
||||
# 禁用特定灯光
|
||||
current_energy = rp_light_object.energy
|
||||
if current_energy != 0.0:
|
||||
light_node.setTag("stored_energy", str(current_energy))
|
||||
elif light_node.hasTag("stored_energy"):
|
||||
stored_energy = float(light_node.getTag("stored_energy"))
|
||||
current_energy = stored_energy
|
||||
else:
|
||||
current_energy = 0.0
|
||||
rp_light_object.energy = 0.0
|
||||
print(f"已禁用灯光: {light_node.getName()}, 保存的能量={current_energy}")
|
||||
# render_pipeline.remove_light(rp_light_object)
|
||||
# print(f"已从渲染管线移除灯光: {light_node.getName()}")
|
||||
except Exception as e:
|
||||
print(f"操作RenderPipeline灯光时出错: {e}")
|
||||
|
||||
# 控制节点显示状态(可选,主要是视觉上的)
|
||||
if visible:
|
||||
light_node.show()
|
||||
else:
|
||||
light_node.hide()
|
||||
|
||||
print(f"灯光可见性设置完成: {visible}")
|
||||
except Exception as e:
|
||||
print(f"切换灯光可见性失败: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
def _recreateLightFromData(self, node_data, parent_node, name):
|
||||
"""根据数据重建光源"""
|
||||
try:
|
||||
light_type = node_data.get('tags', {}).get('light_type', 'spot_light')
|
||||
|
||||
# 创建光源
|
||||
if light_type == 'spot_light':
|
||||
light_node = self.createSpotLight(pos=node_data.get('pos', (0, 0, 0)))
|
||||
else: # point_light
|
||||
light_node = self.createPointLight(pos=node_data.get('pos', (0, 0, 0)))
|
||||
|
||||
if light_node:
|
||||
# 设置名称
|
||||
light_node.setName(name)
|
||||
|
||||
# 恢复其他属性
|
||||
light_data = node_data.get('light_data', {})
|
||||
rp_light = light_node.getPythonTag("rp_light_object")
|
||||
if rp_light and light_data:
|
||||
if 'energy' in light_data:
|
||||
rp_light.energy = light_data['energy']
|
||||
if 'radius' in light_data:
|
||||
rp_light.radius = light_data['radius']
|
||||
if 'fov' in light_data and hasattr(rp_light, 'fov'):
|
||||
rp_light.fov = light_data['fov']
|
||||
if 'inner_radius' in light_data and hasattr(rp_light, 'inner_radius'):
|
||||
rp_light.inner_radius = light_data['inner_radius']
|
||||
if 'casts_shadows' in light_data and hasattr(rp_light, 'casts_shadows'):
|
||||
rp_light.casts_shadows = light_data['casts_shadows']
|
||||
if 'shadow_map_resolution' in light_data and hasattr(rp_light, 'shadow_map_resolution'):
|
||||
rp_light.shadow_map_resolution = light_data['shadow_map_resolution']
|
||||
|
||||
# 恢复其他标签
|
||||
for tag_key, tag_value in node_data.get('tags', {}).items():
|
||||
if tag_key not in ['name', 'light_type']:
|
||||
light_node.setTag(tag_key, str(tag_value))
|
||||
|
||||
return light_node
|
||||
|
||||
except Exception as e:
|
||||
print(f"重建光源失败: {e}")
|
||||
return None
|
||||
|
||||
def createSpotLight(self, pos=(0, 0, 5)):
|
||||
"""创建聚光灯
|
||||
|
||||
Args:
|
||||
pos (tuple): 光源位置 (x, y, z)
|
||||
|
||||
Returns:
|
||||
NodePath: 创建的聚光灯节点
|
||||
"""
|
||||
try:
|
||||
# 检查是否使用RenderPipeline
|
||||
if hasattr(self.world, 'render_pipeline') and self.world.render_pipeline:
|
||||
from RenderPipelineFile.rpcore import SpotLight
|
||||
from core.render_pipeline_utils import get_render_pipeline
|
||||
|
||||
# 创建RenderPipeline聚光灯
|
||||
from panda3d.core import Vec3
|
||||
spotlight = SpotLight()
|
||||
spotlight.direction = Vec3(0, 0, -1) # 向下照射
|
||||
spotlight.fov = 70 # 聚光角度
|
||||
spotlight.set_color_from_temperature(6500) # 日光色温
|
||||
spotlight.energy = 5000 # 光照强度
|
||||
spotlight.radius = 1000 # 光照范围
|
||||
spotlight.casts_shadows = True # 启用阴影
|
||||
spotlight.shadow_map_resolution = 256 # 阴影分辨率
|
||||
spotlight.setPos(pos)
|
||||
|
||||
# 添加到RenderPipeline
|
||||
render_pipeline = get_render_pipeline()
|
||||
if render_pipeline:
|
||||
render_pipeline.add_light(spotlight)
|
||||
print(f"✓ RenderPipeline聚光灯创建成功,位置: {pos}")
|
||||
|
||||
# 创建包装节点用于场景树显示
|
||||
light_name = f"Spotlight_{len(self.Spotlight)}"
|
||||
spotlight_node = self.world.render.attachNewNode(light_name)
|
||||
spotlight_node.setPos(*pos)
|
||||
spotlight_node.setTag("light_type", "spot_light")
|
||||
spotlight_node.setTag("is_scene_element", "1")
|
||||
spotlight_node.setTag("tree_item_type", "LIGHT_NODE")
|
||||
spotlight_node.setTag("light_energy", str(getattr(spotlight, "energy", 5000)))
|
||||
spotlight_node.setTag("created_by_user", "1")
|
||||
spotlight_node.setTag("element_type", "spotlight")
|
||||
spotlight_node.setPythonTag("rp_light_object", spotlight)
|
||||
self.Spotlight.append(spotlight_node)
|
||||
return spotlight_node
|
||||
else:
|
||||
print("✗ 无法获取RenderPipeline实例")
|
||||
return None
|
||||
else:
|
||||
# 使用标准Panda3D光源
|
||||
from panda3d.core import Spotlight, PerspectiveLens
|
||||
|
||||
# 创建聚光灯
|
||||
spotlight = Spotlight('spotlight')
|
||||
spotlight.setColor((1, 1, 1, 1)) # 白色光
|
||||
spotlight.setLens(PerspectiveLens())
|
||||
|
||||
# 创建光源节点
|
||||
spotlight_node = self.world.render.attachNewNode(spotlight)
|
||||
spotlight_node.setPos(pos)
|
||||
spotlight_node.setTag("light_type", "spot_light")
|
||||
spotlight_node.setTag("is_scene_element", "1")
|
||||
spotlight_node.setTag("tree_item_type", "LIGHT_NODE")
|
||||
spotlight_node.setTag("created_by_user", "1")
|
||||
spotlight_node.setTag("element_type", "spotlight")
|
||||
|
||||
# 设置聚光灯方向(向下照射)
|
||||
spotlight_node.lookAt(pos[0], pos[1], pos[2] - 5) # 向下看5个单位
|
||||
|
||||
# 设置聚光灯范围
|
||||
spotlight.setExponent(1.0) # 聚光指数
|
||||
spotlight.setAttenuation((1, 0.1, 0.01)) # 衰减
|
||||
|
||||
# 添加到光源列表
|
||||
self.Spotlight.append(spotlight_node)
|
||||
|
||||
# 启用光源
|
||||
self.world.render.setLight(spotlight_node)
|
||||
|
||||
# 启用阴影
|
||||
if hasattr(spotlight, 'setShadowCaster'):
|
||||
spotlight.setShadowCaster(True)
|
||||
|
||||
print(f"✓ 标准聚光灯创建成功,位置: {pos}")
|
||||
return spotlight_node
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ 创建聚光灯失败: {e}")
|
||||
return None
|
||||
|
||||
def createPointLight(self, pos=(0, 0, 5)):
|
||||
"""创建点光源
|
||||
|
||||
Args:
|
||||
pos (tuple): 光源位置 (x, y, z)
|
||||
|
||||
Returns:
|
||||
NodePath: 创建的点光源节点
|
||||
"""
|
||||
try:
|
||||
# 检查是否使用RenderPipeline
|
||||
if hasattr(self.world, 'render_pipeline') and self.world.render_pipeline:
|
||||
from RenderPipelineFile.rpcore import PointLight
|
||||
from core.render_pipeline_utils import get_render_pipeline
|
||||
|
||||
# 创建RenderPipeline点光源
|
||||
pointlight = PointLight()
|
||||
pointlight.set_color_from_temperature(6500) # 日光色温
|
||||
pointlight.energy = 3000 # 光照强度
|
||||
pointlight.radius = 1000 # 光照范围
|
||||
pointlight.casts_shadows = True # 启用阴影
|
||||
pointlight.shadow_map_resolution = 256 # 阴影分辨率
|
||||
pointlight.setPos(pos)
|
||||
|
||||
# 添加到RenderPipeline
|
||||
render_pipeline = get_render_pipeline()
|
||||
if render_pipeline:
|
||||
render_pipeline.add_light(pointlight)
|
||||
print(f"✓ RenderPipeline点光源创建成功,位置: {pos}")
|
||||
|
||||
# 创建包装节点用于场景树显示
|
||||
light_name = f"Pointlight_{len(self.Pointlight)}"
|
||||
pointlight_node = self.world.render.attachNewNode(light_name)
|
||||
pointlight_node.setPos(*pos)
|
||||
pointlight_node.setTag("light_type", "point_light")
|
||||
pointlight_node.setTag("is_scene_element", "1")
|
||||
pointlight_node.setTag("tree_item_type", "LIGHT_NODE")
|
||||
pointlight_node.setTag("light_energy", str(getattr(pointlight, "energy", 3000)))
|
||||
pointlight_node.setTag("created_by_user", "1")
|
||||
pointlight_node.setTag("element_type", "pointlight")
|
||||
pointlight_node.setPythonTag("rp_light_object", pointlight)
|
||||
self.Pointlight.append(pointlight_node)
|
||||
return pointlight_node
|
||||
else:
|
||||
print("✗ 无法获取RenderPipeline实例")
|
||||
return None
|
||||
else:
|
||||
# 使用标准Panda3D光源
|
||||
from panda3d.core import PointLight
|
||||
|
||||
# 创建点光源
|
||||
pointlight = PointLight('pointlight')
|
||||
pointlight.setColor((1, 1, 1, 1)) # 白色光
|
||||
pointlight.setAttenuation((1, 0.1, 0.01)) # 衰减设置
|
||||
|
||||
# 创建光源节点
|
||||
pointlight_node = self.world.render.attachNewNode(pointlight)
|
||||
pointlight_node.setPos(pos)
|
||||
pointlight_node.setTag("light_type", "point_light")
|
||||
pointlight_node.setTag("is_scene_element", "1")
|
||||
pointlight_node.setTag("tree_item_type", "LIGHT_NODE")
|
||||
pointlight_node.setTag("created_by_user", "1")
|
||||
pointlight_node.setTag("element_type", "pointlight")
|
||||
|
||||
# 添加到光源列表
|
||||
self.Pointlight.append(pointlight_node)
|
||||
|
||||
# 启用光源
|
||||
self.world.render.setLight(pointlight_node)
|
||||
|
||||
# 启用阴影
|
||||
if hasattr(pointlight, 'setShadowCaster'):
|
||||
pointlight.setShadowCaster(True)
|
||||
|
||||
print(f"✓ 标准点光源创建成功,位置: {pos}")
|
||||
return pointlight_node
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ 创建点光源失败: {e}")
|
||||
return None
|
||||
1073
scene/scene_manager_model_mixin.py
Normal file
1073
scene/scene_manager_model_mixin.py
Normal file
File diff suppressed because it is too large
Load Diff
873
scene/scene_manager_serialization_mixin.py
Normal file
873
scene/scene_manager_serialization_mixin.py
Normal file
@ -0,0 +1,873 @@
|
||||
"""Scene manager node serialization 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 SceneManagerSerializationMixin:
|
||||
def _get_editor_context(self):
|
||||
return get_editor_context(self.world)
|
||||
|
||||
def _get_tree_widget(self):
|
||||
return self._get_editor_context().get_tree_widget()
|
||||
|
||||
def _get_gui_manager(self):
|
||||
return self._get_editor_context().get_gui_manager()
|
||||
|
||||
def serializeNode(self, node):
|
||||
"""序列化节点为字典数据"""
|
||||
try:
|
||||
node_data = {
|
||||
'name': node.getName(),
|
||||
'type': type(node.node()).__name__,
|
||||
'pos': (node.getX(), node.getY(), node.getZ()),
|
||||
'hpr': (node.getH(), node.getP(), node.getR()),
|
||||
'scale': (node.getSx(), node.getSy(), node.getSz()),
|
||||
'tags': {},
|
||||
'children': []
|
||||
}
|
||||
|
||||
# 保存所有标签
|
||||
for tag_key in node.getTagKeys():
|
||||
node_data['tags'][tag_key] = node.getTag(tag_key)
|
||||
|
||||
# 特殊处理不同类型的节点
|
||||
if hasattr(node.node(), 'getClassType'):
|
||||
node_class = node.node().getClassType().getName()
|
||||
node_data['node_class'] = node_class
|
||||
|
||||
# 递归序列化子节点
|
||||
for child in node.getChildren():
|
||||
# 跳过辅助节点
|
||||
if not child.getName().startswith(('gizmo', 'selectionBox', 'grid')):
|
||||
child_data = self.serializeNode(child)
|
||||
if child_data:
|
||||
node_data['children'].append(child_data)
|
||||
|
||||
return node_data
|
||||
|
||||
except Exception as e:
|
||||
print(f"序列化节点 {node.getName()} 失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return None
|
||||
|
||||
def deserializeNode(self, node_data, parent_node):
|
||||
"""从字典数据反序列化节点"""
|
||||
try:
|
||||
# 创建新节点
|
||||
node_name = node_data.get('name', 'node')
|
||||
new_node = parent_node.attachNewNode(node_name)
|
||||
|
||||
# 设置变换
|
||||
pos = node_data.get('pos', (0, 0, 0))
|
||||
hpr = node_data.get('hpr', (0, 0, 0))
|
||||
scale = node_data.get('scale', (1, 1, 1))
|
||||
|
||||
new_node.setPos(*pos)
|
||||
new_node.setHpr(*hpr)
|
||||
new_node.setScale(*scale)
|
||||
|
||||
# 恢复标签
|
||||
for tag_key, tag_value in node_data.get('tags', {}).items():
|
||||
new_node.setTag(tag_key, tag_value)
|
||||
|
||||
# 根据节点类型进行特殊处理
|
||||
node_type = node_data.get('type', '')
|
||||
node_class = node_data.get('node_class', '')
|
||||
|
||||
# 特殊处理光源节点
|
||||
if 'light_type' in node_data.get('tags', {}):
|
||||
light_type = node_data['tags']['light_type']
|
||||
if light_type == 'spot_light':
|
||||
self._recreateSpotLight(new_node)
|
||||
elif light_type == 'point_light':
|
||||
self._recreatePointLight(new_node)
|
||||
|
||||
# 递归创建子节点
|
||||
for child_data in node_data.get('children', []):
|
||||
self.deserializeNode(child_data, new_node)
|
||||
|
||||
return new_node
|
||||
|
||||
except Exception as e:
|
||||
print(f"反序列化节点 {node_data.get('name', 'unknown')} 失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return None
|
||||
|
||||
def serializeNodeForCopy(self, node):
|
||||
"""序列化节点用于复制操作,完整保存视觉属性"""
|
||||
try:
|
||||
if not node or node.isEmpty():
|
||||
return None
|
||||
|
||||
node_data = {
|
||||
'name': node.getName(),
|
||||
'type': type(node.node()).__name__,
|
||||
'pos': (node.getX(), node.getY(), node.getZ()),
|
||||
'hpr': (node.getH(), node.getP(), node.getR()),
|
||||
'scale': (node.getSx(), node.getSy(), node.getSz()),
|
||||
'tags': {},
|
||||
'children': []
|
||||
}
|
||||
|
||||
# 保存所有标签
|
||||
try:
|
||||
if hasattr(node, 'getTagKeys'):
|
||||
for tag_key in node.getTagKeys():
|
||||
node_data['tags'][tag_key] = node.getTag(tag_key)
|
||||
except Exception as e:
|
||||
print(f"获取标签时出错: {e}")
|
||||
|
||||
# 保存视觉属性
|
||||
try:
|
||||
# 保存颜色属性
|
||||
if hasattr(node, 'getColor'):
|
||||
color = node.getColor()
|
||||
node_data['color'] = (color.getX(), color.getY(), color.getZ(), color.getW())
|
||||
|
||||
# 保存材质属性
|
||||
if hasattr(node, 'getMaterial'):
|
||||
material = node.getMaterial()
|
||||
if material:
|
||||
material_data = {}
|
||||
material_data['base_color'] = (
|
||||
material.getBaseColor().getX(),
|
||||
material.getBaseColor().getY(),
|
||||
material.getBaseColor().getZ(),
|
||||
material.getBaseColor().getW()
|
||||
)
|
||||
material_data['ambient'] = (
|
||||
material.getAmbient().getX(),
|
||||
material.getAmbient().getY(),
|
||||
material.getAmbient().getZ(),
|
||||
material.getAmbient().getW()
|
||||
)
|
||||
material_data['diffuse'] = (
|
||||
material.getDiffuse().getX(),
|
||||
material.getDiffuse().getY(),
|
||||
material.getDiffuse().getZ(),
|
||||
material.getDiffuse().getW()
|
||||
)
|
||||
material_data['specular'] = (
|
||||
material.getSpecular().getX(),
|
||||
material.getSpecular().getY(),
|
||||
material.getSpecular().getZ(),
|
||||
material.getSpecular().getW()
|
||||
)
|
||||
material_data['shininess'] = material.getShininess()
|
||||
node_data['material'] = material_data
|
||||
|
||||
except Exception as e:
|
||||
print(f"保存视觉属性时出错: {e}")
|
||||
|
||||
# 根据节点类型保存特定信息
|
||||
if node.hasTag("tree_item_type"):
|
||||
node_type = node.getTag("tree_item_type")
|
||||
node_data['node_type'] = node_type
|
||||
|
||||
# 保存特定类型节点的额外信息
|
||||
if node_type in ["LIGHT_NODE", "SPOT_LIGHT_NODE", "POINT_LIGHT_NODE"]:
|
||||
# 保存光源特定信息
|
||||
rp_light = node.getPythonTag("rp_light_object")
|
||||
if rp_light:
|
||||
node_data['light_data'] = {
|
||||
'energy': getattr(rp_light, 'energy', 5000),
|
||||
'radius': getattr(rp_light, 'radius', 1000),
|
||||
'fov': getattr(rp_light, 'fov', 70) if hasattr(rp_light, 'fov') else None,
|
||||
'inner_radius': getattr(rp_light, 'inner_radius', 0.4) if hasattr(rp_light,
|
||||
'inner_radius') else None,
|
||||
'casts_shadows': getattr(rp_light, 'casts_shadows', True) if hasattr(rp_light,
|
||||
'casts_shadows') else True,
|
||||
'shadow_map_resolution': getattr(rp_light, 'shadow_map_resolution', 256) if hasattr(
|
||||
rp_light, 'shadow_map_resolution') else 256
|
||||
}
|
||||
elif node_type in ["GUI_BUTTON", "GUI_LABEL", "GUI_ENTRY", "GUI_IMAGE",
|
||||
"GUI_3D_TEXT", "GUI_3D_IMAGE", "GUI_VIRTUAL_SCREEN"]:
|
||||
# 保存GUI元素特定信息
|
||||
node_data['gui_data'] = self._serializeGUIData(node)
|
||||
elif node_type == "IMPORTED_MODEL_NODE":
|
||||
# 保存模型特定信息
|
||||
node_data['model_data'] = self._serializeModelData(node)
|
||||
|
||||
return node_data
|
||||
|
||||
except Exception as e:
|
||||
print(f"序列化节点失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return None
|
||||
|
||||
def _serializeGUIData(self, node):
|
||||
"""序列化GUI元素数据"""
|
||||
try:
|
||||
gui_data = {}
|
||||
|
||||
# 保存GUI相关的通用属性
|
||||
if node.hasTag("gui_type"):
|
||||
gui_data['gui_type'] = node.getTag("gui_type")
|
||||
|
||||
# 保存文本内容(如果有的话)
|
||||
if node.hasTag("text"):
|
||||
gui_data['text'] = node.getTag("text")
|
||||
|
||||
# 保存其他GUI相关标签
|
||||
gui_tags = ['font', 'font_size', 'text_color', 'bg_color', 'size']
|
||||
for tag in gui_tags:
|
||||
if node.hasTag(tag):
|
||||
gui_data[tag] = node.getTag(tag)
|
||||
|
||||
return gui_data
|
||||
except Exception as e:
|
||||
print(f"序列化GUI数据失败: {e}")
|
||||
return {}
|
||||
|
||||
def _serializeModelTextures(self, node):
|
||||
"""序列化模型纹理信息"""
|
||||
try:
|
||||
texture_data = {}
|
||||
|
||||
# 获取节点的所有纹理阶段
|
||||
from panda3d.core import TextureStage
|
||||
texture_stages = node.findAllTextureStages()
|
||||
|
||||
if texture_stages.getNumTextureStages() > 0:
|
||||
texture_data['textures'] = {}
|
||||
|
||||
# 遍历所有纹理阶段
|
||||
for i in range(texture_stages.getNumTextureStages()):
|
||||
stage = texture_stages.getTextureStage(i)
|
||||
stage_name = stage.getName()
|
||||
|
||||
# 获取该阶段的纹理
|
||||
texture = node.getTexture(stage)
|
||||
if texture:
|
||||
# 保存纹理信息
|
||||
texture_info = {
|
||||
'stage_name': stage_name,
|
||||
'stage_mode': stage.getMode(),
|
||||
'stage_sort': stage.getSort(), # 保存纹理阶段排序
|
||||
'texture_path': texture.getFullpath().toOsSpecific() if texture.hasFullpath() else '',
|
||||
'texture_name': texture.getName(),
|
||||
'wrap_u': texture.getWrapU(),
|
||||
'wrap_v': texture.getWrapV(),
|
||||
'minfilter': texture.getMinfilter(),
|
||||
'magfilter': texture.getMagfilter(),
|
||||
'anisotropic_degree': texture.getAnisotropicDegree()
|
||||
}
|
||||
|
||||
# 保存颜色比例和偏移(使用安全的方法)
|
||||
try:
|
||||
texture_info['color_scale'] = tuple(node.getTextureScale(stage))
|
||||
except:
|
||||
texture_info['color_scale'] = (1.0, 1.0, 1.0, 1.0)
|
||||
|
||||
try:
|
||||
texture_info['color_offset'] = tuple(node.getTextureOffset(stage))
|
||||
except:
|
||||
texture_info['color_offset'] = (0.0, 0.0, 0.0, 0.0)
|
||||
|
||||
texture_data['textures'][stage_name] = texture_info
|
||||
|
||||
return texture_data
|
||||
except Exception as e:
|
||||
print(f"序列化模型纹理时出错: {e}")
|
||||
return {}
|
||||
|
||||
def _serializeModelData(self, node):
|
||||
"""序列化模型数据,包括材质和纹理信息"""
|
||||
try:
|
||||
model_data = {}
|
||||
|
||||
# 保存模型相关的标签
|
||||
model_tags = ['model_path', 'file', 'element_type']
|
||||
for tag in model_tags:
|
||||
if node.hasTag(tag):
|
||||
model_data[tag] = node.getTag(tag)
|
||||
|
||||
# 保存材质信息
|
||||
try:
|
||||
# 获取模型的材质信息
|
||||
if hasattr(node, 'getState'):
|
||||
state = node.getState()
|
||||
if state:
|
||||
# 保存基础颜色信息(使用正确的方法)
|
||||
from panda3d.core import ColorAttrib
|
||||
color_attrib = state.getAttrib(ColorAttrib)
|
||||
if color_attrib and not color_attrib.isOff():
|
||||
color = color_attrib.getColor()
|
||||
model_data['base_color'] = (
|
||||
color.getX(),
|
||||
color.getY(),
|
||||
color.getZ(),
|
||||
color.getW()
|
||||
)
|
||||
|
||||
# 保存其他材质属性
|
||||
from panda3d.core import MaterialAttrib
|
||||
material_attrib = state.getAttrib(MaterialAttrib.getClassType())
|
||||
if material_attrib:
|
||||
material = material_attrib.getMaterial()
|
||||
if material:
|
||||
# 保存基础颜色
|
||||
base_color = material.getBaseColor()
|
||||
model_data['material_base_color'] = (
|
||||
base_color.getX(), base_color.getY(), base_color.getZ(), base_color.getW()
|
||||
)
|
||||
|
||||
# 保存环境光颜色
|
||||
ambient_color = material.getAmbient()
|
||||
model_data['material_ambient_color'] = (
|
||||
ambient_color.getX(), ambient_color.getY(), ambient_color.getZ(),
|
||||
ambient_color.getW()
|
||||
)
|
||||
|
||||
# 保存漫反射颜色
|
||||
diffuse_color = material.getDiffuse()
|
||||
model_data['material_diffuse_color'] = (
|
||||
diffuse_color.getX(), diffuse_color.getY(), diffuse_color.getZ(),
|
||||
diffuse_color.getW()
|
||||
)
|
||||
|
||||
# 保存高光颜色
|
||||
specular_color = material.getSpecular()
|
||||
model_data['material_specular_color'] = (
|
||||
specular_color.getX(), specular_color.getY(), specular_color.getZ(),
|
||||
specular_color.getW()
|
||||
)
|
||||
|
||||
# 保存粗糙度和金属度等参数
|
||||
model_data['material_roughness'] = material.getRoughness()
|
||||
model_data['material_metallic'] = material.getMetallic()
|
||||
|
||||
# 保存自发光颜色
|
||||
emission_color = material.getEmission()
|
||||
model_data['material_emission_color'] = (
|
||||
emission_color.getX(), emission_color.getY(), emission_color.getZ(),
|
||||
emission_color.getW()
|
||||
)
|
||||
|
||||
# 保存光泽度
|
||||
model_data['material_shininess'] = material.getShininess()
|
||||
|
||||
# 保存透明度信息
|
||||
from panda3d.core import TransparencyAttrib
|
||||
transparency_attrib = state.getAttrib(TransparencyAttrib.getClassType())
|
||||
if transparency_attrib:
|
||||
model_data['transparency_mode'] = transparency_attrib.get_mode()
|
||||
|
||||
except Exception as e:
|
||||
print(f"保存材质信息时出错: {e}")
|
||||
|
||||
# 保存纹理信息
|
||||
try:
|
||||
texture_data = self._serializeModelTextures(node)
|
||||
if texture_data:
|
||||
model_data['texture_data'] = texture_data
|
||||
except Exception as e:
|
||||
print(f"保存纹理信息时出错: {e}")
|
||||
|
||||
return model_data
|
||||
except Exception as e:
|
||||
print(f"序列化模型数据失败: {e}")
|
||||
return {}
|
||||
|
||||
def recreateNodeFromData(self, node_data, parent_node):
|
||||
"""根据数据重建节点,并确保在场景树中显示"""
|
||||
try:
|
||||
if not node_data or not parent_node or parent_node.isEmpty():
|
||||
return None
|
||||
|
||||
print(f"正在重建节点 {node_data}")
|
||||
node_type = node_data.get('node_type', '')
|
||||
original_name = node_data.get('name', 'node')
|
||||
|
||||
# 生成唯一名称
|
||||
unique_name = self._generateUniqueName(original_name, parent_node)
|
||||
|
||||
# 根据节点类型调用相应的重建方法
|
||||
new_node = None
|
||||
if node_type in ["LIGHT_NODE", "SPOT_LIGHT_NODE", "POINT_LIGHT_NODE"]:
|
||||
new_node = self._recreateLightFromData(node_data, parent_node, unique_name)
|
||||
elif node_type == "CESIUM_TILESET_NODE":
|
||||
new_node = self._recreateTilesetFromData(node_data, parent_node, unique_name)
|
||||
elif node_type in ["GUI_BUTTON", "GUI_LABEL", "GUI_ENTRY", "GUI_IMAGE",
|
||||
"GUI_3DTEXT", "GUI_3DIMAGE", "GUI_VIDEO_SCREEN","GUI_2D_VIDEO_SCREEN"]:
|
||||
new_node = self._recreateGUIFromData(node_data, parent_node, unique_name)
|
||||
elif node_type == "IMPORTED_MODEL_NODE":
|
||||
new_node = self._recreateModelFromData(node_data, parent_node, unique_name)
|
||||
else:
|
||||
# 创建普通节点
|
||||
new_node = self._createBasicNodeFromData(node_data, parent_node, unique_name)
|
||||
|
||||
# 如果成功创建节点,确保它在场景树中显示
|
||||
if new_node:
|
||||
# 尝试更新场景树以显示新节点
|
||||
try:
|
||||
tree_widget = self._get_tree_widget()
|
||||
if tree_widget:
|
||||
# 查找父节点在场景树中的对应项
|
||||
parent_item = self._findTreeItemForNode(parent_node)
|
||||
if parent_item:
|
||||
# 添加新节点到场景树
|
||||
tree_widget.add_node_to_tree_widget(new_node, parent_item, node_type or "NODE")
|
||||
except Exception as e:
|
||||
print(f"添加节点到场景树时出错: {e}")
|
||||
|
||||
return new_node
|
||||
|
||||
except Exception as e:
|
||||
print(f"重建节点失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return None
|
||||
|
||||
def _findTreeItemForNode(self, node):
|
||||
"""根据节点查找对应的场景树项"""
|
||||
try:
|
||||
tree_widget = self._get_tree_widget()
|
||||
if tree_widget:
|
||||
# 遍历场景树查找匹配的节点项
|
||||
for i in range(tree_widget.topLevelItemCount()):
|
||||
item = tree_widget.topLevelItem(i)
|
||||
result = self._findTreeItemForNodeRecursive(item, node)
|
||||
if result:
|
||||
return result
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"查找场景树项时出错: {e}")
|
||||
return None
|
||||
|
||||
def _findTreeItemForNodeRecursive(self, item, target_node):
|
||||
"""递归查找场景树项"""
|
||||
try:
|
||||
# 检查当前项是否匹配
|
||||
item_node = getattr(item, 'node_path', None)
|
||||
if not item_node:
|
||||
item_node = getattr(item, 'node', None)
|
||||
|
||||
if item_node and item_node == target_node:
|
||||
return item
|
||||
|
||||
# 递归检查子项
|
||||
for i in range(item.childCount()):
|
||||
child_item = item.child(i)
|
||||
result = self._findTreeItemForNodeRecursive(child_item, target_node)
|
||||
if result:
|
||||
return result
|
||||
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"递归查找场景树项时出错: {e}")
|
||||
return None
|
||||
|
||||
def _recreateTilesetFromData(self, node_data, parent_node, name):
|
||||
"""根据数据重建Tileset"""
|
||||
try:
|
||||
tileset_url = node_data.get('tileset_url', '')
|
||||
if not tileset_url:
|
||||
return None
|
||||
|
||||
# 使用现有方法加载tileset
|
||||
position = node_data.get('pos', (0, 0, 0))
|
||||
tileset_node = self.load_cesium_tileset(tileset_url, position)
|
||||
|
||||
if tileset_node:
|
||||
# 设置名称
|
||||
tileset_node.setName(name)
|
||||
|
||||
# 恢复其他标签
|
||||
for tag_key, tag_value in node_data.get('tags', {}).items():
|
||||
if tag_key not in ['name']:
|
||||
tileset_node.setTag(tag_key, str(tag_value))
|
||||
|
||||
return tileset_node
|
||||
|
||||
except Exception as e:
|
||||
print(f"重建Tileset失败: {e}")
|
||||
return None
|
||||
|
||||
def _recreateGUIFromData(self, node_data, parent_node, name):
|
||||
"""根据数据重建GUI元素"""
|
||||
try:
|
||||
gui_data = node_data.get('gui_data', {})
|
||||
#gui_type = gui_data.get('gui_type', '')
|
||||
gui_type = node_data.get("tags").get("gui_type", "")
|
||||
|
||||
print(f"正在重建GUI元素: {gui_type}")
|
||||
print(f"正在重建GUI元素: {node_data}")
|
||||
|
||||
# 根据GUI类型调用相应的创建方法
|
||||
new_gui_element = None
|
||||
gui_manager = self._get_gui_manager()
|
||||
|
||||
if gui_type == "button" and hasattr(self.world, 'createGUIButton'):
|
||||
pos = node_data.get('pos', (0, 0, 0))
|
||||
text = node_data.get('tags').get('gui_text', '')
|
||||
size = node_data.get('scale', 1)
|
||||
print(pos,text,size)
|
||||
new_gui_element = self.world.createGUIButton(pos,text,size)
|
||||
elif gui_type == "label" and hasattr(self.world, 'createGUILabel'):
|
||||
pos = node_data.get('pos', (0, 0, 0))
|
||||
text = node_data.get('tags').get('gui_text', '')
|
||||
size = node_data.get('scale', 1)
|
||||
new_gui_element = self.world.createGUILabel(pos,text,size)
|
||||
elif gui_type == "entry" and hasattr(self.world, 'createGUIEntry'):
|
||||
pos = node_data.get('pos', (0, 0, 0))
|
||||
text = node_data.get('tags').get('gui_text', '')
|
||||
size = node_data.get('scale', 1)
|
||||
new_gui_element = self.world.createGUIEntry(pos,text,size)
|
||||
elif gui_type == "2d_image" and hasattr(self.world, 'createGUI2DImage'):
|
||||
pos = node_data.get('pos', (0, 0, 0))
|
||||
image_path = node_data.get('tags').get('image_path', '')
|
||||
size = node_data.get('size', 1)
|
||||
new_gui_element = self.world.createGUI2DImage(pos, image_path, size)
|
||||
elif gui_type == "3d_text" and hasattr(self.world, 'createGUI3DText'):
|
||||
print("正在创建3D文本!!!")
|
||||
pos = node_data.get('pos', (0, 0, 0))
|
||||
text = node_data.get('tags', {}).get('gui_text', '')
|
||||
scale = node_data.get('scale', 1)
|
||||
if isinstance(scale, (list, tuple)):
|
||||
scale = scale[0] if len(scale) > 0 else 1
|
||||
print(f"正在创建3D文本: 位置={pos}, 文本={text}, 大小={scale}")
|
||||
new_gui_element = self.world.createGUI3DText(pos, text, scale)
|
||||
elif gui_type == "3d_image" and hasattr(self.world, 'createGUI3DImage'):
|
||||
pos = node_data.get('pos', (0, 0, 0))
|
||||
image_path = node_data.get('tags').get('gui_image_path', '')
|
||||
scale = node_data.get('scale', (1, 1))
|
||||
if isinstance(scale, (int, float)):
|
||||
scale = (scale, scale)
|
||||
elif isinstance(scale, (list, tuple)) and len(scale) >= 2:
|
||||
scale = (scale[0], scale[1])
|
||||
else:
|
||||
scale = (1, 1)
|
||||
print(f"正在创建3D图片: 位置={pos}, 路径={image_path}, 大小={scale}")
|
||||
if gui_manager and hasattr(gui_manager, 'createGUI3DImage'):
|
||||
new_gui_element = gui_manager.createGUI3DImage(pos, image_path, scale)
|
||||
elif gui_type == "video_screen" and gui_manager and hasattr(gui_manager, 'createVideoScreen'):
|
||||
pos = node_data.get('pos', (0, 0, 0))
|
||||
video_path = node_data.get('tags').get('video_path', '')
|
||||
scale = node_data.get('scale', (1, 1,1))
|
||||
new_gui_element = gui_manager.createVideoScreen(pos,scale,video_path)
|
||||
elif gui_type == "2d_video_screen" and gui_manager and hasattr(gui_manager, 'createGUI2DVideoScreen'):
|
||||
pos = node_data.get('pos', (0, 0, 0))
|
||||
video_path = node_data.get('tags').get('video_path', '')
|
||||
scale = node_data.get('scale', (1, 1, 1))
|
||||
new_gui_element = gui_manager.createGUI2DVideoScreen(pos,scale,video_path)
|
||||
|
||||
if new_gui_element:
|
||||
# 设置名称和变换
|
||||
if hasattr(new_gui_element, 'setName'):
|
||||
new_gui_element.setName(name)
|
||||
|
||||
# 设置位置、旋转、缩放
|
||||
pos = node_data.get('pos', (0, 0, 0))
|
||||
hpr = node_data.get('hpr', (0, 0, 0))
|
||||
scale = node_data.get('scale', (1, 1, 1))
|
||||
|
||||
if hasattr(new_gui_element, 'setPos'):
|
||||
new_gui_element.setPos(*pos)
|
||||
if hasattr(new_gui_element, 'setHpr'):
|
||||
new_gui_element.setHpr(*hpr)
|
||||
if hasattr(new_gui_element, 'setScale'):
|
||||
new_gui_element.setScale(*scale)
|
||||
|
||||
# 恢复文本内容
|
||||
if 'text' in gui_data and hasattr(new_gui_element, 'setText'):
|
||||
new_gui_element.setText(gui_data['text'])
|
||||
|
||||
# 恢复其他标签
|
||||
for tag_key, tag_value in node_data.get('tags', {}).items():
|
||||
if hasattr(new_gui_element, 'setTag') and tag_key not in ['name']:
|
||||
new_gui_element.setTag(tag_key, str(tag_value))
|
||||
|
||||
print(f"GUI元素重建成功: {name}")
|
||||
|
||||
return new_gui_element
|
||||
|
||||
except Exception as e:
|
||||
print(f"重建GUI元素失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return None
|
||||
|
||||
def _recreateModelFromData(self, node_data, parent_node, name):
|
||||
"""根据数据重建模型,保持材质"""
|
||||
try:
|
||||
model_data = node_data.get('model_data', {})
|
||||
model_path = model_data.get('model_path', model_data.get('file', ''))
|
||||
|
||||
if not model_path or not os.path.exists(model_path):
|
||||
# 如果原始模型文件不存在,创建一个基本节点
|
||||
return self._createBasicNodeFromData(node_data, parent_node, name)
|
||||
|
||||
# 导入模型,保持原有参数
|
||||
model = self.importModel(
|
||||
model_path,
|
||||
apply_unit_conversion=False, # 已经处理过的模型不需要再次转换
|
||||
normalize_scales=False, # 保持原有缩放
|
||||
auto_convert_to_glb=False # 已经处理过的模型不需要再次转换
|
||||
)
|
||||
|
||||
if model:
|
||||
# 设置名称
|
||||
model.setName(name)
|
||||
|
||||
# 设置变换
|
||||
pos = node_data.get('pos', (0, 0, 0))
|
||||
hpr = node_data.get('hpr', (0, 0, 0))
|
||||
scale = node_data.get('scale', (1, 1, 1))
|
||||
|
||||
model.setPos(*pos)
|
||||
model.setHpr(*hpr)
|
||||
model.setScale(*scale)
|
||||
|
||||
# 恢复材质和纹理信息
|
||||
try:
|
||||
self._restoreModelMaterial(model, model_data)
|
||||
except Exception as e:
|
||||
print(f"恢复模型材质时出错: {e}")
|
||||
|
||||
# 恢复标签
|
||||
for tag_key, tag_value in node_data.get('tags', {}).items():
|
||||
if tag_key not in ['name']:
|
||||
model.setTag(tag_key, str(tag_value))
|
||||
|
||||
# 添加到模型列表
|
||||
if model not in self.models:
|
||||
self.models.append(model)
|
||||
|
||||
return model
|
||||
|
||||
except Exception as e:
|
||||
print(f"重建模型失败: {e}")
|
||||
# 出错时创建基本节点
|
||||
return self._createBasicNodeFromData(node_data, parent_node, name)
|
||||
|
||||
def _restoreModelTextures(self, model, texture_data):
|
||||
"""恢复模型纹理"""
|
||||
try:
|
||||
if not texture_data or 'textures' not in texture_data:
|
||||
return
|
||||
|
||||
from panda3d.core import TextureStage, SamplerState
|
||||
|
||||
textures_info = texture_data['textures']
|
||||
|
||||
# 为每个纹理阶段恢复纹理
|
||||
for stage_name, texture_info in textures_info.items():
|
||||
# 创建纹理阶段
|
||||
stage = TextureStage(stage_name)
|
||||
stage.setMode(texture_info.get('stage_mode', TextureStage.M_modulate))
|
||||
# 恢复纹理阶段排序
|
||||
stage.setSort(texture_info.get('stage_sort', 0)) # 默认为0(p3d_Texture0)
|
||||
|
||||
# 加载纹理
|
||||
texture_path = texture_info['texture_path']
|
||||
if texture_path and os.path.exists(texture_path):
|
||||
texture = self.world.loader.loadTexture(texture_path)
|
||||
if texture:
|
||||
# 设置纹理属性
|
||||
texture.setWrapU(texture_info.get('wrap_u', SamplerState.WM_repeat))
|
||||
texture.setWrapV(texture_info.get('wrap_v', SamplerState.WM_repeat))
|
||||
texture.setMinfilter(texture_info.get('minfilter', SamplerState.FT_linear))
|
||||
texture.setMagfilter(texture_info.get('magfilter', SamplerState.FT_linear))
|
||||
texture.setAnisotropicDegree(texture_info.get('anisotropic_degree', 1))
|
||||
|
||||
# 应用纹理到模型
|
||||
model.setTexture(stage, texture, 1) # 1 表示强制应用
|
||||
|
||||
# 恢复颜色比例和偏移(使用安全的方法)
|
||||
if 'color_scale' in texture_info:
|
||||
try:
|
||||
model.setTextureScale(stage, *texture_info['color_scale'])
|
||||
except Exception as e:
|
||||
print(f"恢复纹理比例失败: {e}")
|
||||
|
||||
if 'color_offset' in texture_info:
|
||||
try:
|
||||
model.setTextureOffset(stage, *texture_info['color_offset'])
|
||||
except Exception as e:
|
||||
print(f"恢复纹理偏移失败: {e}")
|
||||
|
||||
print(f"恢复纹理: {stage_name} <- {texture_path}")
|
||||
else:
|
||||
print(f"纹理文件不存在或路径为空: {texture_path}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"恢复模型纹理时出错: {e}")
|
||||
|
||||
def _restoreModelMaterial(self, model, model_data):
|
||||
"""恢复模型材质和纹理"""
|
||||
try:
|
||||
# 恢复基础颜色
|
||||
if 'base_color' in model_data:
|
||||
from panda3d.core import ColorAttrib
|
||||
base_color = model_data['base_color']
|
||||
color = (base_color[0], base_color[1], base_color[2], base_color[3])
|
||||
model.setColor(color)
|
||||
|
||||
# 恢复复杂材质属性
|
||||
if any(key.startswith('material_') for key in model_data.keys()):
|
||||
from panda3d.core import Material
|
||||
|
||||
# 创建新材质或获取现有材质
|
||||
material = Material()
|
||||
|
||||
# 恢复基础颜色
|
||||
if 'material_base_color' in model_data:
|
||||
base_color = model_data['material_base_color']
|
||||
material.setBaseColor((base_color[0], base_color[1], base_color[2], base_color[3]))
|
||||
|
||||
# 恢复环境光颜色
|
||||
if 'material_ambient_color' in model_data:
|
||||
ambient_color = model_data['material_ambient_color']
|
||||
material.setAmbient((ambient_color[0], ambient_color[1], ambient_color[2], ambient_color[3]))
|
||||
|
||||
# 恢复漫反射颜色
|
||||
if 'material_diffuse_color' in model_data:
|
||||
diffuse_color = model_data['material_diffuse_color']
|
||||
material.setDiffuse((diffuse_color[0], diffuse_color[1], diffuse_color[2], diffuse_color[3]))
|
||||
|
||||
# 恢复高光颜色
|
||||
if 'material_specular_color' in model_data:
|
||||
specular_color = model_data['material_specular_color']
|
||||
material.setSpecular((specular_color[0], specular_color[1], specular_color[2], specular_color[3]))
|
||||
|
||||
# 恢复自发光颜色
|
||||
if 'material_emission_color' in model_data:
|
||||
emission_color = model_data['material_emission_color']
|
||||
material.setEmission((emission_color[0], emission_color[1], emission_color[2], emission_color[3]))
|
||||
|
||||
# 恢复粗糙度和金属度
|
||||
if 'material_roughness' in model_data:
|
||||
material.setRoughness(model_data['material_roughness'])
|
||||
|
||||
if 'material_metallic' in model_data:
|
||||
material.setMetallic(model_data['material_metallic'])
|
||||
|
||||
# 恢复光泽度
|
||||
if 'material_shininess' in model_data:
|
||||
material.setShininess(model_data['material_shininess'])
|
||||
|
||||
# 应用材质到模型
|
||||
model.setMaterial(material)
|
||||
|
||||
# 恢复透明度设置
|
||||
if 'transparency_mode' in model_data:
|
||||
from panda3d.core import TransparencyAttrib
|
||||
transparency_mode = model_data['transparency_mode']
|
||||
model.setTransparency(transparency_mode)
|
||||
|
||||
# 恢复纹理信息
|
||||
if 'texture_data' in model_data:
|
||||
self._restoreModelTextures(model, model_data['texture_data'])
|
||||
|
||||
except Exception as e:
|
||||
print(f"恢复材质失败: {e}")
|
||||
|
||||
def _createBasicNodeFromData(self, node_data, parent_node, name):
|
||||
"""创建基本节点,保持视觉属性"""
|
||||
try:
|
||||
new_node = parent_node.attachNewNode(name)
|
||||
|
||||
# 设置变换
|
||||
pos = node_data.get('pos', (0, 0, 0))
|
||||
hpr = node_data.get('hpr', (0, 0, 0))
|
||||
scale = node_data.get('scale', (1, 1, 1))
|
||||
|
||||
new_node.setPos(*pos)
|
||||
new_node.setHpr(*hpr)
|
||||
new_node.setScale(*scale)
|
||||
|
||||
# 恢复视觉属性
|
||||
try:
|
||||
# 恢复颜色
|
||||
if 'color' in node_data:
|
||||
color_data = node_data['color']
|
||||
new_node.setColor(color_data[0], color_data[1], color_data[2], color_data[3])
|
||||
|
||||
# 恢复材质
|
||||
if 'material' in node_data:
|
||||
from panda3d.core import Material
|
||||
material_data = node_data['material']
|
||||
material = Material()
|
||||
|
||||
if 'base_color' in material_data:
|
||||
bc = material_data['base_color']
|
||||
material.setBaseColor((bc[0], bc[1], bc[2], bc[3]))
|
||||
|
||||
if 'ambient' in material_data:
|
||||
ac = material_data['ambient']
|
||||
material.setAmbient((ac[0], ac[1], ac[2], ac[3]))
|
||||
|
||||
if 'diffuse' in material_data:
|
||||
dc = material_data['diffuse']
|
||||
material.setDiffuse((dc[0], dc[1], dc[2], dc[3]))
|
||||
|
||||
if 'specular' in material_data:
|
||||
sc = material_data['specular']
|
||||
material.setSpecular((sc[0], sc[1], sc[2], sc[3]))
|
||||
|
||||
if 'shininess' in material_data:
|
||||
material.setShininess(material_data['shininess'])
|
||||
|
||||
new_node.setMaterial(material)
|
||||
|
||||
except Exception as e:
|
||||
print(f"恢复视觉属性时出错: {e}")
|
||||
|
||||
# 恢复标签
|
||||
for tag_key, tag_value in node_data.get('tags', {}).items():
|
||||
if tag_key not in ['name']:
|
||||
new_node.setTag(tag_key, str(tag_value))
|
||||
|
||||
return new_node
|
||||
|
||||
except Exception as e:
|
||||
print(f"创建基本节点失败: {e}")
|
||||
return None
|
||||
|
||||
def _generateUniqueName(self, base_name, parent_node):
|
||||
"""生成唯一节点名称"""
|
||||
try:
|
||||
# 移除可能的数字后缀
|
||||
import re
|
||||
import time
|
||||
name_base = re.sub(r'_\d+$', '', base_name)
|
||||
|
||||
# 查找现有同名节点
|
||||
counter = 1
|
||||
unique_name = base_name
|
||||
while True:
|
||||
# 检查父节点下是否已存在同名子节点
|
||||
existing_node = parent_node.find(unique_name)
|
||||
if existing_node.isEmpty():
|
||||
break
|
||||
unique_name = f"{name_base}_{counter}"
|
||||
counter += 1
|
||||
if counter > 1000: # 防止无限循环
|
||||
break
|
||||
|
||||
return unique_name
|
||||
|
||||
except Exception as e:
|
||||
print(f"生成唯一名称时出错: {e}")
|
||||
return f"{base_name}_{int(time.time())}"
|
||||
5
scene/tree_roles.py
Normal file
5
scene/tree_roles.py
Normal file
@ -0,0 +1,5 @@
|
||||
"""Shared tree data-role constants."""
|
||||
|
||||
# Keep this value stable for legacy tree-item data payloads.
|
||||
TREE_USER_ROLE = 256
|
||||
|
||||
@ -1,51 +1,193 @@
|
||||
|
||||
import math
|
||||
from panda3d.core import (
|
||||
GeomVertexFormat, GeomVertexWriter, GeomVertexReader, GeomVertexRewriter,
|
||||
InternalName, Vec3, Vec4, LMatrix4f, ShaderBuffer, GeomEnums,
|
||||
BoundingSphere, NodePath, GeomNode, Texture, SamplerState,
|
||||
Point3, BoundingBox, Quat
|
||||
)
|
||||
import struct
|
||||
import time
|
||||
|
||||
class ObjectController:
|
||||
"""
|
||||
物体控制器 (No Custom Shader Mode)
|
||||
====================================
|
||||
Uses RP's default rendering (no rp.set_effect) for maximum FPS.
|
||||
Vertex colors baked for picking. Movement modifies vertex data directly.
|
||||
Stores original vertex positions per object for rotation/translation.
|
||||
混合架构控制器 (Chunked Static + Dynamic Editing)
|
||||
================================================
|
||||
- 默认: 每个 chunk 使用 flatten 后的静态表示
|
||||
- 编辑: 被选中对象所属 chunk 切换为动态表示,直接改 NodePath 变换
|
||||
- 提交: 离开 chunk 时仅重建该 chunk 的静态表示
|
||||
"""
|
||||
def __init__(self):
|
||||
|
||||
def __init__(self, chunk_size=64, chunk_world_size=40.0):
|
||||
self.chunk_size = max(8, int(chunk_size))
|
||||
self.chunk_world_size = max(8.0, float(chunk_world_size))
|
||||
self._reset_state()
|
||||
|
||||
def _reset_state(self):
|
||||
self.name_to_ids = {}
|
||||
self.id_to_name = {}
|
||||
self.key_to_node = {}
|
||||
self.node_list = []
|
||||
self.display_names = {}
|
||||
self.global_transforms = [] # Original transforms (for center/position)
|
||||
|
||||
self.id_to_chunk = {} # global_id -> (chunk_key, local_idx)
|
||||
self.chunks = {} # chunk_key -> dict with 'node' key
|
||||
|
||||
# Vertex index: local_id -> list of (geom_node_np, geom_idx, [row_indices])
|
||||
self.vertex_index = {}
|
||||
|
||||
# Original vertex positions: local_id -> list of (Vec3,) matching row order
|
||||
self.original_positions = {}
|
||||
|
||||
# Current position offsets: local_id -> Vec3 delta
|
||||
self.global_transforms = []
|
||||
self.position_offsets = {}
|
||||
self.vertex_index = {}
|
||||
self.original_positions = {}
|
||||
self.local_to_global_id = {}
|
||||
self.local_transform_state = {}
|
||||
self.local_transform_base_positions = {}
|
||||
self.pick_vertex_index = {}
|
||||
self.virtual_tree = None
|
||||
self.virtual_tree_meta = None
|
||||
|
||||
|
||||
self.model = None
|
||||
self.pick_model = None
|
||||
self.chunk_node = None # Single chunk node
|
||||
self._source_model_name = ""
|
||||
self._source_model_stem = ""
|
||||
self.id_to_chunk = {} # global_id -> chunk_id
|
||||
self.id_to_object_np = {} # global_id -> dynamic object nodepath
|
||||
self.id_to_pick_np = {} # global_id -> pick-scene nodepath
|
||||
|
||||
# chunk_id -> {
|
||||
# "dynamic_np": NodePath,
|
||||
# "static_np": NodePath or None,
|
||||
# "members": [global_id],
|
||||
# "dirty": bool,
|
||||
# "dynamic_enabled": bool
|
||||
# }
|
||||
self.chunks = {}
|
||||
self.active_chunks = set()
|
||||
self._next_chunk_id = 0
|
||||
# spatial cell key -> [chunk_id, ...]
|
||||
self._cell_to_chunks = {}
|
||||
|
||||
# UI hierarchy metadata (matches source model parent/child structure)
|
||||
self.tree_root_key = None
|
||||
self.tree_nodes = {}
|
||||
self._path_to_tree_key = {}
|
||||
|
||||
def _register_tree_node(self, key, display_name, parent_key):
|
||||
self.tree_nodes[key] = {
|
||||
"name": display_name,
|
||||
"parent": parent_key,
|
||||
"children": [],
|
||||
"local_ids": [],
|
||||
}
|
||||
self.display_names[key] = display_name
|
||||
self.name_to_ids[key] = []
|
||||
if parent_key is not None and parent_key in self.tree_nodes:
|
||||
self.tree_nodes[parent_key]["children"].append(key)
|
||||
|
||||
def _build_scene_tree(self, root_np):
|
||||
"""Capture source model hierarchy for UI (independent from render batching)."""
|
||||
self.tree_root_key = "0"
|
||||
|
||||
def walk(np, parent_key, key):
|
||||
display_name = np.get_name() or "Unnamed"
|
||||
self._register_tree_node(key, display_name, parent_key)
|
||||
self._path_to_tree_key[str(np)] = key
|
||||
|
||||
children = list(np.get_children())
|
||||
for i, child in enumerate(children):
|
||||
walk(child, key, f"{key}/{i}")
|
||||
|
||||
walk(root_np, None, self.tree_root_key)
|
||||
|
||||
def _get_model_world_mat(self):
|
||||
"""Return current model net transform matrix (to top/root)."""
|
||||
if not self.model:
|
||||
return LMatrix4f.ident_mat()
|
||||
try:
|
||||
if self.model.isEmpty():
|
||||
return LMatrix4f.ident_mat()
|
||||
except Exception:
|
||||
try:
|
||||
if self.model.is_empty():
|
||||
return LMatrix4f.ident_mat()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
return LMatrix4f(self.model.getNetTransform().getMat())
|
||||
except Exception:
|
||||
try:
|
||||
# snake_case fallback in newer Panda3D bindings
|
||||
return LMatrix4f(self.model.get_net_transform().get_mat())
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
top = self.model.getTop()
|
||||
if top and not top.isEmpty():
|
||||
return LMatrix4f(self.model.getMat(top))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
return LMatrix4f(self.model.getMat())
|
||||
except Exception:
|
||||
return LMatrix4f.ident_mat()
|
||||
|
||||
def get_model_world_mat(self):
|
||||
"""Public accessor for current model net transform matrix."""
|
||||
return self._get_model_world_mat()
|
||||
|
||||
def _local_point_to_world(self, local_pos):
|
||||
"""Convert a local-space point to world-space based on model net transform."""
|
||||
mat = self._get_model_world_mat()
|
||||
p = Point3(float(local_pos.x), float(local_pos.y), float(local_pos.z))
|
||||
wp = mat.xformPoint(p)
|
||||
return Vec3(wp.x, wp.y, wp.z)
|
||||
|
||||
def _world_vec_to_local(self, world_vec):
|
||||
"""Convert a world-space vector to model-local space."""
|
||||
mat = self._get_model_world_mat()
|
||||
inv = LMatrix4f(mat)
|
||||
try:
|
||||
inv.invertInPlace()
|
||||
except Exception:
|
||||
try:
|
||||
inv.invert_in_place()
|
||||
except Exception:
|
||||
return Vec3(world_vec)
|
||||
v = Vec3(world_vec)
|
||||
lv = inv.xformVec(v)
|
||||
return Vec3(lv.x, lv.y, lv.z)
|
||||
|
||||
def world_vector_to_model_local(self, world_vec):
|
||||
"""Public converter from world delta vector to model-local delta vector."""
|
||||
return self._world_vec_to_local(world_vec)
|
||||
|
||||
def get_model_world_quat(self):
|
||||
"""Return current model world quaternion."""
|
||||
if not self.model:
|
||||
return Quat.identQuat()
|
||||
try:
|
||||
if self.model.isEmpty():
|
||||
return Quat.identQuat()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
top = self.model.getTop()
|
||||
if top and not top.isEmpty():
|
||||
return Quat(self.model.getQuat(top))
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
return Quat(self.model.getQuat())
|
||||
except Exception:
|
||||
return Quat.identQuat()
|
||||
|
||||
def world_quat_delta_to_model_local(self, delta_quat_world):
|
||||
"""
|
||||
Convert world-space delta quaternion to model-local delta quaternion.
|
||||
local = inv(model_world_rot) * world_delta * model_world_rot
|
||||
"""
|
||||
if delta_quat_world is None:
|
||||
return Quat.identQuat()
|
||||
model_q = self.get_model_world_quat()
|
||||
inv_model_q = Quat(model_q)
|
||||
inv_model_q.invertInPlace()
|
||||
local_q = inv_model_q * Quat(delta_quat_world) * model_q
|
||||
local_q.normalize()
|
||||
return local_q
|
||||
|
||||
def _build_original_hierarchy_key(self, np, model_root):
|
||||
"""Capture hierarchy path before flatten/reparent."""
|
||||
@ -60,6 +202,71 @@ class ObjectController:
|
||||
if not parts:
|
||||
return np.get_name() or "Unnamed"
|
||||
return "/".join(parts)
|
||||
def _aggregate_tree_ids(self, key):
|
||||
node = self.tree_nodes[key]
|
||||
agg_ids = list(node["local_ids"])
|
||||
for child_key in node["children"]:
|
||||
agg_ids.extend(self._aggregate_tree_ids(child_key))
|
||||
self.name_to_ids[key] = agg_ids
|
||||
return agg_ids
|
||||
|
||||
def _build_tree_preorder(self, key, out):
|
||||
out.append(key)
|
||||
for child_key in self.tree_nodes[key]["children"]:
|
||||
self._build_tree_preorder(child_key, out)
|
||||
|
||||
def should_hide_tree_node(self, key):
|
||||
"""
|
||||
Hide a redundant wrapper node directly below the file root, e.g. ROOT.
|
||||
This keeps `model.glb` as the visible root in the UI.
|
||||
"""
|
||||
node = self.tree_nodes.get(key)
|
||||
if not node:
|
||||
return False
|
||||
if node["parent"] != self.tree_root_key:
|
||||
return False
|
||||
|
||||
name = (node["name"] or "").strip().lower()
|
||||
if name != "root":
|
||||
return False
|
||||
|
||||
# Keep node visible if it actually carries direct geoms.
|
||||
if node["local_ids"]:
|
||||
return False
|
||||
|
||||
return len(node["children"]) > 0
|
||||
|
||||
def _encode_id_color(self, vdata, object_id):
|
||||
if not vdata.has_column("color"):
|
||||
new_fmt = vdata.get_format().get_union_format(GeomVertexFormat.get_v3c4())
|
||||
vdata.set_format(new_fmt)
|
||||
|
||||
low = object_id & 0xFF
|
||||
high = (object_id >> 8) & 0xFF
|
||||
r = low / 255.0
|
||||
g = high / 255.0
|
||||
|
||||
writer = GeomVertexWriter(vdata, InternalName.make("color"))
|
||||
for row in range(vdata.get_num_rows()):
|
||||
writer.set_row(row)
|
||||
writer.set_data4f(r, g, 0.0, 1.0)
|
||||
|
||||
def _ensure_chunk(self, root_np, chunk_id):
|
||||
if chunk_id in self.chunks:
|
||||
return self.chunks[chunk_id]
|
||||
|
||||
dynamic_np = root_np.attach_new_node(f"chunk_{chunk_id:04d}_dynamic")
|
||||
dynamic_np.stash()
|
||||
|
||||
chunk_data = {
|
||||
"dynamic_np": dynamic_np,
|
||||
"static_np": None,
|
||||
"members": [],
|
||||
"dirty": False,
|
||||
"dynamic_enabled": False,
|
||||
}
|
||||
self.chunks[chunk_id] = chunk_data
|
||||
return chunk_data
|
||||
|
||||
def _is_wrapper_segment(self, segment):
|
||||
s = (segment or "").strip().lower()
|
||||
@ -99,20 +306,21 @@ class ObjectController:
|
||||
self.local_to_global_id = {}
|
||||
self.local_transform_state = {}
|
||||
self.local_transform_base_positions = {}
|
||||
self.pick_vertex_index = {}
|
||||
self.virtual_tree = None
|
||||
self.virtual_tree_meta = None
|
||||
self.pick_model = None
|
||||
model_name = (model.get_name() or "").strip()
|
||||
self._source_model_name = model_name.lower()
|
||||
self._source_model_stem = model_name.rsplit(".", 1)[0].lower() if "." in model_name else model_name.lower()
|
||||
|
||||
|
||||
global_id_counter = 0
|
||||
chunk_key = model.get_name() or "default"
|
||||
|
||||
|
||||
# No chunk wrapper — flatten directly on model (same as load_jyc_flatten.py)
|
||||
self.chunk_node = model
|
||||
self.chunks[chunk_key] = {'node': model, 'base_id': 0}
|
||||
|
||||
|
||||
# Cache original hierarchy path BEFORE flatten/reparent.
|
||||
original_keys = {}
|
||||
for np in geom_nodes:
|
||||
@ -121,12 +329,12 @@ class ObjectController:
|
||||
# Flatten hierarchy
|
||||
for np in geom_nodes:
|
||||
np.wrt_reparent_to(model)
|
||||
|
||||
|
||||
local_idx = 0
|
||||
|
||||
|
||||
for np in geom_nodes:
|
||||
gnode = np.node()
|
||||
|
||||
|
||||
if gnode.get_num_parents() > 1:
|
||||
parent = np.get_parent()
|
||||
if not parent.is_empty():
|
||||
@ -134,57 +342,57 @@ class ObjectController:
|
||||
np.detach_node()
|
||||
np = new_np
|
||||
gnode = np.node()
|
||||
|
||||
|
||||
unique_key = original_keys.get(id(np), str(np))
|
||||
display_name = np.get_name() or f"Object_{global_id_counter}"
|
||||
|
||||
|
||||
if unique_key not in self.name_to_ids:
|
||||
self.name_to_ids[unique_key] = []
|
||||
self.key_to_node[unique_key] = np
|
||||
self.node_list.append(unique_key)
|
||||
self.display_names[unique_key] = display_name
|
||||
|
||||
|
||||
# Save original transform
|
||||
mat_double = np.get_mat()
|
||||
original_transform = LMatrix4f(mat_double)
|
||||
|
||||
|
||||
for i in range(gnode.get_num_geoms()):
|
||||
geom = gnode.modify_geom(i)
|
||||
vdata = geom.modify_vertex_data()
|
||||
|
||||
|
||||
if not vdata.has_column("color"):
|
||||
new_format = vdata.get_format().get_union_format(GeomVertexFormat.get_v3c4())
|
||||
vdata.set_format(new_format)
|
||||
|
||||
|
||||
# Encode Local ID in R/G
|
||||
low = local_idx % 256
|
||||
high = local_idx // 256
|
||||
r = low / 255.0
|
||||
g = high / 255.0
|
||||
|
||||
|
||||
writer = GeomVertexWriter(vdata, InternalName.make("color"))
|
||||
for row in range(vdata.get_num_rows()):
|
||||
writer.set_row(row)
|
||||
writer.set_data4f(r, g, 0.0, 1.0)
|
||||
|
||||
|
||||
self.global_transforms.append(original_transform)
|
||||
self.id_to_chunk[global_id_counter] = (chunk_key, local_idx)
|
||||
self.name_to_ids[unique_key].append(global_id_counter)
|
||||
self.id_to_name[global_id_counter] = unique_key
|
||||
self.local_to_global_id[local_idx] = global_id_counter
|
||||
self.position_offsets[local_idx] = Vec3(0, 0, 0)
|
||||
|
||||
|
||||
global_id_counter += 1
|
||||
local_idx += 1
|
||||
|
||||
|
||||
# DO NOT reset transform — keep world-space positions
|
||||
|
||||
|
||||
# Flatten directly on model — NO set_final, allows per-geom frustum culling
|
||||
model.flatten_strong()
|
||||
|
||||
|
||||
t1 = time.time()
|
||||
print(f"[控制器] Flatten took {(t1-t0)*1000:.0f}ms")
|
||||
|
||||
|
||||
# Build vertex index AFTER flatten
|
||||
self._build_vertex_index(model)
|
||||
self._init_local_transform_state()
|
||||
@ -192,6 +400,7 @@ class ObjectController:
|
||||
|
||||
# Keep ID colors only in picking clone to avoid affecting visible shading.
|
||||
self.pick_model = model.copy_to(NodePath("ssbo_pick_root"))
|
||||
self._build_pick_vertex_index(self.pick_model)
|
||||
self._set_uniform_vertex_color(model, 1.0, 1.0, 1.0, 1.0)
|
||||
|
||||
t2 = time.time()
|
||||
@ -219,6 +428,114 @@ class ObjectController:
|
||||
writer.set_row(row)
|
||||
writer.set_data4f(r, g, b, a)
|
||||
|
||||
def _build_tree_preorder(self, key, out):
|
||||
out.append(key)
|
||||
for child_key in self.tree_nodes[key]["children"]:
|
||||
self._build_tree_preorder(child_key, out)
|
||||
|
||||
def should_hide_tree_node(self, key):
|
||||
"""
|
||||
Hide a redundant wrapper node directly below the file root, e.g. ROOT.
|
||||
This keeps `model.glb` as the visible root in the UI.
|
||||
"""
|
||||
node = self.tree_nodes.get(key)
|
||||
if not node:
|
||||
return False
|
||||
if node["parent"] != self.tree_root_key:
|
||||
return False
|
||||
|
||||
name = (node["name"] or "").strip().lower()
|
||||
if name != "root":
|
||||
return False
|
||||
|
||||
# Keep node visible if it actually carries direct geoms.
|
||||
if node["local_ids"]:
|
||||
return False
|
||||
|
||||
return len(node["children"]) > 0
|
||||
|
||||
def _encode_id_color(self, vdata, object_id):
|
||||
if not vdata.has_column("color"):
|
||||
new_fmt = vdata.get_format().get_union_format(GeomVertexFormat.get_v3c4())
|
||||
vdata.set_format(new_fmt)
|
||||
|
||||
low = object_id & 0xFF
|
||||
high = (object_id >> 8) & 0xFF
|
||||
r = low / 255.0
|
||||
g = high / 255.0
|
||||
|
||||
writer = GeomVertexWriter(vdata, InternalName.make("color"))
|
||||
for row in range(vdata.get_num_rows()):
|
||||
writer.set_row(row)
|
||||
writer.set_data4f(r, g, 0.0, 1.0)
|
||||
|
||||
def _ensure_chunk(self, root_np, chunk_id):
|
||||
if chunk_id in self.chunks:
|
||||
return self.chunks[chunk_id]
|
||||
|
||||
dynamic_np = root_np.attach_new_node(f"chunk_{chunk_id:04d}_dynamic")
|
||||
dynamic_np.stash()
|
||||
|
||||
chunk_data = {
|
||||
"dynamic_np": dynamic_np,
|
||||
"static_np": None,
|
||||
"members": [],
|
||||
"dirty": False,
|
||||
"dynamic_enabled": False,
|
||||
}
|
||||
self.chunks[chunk_id] = chunk_data
|
||||
return chunk_data
|
||||
|
||||
def _get_cell_key_from_pos(self, pos):
|
||||
inv = 1.0 / self.chunk_world_size
|
||||
return (
|
||||
int(math.floor(pos.x * inv)),
|
||||
int(math.floor(pos.y * inv)),
|
||||
int(math.floor(pos.z * inv)),
|
||||
)
|
||||
|
||||
def _allocate_spatial_chunk(self, root_np, world_pos):
|
||||
"""
|
||||
Allocate object into a spatially-local chunk for better frustum culling.
|
||||
Objects in the same world cell are grouped together, and overflow creates
|
||||
another chunk for that same cell.
|
||||
"""
|
||||
cell_key = self._get_cell_key_from_pos(world_pos)
|
||||
chunk_ids = self._cell_to_chunks.setdefault(cell_key, [])
|
||||
|
||||
for chunk_id in chunk_ids:
|
||||
chunk = self.chunks.get(chunk_id)
|
||||
if chunk and len(chunk["members"]) < self.chunk_size:
|
||||
return chunk_id, chunk
|
||||
|
||||
chunk_id = self._next_chunk_id
|
||||
self._next_chunk_id += 1
|
||||
chunk_ids.append(chunk_id)
|
||||
return chunk_id, self._ensure_chunk(root_np, chunk_id)
|
||||
|
||||
def _rebuild_static_chunk(self, chunk_id):
|
||||
chunk = self.chunks.get(chunk_id)
|
||||
if not chunk:
|
||||
return
|
||||
|
||||
old_static = chunk.get("static_np")
|
||||
if old_static and not old_static.is_empty():
|
||||
old_static.remove_node()
|
||||
|
||||
static_np = chunk["dynamic_np"].copy_to(self.model)
|
||||
static_np.set_name(f"chunk_{chunk_id:04d}_static")
|
||||
static_np.unstash()
|
||||
static_np.flatten_strong()
|
||||
|
||||
chunk["static_np"] = static_np
|
||||
chunk["dirty"] = False
|
||||
|
||||
# Keep visibility coherent with current mode after rebuild.
|
||||
if chunk["dynamic_enabled"]:
|
||||
static_np.stash()
|
||||
else:
|
||||
static_np.unstash()
|
||||
|
||||
def build_virtual_hierarchy(self):
|
||||
"""Build a readonly virtual tree from node_list path keys."""
|
||||
root = {
|
||||
@ -403,6 +720,228 @@ class ObjectController:
|
||||
self.vertex_index[uid].append((gn_np, gi, rows))
|
||||
self.original_positions[uid].append(pos.copy())
|
||||
|
||||
def _set_chunk_dynamic(self, chunk_id, enabled):
|
||||
chunk = self.chunks.get(chunk_id)
|
||||
if not chunk:
|
||||
return
|
||||
|
||||
if enabled:
|
||||
if chunk["dynamic_enabled"]:
|
||||
return
|
||||
chunk["dynamic_np"].unstash()
|
||||
if chunk["static_np"] and not chunk["static_np"].is_empty():
|
||||
chunk["static_np"].stash()
|
||||
chunk["dynamic_enabled"] = True
|
||||
self.active_chunks.add(chunk_id)
|
||||
return
|
||||
|
||||
if not chunk["dynamic_enabled"]:
|
||||
return
|
||||
if chunk["static_np"] and not chunk["static_np"].is_empty():
|
||||
chunk["static_np"].unstash()
|
||||
chunk["dynamic_np"].stash()
|
||||
chunk["dynamic_enabled"] = False
|
||||
self.active_chunks.discard(chunk_id)
|
||||
|
||||
def _resolve_chunk_and_local_idx(self, global_id):
|
||||
"""
|
||||
Compatibility helper for merged branches:
|
||||
- legacy: id_to_chunk[gid] -> (chunk_id, local_idx)
|
||||
- current: id_to_chunk[gid] -> chunk_id (local_idx defaults to gid)
|
||||
"""
|
||||
mapping = self.id_to_chunk.get(global_id)
|
||||
if mapping is None:
|
||||
return None, None
|
||||
if isinstance(mapping, (tuple, list)):
|
||||
if not mapping:
|
||||
return None, None
|
||||
chunk_id = mapping[0]
|
||||
local_idx = mapping[1] if len(mapping) > 1 else global_id
|
||||
return chunk_id, local_idx
|
||||
return mapping, global_id
|
||||
|
||||
def set_active_ids(self, active_ids):
|
||||
"""切换编辑激活集合,仅保留 active_ids 对应 chunk 为动态模式。"""
|
||||
target_chunks = set()
|
||||
for obj_id in active_ids:
|
||||
chunk_id, _ = self._resolve_chunk_and_local_idx(obj_id)
|
||||
if chunk_id is not None:
|
||||
target_chunks.add(chunk_id)
|
||||
|
||||
# Demote no-longer-active chunks. Dirty chunks are re-baked before demotion.
|
||||
for chunk_id in list(self.active_chunks):
|
||||
if chunk_id in target_chunks:
|
||||
continue
|
||||
if self.chunks[chunk_id]["dirty"]:
|
||||
self._rebuild_static_chunk(chunk_id)
|
||||
self._set_chunk_dynamic(chunk_id, False)
|
||||
|
||||
# Promote target chunks.
|
||||
for chunk_id in target_chunks:
|
||||
self._set_chunk_dynamic(chunk_id, True)
|
||||
|
||||
def bake_ids_and_collect(self, model):
|
||||
"""
|
||||
构建混合架构:
|
||||
1) 把每个 geom 拆成可独立编辑的动态对象
|
||||
2) 按 chunk 生成 flatten 后的静态副本
|
||||
"""
|
||||
t0 = time.time()
|
||||
self._reset_state()
|
||||
|
||||
geom_nodes = list(model.find_all_matches("**/+GeomNode"))
|
||||
print(f"[控制器] 找到 {len(geom_nodes)} 个 GeomNode")
|
||||
|
||||
# Build hierarchy metadata first so UI can mirror source model tree.
|
||||
self._build_scene_tree(model)
|
||||
|
||||
root_name = model.get_name() or "scene"
|
||||
scene_root = NodePath(root_name)
|
||||
pick_root = NodePath(root_name + "_pick")
|
||||
self.model = scene_root
|
||||
self.pick_model = pick_root
|
||||
|
||||
global_id = 0
|
||||
for np in geom_nodes:
|
||||
gnode = np.node()
|
||||
owner_key = self._path_to_tree_key.get(str(np), self.tree_root_key)
|
||||
|
||||
world_mat = LMatrix4f(np.get_mat(model))
|
||||
|
||||
for gi in range(gnode.get_num_geoms()):
|
||||
# Render geometry stays untouched (keep original material/color behavior).
|
||||
render_geom = gnode.get_geom(gi).make_copy()
|
||||
render_gnode = GeomNode(f"obj_{global_id}")
|
||||
render_gnode.add_geom(render_geom, gnode.get_geom_state(gi))
|
||||
|
||||
# Picking geometry gets encoded ID in vertex color.
|
||||
pick_geom = gnode.get_geom(gi).make_copy()
|
||||
pick_vdata = pick_geom.modify_vertex_data()
|
||||
self._encode_id_color(pick_vdata, global_id)
|
||||
pick_gnode = GeomNode(f"pick_{global_id}")
|
||||
pick_gnode.add_geom(pick_geom, gnode.get_geom_state(gi))
|
||||
|
||||
world_pos = world_mat.get_row3(3)
|
||||
chunk_id, chunk = self._allocate_spatial_chunk(scene_root, world_pos)
|
||||
|
||||
obj_np = chunk["dynamic_np"].attach_new_node(render_gnode)
|
||||
obj_np.set_mat(world_mat)
|
||||
pick_np = pick_root.attach_new_node(pick_gnode)
|
||||
pick_np.set_mat(world_mat)
|
||||
|
||||
chunk["members"].append(global_id)
|
||||
self.id_to_chunk[global_id] = chunk_id
|
||||
self.id_to_object_np[global_id] = obj_np
|
||||
self.id_to_pick_np[global_id] = pick_np
|
||||
self.tree_nodes[owner_key]["local_ids"].append(global_id)
|
||||
self.id_to_name[global_id] = owner_key
|
||||
self.global_transforms.append(LMatrix4f(world_mat))
|
||||
self.position_offsets[global_id] = Vec3(0, 0, 0)
|
||||
global_id += 1
|
||||
|
||||
t1 = time.time()
|
||||
print(f"[控制器] Dynamic object build took {(t1 - t0) * 1000:.0f}ms")
|
||||
|
||||
for chunk_id in sorted(self.chunks):
|
||||
self._rebuild_static_chunk(chunk_id)
|
||||
self._set_chunk_dynamic(chunk_id, False)
|
||||
|
||||
t2 = time.time()
|
||||
print(f"[控制器] Static chunk flatten took {(t2 - t1) * 1000:.0f}ms")
|
||||
print(f"[控制器] Built {len(self.chunks)} chunks, {global_id} objects")
|
||||
print(f"[控制器] Spatial chunking: cell={self.chunk_world_size:.1f}, max_members={self.chunk_size}")
|
||||
|
||||
# Fill per-node aggregate IDs and build deterministic preorder list for UI.
|
||||
self._aggregate_tree_ids(self.tree_root_key)
|
||||
self.node_list = []
|
||||
self._build_tree_preorder(self.tree_root_key, self.node_list)
|
||||
|
||||
model.remove_node()
|
||||
return global_id
|
||||
|
||||
def _build_pick_vertex_index(self, pick_root):
|
||||
"""
|
||||
Build local_id -> [(geom_node_np, geom_idx, row_indices_array)] for pick model.
|
||||
This keeps GPU-picking geometry writable in sync with visible geometry edits.
|
||||
"""
|
||||
import numpy as np
|
||||
|
||||
self.pick_vertex_index = {}
|
||||
if not pick_root:
|
||||
return
|
||||
|
||||
for gn_np in pick_root.find_all_matches("**/+GeomNode"):
|
||||
gnode = gn_np.node()
|
||||
for gi in range(gnode.get_num_geoms()):
|
||||
geom = gnode.get_geom(gi)
|
||||
vdata = geom.get_vertex_data()
|
||||
num_rows = vdata.get_num_rows()
|
||||
if num_rows == 0:
|
||||
continue
|
||||
|
||||
fmt = vdata.get_format()
|
||||
color_col = fmt.get_column(InternalName.make("color"))
|
||||
if color_col is None:
|
||||
continue
|
||||
|
||||
color_array_idx = fmt.get_array_with(InternalName.make("color"))
|
||||
color_start = color_col.get_start()
|
||||
color_array_format = fmt.get_array(color_array_idx)
|
||||
color_stride = color_array_format.get_stride()
|
||||
|
||||
color_handle = vdata.get_array(color_array_idx).get_handle()
|
||||
color_raw = bytes(color_handle.get_data())
|
||||
color_buf = np.frombuffer(color_raw, dtype=np.uint8).reshape(num_rows, color_stride)
|
||||
|
||||
num_components = color_col.get_num_components()
|
||||
component_bytes = color_col.get_component_bytes()
|
||||
|
||||
if component_bytes == 4:
|
||||
color_data = np.ndarray(
|
||||
(num_rows, num_components),
|
||||
dtype=np.float32,
|
||||
buffer=color_buf[:, color_start:color_start + num_components * 4].tobytes()
|
||||
)
|
||||
r_vals = (color_data[:, 0] * 255.0 + 0.5).astype(np.int32)
|
||||
g_vals = (color_data[:, 1] * 255.0 + 0.5).astype(np.int32)
|
||||
elif component_bytes == 1:
|
||||
color_bytes = color_buf[:, color_start:color_start + num_components].copy()
|
||||
r_vals = color_bytes[:, 0].astype(np.int32)
|
||||
g_vals = color_bytes[:, 1].astype(np.int32)
|
||||
else:
|
||||
continue
|
||||
|
||||
local_ids = r_vals + (g_vals << 8)
|
||||
sort_idx = np.argsort(local_ids)
|
||||
sorted_ids = local_ids[sort_idx]
|
||||
boundaries = np.where(np.diff(sorted_ids) != 0)[0] + 1
|
||||
id_groups = np.split(sort_idx, boundaries)
|
||||
group_ids = sorted_ids[np.concatenate([[0], boundaries])]
|
||||
|
||||
for k in range(len(group_ids)):
|
||||
uid = int(group_ids[k])
|
||||
rows = id_groups[k]
|
||||
if uid not in self.pick_vertex_index:
|
||||
self.pick_vertex_index[uid] = []
|
||||
self.pick_vertex_index[uid].append((gn_np, gi, rows))
|
||||
|
||||
def _apply_vertices_to_pick(self, local_idx, entry_idx, new_pos):
|
||||
"""Mirror one transformed vertex group to pick-model geometry."""
|
||||
pick_entries = self.pick_vertex_index.get(local_idx)
|
||||
if not pick_entries or entry_idx >= len(pick_entries):
|
||||
return
|
||||
|
||||
pick_gn_np, pick_gi, pick_rows = pick_entries[entry_idx]
|
||||
gnode = pick_gn_np.node()
|
||||
geom = gnode.modify_geom(pick_gi)
|
||||
vdata = geom.modify_vertex_data()
|
||||
writer = GeomVertexWriter(vdata, "vertex")
|
||||
|
||||
max_rows = min(len(pick_rows), len(new_pos))
|
||||
for j in range(max_rows):
|
||||
writer.set_row(int(pick_rows[j]))
|
||||
writer.set_data3f(float(new_pos[j, 0]), float(new_pos[j, 1]), float(new_pos[j, 2]))
|
||||
|
||||
def _init_local_transform_state(self):
|
||||
"""Initialize transform state for each local_idx after vertex index is ready."""
|
||||
self.local_transform_state = {}
|
||||
@ -424,10 +963,9 @@ class ObjectController:
|
||||
return local_indices
|
||||
seen = set()
|
||||
for global_id in global_ids:
|
||||
mapping = self.id_to_chunk.get(global_id)
|
||||
if not mapping:
|
||||
_, local_idx = self._resolve_chunk_and_local_idx(global_id)
|
||||
if local_idx is None:
|
||||
continue
|
||||
_, local_idx = mapping
|
||||
if local_idx in seen:
|
||||
continue
|
||||
if local_idx not in self.vertex_index:
|
||||
@ -437,7 +975,7 @@ class ObjectController:
|
||||
return local_indices
|
||||
|
||||
def get_local_pivot(self, local_idx):
|
||||
"""Get pivot for one local object (world-space center)."""
|
||||
"""Get pivot for one local object (model-local center)."""
|
||||
global_id = self.local_to_global_id.get(local_idx)
|
||||
if global_id is None:
|
||||
return Vec3(0, 0, 0)
|
||||
@ -457,7 +995,8 @@ class ObjectController:
|
||||
valid += 1
|
||||
if valid == 0:
|
||||
return Vec3(0, 0, 0)
|
||||
return acc / float(valid)
|
||||
center_local = acc / float(valid)
|
||||
return self._local_point_to_world(center_local)
|
||||
|
||||
def begin_transform_session(self, local_indices):
|
||||
"""Create immutable baseline snapshot for one gizmo drag session."""
|
||||
@ -543,6 +1082,7 @@ class ObjectController:
|
||||
for j in range(len(rows)):
|
||||
writer.set_row(int(rows[j]))
|
||||
writer.set_data3f(float(new_pos[j, 0]), float(new_pos[j, 1]), float(new_pos[j, 2]))
|
||||
self._apply_vertices_to_pick(local_idx, i, new_pos)
|
||||
|
||||
def _quat_to_np_mat3(self, quat):
|
||||
"""Convert Panda3D Quat to 3x3 numpy rotation matrix."""
|
||||
@ -571,7 +1111,7 @@ class ObjectController:
|
||||
], dtype=np.float32)
|
||||
|
||||
def create_ssbo(self):
|
||||
"""No SSBO needed — using RP default rendering."""
|
||||
"""No SSBO needed in hybrid mode."""
|
||||
return None
|
||||
|
||||
def move_object(self, global_id, delta):
|
||||
@ -584,10 +1124,28 @@ class ObjectController:
|
||||
|
||||
if global_id not in self.id_to_chunk:
|
||||
return
|
||||
|
||||
_, local_idx = self.id_to_chunk[global_id]
|
||||
|
||||
if local_idx not in self.vertex_index:
|
||||
|
||||
chunk_id, local_idx = self._resolve_chunk_and_local_idx(global_id)
|
||||
if local_idx is None:
|
||||
return
|
||||
|
||||
# Hybrid chunk mode (current) may move NodePaths directly without
|
||||
# vertex_index/original_positions populated.
|
||||
if local_idx not in self.vertex_index or local_idx not in self.original_positions:
|
||||
obj_np = self.id_to_object_np.get(global_id)
|
||||
if not obj_np or obj_np.is_empty():
|
||||
return
|
||||
next_pos = obj_np.get_pos() + delta
|
||||
if hasattr(obj_np, "set_fluid_pos"):
|
||||
obj_np.set_fluid_pos(next_pos)
|
||||
else:
|
||||
obj_np.set_pos(next_pos)
|
||||
pick_np = self.id_to_pick_np.get(global_id)
|
||||
if pick_np and not pick_np.is_empty():
|
||||
pick_np.set_mat(self.model, obj_np.get_mat(self.model))
|
||||
if chunk_id is not None and chunk_id in self.chunks:
|
||||
self.chunks[chunk_id]["dirty"] = True
|
||||
self.position_offsets[local_idx] = self.position_offsets.get(local_idx, Vec3(0)) + delta
|
||||
return
|
||||
|
||||
# Accumulate offset
|
||||
@ -611,28 +1169,35 @@ class ObjectController:
|
||||
for j in range(len(rows)):
|
||||
writer.set_row(int(rows[j]))
|
||||
writer.set_data3f(float(new_pos[j, 0]), float(new_pos[j, 1]), float(new_pos[j, 2]))
|
||||
self._apply_vertices_to_pick(local_idx, i, new_pos)
|
||||
|
||||
def get_world_pos(self, global_id):
|
||||
"""Get current world position of an object."""
|
||||
if global_id not in self.id_to_chunk:
|
||||
if not self.model:
|
||||
return Vec3(0, 0, 0)
|
||||
|
||||
obj_np = self.id_to_object_np.get(global_id)
|
||||
if obj_np and not obj_np.is_empty():
|
||||
p = obj_np.get_pos(self.model)
|
||||
return self._local_point_to_world(Vec3(p))
|
||||
|
||||
_, local_idx = self._resolve_chunk_and_local_idx(global_id)
|
||||
if local_idx is None:
|
||||
return Vec3(0, 0, 0)
|
||||
_, local_idx = self.id_to_chunk[global_id]
|
||||
|
||||
original_mat = self.global_transforms[global_id]
|
||||
original_pos = original_mat.get_row3(3)
|
||||
offset = self.position_offsets.get(local_idx, Vec3(0))
|
||||
|
||||
return Vec3(original_pos) + offset
|
||||
|
||||
local_pos = Vec3(original_pos) + offset
|
||||
return self._local_point_to_world(local_pos)
|
||||
|
||||
def get_object_center(self, global_id):
|
||||
"""Get the original center position of an object (for rotation pivot)."""
|
||||
if global_id >= len(self.global_transforms):
|
||||
return Vec3(0, 0, 0)
|
||||
mat = self.global_transforms[global_id]
|
||||
return Vec3(mat.get_row3(3))
|
||||
|
||||
def get_transform(self, global_id):
|
||||
"""Get original transform."""
|
||||
if global_id >= len(self.global_transforms):
|
||||
return LMatrix4f.ident_mat()
|
||||
return self.global_transforms[global_id]
|
||||
|
||||
@ -3,31 +3,53 @@ import sys
|
||||
import os
|
||||
import struct
|
||||
import time
|
||||
import types
|
||||
from panda3d.core import (
|
||||
Filename, loadPrcFileData, GeomVertexFormat,
|
||||
GeomVertexWriter, InternalName, Shader, Texture, SamplerState,
|
||||
Vec3, Vec4, Point2, Point3, LMatrix4f, ShaderBuffer, GeomEnums, OmniBoundingVolume, Quat,
|
||||
Vec3, Vec4, Point2, Point3, LMatrix4f, ShaderBuffer, GeomEnums, OmniBoundingVolume,
|
||||
TransparencyAttrib, BoundingSphere, NodePath,
|
||||
GraphicsEngine, WindowProperties, FrameBufferProperties,
|
||||
GraphicsPipe, GraphicsOutput, Camera, DisplayRegion, OrthographicLens,
|
||||
BoundingBox
|
||||
BoundingBox, BitMask32
|
||||
)
|
||||
|
||||
import p3dimgui.backend as p3dimgui_backend
|
||||
import p3dimgui.shaders as p3dimgui_shaders
|
||||
# p3dimgui.backend first tries `from shaders import *`, which can be shadowed by
|
||||
# project folders named `shaders/` and leave VERT_SHADER/FRAG_SHADER undefined.
|
||||
# Seed a valid fallback module before importing p3dimgui.
|
||||
_shaders_mod = sys.modules.get("shaders")
|
||||
if not (_shaders_mod and hasattr(_shaders_mod, "VERT_SHADER") and hasattr(_shaders_mod, "FRAG_SHADER")):
|
||||
_shaders_mod = types.ModuleType("shaders")
|
||||
_shaders_mod.FRAG_SHADER = """
|
||||
#version 120
|
||||
varying vec2 texcoord;
|
||||
varying vec4 color;
|
||||
uniform sampler2D p3d_Texture0;
|
||||
void main() {
|
||||
gl_FragColor = color * texture2D(p3d_Texture0, texcoord);
|
||||
}
|
||||
"""
|
||||
_shaders_mod.VERT_SHADER = """
|
||||
#version 120
|
||||
attribute vec4 p3d_Vertex;
|
||||
attribute vec4 p3d_Color;
|
||||
varying vec2 texcoord;
|
||||
varying vec4 color;
|
||||
uniform mat4 p3d_ModelViewProjectionMatrix;
|
||||
void main() {
|
||||
texcoord = p3d_Vertex.zw;
|
||||
color = p3d_Color.bgra;
|
||||
gl_Position = p3d_ModelViewProjectionMatrix * vec4(p3d_Vertex.x, 0.0, -p3d_Vertex.y, 1.0);
|
||||
}
|
||||
"""
|
||||
sys.modules["shaders"] = _shaders_mod
|
||||
|
||||
from p3dimgui import ImGuiBackend
|
||||
from imgui_bundle import imgui
|
||||
from rpcore.effect import Effect
|
||||
|
||||
# Work around p3dimgui import-order issue where backend may import an unrelated
|
||||
# top-level "shaders" module and miss these globals.
|
||||
if not hasattr(p3dimgui_backend, "VERT_SHADER"):
|
||||
p3dimgui_backend.VERT_SHADER = p3dimgui_shaders.VERT_SHADER
|
||||
if not hasattr(p3dimgui_backend, "FRAG_SHADER"):
|
||||
p3dimgui_backend.FRAG_SHADER = p3dimgui_shaders.FRAG_SHADER
|
||||
|
||||
ImGuiBackend = p3dimgui_backend.ImGuiBackend
|
||||
|
||||
from .ssbo_controller import ObjectController
|
||||
from core.selection_outline import SelectionOutlineManager
|
||||
|
||||
class SSBOEditor:
|
||||
"""
|
||||
@ -44,12 +66,10 @@ class SSBOEditor:
|
||||
self.model = None
|
||||
self.ssbo = None
|
||||
self.font_path = font_path
|
||||
# Picking resources may be created later when a model is loaded.
|
||||
self.pick_buffer = None
|
||||
self.pick_texture = None
|
||||
self.pick_cam = None
|
||||
self.pick_cam_np = None
|
||||
self.pick_lens = None
|
||||
self._transform_gizmo = None
|
||||
self._ssbo_transform_active = False
|
||||
self._ssbo_selected_local_indices = []
|
||||
self._ssbo_gizmo_proxy = None
|
||||
|
||||
# Internal State
|
||||
self.selected_name = None
|
||||
@ -59,12 +79,23 @@ class SSBOEditor:
|
||||
self.filtered_nodes = []
|
||||
self.debug_mode = False
|
||||
self.keys = {}
|
||||
self._ssbo_transform_active = False
|
||||
self._ssbo_selected_local_indices = []
|
||||
self._ssbo_transform_snapshot = None
|
||||
self._ssbo_gizmo_proxy = None
|
||||
self._ssbo_proxy_start = {"pos": None, "quat": None, "scale": None}
|
||||
self._bound_transform_gizmo = None
|
||||
self.pick_mask = BitMask32.bit(29)
|
||||
self.pick_buffer = None
|
||||
self._empty_pick_scene = NodePath("ssbo_pick_empty")
|
||||
# Avoid heavy per-frame sync for huge group selections.
|
||||
self._pick_sync_bg_limit = 256
|
||||
self._last_group_sync_mat = None
|
||||
self._last_single_sync_gid = None
|
||||
self._last_single_sync_mat = None
|
||||
# Performance toggle: forcing shadow tasks every frame is expensive.
|
||||
# Keep it off by default so frustum/content reduction has clearer FPS impact.
|
||||
self.realtime_shadow_updates = False
|
||||
self._scheduler_tasks_original = None
|
||||
self._realtime_shadow_tasks_enabled = False
|
||||
self._outline_manager = getattr(self.base, "_selection_outline_manager", None)
|
||||
if self._outline_manager is None:
|
||||
self._outline_manager = SelectionOutlineManager(self.base)
|
||||
setattr(self.base, "_selection_outline_manager", self._outline_manager)
|
||||
|
||||
# Initialize ImGui Backend if not already present
|
||||
if not hasattr(self.base, 'imgui_backend'):
|
||||
@ -91,6 +122,61 @@ class SSBOEditor:
|
||||
if model_path:
|
||||
self.load_model(model_path)
|
||||
|
||||
def _capture_scheduler_tasks_snapshot(self):
|
||||
scheduler = getattr(self.rp, "task_scheduler", None)
|
||||
if not scheduler or not hasattr(scheduler, "_tasks"):
|
||||
return
|
||||
if self._scheduler_tasks_original is None:
|
||||
self._scheduler_tasks_original = [list(frame_tasks) for frame_tasks in scheduler._tasks]
|
||||
|
||||
def _enable_realtime_shadow_tasks(self):
|
||||
"""
|
||||
Force PSSM-related scheduled tasks to run every frame to avoid visible
|
||||
shadow lag/ghosting while editing moving objects.
|
||||
"""
|
||||
scheduler = getattr(self.rp, "task_scheduler", None)
|
||||
if not scheduler or not hasattr(scheduler, "_tasks"):
|
||||
return
|
||||
|
||||
self._capture_scheduler_tasks_snapshot()
|
||||
|
||||
required = {
|
||||
"pssm_scene_shadows",
|
||||
"pssm_distant_shadows",
|
||||
"pssm_convert_distant_to_esm",
|
||||
"pssm_blur_distant_vert",
|
||||
"pssm_blur_distant_horiz",
|
||||
}
|
||||
changed = False
|
||||
for frame_tasks in scheduler._tasks:
|
||||
for task_name in required:
|
||||
if task_name not in frame_tasks:
|
||||
frame_tasks.append(task_name)
|
||||
changed = True
|
||||
|
||||
if changed:
|
||||
print("[SSBOEditor] Realtime shadow tasks enabled (PSSM updates every frame).")
|
||||
self._realtime_shadow_tasks_enabled = True
|
||||
|
||||
def _disable_realtime_shadow_tasks(self):
|
||||
"""Restore scheduler layout captured before realtime shadow override."""
|
||||
scheduler = getattr(self.rp, "task_scheduler", None)
|
||||
if not scheduler or not hasattr(scheduler, "_tasks"):
|
||||
return
|
||||
if self._scheduler_tasks_original is None:
|
||||
self._realtime_shadow_tasks_enabled = False
|
||||
return
|
||||
scheduler._tasks[:] = [list(frame_tasks) for frame_tasks in self._scheduler_tasks_original]
|
||||
self._realtime_shadow_tasks_enabled = False
|
||||
|
||||
def set_realtime_shadow_updates(self, enabled):
|
||||
"""Public toggle for aggressive per-frame shadow updates."""
|
||||
self.realtime_shadow_updates = bool(enabled)
|
||||
if self.realtime_shadow_updates:
|
||||
self._enable_realtime_shadow_tasks()
|
||||
else:
|
||||
self._disable_realtime_shadow_tasks()
|
||||
|
||||
def load_font(self):
|
||||
"""Load custom font for ImGui"""
|
||||
io = imgui.get_io()
|
||||
@ -128,26 +214,31 @@ class SSBOEditor:
|
||||
io.fonts.add_font_default()
|
||||
|
||||
def load_model(self, model_path):
|
||||
"""Load and process a model — NO custom shader, uses RP default rendering."""
|
||||
"""Load and process a model using hybrid static/dynamic chunks."""
|
||||
print(f"[SSBOEditor] Loading model: {model_path}")
|
||||
fn = Filename.fromOsSpecific(model_path)
|
||||
self.model = self.base.loader.loadModel(fn)
|
||||
source_model = self.base.loader.loadModel(fn)
|
||||
model_name = os.path.basename(model_path)
|
||||
if model_name:
|
||||
source_model.set_name(model_name)
|
||||
|
||||
self.controller = ObjectController()
|
||||
count = self.controller.bake_ids_and_collect(self.model)
|
||||
self._ssbo_transform_active = False
|
||||
self._ssbo_selected_local_indices = []
|
||||
self._ssbo_transform_snapshot = None
|
||||
self._cleanup_ssbo_proxy()
|
||||
count = self.controller.bake_ids_and_collect(source_model)
|
||||
self.model = self.controller.model
|
||||
|
||||
self.model.reparent_to(self.base.render)
|
||||
|
||||
|
||||
# Keep this off by default for better overall FPS/scaling with visibility.
|
||||
self.set_realtime_shadow_updates(self.realtime_shadow_updates)
|
||||
|
||||
# NO rp.set_effect() — use RP default rendering for max FPS
|
||||
# NO SSBO creation — vertex positions are baked
|
||||
|
||||
# Setup GPU Picking (uses simple vertex-color shader)
|
||||
self.setup_gpu_picking()
|
||||
|
||||
# Keep pick clone aligned with source model transform.
|
||||
self._sync_pick_model_transform()
|
||||
|
||||
print(f"[SSBOEditor] Model loaded. Total objects: {count}")
|
||||
|
||||
# No custom effect needed — RP default rendering for maximum FPS
|
||||
@ -209,6 +300,7 @@ class SSBOEditor:
|
||||
self.pick_buffer.add_render_texture(self.pick_texture, GraphicsOutput.RTM_copy_ram)
|
||||
|
||||
self.pick_cam = Camera("pick_camera")
|
||||
self.pick_cam.set_camera_mask(self.pick_mask)
|
||||
self.pick_cam_np = self.base.cam.attach_new_node(self.pick_cam)
|
||||
self.pick_lens = self.base.camLens.make_copy()
|
||||
self.pick_cam.set_lens(self.pick_lens)
|
||||
@ -218,22 +310,21 @@ class SSBOEditor:
|
||||
|
||||
# Load pick shader
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
pick_vert = os.path.join(current_dir, "shaders", "pick_id.vert")
|
||||
pick_frag = os.path.join(current_dir, "shaders", "pick_id.frag")
|
||||
|
||||
pick_vert = Filename.fromOsSpecific(pick_vert).getFullpath()
|
||||
pick_frag = Filename.fromOsSpecific(pick_frag).getFullpath()
|
||||
pick_vert_path = os.path.join(current_dir, "shaders", "pick_id.vert")
|
||||
pick_frag_path = os.path.join(current_dir, "shaders", "pick_id.frag")
|
||||
|
||||
try:
|
||||
pick_shader = Shader.load(
|
||||
Shader.SL_GLSL,
|
||||
pick_vert,
|
||||
pick_frag
|
||||
)
|
||||
pick_scene = getattr(self.controller, "pick_model", None) if self.controller else None
|
||||
if pick_scene is None:
|
||||
pick_scene = self.model
|
||||
self.pick_cam.set_scene(pick_scene)
|
||||
# Read shader source directly from OS filesystem to avoid
|
||||
# Panda3D VFS case-mismatch issues on Windows.
|
||||
with open(pick_vert_path, 'r', encoding='utf-8') as f:
|
||||
vert_src = f.read().replace('\r', '')
|
||||
with open(pick_frag_path, 'r', encoding='utf-8') as f:
|
||||
frag_src = f.read().replace('\r', '')
|
||||
pick_shader = Shader.make(Shader.SL_GLSL, vert_src, frag_src)
|
||||
pick_scene = getattr(self.controller, "pick_model", None) or self.model
|
||||
if pick_scene and not pick_scene.is_empty():
|
||||
pick_scene.show(self.pick_mask)
|
||||
self.pick_cam.set_scene(pick_scene or self._empty_pick_scene)
|
||||
initial_state = NodePath("initial")
|
||||
initial_state.set_shader(pick_shader, 100)
|
||||
# Remove global SSBO input, Chunks have their own inputs
|
||||
@ -248,10 +339,81 @@ class SSBOEditor:
|
||||
self.pick_buffer.set_clear_color(Vec4(0, 0, 0, 0))
|
||||
self.pick_buffer.set_clear_color_active(True)
|
||||
|
||||
def _sync_pick_model_transform(self):
|
||||
"""Sync pick-scene clone to current source model world transform."""
|
||||
if not self.controller or not self.model:
|
||||
return
|
||||
pick_model = getattr(self.controller, "pick_model", None)
|
||||
if pick_model is None:
|
||||
return
|
||||
try:
|
||||
if pick_model.isEmpty():
|
||||
return
|
||||
except Exception:
|
||||
try:
|
||||
if pick_model.is_empty():
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
if hasattr(self.controller, "get_model_world_mat"):
|
||||
world_mat = self.controller.get_model_world_mat()
|
||||
else:
|
||||
world_mat = LMatrix4f(self.model.getNetTransform().getMat())
|
||||
try:
|
||||
pick_model.set_mat(world_mat)
|
||||
except Exception:
|
||||
pick_model.setMat(world_mat)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _refresh_ssbo_proxy_center(self):
|
||||
"""Update proxy center when source model transform changes."""
|
||||
if self._ssbo_transform_active:
|
||||
return
|
||||
if not self.controller or not self._ssbo_selected_local_indices:
|
||||
return
|
||||
if self._ssbo_gizmo_proxy is None:
|
||||
return
|
||||
try:
|
||||
if self._ssbo_gizmo_proxy.isEmpty():
|
||||
return
|
||||
except Exception:
|
||||
return
|
||||
try:
|
||||
center = self.controller.get_selection_center(self._ssbo_selected_local_indices)
|
||||
self._ssbo_gizmo_proxy.set_pos(center)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _is_model_attached(self):
|
||||
"""Whether the SSBO render root is still attached to scene graph."""
|
||||
if not self.model or self.model.is_empty():
|
||||
return False
|
||||
parent = self.model.get_parent()
|
||||
return bool(parent) and not parent.is_empty()
|
||||
|
||||
def _sync_pick_scene_binding(self):
|
||||
"""Switch pick camera scene based on current model attachment state."""
|
||||
if not hasattr(self, "pick_cam") or not self.pick_cam:
|
||||
return
|
||||
|
||||
if self._is_model_attached() and self.controller:
|
||||
target_scene = getattr(self.controller, "pick_model", None) or self.model
|
||||
else:
|
||||
target_scene = self._empty_pick_scene
|
||||
|
||||
if not target_scene:
|
||||
target_scene = self._empty_pick_scene
|
||||
|
||||
if self.pick_cam.get_scene() != target_scene:
|
||||
self.pick_cam.set_scene(target_scene)
|
||||
|
||||
def pick_object(self, mx, my):
|
||||
if (not self.pick_buffer or not self.pick_texture or not self.pick_lens or
|
||||
not self.controller or not self.model):
|
||||
return False
|
||||
self._sync_pick_model_transform()
|
||||
|
||||
self.pick_lens.set_fov(0.1)
|
||||
self.pick_lens.set_film_offset(0, 0)
|
||||
@ -264,281 +426,358 @@ class SSBOEditor:
|
||||
self.pick_cam_np.set_pos(0, 0, 0)
|
||||
self.pick_cam_np.look_at(far_point)
|
||||
|
||||
# Ensure pick transforms are up-to-date before rendering the pick buffer.
|
||||
# The per-frame sync task may not have run yet for this frame.
|
||||
self._sync_pick_transforms()
|
||||
|
||||
self.pick_buffer.set_active(True)
|
||||
self.base.graphicsEngine.render_frame()
|
||||
self.pick_buffer.set_active(False)
|
||||
self.base.graphicsEngine.extract_texture_data(
|
||||
self.pick_texture, self.base.win.get_gsg()
|
||||
)
|
||||
|
||||
ram_image = self.pick_texture.get_ram_image_as("RGBA")
|
||||
if ram_image:
|
||||
data = memoryview(ram_image)
|
||||
if len(data) >= 4:
|
||||
r, g, b, a = data[0], data[1], data[2], data[3]
|
||||
if a > 0:
|
||||
if a > 0 and b == 0:
|
||||
hit_id = r + (g << 8)
|
||||
node_key = self.controller.id_to_name.get(hit_id)
|
||||
if node_key:
|
||||
print(f"[Pick] Hit: ID={hit_id} -> {node_key}")
|
||||
self.select_node(node_key)
|
||||
return True
|
||||
return
|
||||
|
||||
self.selected_name = None
|
||||
self.selected_ids = []
|
||||
return False
|
||||
self.clear_selection()
|
||||
|
||||
|
||||
def on_mouse_click(self):
|
||||
io = imgui.get_io()
|
||||
if io.want_capture_mouse:
|
||||
if io.want_capture_mouse: return
|
||||
# Skip SSBO picking when user is interacting with the TransformGizmo,
|
||||
# otherwise pick_object would clear the selection and detach the gizmo
|
||||
# before the gizmo's own mouse handler fires.
|
||||
if self._transform_gizmo and self._transform_gizmo.is_hovering:
|
||||
return
|
||||
if self.base.mouseWatcherNode.has_mouse():
|
||||
self._sync_pick_model_transform()
|
||||
self._refresh_ssbo_proxy_center()
|
||||
mpos = self.base.mouseWatcherNode.get_mouse()
|
||||
# If clicking gizmo, skip SSBO pick.
|
||||
if self._try_start_gizmo_drag(mpos.x, mpos.y):
|
||||
return
|
||||
prev_selected = self.selected_name
|
||||
hit = self.pick_object(mpos.x, mpos.y)
|
||||
# SSBO miss must clear current selection.
|
||||
if not hit:
|
||||
self._sync_selection_none()
|
||||
# Always fallback to legacy ray pick when SSBO misses.
|
||||
# This keeps scene selection usable if SSBO ID mapping is incomplete.
|
||||
self._fallback_legacy_pick(mpos.x, mpos.y)
|
||||
elif prev_selected != self.selected_name:
|
||||
# Ensure selection visuals refresh when SSBO selection changes.
|
||||
self._sync_selection_from_key(self.selected_name)
|
||||
self.pick_object(mpos.x, mpos.y)
|
||||
|
||||
def toggle_debug(self):
|
||||
self.debug_mode = not self.debug_mode
|
||||
|
||||
def bind_transform_gizmo(self, gizmo):
|
||||
"""Bind a TransformGizmo so it follows SSBO selection."""
|
||||
self._transform_gizmo = gizmo
|
||||
|
||||
def _start_pick_sync_task(self):
|
||||
"""Start a per-frame task that syncs pick transforms for selected objects."""
|
||||
self.base.task_mgr.remove("ssbo_pick_sync")
|
||||
self.base.task_mgr.add(self._pick_sync_task, "ssbo_pick_sync")
|
||||
|
||||
def _stop_pick_sync_task(self):
|
||||
"""Stop the per-frame pick sync task."""
|
||||
self.base.task_mgr.remove("ssbo_pick_sync")
|
||||
|
||||
def _pick_sync_task(self, task):
|
||||
"""Per-frame: keep pick model transforms in sync with render model."""
|
||||
self._sync_pick_transforms()
|
||||
return task.cont
|
||||
|
||||
def _reset_pick_sync_cache(self):
|
||||
self._last_group_sync_mat = None
|
||||
self._last_single_sync_gid = None
|
||||
self._last_single_sync_mat = None
|
||||
|
||||
def _matrices_close(self, a, b, eps=1e-5):
|
||||
"""Small helper for robust matrix change detection."""
|
||||
for r in range(4):
|
||||
ra = a.get_row(r)
|
||||
rb = b.get_row(r)
|
||||
if (abs(ra[0] - rb[0]) > eps or
|
||||
abs(ra[1] - rb[1]) > eps or
|
||||
abs(ra[2] - rb[2]) > eps or
|
||||
abs(ra[3] - rb[3]) > eps):
|
||||
return False
|
||||
return True
|
||||
|
||||
def _sync_pick_root_transform(self):
|
||||
"""
|
||||
Keep pick root aligned with the render root transform.
|
||||
This covers transforms applied to the whole imported model
|
||||
(for example, moving box.glb from scene hierarchy).
|
||||
"""
|
||||
if not self.controller or not self.model or not self._is_model_attached():
|
||||
return
|
||||
|
||||
pick_root = getattr(self.controller, "pick_model", None)
|
||||
if not pick_root:
|
||||
return
|
||||
|
||||
if self.model.is_empty() or pick_root.is_empty():
|
||||
return
|
||||
|
||||
pick_root.set_mat(self.base.render, self.model.get_mat(self.base.render))
|
||||
|
||||
def _sync_pick_transforms(self):
|
||||
"""Sync pick model transforms to match render model transforms."""
|
||||
if not self.controller:
|
||||
return
|
||||
self._sync_pick_root_transform()
|
||||
if not self.selected_ids:
|
||||
return
|
||||
|
||||
# Group selection can contain thousands of objects.
|
||||
# Only resync when proxy transform has changed.
|
||||
proxy = getattr(self, "_group_proxy", None)
|
||||
if proxy and not proxy.is_empty() and len(self.selected_ids) > 1:
|
||||
proxy_world = proxy.get_mat(self.base.render)
|
||||
if self._last_group_sync_mat and self._matrices_close(proxy_world, self._last_group_sync_mat):
|
||||
return
|
||||
self._last_group_sync_mat = LMatrix4f(proxy_world)
|
||||
else:
|
||||
self._last_group_sync_mat = None
|
||||
|
||||
if len(self.selected_ids) == 1:
|
||||
gid = self.selected_ids[0]
|
||||
obj_np = self.controller.id_to_object_np.get(gid)
|
||||
pick_np = self.controller.id_to_pick_np.get(gid)
|
||||
if not (obj_np and pick_np and not obj_np.is_empty() and not pick_np.is_empty()):
|
||||
return
|
||||
obj_world_mat = obj_np.get_mat(self.base.render)
|
||||
if (
|
||||
self._last_single_sync_gid == gid and
|
||||
self._last_single_sync_mat and
|
||||
self._matrices_close(obj_world_mat, self._last_single_sync_mat)
|
||||
):
|
||||
return
|
||||
self._last_single_sync_gid = gid
|
||||
self._last_single_sync_mat = LMatrix4f(obj_world_mat)
|
||||
pick_world_mat = pick_np.get_mat(self.base.render)
|
||||
if not self._matrices_close(obj_world_mat, pick_world_mat):
|
||||
pick_np.set_mat(self.base.render, obj_world_mat)
|
||||
chunk_id = self.controller.id_to_chunk.get(gid)
|
||||
if chunk_id is not None and chunk_id in self.controller.chunks:
|
||||
self.controller.chunks[chunk_id]["dirty"] = True
|
||||
return
|
||||
|
||||
self._last_single_sync_gid = None
|
||||
self._last_single_sync_mat = None
|
||||
for gid in self.selected_ids:
|
||||
obj_np = self.controller.id_to_object_np.get(gid)
|
||||
pick_np = self.controller.id_to_pick_np.get(gid)
|
||||
if obj_np and pick_np and not obj_np.is_empty() and not pick_np.is_empty():
|
||||
obj_world_mat = obj_np.get_mat(self.base.render)
|
||||
pick_world_mat = pick_np.get_mat(self.base.render)
|
||||
if not self._matrices_close(obj_world_mat, pick_world_mat):
|
||||
# Sync by world transform so this stays correct even when
|
||||
# the model root itself has been moved in scene hierarchy.
|
||||
pick_np.set_mat(self.base.render, obj_world_mat)
|
||||
chunk_id = self.controller.id_to_chunk.get(gid)
|
||||
if chunk_id is not None and chunk_id in self.controller.chunks:
|
||||
self.controller.chunks[chunk_id]["dirty"] = True
|
||||
|
||||
def _update_outline_for_selection(self):
|
||||
if not self._outline_manager:
|
||||
return
|
||||
if not self.controller or not self.selected_ids:
|
||||
self._outline_manager.clear()
|
||||
return
|
||||
|
||||
is_root_selection = (
|
||||
self.controller and
|
||||
self.selected_name == getattr(self.controller, "tree_root_key", None)
|
||||
)
|
||||
if is_root_selection:
|
||||
self._outline_manager.clear()
|
||||
return
|
||||
|
||||
targets = []
|
||||
target_limit = max(1, int(getattr(self._outline_manager, "max_targets", 64)))
|
||||
for gid in self.selected_ids:
|
||||
obj_np = self.controller.id_to_object_np.get(gid)
|
||||
if obj_np and not obj_np.is_empty():
|
||||
targets.append(obj_np)
|
||||
if len(targets) >= target_limit:
|
||||
break
|
||||
|
||||
self._outline_manager.set_targets(targets)
|
||||
|
||||
def clear_selection(self):
|
||||
pass # No selection mask texture needed without custom shader
|
||||
self._stop_pick_sync_task()
|
||||
self._reset_pick_sync_cache()
|
||||
self._cleanup_group_proxy()
|
||||
self.selected_name = None
|
||||
self.selected_ids = []
|
||||
if self._outline_manager:
|
||||
self._outline_manager.clear()
|
||||
if self.controller:
|
||||
self.controller.set_active_ids([])
|
||||
if self._transform_gizmo:
|
||||
self._transform_gizmo.detach()
|
||||
|
||||
def on_model_deleted(self, deleted_node):
|
||||
"""Called by app deletion flow when SSBO root model is deleted."""
|
||||
if not deleted_node or deleted_node.is_empty() or not self.model:
|
||||
return
|
||||
if deleted_node != self.model:
|
||||
return
|
||||
self.clear_selection()
|
||||
self._sync_pick_scene_binding()
|
||||
|
||||
def _cleanup_group_proxy(self):
|
||||
"""Reparent objects back to their chunk and remove the group proxy."""
|
||||
proxy = getattr(self, '_group_proxy', None)
|
||||
if not proxy:
|
||||
return
|
||||
originals = getattr(self, '_group_original_parents', {})
|
||||
# Sync pick transforms and mark chunks dirty before reparenting
|
||||
for gid in originals:
|
||||
obj_np = self.controller.id_to_object_np.get(gid)
|
||||
pick_np = self.controller.id_to_pick_np.get(gid)
|
||||
if obj_np and pick_np and not obj_np.is_empty() and not pick_np.is_empty():
|
||||
pick_np.set_mat(self.base.render, obj_np.get_mat(self.base.render))
|
||||
chunk_id = self.controller.id_to_chunk.get(gid)
|
||||
if chunk_id is not None and chunk_id in self.controller.chunks:
|
||||
self.controller.chunks[chunk_id]["dirty"] = True
|
||||
# Reparent objects back to their original chunk parents
|
||||
for gid, parent_np in originals.items():
|
||||
obj_np = self.controller.id_to_object_np.get(gid)
|
||||
if obj_np and not obj_np.is_empty() and parent_np and not parent_np.is_empty():
|
||||
obj_np.wrt_reparent_to(parent_np)
|
||||
if not proxy.is_empty():
|
||||
proxy.remove_node()
|
||||
self._group_proxy = None
|
||||
self._group_original_parents = {}
|
||||
|
||||
def update_selection_mask(self):
|
||||
pass # No selection mask texture needed without custom shader
|
||||
|
||||
def select_node(self, key):
|
||||
if not self.controller or key not in self.controller.name_to_ids:
|
||||
return
|
||||
# Clean up previous group proxy before changing selection
|
||||
self._cleanup_group_proxy()
|
||||
self._reset_pick_sync_cache()
|
||||
|
||||
self.selected_name = key
|
||||
self.selected_ids = self.controller.name_to_ids.get(key, [])
|
||||
self._sync_selection_from_key(key)
|
||||
is_root_selection = (
|
||||
self.controller and
|
||||
key == getattr(self.controller, "tree_root_key", None)
|
||||
)
|
||||
|
||||
def _sync_selection_from_key(self, key):
|
||||
"""Sync SSBO picked key to legacy SelectionSystem."""
|
||||
try:
|
||||
if hasattr(self.base, "selection") and self.base.selection:
|
||||
kind, target = self._resolve_ssbo_selection_target(key)
|
||||
if kind == "proxy":
|
||||
target_np = target
|
||||
else:
|
||||
target_np = target if target is not None else self.model
|
||||
if target_np is None or target_np.isEmpty():
|
||||
target_np = self.model
|
||||
self.base.selection.updateSelection(target_np)
|
||||
except Exception as e:
|
||||
print(f"[SSBOEditor] selection sync failed: {e}")
|
||||
|
||||
def _sync_selection_none(self):
|
||||
"""Clear legacy SelectionSystem selection."""
|
||||
try:
|
||||
self._ssbo_transform_active = False
|
||||
self._ssbo_selected_local_indices = []
|
||||
self._ssbo_transform_snapshot = None
|
||||
self._cleanup_ssbo_proxy()
|
||||
if hasattr(self.base, "selection") and self.base.selection:
|
||||
self.base.selection.updateSelection(None)
|
||||
except Exception as e:
|
||||
print(f"[SSBOEditor] clear selection sync failed: {e}")
|
||||
|
||||
def bind_transform_gizmo(self, transform_gizmo):
|
||||
"""Bind TransformGizmo drag hooks so SSBO sub-object transforms can follow gizmo."""
|
||||
self._bound_transform_gizmo = transform_gizmo
|
||||
if not transform_gizmo:
|
||||
# Root selection should stay lightweight:
|
||||
# keep static chunks active and transform the model root directly.
|
||||
if is_root_selection:
|
||||
self.controller.set_active_ids([])
|
||||
if self._outline_manager:
|
||||
self._outline_manager.clear()
|
||||
if self._transform_gizmo and self.model and not self.model.is_empty():
|
||||
self._transform_gizmo.attach(self.model)
|
||||
else:
|
||||
if self._transform_gizmo:
|
||||
self._transform_gizmo.detach()
|
||||
self._stop_pick_sync_task()
|
||||
return
|
||||
hooks = {
|
||||
"move": {
|
||||
"drag_start": [self._on_ssbo_gizmo_drag_start],
|
||||
"drag_move": [self._on_ssbo_gizmo_drag_move],
|
||||
"drag_end": [self._on_ssbo_gizmo_drag_end],
|
||||
},
|
||||
"rotate": {
|
||||
"drag_start": [self._on_ssbo_gizmo_drag_start],
|
||||
"drag_move": [self._on_ssbo_gizmo_drag_move],
|
||||
"drag_end": [self._on_ssbo_gizmo_drag_end],
|
||||
},
|
||||
"scale": {
|
||||
"drag_start": [self._on_ssbo_gizmo_drag_start],
|
||||
"drag_move": [self._on_ssbo_gizmo_drag_move],
|
||||
"drag_end": [self._on_ssbo_gizmo_drag_end],
|
||||
},
|
||||
}
|
||||
try:
|
||||
if hasattr(transform_gizmo, "set_event_hooks"):
|
||||
transform_gizmo.set_event_hooks(hooks, replace=False)
|
||||
print("[SSBOEditor] TransformGizmo hooks bound")
|
||||
except Exception as e:
|
||||
print(f"[SSBOEditor] bind transform gizmo failed: {e}")
|
||||
|
||||
def _resolve_ssbo_selection_target(self, key):
|
||||
"""Resolve selected SSBO key to proxy node (preferred) or regular node."""
|
||||
self._ssbo_transform_active = False
|
||||
self._ssbo_transform_snapshot = None
|
||||
self._ssbo_selected_local_indices = []
|
||||
self.controller.set_active_ids(self.selected_ids)
|
||||
self._update_outline_for_selection()
|
||||
|
||||
if not self.controller or not key:
|
||||
return "node", self.model
|
||||
global_ids = self.controller.name_to_ids.get(key, [])
|
||||
local_indices = self.controller.get_local_indices_from_global_ids(global_ids)
|
||||
self._ssbo_selected_local_indices = local_indices
|
||||
if local_indices:
|
||||
print(f"[SSBOEditor] selection locals={len(local_indices)} key={key}")
|
||||
center = self.controller.get_selection_center(local_indices)
|
||||
proxy = self._ensure_ssbo_proxy(center)
|
||||
return "proxy", proxy
|
||||
target_np = self.controller.key_to_node.get(key)
|
||||
if target_np is None or target_np.isEmpty():
|
||||
target_np = self.model
|
||||
return "node", target_np
|
||||
if not self._transform_gizmo or not self.selected_ids:
|
||||
if self._transform_gizmo:
|
||||
self._transform_gizmo.detach()
|
||||
return
|
||||
|
||||
def _ensure_ssbo_proxy(self, center):
|
||||
if self._ssbo_gizmo_proxy is None or self._ssbo_gizmo_proxy.isEmpty():
|
||||
self._ssbo_gizmo_proxy = self.base.render.attach_new_node("ssbo_transform_proxy")
|
||||
self._ssbo_gizmo_proxy.setTag("is_ssbo_proxy", "1")
|
||||
self._ssbo_gizmo_proxy.set_pos(center)
|
||||
self._ssbo_gizmo_proxy.set_hpr(0, 0, 0)
|
||||
self._ssbo_gizmo_proxy.set_scale(1, 1, 1)
|
||||
return self._ssbo_gizmo_proxy
|
||||
if len(self.selected_ids) == 1:
|
||||
# Single object: attach gizmo directly
|
||||
obj_np = self.controller.id_to_object_np.get(self.selected_ids[0])
|
||||
if obj_np and not obj_np.is_empty():
|
||||
self._transform_gizmo.attach(obj_np)
|
||||
self._start_pick_sync_task()
|
||||
return
|
||||
|
||||
def _cleanup_ssbo_proxy(self):
|
||||
if self._ssbo_gizmo_proxy and not self._ssbo_gizmo_proxy.isEmpty():
|
||||
self._ssbo_gizmo_proxy.removeNode()
|
||||
self._ssbo_gizmo_proxy = None
|
||||
# Multiple objects (parent node): create a group proxy so all children
|
||||
# follow the gizmo transform together.
|
||||
from panda3d.core import Vec3
|
||||
proxy = self.base.render.attach_new_node("ssbo_group_proxy")
|
||||
center = Vec3(0, 0, 0)
|
||||
valid = []
|
||||
for gid in self.selected_ids:
|
||||
obj_np = self.controller.id_to_object_np.get(gid)
|
||||
if obj_np and not obj_np.is_empty():
|
||||
center += obj_np.get_pos(self.base.render)
|
||||
valid.append(gid)
|
||||
if not valid:
|
||||
proxy.remove_node()
|
||||
return
|
||||
|
||||
def _on_ssbo_gizmo_drag_start(self, payload):
|
||||
try:
|
||||
target = payload.get("target") if payload else None
|
||||
if not target or target != self._ssbo_gizmo_proxy:
|
||||
self._ssbo_transform_active = False
|
||||
return
|
||||
if not self.controller or not self._ssbo_selected_local_indices:
|
||||
self._ssbo_transform_active = False
|
||||
return
|
||||
self._ssbo_transform_snapshot = self.controller.begin_transform_session(
|
||||
self._ssbo_selected_local_indices
|
||||
)
|
||||
self._ssbo_proxy_start = {
|
||||
"pos": Vec3(target.getPos(self.base.render)),
|
||||
"quat": Quat(target.getQuat(self.base.render)),
|
||||
"scale": Vec3(target.getScale()),
|
||||
}
|
||||
self._ssbo_transform_active = True
|
||||
print(f"[SSBOEditor] drag_start locals={len(self._ssbo_selected_local_indices)}")
|
||||
except Exception as e:
|
||||
self._ssbo_transform_active = False
|
||||
print(f"[SSBOEditor] drag_start bridge failed: {e}")
|
||||
center /= len(valid)
|
||||
proxy.set_pos(self.base.render, center)
|
||||
|
||||
def _on_ssbo_gizmo_drag_move(self, payload):
|
||||
try:
|
||||
if not self._ssbo_transform_active:
|
||||
return
|
||||
target = payload.get("target") if payload else None
|
||||
if not target or target != self._ssbo_gizmo_proxy:
|
||||
return
|
||||
start_pos = self._ssbo_proxy_start.get("pos")
|
||||
start_quat = self._ssbo_proxy_start.get("quat")
|
||||
start_scale = self._ssbo_proxy_start.get("scale")
|
||||
if start_pos is None or start_quat is None or start_scale is None:
|
||||
return
|
||||
self._group_proxy = proxy
|
||||
self._group_original_parents = {}
|
||||
for gid in valid:
|
||||
obj_np = self.controller.id_to_object_np[gid]
|
||||
self._group_original_parents[gid] = obj_np.get_parent()
|
||||
obj_np.wrt_reparent_to(proxy)
|
||||
|
||||
curr_pos = Vec3(target.getPos(self.base.render))
|
||||
curr_quat = Quat(target.getQuat(self.base.render))
|
||||
curr_scale = Vec3(target.getScale())
|
||||
self._transform_gizmo.attach(proxy)
|
||||
# For huge groups, avoid per-frame full sync; we still sync on demand
|
||||
# right before picking via pick_object().
|
||||
if len(valid) <= self._pick_sync_bg_limit:
|
||||
self._start_pick_sync_task()
|
||||
else:
|
||||
self._stop_pick_sync_task()
|
||||
|
||||
delta_pos = curr_pos - start_pos
|
||||
inv_start_quat = Quat(start_quat)
|
||||
inv_start_quat.invertInPlace()
|
||||
delta_quat = curr_quat * inv_start_quat
|
||||
delta_scale = Vec3(
|
||||
curr_scale.x / start_scale.x if abs(start_scale.x) > 1e-8 else 1.0,
|
||||
curr_scale.y / start_scale.y if abs(start_scale.y) > 1e-8 else 1.0,
|
||||
curr_scale.z / start_scale.z if abs(start_scale.z) > 1e-8 else 1.0,
|
||||
)
|
||||
def _rebuild_filtered_tree_rows(self):
|
||||
"""
|
||||
Build a flattened tree-row list with depth info for rendering in ImGui,
|
||||
while preserving source-model parent/child hierarchy.
|
||||
"""
|
||||
self.filtered_nodes = []
|
||||
if not self.controller or not self.controller.tree_root_key:
|
||||
return
|
||||
|
||||
self.controller.apply_transform_session(
|
||||
self._ssbo_transform_snapshot,
|
||||
delta_pos,
|
||||
delta_quat,
|
||||
delta_scale,
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"[SSBOEditor] drag_move bridge failed: {e}")
|
||||
search_lower = self.search_text.strip().lower()
|
||||
|
||||
def _on_ssbo_gizmo_drag_end(self, payload):
|
||||
try:
|
||||
if self._ssbo_transform_active:
|
||||
print(f"[SSBOEditor] drag_end locals={len(self._ssbo_selected_local_indices)}")
|
||||
self._ssbo_transform_active = False
|
||||
self._ssbo_transform_snapshot = None
|
||||
except Exception as e:
|
||||
print(f"[SSBOEditor] drag_end bridge failed: {e}")
|
||||
def walk(key, depth):
|
||||
node = self.controller.tree_nodes.get(key)
|
||||
if not node:
|
||||
return False, []
|
||||
|
||||
def _fallback_legacy_pick(self, mx, my):
|
||||
"""Fallback to legacy ray picking when SSBO misses."""
|
||||
try:
|
||||
if not hasattr(self.base, "event_handler") or not self.base.event_handler:
|
||||
return
|
||||
win_w, win_h = self.base.win.getSize()
|
||||
x = (mx + 1.0) * 0.5 * win_w
|
||||
y = (1.0 - my) * 0.5 * win_h
|
||||
self.base.event_handler.mousePressEventLeft({"x": x, "y": y})
|
||||
except Exception as e:
|
||||
print(f"[SSBOEditor] legacy fallback pick failed: {e}")
|
||||
# Skip redundant wrapper nodes (e.g. ROOT under model file root),
|
||||
# while preserving child hierarchy and selection mapping.
|
||||
if self.controller.should_hide_tree_node(key):
|
||||
merged_rows = []
|
||||
merged_visible = False
|
||||
for child_key in node["children"]:
|
||||
visible, rows = walk(child_key, depth)
|
||||
if visible:
|
||||
merged_visible = True
|
||||
merged_rows.extend(rows)
|
||||
return merged_visible, merged_rows
|
||||
|
||||
def _try_start_gizmo_drag(self, mouse_x=None, mouse_y=None):
|
||||
"""Try to start gizmo drag using the existing SelectionSystem pipeline."""
|
||||
try:
|
||||
new_transform = getattr(self.base, "newTransform", None)
|
||||
if (
|
||||
new_transform is not None and
|
||||
mouse_x is not None and
|
||||
mouse_y is not None and
|
||||
self._is_mouse_on_new_gizmo(new_transform, mouse_x, mouse_y)
|
||||
):
|
||||
return True
|
||||
selection = getattr(self.base, "selection", None)
|
||||
if not selection or not selection.gizmo:
|
||||
return False
|
||||
win_w, win_h = self.base.win.getSize()
|
||||
mpos = self.base.mouseWatcherNode.get_mouse()
|
||||
x = (mpos.x + 1.0) * 0.5 * win_w
|
||||
y = (1.0 - mpos.y) * 0.5 * win_h
|
||||
display = self.controller.display_names.get(key, key)
|
||||
obj_count = len(self.controller.name_to_ids.get(key, []))
|
||||
name_match = (not search_lower) or (search_lower in display.lower())
|
||||
|
||||
axis = selection.gizmoHighlightAxis or selection.checkGizmoClick(x, y)
|
||||
if axis:
|
||||
selection.startGizmoDrag(axis, x, y)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"[SSBOEditor] gizmo drag start failed: {e}")
|
||||
return False
|
||||
child_rows = []
|
||||
child_match = False
|
||||
for child_key in node["children"]:
|
||||
visible, rows = walk(child_key, depth + 1)
|
||||
if visible:
|
||||
child_match = True
|
||||
child_rows.extend(rows)
|
||||
|
||||
def _is_mouse_on_new_gizmo(self, new_transform, mouse_x, mouse_y):
|
||||
"""Refresh and query hover state for TransformGizmo on current click position."""
|
||||
try:
|
||||
mouse_pos = Point3(mouse_x, mouse_y, 0.0)
|
||||
for gizmo_name in ("move_gizmo", "rotate_gizmo", "scale_gizmo"):
|
||||
gizmo = getattr(new_transform, gizmo_name, None)
|
||||
if not gizmo or not getattr(gizmo, "attached", False):
|
||||
continue
|
||||
hover_updater = getattr(gizmo, "_update_hover_highlight", None)
|
||||
if callable(hover_updater):
|
||||
hover_updater(mouse_pos)
|
||||
return bool(getattr(new_transform, "is_hovering", False))
|
||||
except Exception as e:
|
||||
print(f"[SSBOEditor] new gizmo hover check failed: {e}")
|
||||
return False
|
||||
visible = (not search_lower) or name_match or child_match
|
||||
if not visible:
|
||||
return False, []
|
||||
|
||||
row = (key, depth, display, obj_count)
|
||||
return True, [row] + child_rows
|
||||
|
||||
_, rows = walk(self.controller.tree_root_key, 0)
|
||||
self.filtered_nodes = rows
|
||||
|
||||
def focus_on_selected(self):
|
||||
if self.selected_name and self.selected_ids:
|
||||
@ -562,28 +801,18 @@ class SSBOEditor:
|
||||
changed, self.search_text = imgui.input_text("Search", self.search_text, 256)
|
||||
|
||||
if imgui.begin_child("ObjectList", (0, 380), child_flags=imgui.ChildFlags_.borders):
|
||||
if self.search_text != self.last_search_text:
|
||||
if self.search_text != self.last_search_text or not self.filtered_nodes:
|
||||
self.last_search_text = self.search_text
|
||||
search_lower = self.search_text.lower()
|
||||
self.filtered_nodes = []
|
||||
for key in self.controller.node_list:
|
||||
display = self.controller.display_names.get(key, key.split('/')[-1])
|
||||
if not search_lower or (search_lower in display.lower() or search_lower in key.lower()):
|
||||
geom_count = len(self.controller.name_to_ids.get(key, []))
|
||||
self.filtered_nodes.append((key, display, geom_count))
|
||||
|
||||
# If list is empty initially (no search), show all
|
||||
if not self.search_text and not self.filtered_nodes:
|
||||
if len(self.filtered_nodes) != len(self.controller.node_list):
|
||||
self.filtered_nodes = [(k, self.controller.display_names.get(k, k), len(self.controller.name_to_ids.get(k,[]))) for k in self.controller.node_list]
|
||||
self._rebuild_filtered_tree_rows()
|
||||
|
||||
count = len(self.filtered_nodes)
|
||||
clipper = imgui.ListClipper()
|
||||
clipper.begin(count)
|
||||
while clipper.step():
|
||||
for i in range(clipper.display_start, clipper.display_end):
|
||||
key, display, geom_count = self.filtered_nodes[i]
|
||||
label = f"{display} ({geom_count})"
|
||||
key, depth, display, geom_count = self.filtered_nodes[i]
|
||||
indent = " " * depth
|
||||
label = f"{indent}{display} ({geom_count})##{key}"
|
||||
is_selected = (key == self.selected_name)
|
||||
if imgui.selectable(label, is_selected)[0]:
|
||||
self.select_node(key)
|
||||
@ -591,7 +820,8 @@ class SSBOEditor:
|
||||
|
||||
imgui.separator()
|
||||
if self.selected_name:
|
||||
imgui.text_colored((1, 0.8, 0.2, 1), f"Selected: {self.selected_name}")
|
||||
selected_display = self.controller.display_names.get(self.selected_name, self.selected_name)
|
||||
imgui.text_colored((1, 0.8, 0.2, 1), f"Selected: {selected_display}")
|
||||
if imgui.button("Focus (F)"): self.focus_on_selected()
|
||||
imgui.end()
|
||||
|
||||
@ -600,7 +830,12 @@ class SSBOEditor:
|
||||
def update_task(self, task):
|
||||
dt = globalClock.getDt()
|
||||
io = imgui.get_io()
|
||||
|
||||
self._sync_pick_model_transform()
|
||||
self._refresh_ssbo_proxy_center()
|
||||
self._sync_pick_scene_binding()
|
||||
# Scene-hierarchy transforms may move the whole SSBO model root; keep pick root in sync.
|
||||
self._sync_pick_root_transform()
|
||||
|
||||
if io.want_capture_keyboard: return task.cont
|
||||
|
||||
if self.selected_ids and self.controller:
|
||||
@ -614,7 +849,18 @@ class SSBOEditor:
|
||||
if self.keys.get('x'): acc.y -= speed
|
||||
|
||||
if acc.length_squared() > 0:
|
||||
for idx in self.selected_ids:
|
||||
self.controller.move_object(idx, acc)
|
||||
is_root_selection = (
|
||||
self.selected_name == getattr(self.controller, "tree_root_key", None)
|
||||
)
|
||||
if is_root_selection and self.model and not self.model.is_empty():
|
||||
next_pos = self.model.get_pos() + acc
|
||||
if hasattr(self.model, "set_fluid_pos"):
|
||||
self.model.set_fluid_pos(next_pos)
|
||||
else:
|
||||
self.model.set_pos(next_pos)
|
||||
self._sync_pick_root_transform()
|
||||
else:
|
||||
for idx in self.selected_ids:
|
||||
self.controller.move_object(idx, acc)
|
||||
|
||||
return task.cont
|
||||
|
||||
@ -2,8 +2,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
测试动画模型 - Panda3D应用程序
|
||||
使用Panda3D引擎编辑器创建
|
||||
打包
|
||||
"""
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
1
ui/LUI/__init__.py
Normal file
1
ui/LUI/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""LUI split modules."""
|
||||
1247
ui/LUI/lui_function_components.py
Normal file
1247
ui/LUI/lui_function_components.py
Normal file
File diff suppressed because it is too large
Load Diff
1778
ui/LUI/lui_function_properties.py
Normal file
1778
ui/LUI/lui_function_properties.py
Normal file
File diff suppressed because it is too large
Load Diff
1738
ui/LUI/lui_manager_editor.py
Normal file
1738
ui/LUI/lui_manager_editor.py
Normal file
File diff suppressed because it is too large
Load Diff
1563
ui/LUI/lui_manager_interaction.py
Normal file
1563
ui/LUI/lui_manager_interaction.py
Normal file
File diff suppressed because it is too large
Load Diff
69
ui/LUI/lui_shared.py
Normal file
69
ui/LUI/lui_shared.py
Normal file
@ -0,0 +1,69 @@
|
||||
"""Shared LUI bootstrap and imports."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import panda3d.core as p3d
|
||||
from panda3d.core import NodePath, CardMaker
|
||||
from imgui_bundle import imgui, imgui_ctx
|
||||
|
||||
UI_DIR = Path(__file__).resolve().parents[1]
|
||||
if str(UI_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(UI_DIR))
|
||||
|
||||
BUILTIN_DIR = UI_DIR / "Builtin"
|
||||
if str(BUILTIN_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(BUILTIN_DIR))
|
||||
|
||||
import panda3d
|
||||
panda_dir = os.path.dirname(panda3d.__file__)
|
||||
if hasattr(os, "add_dll_directory"):
|
||||
try:
|
||||
os.add_dll_directory(panda_dir)
|
||||
os.add_dll_directory(str(UI_DIR))
|
||||
except Exception as e:
|
||||
print(f"Warning: Failed to add DLL directory: {e}")
|
||||
|
||||
try:
|
||||
import lui
|
||||
panda3d.lui = lui
|
||||
sys.modules["panda3d.lui"] = lui
|
||||
|
||||
from Builtin.LUIRegion import LUIRegion
|
||||
from Builtin.LUIInputHandler import LUIInputHandler
|
||||
from Builtin.LUIButton import LUIButton
|
||||
from Builtin.LUILabel import LUILabel
|
||||
from Builtin.LUIInputField import LUIInputField
|
||||
from Builtin.LUISlider import LUISlider
|
||||
from Builtin.LUIFrame import LUIFrame
|
||||
from Builtin.LUISkin import LUIDefaultSkin
|
||||
from Builtin.LUISprite import LUISprite
|
||||
from Builtin.LUIObject import LUIObject
|
||||
from Builtin.LUICheckbox import LUICheckbox
|
||||
from Builtin.LUIProgressbar import LUIProgressbar
|
||||
from Builtin.LUISelectbox import LUISelectbox
|
||||
from Builtin.LUIScrollableRegion import LUIScrollableRegion
|
||||
from Builtin.LUITabbedFrame import LUITabbedFrame
|
||||
from Builtin.LUIVerticalLayout import LUIVerticalLayout
|
||||
from Builtin.LUIHorizontalLayout import LUIHorizontalLayout
|
||||
except ImportError as e:
|
||||
print(f"Error: Failed to import LUI: {e}")
|
||||
lui = None
|
||||
LUIRegion = None
|
||||
LUIInputHandler = None
|
||||
LUIButton = None
|
||||
LUILabel = None
|
||||
LUIInputField = None
|
||||
LUISlider = None
|
||||
LUIFrame = None
|
||||
LUIDefaultSkin = None
|
||||
LUISprite = None
|
||||
LUIObject = None
|
||||
LUICheckbox = None
|
||||
LUIProgressbar = None
|
||||
LUISelectbox = None
|
||||
LUIScrollableRegion = None
|
||||
LUITabbedFrame = None
|
||||
LUIVerticalLayout = None
|
||||
LUIHorizontalLayout = None
|
||||
@ -1,342 +1,154 @@
|
||||
"""
|
||||
图标管理工具
|
||||
|
||||
负责统一管理应用程序中的所有图标:
|
||||
- 图标路径解析
|
||||
- 图标缓存
|
||||
- 图标预加载
|
||||
- 图标错误处理
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
from typing import Dict, Optional
|
||||
from PyQt5.QtGui import QIcon, QPixmap
|
||||
from PyQt5.QtCore import QSize
|
||||
|
||||
|
||||
class IconManager:
|
||||
"""图标管理器类"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化图标管理器"""
|
||||
self.icon_cache: Dict[str, QIcon] = {}
|
||||
self.icon_directory = self._get_icon_directory()
|
||||
self.default_icon = None
|
||||
|
||||
# 预定义的图标映射
|
||||
self.icon_map = {
|
||||
# 主窗口图标
|
||||
'app_logo': 'logo.png',
|
||||
|
||||
# 工具栏图标
|
||||
'select_tool': 'select_tool.png',
|
||||
'move_tool': 'move_tool.png',
|
||||
'rotate_tool': 'rotate_tool.png',
|
||||
'scale_tool': 'scale_tool.png',
|
||||
|
||||
# 菜单图标(如果有的话)
|
||||
'new_file': 'new_file.png',
|
||||
'open_file': 'open_file.png',
|
||||
'save_file': 'save_file.png',
|
||||
'exit': 'exit.png',
|
||||
|
||||
# 对象类型图标
|
||||
'object_3d': 'object_3d.png',
|
||||
'light': 'light.png',
|
||||
'camera': 'camera.png',
|
||||
'terrain': 'terrain.png',
|
||||
'script': 'script.png',
|
||||
|
||||
# 状态图标
|
||||
'success': 'success.png',
|
||||
'warning': 'warning.png',
|
||||
'error': 'error.png',
|
||||
'info': 'info.png',
|
||||
|
||||
'up_arrow': 'up_arrows.png',
|
||||
'down_arrow': 'down_arrows.png',
|
||||
'left_arrow': 'left_arrows.png',
|
||||
'right_arrow': 'right_arrows.png',
|
||||
|
||||
'solid_down_arrows': 'solid_down_arrows.png',
|
||||
'solid_right_arrows': 'solid_right_arrows.png',
|
||||
|
||||
'minimize_icon': 'minimize_icon.png',
|
||||
'windowing_icon': 'windowing_icon.png',
|
||||
'close_icon': 'close_icon.png',
|
||||
|
||||
# 弹窗图标
|
||||
'success_icon': 'success_icon.png',
|
||||
'warning_icon': 'warning_icon.png',
|
||||
'fail_icon': 'delete_fail_icon.png',
|
||||
|
||||
# 属性面板图标
|
||||
'property_select_image': 'property_select_image.png',
|
||||
}
|
||||
|
||||
# 初始化默认图标
|
||||
self._create_default_icon()
|
||||
|
||||
# 预加载常用图标
|
||||
self._preload_icons()
|
||||
|
||||
def _get_icon_directory(self) -> str:
|
||||
"""获取图标目录的绝对路径"""
|
||||
# 获取当前文件的目录(ui目录)
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
# 获取项目根目录(ui的父目录)
|
||||
project_root = os.path.dirname(current_dir)
|
||||
# 拼接icons目录路径
|
||||
icon_dir = os.path.join(project_root, "icons")
|
||||
|
||||
print(f"🔍 图标目录路径: {icon_dir}")
|
||||
|
||||
# 检查目录是否存在
|
||||
if not os.path.exists(icon_dir):
|
||||
print(f"⚠️ 图标目录不存在: {icon_dir}")
|
||||
# 尝试创建目录
|
||||
try:
|
||||
os.makedirs(icon_dir, exist_ok=True)
|
||||
print(f"✅ 已创建图标目录: {icon_dir}")
|
||||
except Exception as e:
|
||||
print(f"❌ 创建图标目录失败: {e}")
|
||||
|
||||
return icon_dir
|
||||
|
||||
def _create_default_icon(self):
|
||||
"""创建默认图标"""
|
||||
# 创建一个简单的默认图标
|
||||
pixmap = QPixmap(16, 16)
|
||||
pixmap.fill() # 填充为白色
|
||||
self.default_icon = QIcon(pixmap)
|
||||
|
||||
def _preload_icons(self):
|
||||
"""预加载常用图标"""
|
||||
print("🔄 开始预加载图标...")
|
||||
|
||||
for icon_name, file_name in self.icon_map.items():
|
||||
icon_path = os.path.join(self.icon_directory, file_name)
|
||||
if os.path.exists(icon_path):
|
||||
try:
|
||||
icon = QIcon(icon_path)
|
||||
self.icon_cache[icon_name] = icon
|
||||
print(f"✅ 已加载图标: {icon_name} -> {file_name}")
|
||||
except Exception as e:
|
||||
print(f"❌ 加载图标失败: {icon_name} -> {file_name}, 错误: {e}")
|
||||
else:
|
||||
print(f"⚠️ 图标文件不存在: {icon_path}")
|
||||
|
||||
print(f"📊 预加载完成,共加载 {len(self.icon_cache)} 个图标")
|
||||
|
||||
def get_icon(self, icon_name: str, size: Optional[QSize] = None) -> QIcon:
|
||||
"""
|
||||
获取图标
|
||||
|
||||
Args:
|
||||
icon_name: 图标名称(可以是预定义名称或文件名)
|
||||
size: 图标尺寸
|
||||
|
||||
Returns:
|
||||
QIcon对象
|
||||
"""
|
||||
# 首先检查缓存
|
||||
if icon_name in self.icon_cache:
|
||||
icon = self.icon_cache[icon_name]
|
||||
if size:
|
||||
# 如果指定了尺寸,返回指定尺寸的图标
|
||||
pixmap = icon.pixmap(size)
|
||||
return QIcon(pixmap)
|
||||
return icon
|
||||
|
||||
# 如果不在缓存中,尝试从映射中获取
|
||||
if icon_name in self.icon_map:
|
||||
file_name = self.icon_map[icon_name]
|
||||
icon_path = os.path.join(self.icon_directory, file_name)
|
||||
else:
|
||||
# 直接使用文件名
|
||||
icon_path = os.path.join(self.icon_directory, icon_name)
|
||||
if not icon_name.endswith(('.png', '.jpg', '.jpeg', '.svg', '.ico')):
|
||||
icon_path += '.png' # 默认添加.png扩展名
|
||||
|
||||
# 尝试加载图标
|
||||
if os.path.exists(icon_path):
|
||||
try:
|
||||
icon = QIcon(icon_path)
|
||||
# 缓存图标
|
||||
self.icon_cache[icon_name] = icon
|
||||
print(f"✅ 动态加载图标: {icon_name} -> {os.path.basename(icon_path)}")
|
||||
|
||||
if size:
|
||||
pixmap = icon.pixmap(size)
|
||||
return QIcon(pixmap)
|
||||
return icon
|
||||
except Exception as e:
|
||||
print(f"❌ 加载图标失败: {icon_path}, 错误: {e}")
|
||||
else:
|
||||
print(f"⚠️ 图标文件不存在: {icon_path}")
|
||||
|
||||
# 返回默认图标
|
||||
return self.default_icon
|
||||
|
||||
def get_icon_path(self, icon_name: str) -> str:
|
||||
"""
|
||||
获取图标文件的完整路径
|
||||
|
||||
Args:
|
||||
icon_name: 图标名称
|
||||
|
||||
Returns:
|
||||
图标文件的完整路径
|
||||
"""
|
||||
if icon_name in self.icon_map:
|
||||
file_name = self.icon_map[icon_name]
|
||||
else:
|
||||
file_name = icon_name
|
||||
if not file_name.endswith(('.png', '.jpg', '.jpeg', '.svg', '.ico')):
|
||||
file_name += '.png'
|
||||
|
||||
icon_path = os.path.join(self.icon_directory, file_name)
|
||||
|
||||
if os.path.exists(icon_path):
|
||||
return icon_path
|
||||
else:
|
||||
print(f"⚠️ 图标文件不存在: {icon_path}")
|
||||
return ""
|
||||
|
||||
def has_icon(self, icon_name: str) -> bool:
|
||||
"""
|
||||
检查图标是否存在
|
||||
|
||||
Args:
|
||||
icon_name: 图标名称
|
||||
|
||||
Returns:
|
||||
是否存在
|
||||
"""
|
||||
return bool(self.get_icon_path(icon_name))
|
||||
|
||||
def add_icon(self, icon_name: str, icon_path: str) -> bool:
|
||||
"""
|
||||
添加新图标到缓存
|
||||
|
||||
Args:
|
||||
icon_name: 图标名称
|
||||
icon_path: 图标文件路径
|
||||
|
||||
Returns:
|
||||
是否添加成功
|
||||
"""
|
||||
try:
|
||||
if os.path.exists(icon_path):
|
||||
icon = QIcon(icon_path)
|
||||
self.icon_cache[icon_name] = icon
|
||||
print(f"✅ 已添加图标到缓存: {icon_name} -> {icon_path}")
|
||||
return True
|
||||
else:
|
||||
print(f"❌ 图标文件不存在: {icon_path}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"❌ 添加图标失败: {icon_name} -> {icon_path}, 错误: {e}")
|
||||
return False
|
||||
|
||||
def refresh_cache(self):
|
||||
"""刷新图标缓存"""
|
||||
print("🔄 刷新图标缓存...")
|
||||
self.icon_cache.clear()
|
||||
self._preload_icons()
|
||||
|
||||
def get_available_icons(self) -> list:
|
||||
"""获取所有可用的图标列表"""
|
||||
available_icons = []
|
||||
|
||||
# 添加预定义的图标
|
||||
available_icons.extend(self.icon_map.keys())
|
||||
|
||||
# 扫描图标目录中的所有图标文件
|
||||
if os.path.exists(self.icon_directory):
|
||||
for file_name in os.listdir(self.icon_directory):
|
||||
if file_name.lower().endswith(('.png', '.jpg', '.jpeg', '.svg', '.ico')):
|
||||
icon_name = os.path.splitext(file_name)[0]
|
||||
if icon_name not in available_icons:
|
||||
available_icons.append(icon_name)
|
||||
|
||||
return sorted(available_icons)
|
||||
|
||||
def get_cache_info(self) -> dict:
|
||||
"""获取缓存信息"""
|
||||
return {
|
||||
'cached_icons': len(self.icon_cache),
|
||||
'icon_directory': self.icon_directory,
|
||||
'available_icons': len(self.get_available_icons()),
|
||||
'cache_keys': list(self.icon_cache.keys())
|
||||
}
|
||||
|
||||
def debug_info(self):
|
||||
"""打印调试信息"""
|
||||
print("=" * 50)
|
||||
print("📋 图标管理器调试信息")
|
||||
print("=" * 50)
|
||||
|
||||
info = self.get_cache_info()
|
||||
print(f"图标目录: {info['icon_directory']}")
|
||||
print(f"目录存在: {os.path.exists(info['icon_directory'])}")
|
||||
print(f"缓存图标数: {info['cached_icons']}")
|
||||
print(f"可用图标数: {info['available_icons']}")
|
||||
|
||||
if info['cache_keys']:
|
||||
print("\n已缓存的图标:")
|
||||
for key in info['cache_keys']:
|
||||
print(f" - {key}")
|
||||
|
||||
print("\n图标目录内容:")
|
||||
if os.path.exists(self.icon_directory):
|
||||
for file_name in os.listdir(self.icon_directory):
|
||||
file_path = os.path.join(self.icon_directory, file_name)
|
||||
size = os.path.getsize(file_path) if os.path.isfile(file_path) else 0
|
||||
print(f" - {file_name} ({size} bytes)")
|
||||
else:
|
||||
print(" 目录不存在")
|
||||
|
||||
print("=" * 50)
|
||||
|
||||
|
||||
# 全局图标管理器实例
|
||||
_icon_manager = None
|
||||
|
||||
|
||||
def get_icon_manager() -> IconManager:
|
||||
"""获取全局图标管理器实例"""
|
||||
global _icon_manager
|
||||
if _icon_manager is None:
|
||||
_icon_manager = IconManager()
|
||||
return _icon_manager
|
||||
|
||||
|
||||
def get_icon(icon_name: str, size: Optional[QSize] = None) -> QIcon:
|
||||
"""便捷函数:获取图标"""
|
||||
return get_icon_manager().get_icon(icon_name, size)
|
||||
|
||||
|
||||
def get_icon_path(icon_name: str) -> str:
|
||||
"""便捷函数:获取图标路径"""
|
||||
return get_icon_manager().get_icon_path(icon_name)
|
||||
|
||||
|
||||
def has_icon(icon_name: str) -> bool:
|
||||
"""便捷函数:检查图标是否存在"""
|
||||
return get_icon_manager().has_icon(icon_name)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 测试代码
|
||||
print("🧪 测试图标管理器...")
|
||||
|
||||
manager = IconManager()
|
||||
manager.debug_info()
|
||||
|
||||
# 测试获取图标
|
||||
logo_icon = manager.get_icon('app_logo')
|
||||
print(f"\n📱 应用图标是否有效: {not logo_icon.isNull()}")
|
||||
|
||||
move_tool_icon = manager.get_icon('move_tool')
|
||||
print(f"🔧 移动工具图标是否有效: {not move_tool_icon.isNull()}")
|
||||
"""
|
||||
Icon manager compatibility layer for the ImGui-only runtime.
|
||||
|
||||
This module keeps the legacy public API so older imports do not break
|
||||
immediately.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional, Tuple
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class IconHandle:
|
||||
"""Lightweight icon placeholder used by legacy call sites."""
|
||||
|
||||
path: str = ""
|
||||
|
||||
def isNull(self) -> bool:
|
||||
return not self.path
|
||||
|
||||
def pixmap(self, _size: Optional[Tuple[int, int]] = None) -> "IconHandle":
|
||||
return self
|
||||
|
||||
|
||||
class IconManager:
|
||||
def __init__(self):
|
||||
self.icon_cache: Dict[str, IconHandle] = {}
|
||||
self.icon_directory = self._get_icon_directory()
|
||||
self.default_icon = IconHandle("")
|
||||
|
||||
self.icon_map = {
|
||||
"app_logo": "logo.png",
|
||||
"select_tool": "select_tool.png",
|
||||
"move_tool": "move_tool.png",
|
||||
"rotate_tool": "rotate_tool.png",
|
||||
"scale_tool": "scale_tool.png",
|
||||
"new_file": "new_file.png",
|
||||
"open_file": "open_file.png",
|
||||
"save_file": "save_file.png",
|
||||
"exit": "exit.png",
|
||||
"object_3d": "object_3d.png",
|
||||
"light": "light.png",
|
||||
"camera": "camera.png",
|
||||
"terrain": "terrain.png",
|
||||
"script": "script.png",
|
||||
"success": "success.png",
|
||||
"warning": "warning.png",
|
||||
"error": "error.png",
|
||||
"info": "info.png",
|
||||
"up_arrow": "up_arrows.png",
|
||||
"down_arrow": "down_arrows.png",
|
||||
"left_arrow": "left_arrows.png",
|
||||
"right_arrow": "right_arrows.png",
|
||||
"solid_down_arrows": "solid_down_arrows.png",
|
||||
"solid_right_arrows": "solid_right_arrows.png",
|
||||
"minimize_icon": "minimize_icon.png",
|
||||
"windowing_icon": "windowing_icon.png",
|
||||
"close_icon": "close_icon.png",
|
||||
"success_icon": "success_icon.png",
|
||||
"warning_icon": "warning_icon.png",
|
||||
"fail_icon": "delete_fail_icon.png",
|
||||
"property_select_image": "property_select_image.png",
|
||||
}
|
||||
|
||||
def _get_icon_directory(self) -> str:
|
||||
current_dir = Path(__file__).resolve().parent
|
||||
project_root = current_dir.parent
|
||||
icon_dir = project_root / "icons"
|
||||
return str(icon_dir)
|
||||
|
||||
def _resolve_icon_path(self, icon_name: str) -> Path:
|
||||
if icon_name in self.icon_map:
|
||||
file_name = self.icon_map[icon_name]
|
||||
else:
|
||||
file_name = icon_name
|
||||
if not file_name.lower().endswith((".png", ".jpg", ".jpeg", ".svg", ".ico")):
|
||||
file_name += ".png"
|
||||
return Path(self.icon_directory) / file_name
|
||||
|
||||
def get_icon(self, icon_name: str, _size: Optional[Tuple[int, int]] = None) -> IconHandle:
|
||||
if icon_name in self.icon_cache:
|
||||
return self.icon_cache[icon_name]
|
||||
|
||||
icon_path = self._resolve_icon_path(icon_name)
|
||||
if icon_path.exists():
|
||||
icon = IconHandle(str(icon_path))
|
||||
self.icon_cache[icon_name] = icon
|
||||
return icon
|
||||
return self.default_icon
|
||||
|
||||
def get_icon_path(self, icon_name: str) -> str:
|
||||
icon_path = self._resolve_icon_path(icon_name)
|
||||
return str(icon_path) if icon_path.exists() else ""
|
||||
|
||||
def has_icon(self, icon_name: str) -> bool:
|
||||
return bool(self.get_icon_path(icon_name))
|
||||
|
||||
def add_icon(self, icon_name: str, icon_path: str) -> bool:
|
||||
path_obj = Path(icon_path)
|
||||
if not path_obj.exists():
|
||||
return False
|
||||
self.icon_cache[icon_name] = IconHandle(str(path_obj))
|
||||
return True
|
||||
|
||||
def refresh_cache(self):
|
||||
self.icon_cache.clear()
|
||||
|
||||
def get_available_icons(self) -> list:
|
||||
available_icons = sorted(self.icon_map.keys())
|
||||
icon_dir = Path(self.icon_directory)
|
||||
if icon_dir.exists():
|
||||
for p in icon_dir.iterdir():
|
||||
if p.suffix.lower() in {".png", ".jpg", ".jpeg", ".svg", ".ico"}:
|
||||
name = p.stem
|
||||
if name not in available_icons:
|
||||
available_icons.append(name)
|
||||
return sorted(available_icons)
|
||||
|
||||
def get_cache_info(self) -> dict:
|
||||
return {
|
||||
"cached_icons": len(self.icon_cache),
|
||||
"icon_directory": self.icon_directory,
|
||||
"available_icons": len(self.get_available_icons()),
|
||||
"cache_keys": list(self.icon_cache.keys()),
|
||||
}
|
||||
|
||||
def debug_info(self):
|
||||
info = self.get_cache_info()
|
||||
print(f"icon_directory: {info['icon_directory']}")
|
||||
print(f"cached_icons: {info['cached_icons']}")
|
||||
print(f"available_icons: {info['available_icons']}")
|
||||
|
||||
|
||||
_icon_manager: Optional[IconManager] = None
|
||||
|
||||
|
||||
def get_icon_manager() -> IconManager:
|
||||
global _icon_manager
|
||||
if _icon_manager is None:
|
||||
_icon_manager = IconManager()
|
||||
return _icon_manager
|
||||
|
||||
|
||||
def get_icon(icon_name: str, size: Optional[Tuple[int, int]] = None) -> IconHandle:
|
||||
return get_icon_manager().get_icon(icon_name, size)
|
||||
|
||||
|
||||
def get_icon_path(icon_name: str) -> str:
|
||||
return get_icon_manager().get_icon_path(icon_name)
|
||||
|
||||
|
||||
def has_icon(icon_name: str) -> bool:
|
||||
return get_icon_manager().has_icon(icon_name)
|
||||
|
||||
2590
ui/lui_function.py
2590
ui/lui_function.py
File diff suppressed because it is too large
Load Diff
3255
ui/lui_manager.py
3255
ui/lui_manager.py
File diff suppressed because it is too large
Load Diff
6724
ui/main_window.py
6724
ui/main_window.py
File diff suppressed because it is too large
Load Diff
@ -3,7 +3,7 @@ from pathlib import Path
|
||||
|
||||
from direct.actor.Actor import Actor
|
||||
from direct.task.TaskManagerGlobal import taskMgr
|
||||
from panda3d.core import NodePath, PartSubset
|
||||
from panda3d.core import NodePath, PartSubset, Filename
|
||||
|
||||
|
||||
class _BoundAnimationProxy:
|
||||
@ -652,15 +652,8 @@ class AnimationTools:
|
||||
return node
|
||||
|
||||
|
||||
def _getActor(self, origin_model):
|
||||
"""
|
||||
获取或创建模型的Actor,用于动画控制
|
||||
复用Qt版本经过验证的实现方式
|
||||
"""
|
||||
owner_model = self._resolve_animation_owner_model(origin_model)
|
||||
owner_model = self._prefer_owner_with_visible_geometry(origin_model, owner_model)
|
||||
|
||||
# owner 节点缺失路径标签时,尝试从当前节点及其祖先继承路径,避免只能走内存分支
|
||||
def _sync_owner_model_path_tags(self, origin_model, owner_model):
|
||||
# Backfill model path tags from origin node or ancestor chain.
|
||||
try:
|
||||
needs_path = (not owner_model.hasTag("model_path")) or (not owner_model.getTag("model_path"))
|
||||
if needs_path:
|
||||
@ -681,7 +674,7 @@ class AnimationTools:
|
||||
break
|
||||
walker = parent
|
||||
|
||||
# 仍缺路径时,尝试更激进的标签恢复
|
||||
# Try aggressive recovery when path tags are still missing.
|
||||
if (not owner_model.hasTag("model_path")) or (not owner_model.getTag("model_path")):
|
||||
recovered = self._recover_model_path_from_tags(owner_model)
|
||||
if recovered:
|
||||
@ -691,213 +684,215 @@ class AnimationTools:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _cleanup_actor(actor):
|
||||
def _cleanup_actor_instance(self, actor):
|
||||
try:
|
||||
if actor is None:
|
||||
return
|
||||
if hasattr(actor, "cleanup"):
|
||||
actor.cleanup()
|
||||
if hasattr(actor, "removeNode"):
|
||||
actor.removeNode()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _try_create_actor_from_source(self, source, source_desc, owner_model):
|
||||
try:
|
||||
resolved_source = source
|
||||
if isinstance(source, (str, os.PathLike)):
|
||||
src_text = os.fspath(source)
|
||||
resolved_source = Filename.from_os_specific(src_text).get_fullpath()
|
||||
actor = Actor(resolved_source)
|
||||
|
||||
try:
|
||||
if actor is None:
|
||||
return
|
||||
if hasattr(actor, "cleanup"):
|
||||
actor.cleanup()
|
||||
if hasattr(actor, "removeNode"):
|
||||
actor.removeNode()
|
||||
actor.bindAllAnims(allowAsyncBind=False)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _try_create_actor_from_source(source, source_desc):
|
||||
try:
|
||||
actor = Actor(source)
|
||||
# 无论是否已检测到动画名,都显式绑定一次,避免“有名字但无可播放控制”
|
||||
anims = actor.getAnimNames()
|
||||
print(f"[ActorLoad] {source_desc} anims: {anims}")
|
||||
if not anims:
|
||||
self._cleanup_actor_instance(actor)
|
||||
return None
|
||||
|
||||
playable = False
|
||||
for anim_name in anims:
|
||||
try:
|
||||
actor.bindAllAnims(allowAsyncBind=False)
|
||||
control = actor.getAnimControl(anim_name)
|
||||
if control and control.getNumFrames() > 1:
|
||||
playable = True
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if not playable:
|
||||
print(f"[ActorLoad] {source_desc} has no playable controls")
|
||||
self._cleanup_actor_instance(actor)
|
||||
return None
|
||||
|
||||
has_geom = False
|
||||
try:
|
||||
has_geom = actor.findAllMatches("**/+GeomNode").getNumPaths() > 0
|
||||
except Exception:
|
||||
has_geom = False
|
||||
if not has_geom:
|
||||
print(f"[ActorLoad] {source_desc} has no visible geometry")
|
||||
self._cleanup_actor_instance(actor)
|
||||
return None
|
||||
|
||||
actor.reparentTo(self._get_owner_parent_node(owner_model))
|
||||
actor.hide()
|
||||
return actor
|
||||
except Exception as e:
|
||||
print(f"[ActorLoad] {source_desc} failed: {e}")
|
||||
return None
|
||||
|
||||
def _try_create_autobind_proxy(self, model_np, source_desc, owns_node=False):
|
||||
try:
|
||||
from panda3d.core import AnimControlCollection, autoBind
|
||||
|
||||
controls = {}
|
||||
|
||||
def collect_controls(np):
|
||||
if not np or np.isEmpty():
|
||||
return
|
||||
try:
|
||||
acc = AnimControlCollection()
|
||||
autoBind(np.node(), acc, ~0)
|
||||
for i in range(acc.getNumAnims()):
|
||||
name = acc.getAnimName(i) or f"anim_{i}"
|
||||
control = acc.getAnim(i)
|
||||
if not control:
|
||||
continue
|
||||
if name in controls:
|
||||
try:
|
||||
old_frames = controls[name].getNumFrames()
|
||||
except Exception:
|
||||
old_frames = -1
|
||||
try:
|
||||
new_frames = control.getNumFrames()
|
||||
except Exception:
|
||||
new_frames = -1
|
||||
if new_frames >= old_frames:
|
||||
controls[name] = control
|
||||
else:
|
||||
controls[name] = control
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
anims = actor.getAnimNames()
|
||||
print(f"[Actor加载] {source_desc} 检测到动画: {anims}")
|
||||
if not anims:
|
||||
_cleanup_actor(actor)
|
||||
return None
|
||||
collect_controls(model_np)
|
||||
character_nodes = model_np.findAllMatches("**/+Character")
|
||||
for i in range(character_nodes.getNumPaths()):
|
||||
collect_controls(character_nodes.getPath(i))
|
||||
|
||||
# 确认至少有一个可用的 AnimControl
|
||||
playable = False
|
||||
for anim_name in anims:
|
||||
if not controls and character_nodes.getNumPaths() > 0:
|
||||
anim_bundle_nodes = model_np.findAllMatches("**/+AnimBundleNode")
|
||||
subset = PartSubset()
|
||||
for ci in range(character_nodes.getNumPaths()):
|
||||
try:
|
||||
control = actor.getAnimControl(anim_name)
|
||||
if control and control.getNumFrames() > 1:
|
||||
playable = True
|
||||
break
|
||||
character = character_nodes.getPath(ci).node()
|
||||
for bi in range(character.getNumBundles()):
|
||||
part_bundle = character.getBundle(bi)
|
||||
for ai in range(anim_bundle_nodes.getNumPaths()):
|
||||
try:
|
||||
anim_bundle_node = anim_bundle_nodes.getPath(ai).node()
|
||||
anim_bundle = anim_bundle_node.getBundle()
|
||||
if not anim_bundle:
|
||||
continue
|
||||
anim_name = anim_bundle.getName() or anim_bundle_node.getName() or f"anim_{ai}"
|
||||
control = part_bundle.bindAnim(anim_bundle, -1, subset)
|
||||
if not control:
|
||||
continue
|
||||
if anim_name in controls:
|
||||
try:
|
||||
old_frames = controls[anim_name].getNumFrames()
|
||||
except Exception:
|
||||
old_frames = -1
|
||||
try:
|
||||
new_frames = control.getNumFrames()
|
||||
except Exception:
|
||||
new_frames = -1
|
||||
if new_frames >= old_frames:
|
||||
controls[anim_name] = control
|
||||
else:
|
||||
controls[anim_name] = control
|
||||
except Exception:
|
||||
continue
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if not playable:
|
||||
print(f"[Actor加载] {source_desc} 动画控制无效,尝试其他加载路径")
|
||||
_cleanup_actor(actor)
|
||||
return None
|
||||
if not controls:
|
||||
return None
|
||||
|
||||
# 无可见几何体的 Actor 会导致“播放后什么都看不到”
|
||||
try:
|
||||
has_geom = model_np.findAllMatches("**/+GeomNode").getNumPaths() > 0
|
||||
except Exception:
|
||||
has_geom = False
|
||||
try:
|
||||
has_geom = actor.findAllMatches("**/+GeomNode").getNumPaths() > 0
|
||||
except Exception:
|
||||
has_geom = False
|
||||
if not has_geom:
|
||||
print(f"[Actor加载] {source_desc} 无可见几何体,尝试其他加载路径")
|
||||
_cleanup_actor(actor)
|
||||
return None
|
||||
|
||||
actor.reparentTo(self._get_owner_parent_node(owner_model))
|
||||
actor.hide()
|
||||
return actor
|
||||
except Exception as e:
|
||||
print(f"[Actor加载] {source_desc} 失败: {e}")
|
||||
if not has_geom:
|
||||
print(f"[ActorLoad] {source_desc} autobind has no visible geometry")
|
||||
return None
|
||||
|
||||
def _try_create_autobind_proxy(model_np, source_desc, owns_node=False):
|
||||
try:
|
||||
from panda3d.core import AnimControlCollection, autoBind
|
||||
|
||||
controls = {}
|
||||
|
||||
def collect_controls(np):
|
||||
if not np or np.isEmpty():
|
||||
return
|
||||
try:
|
||||
acc = AnimControlCollection()
|
||||
autoBind(np.node(), acc, ~0)
|
||||
for i in range(acc.getNumAnims()):
|
||||
name = acc.getAnimName(i) or f"anim_{i}"
|
||||
control = acc.getAnim(i)
|
||||
if not control:
|
||||
continue
|
||||
if name in controls:
|
||||
try:
|
||||
old_frames = controls[name].getNumFrames()
|
||||
except Exception:
|
||||
old_frames = -1
|
||||
try:
|
||||
new_frames = control.getNumFrames()
|
||||
except Exception:
|
||||
new_frames = -1
|
||||
# 重名时保留帧数更多的控制,避免默认选到“看起来无效”的同名动画
|
||||
if new_frames >= old_frames:
|
||||
controls[name] = control
|
||||
else:
|
||||
controls[name] = control
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 先在根节点尝试,再补扫 Character 节点
|
||||
collect_controls(model_np)
|
||||
character_nodes = model_np.findAllMatches("**/+Character")
|
||||
for i in range(character_nodes.getNumPaths()):
|
||||
collect_controls(character_nodes.getPath(i))
|
||||
|
||||
# autoBind 无结果时,尝试手动把 AnimBundle 绑定到 Character 的 PartBundle
|
||||
if not controls and character_nodes.getNumPaths() > 0:
|
||||
anim_bundle_nodes = model_np.findAllMatches("**/+AnimBundleNode")
|
||||
subset = PartSubset()
|
||||
for ci in range(character_nodes.getNumPaths()):
|
||||
try:
|
||||
character = character_nodes.getPath(ci).node()
|
||||
for bi in range(character.getNumBundles()):
|
||||
part_bundle = character.getBundle(bi)
|
||||
for ai in range(anim_bundle_nodes.getNumPaths()):
|
||||
try:
|
||||
anim_bundle_node = anim_bundle_nodes.getPath(ai).node()
|
||||
anim_bundle = anim_bundle_node.getBundle()
|
||||
if not anim_bundle:
|
||||
continue
|
||||
anim_name = anim_bundle.getName() or anim_bundle_node.getName() or f"anim_{ai}"
|
||||
control = part_bundle.bindAnim(anim_bundle, -1, subset)
|
||||
if not control:
|
||||
continue
|
||||
if anim_name in controls:
|
||||
try:
|
||||
old_frames = controls[anim_name].getNumFrames()
|
||||
except Exception:
|
||||
old_frames = -1
|
||||
try:
|
||||
new_frames = control.getNumFrames()
|
||||
except Exception:
|
||||
new_frames = -1
|
||||
if new_frames >= old_frames:
|
||||
controls[anim_name] = control
|
||||
else:
|
||||
controls[anim_name] = control
|
||||
except Exception:
|
||||
continue
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if not controls:
|
||||
return None
|
||||
|
||||
# 仅骨骼无几何体时会出现“能触发播放但场景不可见”
|
||||
try:
|
||||
has_geom = model_np.findAllMatches("**/+GeomNode").getNumPaths() > 0
|
||||
except Exception:
|
||||
has_geom = False
|
||||
if not has_geom:
|
||||
print(f"[Actor加载] {source_desc} autoBind 检测到动画但无可见几何体,尝试其他节点")
|
||||
return None
|
||||
|
||||
print(f"[Actor加载] {source_desc} autoBind 检测到动画: {list(controls.keys())}")
|
||||
return _BoundAnimationProxy(model_np, controls, owns_node=owns_node)
|
||||
except Exception as e:
|
||||
print(f"[Actor加载] {source_desc} autoBind 失败: {e}")
|
||||
return None
|
||||
|
||||
def _try_create_actor_via_gltf_path(path):
|
||||
"""针对部分 GLB/GLTF 文件,使用 gltf 插件强制加载动画后再创建 Actor。"""
|
||||
lower_path = str(path).lower()
|
||||
if not (lower_path.endswith(".glb") or lower_path.endswith(".gltf")):
|
||||
return None
|
||||
|
||||
model_np = None
|
||||
succeeded = False
|
||||
try:
|
||||
import gltf
|
||||
from panda3d.core import Filename
|
||||
|
||||
panda_path = Filename.from_os_specific(path).get_fullpath()
|
||||
os_path = Filename(panda_path).to_os_specific()
|
||||
settings = gltf.GltfSettings(skip_animations=False)
|
||||
model_root = gltf.load_model(os_path, settings)
|
||||
if not model_root:
|
||||
return None
|
||||
|
||||
model_np = NodePath(model_root)
|
||||
actor = _try_create_actor_from_source(model_np, f"GLTF专用加载({path})")
|
||||
if actor:
|
||||
succeeded = True
|
||||
return actor
|
||||
|
||||
proxy = _try_create_autobind_proxy(model_np, f"GLTF专用加载({path})", owns_node=True)
|
||||
if proxy:
|
||||
proxy.reparentTo(self._get_owner_parent_node(owner_model))
|
||||
proxy.hide()
|
||||
succeeded = True
|
||||
return proxy
|
||||
except Exception as e:
|
||||
print(f"[Actor加载] GLTF专用加载失败 ({path}): {e}")
|
||||
finally:
|
||||
# 仅在失败且本地临时节点仍存在时清理,避免泄漏
|
||||
try:
|
||||
if (not succeeded) and model_np and not model_np.isEmpty():
|
||||
model_np.removeNode()
|
||||
except Exception:
|
||||
pass
|
||||
print(f"[ActorLoad] {source_desc} autobind anims: {list(controls.keys())}")
|
||||
return _BoundAnimationProxy(model_np, controls, owns_node=owns_node)
|
||||
except Exception as e:
|
||||
print(f"[ActorLoad] {source_desc} autobind failed: {e}")
|
||||
return None
|
||||
|
||||
# 检查缓存(无效缓存自动清理,避免“无动画”结果被永久缓存)
|
||||
def _try_create_actor_via_gltf_path(self, path, owner_model):
|
||||
"""GLTF-specific path loader fallback for animation extraction."""
|
||||
lower_path = str(path).lower()
|
||||
if not (lower_path.endswith(".glb") or lower_path.endswith(".gltf")):
|
||||
return None
|
||||
|
||||
model_np = None
|
||||
succeeded = False
|
||||
try:
|
||||
import gltf
|
||||
|
||||
panda_path = Filename.from_os_specific(path).get_fullpath()
|
||||
os_path = Filename(panda_path).to_os_specific()
|
||||
settings = gltf.GltfSettings(skip_animations=False)
|
||||
model_root = gltf.load_model(os_path, settings)
|
||||
if not model_root:
|
||||
return None
|
||||
|
||||
model_np = NodePath(model_root)
|
||||
actor = self._try_create_actor_from_source(model_np, f"GLTF-special({path})", owner_model)
|
||||
if actor:
|
||||
succeeded = True
|
||||
return actor
|
||||
|
||||
proxy = self._try_create_autobind_proxy(model_np, f"GLTF-special({path})", owns_node=True)
|
||||
if proxy:
|
||||
proxy.reparentTo(self._get_owner_parent_node(owner_model))
|
||||
proxy.hide()
|
||||
succeeded = True
|
||||
return proxy
|
||||
except Exception as e:
|
||||
print(f"[ActorLoad] GLTF-special failed ({path}): {e}")
|
||||
finally:
|
||||
try:
|
||||
if (not succeeded) and model_np and not model_np.isEmpty():
|
||||
model_np.removeNode()
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
def _finalize_actor_cache(self, owner_model, actor, model_path=""):
|
||||
if model_path:
|
||||
owner_model.setTag("model_path", model_path)
|
||||
owner_model.setTag("has_animations", "true")
|
||||
self._actor_cache[owner_model] = actor
|
||||
return actor
|
||||
|
||||
def _get_valid_cached_actor(self, owner_model):
|
||||
cached_actor = self._actor_cache.get(owner_model)
|
||||
if cached_actor:
|
||||
# 若已获得模型路径,优先重建真实 Actor,避免长期停留在 autoBind 代理导致“能触发但不可见”
|
||||
try:
|
||||
owner_has_path = owner_model.hasTag("model_path") and bool(owner_model.getTag("model_path"))
|
||||
except Exception:
|
||||
owner_has_path = False
|
||||
if owner_has_path and isinstance(cached_actor, _BoundAnimationProxy):
|
||||
_cleanup_actor(cached_actor)
|
||||
self._cleanup_actor_instance(cached_actor)
|
||||
self._actor_cache.pop(owner_model, None)
|
||||
cached_actor = None
|
||||
|
||||
@ -916,10 +911,11 @@ class AnimationTools:
|
||||
return cached_actor
|
||||
except Exception:
|
||||
pass
|
||||
_cleanup_actor(cached_actor)
|
||||
self._cleanup_actor_instance(cached_actor)
|
||||
self._actor_cache.pop(owner_model, None)
|
||||
return None
|
||||
|
||||
# 先检测模型树中的动画结构,并写入标签供属性面板快速判断
|
||||
def _detect_animation_nodes(self, owner_model):
|
||||
try:
|
||||
character_nodes = owner_model.findAllMatches("**/+Character")
|
||||
anim_bundle_nodes = owner_model.findAllMatches("**/+AnimBundleNode")
|
||||
@ -930,160 +926,130 @@ class AnimationTools:
|
||||
owner_model.setTag("has_animations", "true" if has_animation_nodes else "false")
|
||||
if has_animation_nodes:
|
||||
owner_model.setTag("can_create_actor_from_memory", "true")
|
||||
return has_animation_nodes
|
||||
except Exception:
|
||||
has_animation_nodes = False
|
||||
return False
|
||||
|
||||
def _try_memory_fallback():
|
||||
def _collect_autobind_source_candidates():
|
||||
candidates = []
|
||||
def _collect_autobind_source_candidates(self, origin_model, owner_model):
|
||||
candidates = []
|
||||
|
||||
def add_candidate(node):
|
||||
try:
|
||||
if not node or node.isEmpty() or self._is_scene_root_node(node):
|
||||
return
|
||||
for existing in candidates:
|
||||
if existing == node:
|
||||
return
|
||||
candidates.append(node)
|
||||
except Exception:
|
||||
def add_candidate(node):
|
||||
try:
|
||||
if not node or node.isEmpty() or self._is_scene_root_node(node):
|
||||
return
|
||||
for existing in candidates:
|
||||
if existing == node:
|
||||
return
|
||||
candidates.append(node)
|
||||
except Exception:
|
||||
return
|
||||
|
||||
add_candidate(owner_model)
|
||||
add_candidate(origin_model)
|
||||
add_candidate(self._prefer_owner_with_visible_geometry(origin_model, owner_model))
|
||||
add_candidate(self._find_scene_model_owner(origin_model))
|
||||
add_candidate(self._find_scene_model_owner(owner_model))
|
||||
add_candidate(owner_model)
|
||||
add_candidate(origin_model)
|
||||
add_candidate(self._prefer_owner_with_visible_geometry(origin_model, owner_model))
|
||||
add_candidate(self._find_scene_model_owner(origin_model))
|
||||
add_candidate(self._find_scene_model_owner(owner_model))
|
||||
|
||||
# 祖先链补齐
|
||||
for seed in (origin_model, owner_model):
|
||||
current = seed
|
||||
for _ in range(48):
|
||||
if not current or current.isEmpty() or self._is_scene_root_node(current):
|
||||
break
|
||||
add_candidate(current)
|
||||
parent = current.getParent()
|
||||
if not parent or parent.isEmpty() or parent == current:
|
||||
break
|
||||
current = parent
|
||||
for seed in (origin_model, owner_model):
|
||||
current = seed
|
||||
for _ in range(48):
|
||||
if not current or current.isEmpty() or self._is_scene_root_node(current):
|
||||
break
|
||||
add_candidate(current)
|
||||
parent = current.getParent()
|
||||
if not parent or parent.isEmpty() or parent == current:
|
||||
break
|
||||
current = parent
|
||||
|
||||
# scene_manager.models 补齐候选
|
||||
scene_manager = getattr(self, "scene_manager", None)
|
||||
models = getattr(scene_manager, "models", None) if scene_manager else None
|
||||
if models:
|
||||
for model in list(models):
|
||||
add_candidate(model)
|
||||
scene_manager = getattr(self, "scene_manager", None)
|
||||
models = getattr(scene_manager, "models", None) if scene_manager else None
|
||||
if models:
|
||||
for model in list(models):
|
||||
add_candidate(model)
|
||||
|
||||
def score(node):
|
||||
try:
|
||||
has_geom = self._node_has_geom(node)
|
||||
has_anim = self._node_has_animation_nodes(node)
|
||||
has_path = (
|
||||
(node.hasTag("model_path") and bool(node.getTag("model_path"))) or
|
||||
(node.hasTag("saved_model_path") and bool(node.getTag("saved_model_path"))) or
|
||||
(node.hasTag("original_path") and bool(node.getTag("original_path")))
|
||||
)
|
||||
related = False
|
||||
try:
|
||||
related = (
|
||||
node == owner_model or
|
||||
node == origin_model or
|
||||
node.isAncestorOf(origin_model) or
|
||||
origin_model.isAncestorOf(node) or
|
||||
node.isAncestorOf(owner_model) or
|
||||
owner_model.isAncestorOf(node)
|
||||
)
|
||||
except Exception:
|
||||
related = False
|
||||
|
||||
s = 0
|
||||
s += 220 if has_geom and has_anim else 0
|
||||
s += 120 if has_geom else 0
|
||||
s += 45 if has_anim else 0
|
||||
s += 35 if has_path else 0
|
||||
s += 40 if node == owner_model else 0
|
||||
s += 30 if node == origin_model else 0
|
||||
s += 25 if related else 0
|
||||
return s
|
||||
except Exception:
|
||||
return -1
|
||||
|
||||
candidates.sort(key=score, reverse=True)
|
||||
return candidates
|
||||
|
||||
can_create_from_memory = False
|
||||
if owner_model.hasTag("can_create_actor_from_memory"):
|
||||
can_create_from_memory = owner_model.getTag("can_create_actor_from_memory").lower() == "true"
|
||||
if not can_create_from_memory and has_animation_nodes:
|
||||
can_create_from_memory = True
|
||||
|
||||
if can_create_from_memory:
|
||||
# 不能直接 Actor(owner_model);会污染当前场景节点,导致播放后模型消失/选择失效。
|
||||
# 先用副本创建真实 Actor,只有失败时才退回 autoBind 代理。
|
||||
clone_parent = self._get_owner_parent_node(owner_model)
|
||||
clone_np = None
|
||||
def score(node):
|
||||
try:
|
||||
has_geom = self._node_has_geom(node)
|
||||
has_anim = self._node_has_animation_nodes(node)
|
||||
has_path = (
|
||||
(node.hasTag("model_path") and bool(node.getTag("model_path"))) or
|
||||
(node.hasTag("saved_model_path") and bool(node.getTag("saved_model_path"))) or
|
||||
(node.hasTag("original_path") and bool(node.getTag("original_path")))
|
||||
)
|
||||
related = False
|
||||
try:
|
||||
clone_np = owner_model.copyTo(clone_parent)
|
||||
clone_np.setName(f"{owner_model.getName()}__anim_runtime")
|
||||
mem_actor = _try_create_actor_from_source(clone_np, "内存模型副本")
|
||||
if mem_actor:
|
||||
self._actor_cache[owner_model] = mem_actor
|
||||
owner_model.setTag("has_animations", "true")
|
||||
return mem_actor
|
||||
except Exception as e:
|
||||
print(f"[Actor加载] 创建内存模型副本失败: {e}")
|
||||
finally:
|
||||
# _try_create_actor_from_source 失败时,清理临时副本
|
||||
try:
|
||||
if clone_np and not clone_np.isEmpty() and owner_model not in self._actor_cache:
|
||||
clone_np.removeNode()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Actor 副本失败后,从多个候选节点中选择“带几何体”的 autoBind 源
|
||||
for source_node in _collect_autobind_source_candidates():
|
||||
mem_proxy = _try_create_autobind_proxy(
|
||||
source_node,
|
||||
f"内存模型({source_node.getName()})",
|
||||
owns_node=False
|
||||
related = (
|
||||
node == owner_model or
|
||||
node == origin_model or
|
||||
node.isAncestorOf(origin_model) or
|
||||
origin_model.isAncestorOf(node) or
|
||||
node.isAncestorOf(owner_model) or
|
||||
owner_model.isAncestorOf(node)
|
||||
)
|
||||
if mem_proxy:
|
||||
self._actor_cache[owner_model] = mem_proxy
|
||||
owner_model.setTag("has_animations", "true")
|
||||
if source_node != owner_model:
|
||||
print(f"[Actor加载] 使用可见节点作为动画源: {owner_model.getName()} -> {source_node.getName()}")
|
||||
return mem_proxy
|
||||
return None
|
||||
except Exception:
|
||||
related = False
|
||||
|
||||
# 始终优先尝试从文件路径加载,因为底层 gltf 插件只有在加载文件时才能抽取完整的动画名称。
|
||||
print(f"[Actor加载调试] 传入的 origin_model: {origin_model.getName() if origin_model else 'None'}")
|
||||
print(f"[Actor加载调试] origin_model tags: {origin_model.getTags() if origin_model else 'None'}")
|
||||
print(f"[Actor加载调试] 解析出的 owner_model: {owner_model.getName() if owner_model else 'None'}")
|
||||
try:
|
||||
print(f"[Actor加载调试] owner_model tags: {owner_model.getTags() if owner_model else 'None'}")
|
||||
print(f"[Actor加载调试] owner_model path tag: {owner_model.getTag('model_path') if owner_model.hasTag('model_path') else 'MISSING'}")
|
||||
except Exception as e:
|
||||
print(f"[Actor加载调试] 获取 tags 异常: {e}")
|
||||
s = 0
|
||||
s += 220 if has_geom and has_anim else 0
|
||||
s += 120 if has_geom else 0
|
||||
s += 45 if has_anim else 0
|
||||
s += 35 if has_path else 0
|
||||
s += 40 if node == owner_model else 0
|
||||
s += 30 if node == origin_model else 0
|
||||
s += 25 if related else 0
|
||||
return s
|
||||
except Exception:
|
||||
return -1
|
||||
|
||||
filepath = owner_model.getTag("model_path") if owner_model.hasTag("model_path") else ""
|
||||
if not filepath and owner_model.hasTag("original_path"):
|
||||
filepath = owner_model.getTag("original_path")
|
||||
|
||||
print(f"[Actor加载调试] 获取到的 filepath: '{filepath}'")
|
||||
if not filepath:
|
||||
print(f"[Actor加载调试] filepath为空,触发 _try_memory_fallback()")
|
||||
return _try_memory_fallback()
|
||||
candidates.sort(key=score, reverse=True)
|
||||
return candidates
|
||||
|
||||
# 针对Actor加载,必须使用Panda3D规范的Unix风格路径,否则Windows绝对路径会导致加载彻底崩溃并返回空节点
|
||||
def _try_memory_fallback_actor(self, origin_model, owner_model, has_animation_nodes):
|
||||
can_create_from_memory = False
|
||||
if owner_model.hasTag("can_create_actor_from_memory"):
|
||||
can_create_from_memory = owner_model.getTag("can_create_actor_from_memory").lower() == "true"
|
||||
if not can_create_from_memory and has_animation_nodes:
|
||||
can_create_from_memory = True
|
||||
|
||||
if can_create_from_memory:
|
||||
clone_parent = self._get_owner_parent_node(owner_model)
|
||||
clone_np = None
|
||||
try:
|
||||
clone_np = owner_model.copyTo(clone_parent)
|
||||
clone_np.setName(f"{owner_model.getName()}__anim_runtime")
|
||||
mem_actor = self._try_create_actor_from_source(clone_np, "memory-clone", owner_model)
|
||||
if mem_actor:
|
||||
return self._finalize_actor_cache(owner_model, mem_actor)
|
||||
except Exception as e:
|
||||
print(f"[ActorLoad] memory-clone failed: {e}")
|
||||
finally:
|
||||
try:
|
||||
if clone_np and not clone_np.isEmpty() and owner_model not in self._actor_cache:
|
||||
clone_np.removeNode()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
for source_node in self._collect_autobind_source_candidates(origin_model, owner_model):
|
||||
mem_proxy = self._try_create_autobind_proxy(
|
||||
source_node,
|
||||
f"memory-node({source_node.getName()})",
|
||||
owns_node=False
|
||||
)
|
||||
if mem_proxy:
|
||||
if source_node != owner_model:
|
||||
print(f"[ActorLoad] use visible source: {owner_model.getName()} -> {source_node.getName()}")
|
||||
return self._finalize_actor_cache(owner_model, mem_proxy)
|
||||
return None
|
||||
|
||||
def _collect_actor_candidate_paths(self, filepath):
|
||||
panda_specific_path = ""
|
||||
try:
|
||||
from panda3d.core import Filename
|
||||
panda_specific_path = Filename.from_os_specific(filepath).get_fullpath()
|
||||
except:
|
||||
except Exception:
|
||||
panda_specific_path = filepath.replace('\\', '/')
|
||||
|
||||
candidate_paths = [panda_specific_path, filepath, os.path.normpath(filepath)]
|
||||
|
||||
# 处理 /d/... 这类路径在 Windows 上无法直接访问的问题 (补充兜底OS路径)
|
||||
|
||||
if filepath.startswith("/") and os.name == "nt":
|
||||
parts = filepath.split("/")
|
||||
if len(parts) > 2 and len(parts[1]) == 1:
|
||||
@ -1091,20 +1057,17 @@ class AnimationTools:
|
||||
candidate_paths.append(win_path)
|
||||
candidate_paths.append(os.path.normpath(win_path))
|
||||
|
||||
# 尝试 Panda3D 路径标准化
|
||||
try:
|
||||
from scene import util
|
||||
candidate_paths.append(util.normalize_model_path(filepath))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 在 Resources/models 中按文件名兜底查找
|
||||
filename = os.path.basename(filepath)
|
||||
if filename:
|
||||
candidate_paths.append(str(Path(__file__).resolve().parents[2] / "Resources" / "models" / filename))
|
||||
candidate_paths.append(str(Path(__file__).resolve().parents[2] / "Resources" / filename))
|
||||
|
||||
# 去重并优先使用真实存在的路径 (同时确保panda专属路径排在第一位尝试)
|
||||
unique_paths = []
|
||||
seen = set()
|
||||
for p in candidate_paths:
|
||||
@ -1116,46 +1079,73 @@ class AnimationTools:
|
||||
seen.add(key)
|
||||
unique_paths.append(p)
|
||||
|
||||
# 过滤时注意如果是以 / 开头的 Panda 路径,os.path.exists 可能判断为 False,所以要额外豁免 panda_specific_path 保底加载
|
||||
existing_paths = [p for p in unique_paths if os.path.exists(p) or p == panda_specific_path]
|
||||
load_paths = existing_paths + [p for p in unique_paths if p not in existing_paths]
|
||||
return existing_paths + [p for p in unique_paths if p not in existing_paths]
|
||||
|
||||
for p in load_paths:
|
||||
print(f"[Actor加载验证] 正在尝试通过路径读取骨骼和动画文件: {p}")
|
||||
actor = _try_create_actor_from_source(p, f"文件路径({p})")
|
||||
def _try_actor_from_path(self, owner_model, path):
|
||||
actor = self._try_create_actor_from_source(path, f"file-path({path})", owner_model)
|
||||
if actor:
|
||||
return self._finalize_actor_cache(owner_model, actor, model_path=path)
|
||||
|
||||
actor = self._try_create_actor_via_gltf_path(path, owner_model)
|
||||
if actor:
|
||||
return self._finalize_actor_cache(owner_model, actor, model_path=path)
|
||||
|
||||
try:
|
||||
model_source = path
|
||||
if isinstance(path, (str, os.PathLike)):
|
||||
model_source = Filename.from_os_specific(os.fspath(path))
|
||||
loaded_model = self.loader.loadModel(model_source)
|
||||
if loaded_model and not loaded_model.isEmpty():
|
||||
proxy = self._try_create_autobind_proxy(loaded_model, f"file-path({path})", owns_node=True)
|
||||
if proxy:
|
||||
loaded_model.reparentTo(self.render)
|
||||
loaded_model.hide()
|
||||
return self._finalize_actor_cache(owner_model, proxy, model_path=path)
|
||||
loaded_model.removeNode()
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
def _getActor(self, origin_model):
|
||||
"""
|
||||
Get or create Actor for animation control.
|
||||
"""
|
||||
owner_model = self._resolve_animation_owner_model(origin_model)
|
||||
owner_model = self._prefer_owner_with_visible_geometry(origin_model, owner_model)
|
||||
self._sync_owner_model_path_tags(origin_model, owner_model)
|
||||
|
||||
cached_actor = self._get_valid_cached_actor(owner_model)
|
||||
if cached_actor:
|
||||
return cached_actor
|
||||
|
||||
has_animation_nodes = self._detect_animation_nodes(owner_model)
|
||||
|
||||
print(f"[ActorLoadDebug] origin_model: {origin_model.getName() if origin_model else 'None'}")
|
||||
print(f"[ActorLoadDebug] origin_model tags: {origin_model.getTags() if origin_model else 'None'}")
|
||||
print(f"[ActorLoadDebug] owner_model: {owner_model.getName() if owner_model else 'None'}")
|
||||
try:
|
||||
print(f"[ActorLoadDebug] owner_model tags: {owner_model.getTags() if owner_model else 'None'}")
|
||||
print(f"[ActorLoadDebug] owner_model path tag: {owner_model.getTag('model_path') if owner_model.hasTag('model_path') else 'MISSING'}")
|
||||
except Exception as e:
|
||||
print(f"[ActorLoadDebug] get tags failed: {e}")
|
||||
|
||||
filepath = owner_model.getTag("model_path") if owner_model.hasTag("model_path") else ""
|
||||
if not filepath and owner_model.hasTag("original_path"):
|
||||
filepath = owner_model.getTag("original_path")
|
||||
|
||||
print(f"[ActorLoadDebug] filepath: '{filepath}'")
|
||||
if not filepath:
|
||||
print("[ActorLoadDebug] empty filepath, fallback to memory path")
|
||||
return self._try_memory_fallback_actor(origin_model, owner_model, has_animation_nodes)
|
||||
|
||||
for path in self._collect_actor_candidate_paths(filepath):
|
||||
print(f"[ActorLoadDebug] try path: {path}")
|
||||
actor = self._try_actor_from_path(owner_model, path)
|
||||
if actor:
|
||||
owner_model.setTag("model_path", p)
|
||||
owner_model.setTag("has_animations", "true")
|
||||
self._actor_cache[owner_model] = actor
|
||||
return actor
|
||||
|
||||
# 标准 Actor 路径失败时,针对 GLTF/GLB 走插件加载兜底
|
||||
actor = _try_create_actor_via_gltf_path(p)
|
||||
if actor:
|
||||
owner_model.setTag("model_path", p)
|
||||
owner_model.setTag("has_animations", "true")
|
||||
self._actor_cache[owner_model] = actor
|
||||
return actor
|
||||
|
||||
# 路径 Actor 失败后,再尝试把文件作为普通模型加载并 autoBind
|
||||
try:
|
||||
loaded_model = self.loader.loadModel(p)
|
||||
if loaded_model and not loaded_model.isEmpty():
|
||||
proxy = _try_create_autobind_proxy(loaded_model, f"文件路径({p})", owns_node=True)
|
||||
if proxy:
|
||||
loaded_model.reparentTo(self.render)
|
||||
loaded_model.hide()
|
||||
owner_model.setTag("model_path", p)
|
||||
owner_model.setTag("has_animations", "true")
|
||||
self._actor_cache[owner_model] = proxy
|
||||
return proxy
|
||||
loaded_model.removeNode()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 所有创建路径失败时由内存加载进行兜底
|
||||
return _try_memory_fallback()
|
||||
|
||||
return self._try_memory_fallback_actor(origin_model, owner_model, has_animation_nodes)
|
||||
|
||||
def _getModelFormat(self, origin_model):
|
||||
"""获取模型格式信息"""
|
||||
|
||||
@ -735,6 +735,9 @@ class AppActions:
|
||||
|
||||
node_name = node.getName() or "未命名节点"
|
||||
parent = node.getParent()
|
||||
ssbo_editor = getattr(self, "ssbo_editor", None)
|
||||
ssbo_model = getattr(ssbo_editor, "model", None) if ssbo_editor else None
|
||||
deleting_ssbo_root = bool(ssbo_model and (node == ssbo_model))
|
||||
|
||||
# 创建删除命令
|
||||
if hasattr(self, 'command_manager') and self.command_manager:
|
||||
@ -747,6 +750,12 @@ class AppActions:
|
||||
print(f"[删除] 命令管理器不可用,直接删除节点: {node_name}")
|
||||
self._perform_node_cleanup(node)
|
||||
node.removeNode()
|
||||
|
||||
if deleting_ssbo_root and ssbo_editor:
|
||||
try:
|
||||
ssbo_editor.on_model_deleted(node)
|
||||
except Exception as e:
|
||||
print(f"[SSBO] 删除模型后清理失败: {e}")
|
||||
|
||||
print(f"[删除] 成功删除节点: {node_name}")
|
||||
return True
|
||||
@ -1027,14 +1036,9 @@ class AppActions:
|
||||
|
||||
def _import_model_for_runtime(self, file_path, prefer_scene_manager=False):
|
||||
"""Import model through the active runtime path.
|
||||
SSBO mode: load via SSBOEditor only (avoid duplicate SceneManager model).
|
||||
SSBO mode: load via SSBOEditor always (regardless of prefer_scene_manager).
|
||||
Legacy mode: load via SceneManager.
|
||||
"""
|
||||
if prefer_scene_manager:
|
||||
if hasattr(self, 'scene_manager') and self.scene_manager:
|
||||
return self.scene_manager.importModel(file_path)
|
||||
return None
|
||||
|
||||
if self.use_ssbo_mouse_picking and getattr(self, 'ssbo_editor', None):
|
||||
try:
|
||||
# Clear selection/gizmo first to avoid dangling references to soon-to-be removed nodes.
|
||||
@ -1172,7 +1176,7 @@ class AppActions:
|
||||
|
||||
def _on_import_model(self):
|
||||
"""处理导入模型菜单项"""
|
||||
self.add_info_message("打开导入模型对话框")
|
||||
self.add_info_message("打开系统文件选择器")
|
||||
self.show_import_dialog = True
|
||||
|
||||
|
||||
|
||||
@ -78,57 +78,6 @@ class CreateActions:
|
||||
except Exception as e:
|
||||
self.add_error_message(f"创建平面失败: {str(e)}")
|
||||
|
||||
|
||||
def _on_create_3d_text(self):
|
||||
"""创建3D文本"""
|
||||
self.show_3d_text_dialog = True
|
||||
|
||||
|
||||
def _on_create_3d_image(self):
|
||||
"""创建3D图片"""
|
||||
self.show_3d_image_dialog = True
|
||||
|
||||
|
||||
def _on_create_gui_button(self):
|
||||
"""创建GUI按钮"""
|
||||
self.show_gui_button_dialog = True
|
||||
|
||||
|
||||
def _on_create_gui_label(self):
|
||||
"""创建GUI标签"""
|
||||
self.show_gui_label_dialog = True
|
||||
|
||||
|
||||
def _on_create_gui_entry(self):
|
||||
"""创建GUI输入框"""
|
||||
self.show_gui_entry_dialog = True
|
||||
|
||||
|
||||
def _on_create_gui_image(self):
|
||||
"""创建GUI图片"""
|
||||
self.show_gui_image_dialog = True
|
||||
|
||||
|
||||
def _on_create_video_screen(self):
|
||||
"""创建视频屏幕"""
|
||||
self.show_video_screen_dialog = True
|
||||
|
||||
|
||||
def _on_create_2d_video_screen(self):
|
||||
"""创建2D视频屏幕"""
|
||||
self.show_2d_video_screen_dialog = True
|
||||
|
||||
|
||||
def _on_create_spherical_video(self):
|
||||
"""创建球形视频"""
|
||||
self.show_spherical_video_dialog = True
|
||||
|
||||
|
||||
def _on_create_virtual_screen(self):
|
||||
"""创建虚拟屏幕"""
|
||||
self.show_virtual_screen_dialog = True
|
||||
|
||||
|
||||
def _on_create_spot_light(self):
|
||||
"""创建聚光灯"""
|
||||
self.show_spot_light_dialog = True
|
||||
|
||||
@ -265,88 +265,80 @@ class DialogPanels:
|
||||
|
||||
|
||||
def _draw_import_dialog(self):
|
||||
"""绘制导入模型对话框"""
|
||||
"""使用系统文件选择器导入模型。"""
|
||||
if not self.show_import_dialog:
|
||||
return
|
||||
|
||||
# 设置对话框标志
|
||||
flags = (imgui.WindowFlags_.no_resize |
|
||||
imgui.WindowFlags_.no_collapse |
|
||||
imgui.WindowFlags_.modal)
|
||||
|
||||
# 获取屏幕尺寸,居中显示对话框
|
||||
display_size = imgui.get_io().display_size
|
||||
dialog_width = 600
|
||||
dialog_height = 500
|
||||
imgui.set_next_window_size((dialog_width, dialog_height))
|
||||
imgui.set_next_window_pos(
|
||||
((display_size.x - dialog_width) / 2, (display_size.y - dialog_height) / 2)
|
||||
)
|
||||
|
||||
with imgui_ctx.begin("导入模型", True, flags) as window:
|
||||
if not window.opened:
|
||||
self.show_import_dialog = False
|
||||
return
|
||||
|
||||
imgui.text("选择要导入的模型文件")
|
||||
imgui.separator()
|
||||
|
||||
# 文件路径输入
|
||||
imgui.text("文件路径:")
|
||||
changed, file_path = imgui.input_text("##import_file_path", self.import_file_path, 512)
|
||||
if changed:
|
||||
self.import_file_path = file_path
|
||||
|
||||
imgui.same_line()
|
||||
if imgui.button("浏览..."):
|
||||
self.path_browser_mode = "import_model"
|
||||
self.path_browser_current_path = os.path.dirname(self.import_file_path) if self.import_file_path else os.getcwd()
|
||||
self.show_path_browser = True
|
||||
self._refresh_path_browser()
|
||||
|
||||
imgui.separator()
|
||||
|
||||
# 支持的格式说明
|
||||
imgui.text("支持的文件格式:")
|
||||
formats_text = ", ".join(self.supported_formats)
|
||||
imgui.text_colored((0.7, 0.7, 0.7, 1.0), formats_text)
|
||||
|
||||
imgui.separator()
|
||||
|
||||
# 文件预览信息
|
||||
if self.import_file_path and os.path.exists(self.import_file_path):
|
||||
file_size = os.path.getsize(self.import_file_path)
|
||||
imgui.text(f"文件大小: {file_size / 1024:.2f} KB")
|
||||
|
||||
file_ext = os.path.splitext(self.import_file_path)[1].lower()
|
||||
if file_ext in self.supported_formats:
|
||||
imgui.text_colored((0.176, 1.0, 0.769, 1.0), "✓ 文件格式支持")
|
||||
else:
|
||||
imgui.text_colored((1.0, 0.3, 0.3, 1.0), "✗ 不支持的文件格式")
|
||||
else:
|
||||
imgui.text_colored((0.7, 0.7, 0.7, 1.0), "请选择有效的文件路径")
|
||||
|
||||
imgui.separator()
|
||||
|
||||
# 按钮区域
|
||||
can_import = (self.import_file_path and
|
||||
os.path.exists(self.import_file_path) and
|
||||
os.path.splitext(self.import_file_path)[1].lower() in self.supported_formats)
|
||||
|
||||
# 根据状态设置按钮颜色
|
||||
if can_import:
|
||||
if imgui.button("导入"):
|
||||
self._import_model()
|
||||
self.show_import_dialog = False
|
||||
else:
|
||||
# 禁用状态的按钮(灰色显示)
|
||||
imgui.push_style_color(imgui.Col_.button, (0.3, 0.3, 0.3, 1.0))
|
||||
imgui.button("导入")
|
||||
imgui.pop_style_color()
|
||||
|
||||
imgui.same_line()
|
||||
if imgui.button("取消"):
|
||||
self.show_import_dialog = False
|
||||
|
||||
# 立即关闭标记,防止每帧重复弹窗
|
||||
self.show_import_dialog = False
|
||||
|
||||
selected_path = self._select_model_file_system_dialog()
|
||||
if not selected_path:
|
||||
self.add_info_message("已取消导入模型")
|
||||
return
|
||||
|
||||
self.import_file_path = selected_path
|
||||
self._import_model()
|
||||
|
||||
def _select_model_file_system_dialog(self):
|
||||
"""弹出系统文件选择器并返回模型路径。"""
|
||||
try:
|
||||
import tkinter as tk
|
||||
from tkinter import filedialog
|
||||
|
||||
initial_dir = os.path.dirname(self.import_file_path) if self.import_file_path else ""
|
||||
if not initial_dir or (not os.path.isdir(initial_dir)):
|
||||
candidates = [
|
||||
os.path.join(os.getcwd(), "Resources", "models"),
|
||||
os.path.join(os.getcwd(), "Resources"),
|
||||
os.getcwd(),
|
||||
]
|
||||
initial_dir = next((p for p in candidates if os.path.isdir(p)), os.getcwd())
|
||||
|
||||
normalized_exts = []
|
||||
for ext in getattr(self, "supported_formats", []):
|
||||
ext = str(ext).strip().lower()
|
||||
if not ext:
|
||||
continue
|
||||
if not ext.startswith("."):
|
||||
ext = f".{ext}"
|
||||
if ext not in normalized_exts:
|
||||
normalized_exts.append(ext)
|
||||
|
||||
model_patterns = " ".join(f"*{ext}" for ext in normalized_exts) or "*.*"
|
||||
filetypes = [
|
||||
("模型文件", model_patterns),
|
||||
("All Files", "*.*"),
|
||||
]
|
||||
|
||||
root = tk.Tk()
|
||||
root.withdraw()
|
||||
try:
|
||||
root.attributes("-topmost", True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
selected_path = filedialog.askopenfilename(
|
||||
title="选择要导入的模型文件",
|
||||
initialdir=initial_dir,
|
||||
filetypes=filetypes,
|
||||
)
|
||||
root.destroy()
|
||||
|
||||
if not selected_path:
|
||||
return ""
|
||||
|
||||
selected_path = os.path.normpath(selected_path)
|
||||
file_ext = os.path.splitext(selected_path)[1].lower()
|
||||
if normalized_exts and file_ext not in normalized_exts:
|
||||
self.add_error_message(f"不支持的文件格式: {file_ext}")
|
||||
return ""
|
||||
|
||||
self.add_info_message(f"已选择文件: {selected_path}")
|
||||
return selected_path
|
||||
except Exception as e:
|
||||
self.add_error_message(f"打开系统文件选择器失败: {e}")
|
||||
return ""
|
||||
|
||||
|
||||
def _refresh_path_browser(self):
|
||||
@ -412,433 +404,7 @@ class DialogPanels:
|
||||
self.add_info_message(f"已选择文件: {self.import_file_path}")
|
||||
except Exception as e:
|
||||
self.add_error_message(f"应用路径失败: {e}")
|
||||
|
||||
# ==================== 创建功能对话框实现 ====================
|
||||
|
||||
|
||||
def _draw_gui_button_dialog(self):
|
||||
"""绘制GUI按钮创建对话框"""
|
||||
if not self.show_gui_button_dialog:
|
||||
return
|
||||
|
||||
flags = (imgui.WindowFlags_.no_resize |
|
||||
imgui.WindowFlags_.no_collapse |
|
||||
imgui.WindowFlags_.modal)
|
||||
|
||||
with imgui_ctx.begin("创建GUI按钮", self.show_gui_button_dialog, flags) as window:
|
||||
if not window:
|
||||
self.show_gui_button_dialog = False
|
||||
return
|
||||
|
||||
# 初始化参数
|
||||
if 'button_text' not in self.dialog_params:
|
||||
self.dialog_params['button_text'] = "按钮"
|
||||
if 'button_pos' not in self.dialog_params:
|
||||
self.dialog_params['button_pos'] = [0.0, 0.0, 0.0]
|
||||
if 'button_size' not in self.dialog_params:
|
||||
self.dialog_params['button_size'] = [0.1, 0.1, 0.1]
|
||||
|
||||
imgui.text("GUI按钮参数设置")
|
||||
imgui.separator()
|
||||
|
||||
# 文本输入
|
||||
changed, self.dialog_params['button_text'] = imgui.input_text("按钮文本", self.dialog_params['button_text'], 256)
|
||||
|
||||
# 位置输入
|
||||
changed, x = imgui.input_float("X坐标", self.dialog_params['button_pos'][0])
|
||||
if changed:
|
||||
self.dialog_params['button_pos'][0] = x
|
||||
changed, y = imgui.input_float("Y坐标", self.dialog_params['button_pos'][1])
|
||||
if changed:
|
||||
self.dialog_params['button_pos'][1] = y
|
||||
changed, z = imgui.input_float("Z坐标", self.dialog_params['button_pos'][2])
|
||||
if changed:
|
||||
self.dialog_params['button_pos'][2] = z
|
||||
|
||||
# 大小输入
|
||||
changed, width = imgui.input_float("宽度", self.dialog_params['button_size'][0])
|
||||
if changed:
|
||||
self.dialog_params['button_size'][0] = width
|
||||
changed, height = imgui.input_float("高度", self.dialog_params['button_size'][1])
|
||||
if changed:
|
||||
self.dialog_params['button_size'][1] = height
|
||||
|
||||
imgui.separator()
|
||||
|
||||
# 按钮
|
||||
if imgui.button("创建"):
|
||||
try:
|
||||
pos = tuple(self.dialog_params['button_pos'])
|
||||
text = self.dialog_params['button_text']
|
||||
size = tuple(self.dialog_params['button_size'][:2])
|
||||
|
||||
result = self.createGUIButton(pos, text, size)
|
||||
self.add_success_message(f"GUI按钮创建成功: {text}")
|
||||
self.show_gui_button_dialog = False
|
||||
except Exception as e:
|
||||
self.add_error_message(f"创建GUI按钮失败: {str(e)}")
|
||||
|
||||
imgui.same_line()
|
||||
if imgui.button("取消"):
|
||||
self.show_gui_button_dialog = False
|
||||
|
||||
|
||||
def _draw_gui_label_dialog(self):
|
||||
"""绘制GUI标签创建对话框"""
|
||||
if not self.show_gui_label_dialog:
|
||||
return
|
||||
|
||||
flags = (imgui.WindowFlags_.no_resize |
|
||||
imgui.WindowFlags_.no_collapse |
|
||||
imgui.WindowFlags_.modal)
|
||||
|
||||
with imgui_ctx.begin("创建GUI标签", self.show_gui_label_dialog, flags) as window:
|
||||
if not window:
|
||||
self.show_gui_label_dialog = False
|
||||
return
|
||||
|
||||
# 初始化参数
|
||||
if 'label_text' not in self.dialog_params:
|
||||
self.dialog_params['label_text'] = "标签"
|
||||
if 'label_pos' not in self.dialog_params:
|
||||
self.dialog_params['label_pos'] = [0.0, 0.0, 0.0]
|
||||
if 'label_size' not in self.dialog_params:
|
||||
self.dialog_params['label_size'] = [0.1, 0.1, 0.1]
|
||||
|
||||
imgui.text("GUI标签参数设置")
|
||||
imgui.separator()
|
||||
|
||||
# 文本输入
|
||||
changed, self.dialog_params['label_text'] = imgui.input_text("标签文本", self.dialog_params['label_text'], 256)
|
||||
|
||||
# 位置输入
|
||||
changed, x = imgui.input_float("X坐标", self.dialog_params['label_pos'][0])
|
||||
if changed:
|
||||
self.dialog_params['label_pos'][0] = x
|
||||
changed, y = imgui.input_float("Y坐标", self.dialog_params['label_pos'][1])
|
||||
if changed:
|
||||
self.dialog_params['label_pos'][1] = y
|
||||
changed, z = imgui.input_float("Z坐标", self.dialog_params['label_pos'][2])
|
||||
if changed:
|
||||
self.dialog_params['label_pos'][2] = z
|
||||
|
||||
# 大小输入
|
||||
changed, width = imgui.input_float("宽度", self.dialog_params['label_size'][0])
|
||||
if changed:
|
||||
self.dialog_params['label_size'][0] = width
|
||||
changed, height = imgui.input_float("高度", self.dialog_params['label_size'][1])
|
||||
if changed:
|
||||
self.dialog_params['label_size'][1] = height
|
||||
|
||||
imgui.separator()
|
||||
|
||||
# 按钮
|
||||
if imgui.button("创建"):
|
||||
try:
|
||||
pos = tuple(self.dialog_params['label_pos'])
|
||||
text = self.dialog_params['label_text']
|
||||
size = tuple(self.dialog_params['label_size'][:2])
|
||||
|
||||
result = self.createGUILabel(pos, text, size)
|
||||
self.add_success_message(f"GUI标签创建成功: {text}")
|
||||
self.show_gui_label_dialog = False
|
||||
except Exception as e:
|
||||
self.add_error_message(f"创建GUI标签失败: {str(e)}")
|
||||
|
||||
imgui.same_line()
|
||||
if imgui.button("取消"):
|
||||
self.show_gui_label_dialog = False
|
||||
|
||||
|
||||
def _draw_gui_entry_dialog(self):
|
||||
"""绘制GUI输入框创建对话框"""
|
||||
if not self.show_gui_entry_dialog:
|
||||
return
|
||||
|
||||
flags = (imgui.WindowFlags_.no_resize |
|
||||
imgui.WindowFlags_.no_collapse |
|
||||
imgui.WindowFlags_.modal)
|
||||
|
||||
with imgui_ctx.begin("创建GUI输入框", self.show_gui_entry_dialog, flags) as window:
|
||||
if not window:
|
||||
self.show_gui_entry_dialog = False
|
||||
return
|
||||
|
||||
# 初始化参数
|
||||
if 'entry_pos' not in self.dialog_params:
|
||||
self.dialog_params['entry_pos'] = [0.0, 0.0, 0.0]
|
||||
if 'entry_size' not in self.dialog_params:
|
||||
self.dialog_params['entry_size'] = [0.2, 0.05, 0.1]
|
||||
|
||||
imgui.text("GUI输入框参数设置")
|
||||
imgui.separator()
|
||||
|
||||
# 位置输入
|
||||
changed, x = imgui.input_float("X坐标", self.dialog_params['entry_pos'][0])
|
||||
if changed:
|
||||
self.dialog_params['entry_pos'][0] = x
|
||||
changed, y = imgui.input_float("Y坐标", self.dialog_params['entry_pos'][1])
|
||||
if changed:
|
||||
self.dialog_params['entry_pos'][1] = y
|
||||
changed, z = imgui.input_float("Z坐标", self.dialog_params['entry_pos'][2])
|
||||
if changed:
|
||||
self.dialog_params['entry_pos'][2] = z
|
||||
|
||||
# 大小输入
|
||||
changed, width = imgui.input_float("宽度", self.dialog_params['entry_size'][0])
|
||||
if changed:
|
||||
self.dialog_params['entry_size'][0] = width
|
||||
changed, height = imgui.input_float("高度", self.dialog_params['entry_size'][1])
|
||||
if changed:
|
||||
self.dialog_params['entry_size'][1] = height
|
||||
|
||||
imgui.separator()
|
||||
|
||||
# 按钮
|
||||
if imgui.button("创建"):
|
||||
try:
|
||||
pos = tuple(self.dialog_params['entry_pos'])
|
||||
size = tuple(self.dialog_params['entry_size'][:2])
|
||||
|
||||
result = self.createGUIEntry(pos, size)
|
||||
self.add_success_message("GUI输入框创建成功")
|
||||
self.show_gui_entry_dialog = False
|
||||
except Exception as e:
|
||||
self.add_error_message(f"创建GUI输入框失败: {str(e)}")
|
||||
|
||||
imgui.same_line()
|
||||
if imgui.button("取消"):
|
||||
self.show_gui_entry_dialog = False
|
||||
|
||||
|
||||
def _draw_gui_image_dialog(self):
|
||||
"""绘制GUI图片创建对话框"""
|
||||
if not self.show_gui_image_dialog:
|
||||
return
|
||||
|
||||
flags = (imgui.WindowFlags_.no_resize |
|
||||
imgui.WindowFlags_.no_collapse |
|
||||
imgui.WindowFlags_.modal)
|
||||
|
||||
with imgui_ctx.begin("创建GUI图片", self.show_gui_image_dialog, flags) as window:
|
||||
if not window:
|
||||
self.show_gui_image_dialog = False
|
||||
return
|
||||
|
||||
# 初始化参数
|
||||
if 'image_pos' not in self.dialog_params:
|
||||
self.dialog_params['image_pos'] = [0.0, 0.0, 0.0]
|
||||
if 'image_size' not in self.dialog_params:
|
||||
self.dialog_params['image_size'] = [0.2, 0.2, 0.1]
|
||||
if 'image_path' not in self.dialog_params:
|
||||
self.dialog_params['image_path'] = ""
|
||||
|
||||
imgui.text("GUI图片参数设置")
|
||||
imgui.separator()
|
||||
|
||||
# 位置输入
|
||||
changed, x = imgui.input_float("X坐标", self.dialog_params['image_pos'][0])
|
||||
if changed:
|
||||
self.dialog_params['image_pos'][0] = x
|
||||
changed, y = imgui.input_float("Y坐标", self.dialog_params['image_pos'][1])
|
||||
if changed:
|
||||
self.dialog_params['image_pos'][1] = y
|
||||
changed, z = imgui.input_float("Z坐标", self.dialog_params['image_pos'][2])
|
||||
if changed:
|
||||
self.dialog_params['image_pos'][2] = z
|
||||
|
||||
# 大小输入
|
||||
changed, width = imgui.input_float("宽度", self.dialog_params['image_size'][0])
|
||||
if changed:
|
||||
self.dialog_params['image_size'][0] = width
|
||||
changed, height = imgui.input_float("高度", self.dialog_params['image_size'][1])
|
||||
if changed:
|
||||
self.dialog_params['image_size'][1] = height
|
||||
|
||||
# 图片路径
|
||||
changed, self.dialog_params['image_path'] = imgui.input_text("图片路径", self.dialog_params['image_path'], 512)
|
||||
|
||||
imgui.separator()
|
||||
|
||||
# 按钮
|
||||
if imgui.button("创建"):
|
||||
try:
|
||||
pos = tuple(self.dialog_params['image_pos'])
|
||||
size = tuple(self.dialog_params['image_size'][:2])
|
||||
image_path = self.dialog_params['image_path']
|
||||
|
||||
result = self.createGUIImage(pos, image_path, size)
|
||||
self.add_success_message("GUI图片创建成功")
|
||||
self.show_gui_image_dialog = False
|
||||
except Exception as e:
|
||||
self.add_error_message(f"创建GUI图片失败: {str(e)}")
|
||||
|
||||
imgui.same_line()
|
||||
if imgui.button("取消"):
|
||||
self.show_gui_image_dialog = False
|
||||
|
||||
|
||||
def _draw_3d_text_dialog(self):
|
||||
"""绘制3D文本创建对话框"""
|
||||
if not self.show_3d_text_dialog:
|
||||
return
|
||||
|
||||
flags = (imgui.WindowFlags_.no_resize |
|
||||
imgui.WindowFlags_.no_collapse |
|
||||
imgui.WindowFlags_.modal)
|
||||
|
||||
with imgui_ctx.begin("创建3D文本", self.show_3d_text_dialog, flags) as window:
|
||||
if not window:
|
||||
self.show_3d_text_dialog = False
|
||||
return
|
||||
|
||||
# 初始化参数
|
||||
if 'text3d_text' not in self.dialog_params:
|
||||
self.dialog_params['text3d_text'] = "3D文本"
|
||||
if 'text3d_pos' not in self.dialog_params:
|
||||
self.dialog_params['text3d_pos'] = [0.0, 0.0, 0.0]
|
||||
if 'text3d_size' not in self.dialog_params:
|
||||
self.dialog_params['text3d_size'] = 1.0
|
||||
|
||||
imgui.text("3D文本参数设置")
|
||||
imgui.separator()
|
||||
|
||||
# 文本输入
|
||||
changed, self.dialog_params['text3d_text'] = imgui.input_text("文本内容", self.dialog_params['text3d_text'], 256)
|
||||
|
||||
# 位置输入
|
||||
changed, x = imgui.input_float("X坐标", self.dialog_params['text3d_pos'][0])
|
||||
if changed:
|
||||
self.dialog_params['text3d_pos'][0] = x
|
||||
changed, y = imgui.input_float("Y坐标", self.dialog_params['text3d_pos'][1])
|
||||
if changed:
|
||||
self.dialog_params['text3d_pos'][1] = y
|
||||
changed, z = imgui.input_float("Z坐标", self.dialog_params['text3d_pos'][2])
|
||||
if changed:
|
||||
self.dialog_params['text3d_pos'][2] = z
|
||||
|
||||
# 大小输入
|
||||
changed, self.dialog_params['text3d_size'] = imgui.input_float("文本大小", self.dialog_params['text3d_size'])
|
||||
|
||||
imgui.separator()
|
||||
|
||||
# 按钮
|
||||
if imgui.button("创建"):
|
||||
try:
|
||||
pos = tuple(self.dialog_params['text3d_pos'])
|
||||
text = self.dialog_params['text3d_text']
|
||||
size = self.dialog_params['text3d_size']
|
||||
|
||||
result = self.create3DText(pos, text, size)
|
||||
self.add_success_message(f"3D文本创建成功: {text}")
|
||||
self.show_3d_text_dialog = False
|
||||
except Exception as e:
|
||||
self.add_error_message(f"创建3D文本失败: {str(e)}")
|
||||
|
||||
imgui.same_line()
|
||||
if imgui.button("取消"):
|
||||
self.show_3d_text_dialog = False
|
||||
|
||||
|
||||
def _draw_3d_image_dialog(self):
|
||||
"""绘制3D图片创建对话框"""
|
||||
if not self.show_3d_image_dialog:
|
||||
return
|
||||
|
||||
flags = (imgui.WindowFlags_.no_resize |
|
||||
imgui.WindowFlags_.no_collapse |
|
||||
imgui.WindowFlags_.modal)
|
||||
|
||||
with imgui_ctx.begin("创建3D图片", self.show_3d_image_dialog, flags) as window:
|
||||
if not window:
|
||||
self.show_3d_image_dialog = False
|
||||
return
|
||||
|
||||
# 初始化参数
|
||||
if 'image3d_pos' not in self.dialog_params:
|
||||
self.dialog_params['image3d_pos'] = [0.0, 0.0, 0.0]
|
||||
if 'image3d_size' not in self.dialog_params:
|
||||
self.dialog_params['image3d_size'] = [1.0, 1.0, 1.0]
|
||||
if 'image3d_path' not in self.dialog_params:
|
||||
self.dialog_params['image3d_path'] = ""
|
||||
|
||||
imgui.text("3D图片参数设置")
|
||||
imgui.separator()
|
||||
|
||||
# 位置输入
|
||||
changed, x = imgui.input_float("X坐标", self.dialog_params['image3d_pos'][0])
|
||||
if changed:
|
||||
self.dialog_params['image3d_pos'][0] = x
|
||||
changed, y = imgui.input_float("Y坐标", self.dialog_params['image3d_pos'][1])
|
||||
if changed:
|
||||
self.dialog_params['image3d_pos'][1] = y
|
||||
changed, z = imgui.input_float("Z坐标", self.dialog_params['image3d_pos'][2])
|
||||
if changed:
|
||||
self.dialog_params['image3d_pos'][2] = z
|
||||
|
||||
# 大小输入
|
||||
changed, width = imgui.input_float("宽度", self.dialog_params['image3d_size'][0])
|
||||
if changed:
|
||||
self.dialog_params['image3d_size'][0] = width
|
||||
changed, height = imgui.input_float("高度", self.dialog_params['image3d_size'][1])
|
||||
if changed:
|
||||
self.dialog_params['image3d_size'][1] = height
|
||||
|
||||
# 图片路径
|
||||
changed, self.dialog_params['image3d_path'] = imgui.input_text("图片路径", self.dialog_params['image3d_path'], 512)
|
||||
|
||||
imgui.separator()
|
||||
|
||||
# 按钮
|
||||
if imgui.button("创建"):
|
||||
try:
|
||||
pos = tuple(self.dialog_params['image3d_pos'])
|
||||
size = tuple(self.dialog_params['image3d_size'][:2])
|
||||
image_path = self.dialog_params['image3d_path']
|
||||
|
||||
result = self.create3DImage(pos, image_path, size)
|
||||
self.add_success_message("3D图片创建成功")
|
||||
self.show_3d_image_dialog = False
|
||||
except Exception as e:
|
||||
self.add_error_message(f"创建3D图片失败: {str(e)}")
|
||||
|
||||
imgui.same_line()
|
||||
if imgui.button("取消"):
|
||||
self.show_3d_image_dialog = False
|
||||
|
||||
# 添加其他创建对话框的占位符方法
|
||||
|
||||
def _draw_video_screen_dialog(self):
|
||||
"""绘制视频屏幕创建对话框"""
|
||||
if not self.show_video_screen_dialog:
|
||||
return
|
||||
self.show_video_screen_dialog = False
|
||||
self.add_info_message("视频屏幕创建功能开发中...")
|
||||
|
||||
|
||||
def _draw_2d_video_screen_dialog(self):
|
||||
"""绘制2D视频屏幕创建对话框"""
|
||||
if not self.show_2d_video_screen_dialog:
|
||||
return
|
||||
self.show_2d_video_screen_dialog = False
|
||||
self.add_info_message("2D视频屏幕创建功能开发中...")
|
||||
|
||||
|
||||
def _draw_spherical_video_dialog(self):
|
||||
"""绘制球形视频创建对话框"""
|
||||
if not self.show_spherical_video_dialog:
|
||||
return
|
||||
self.show_spherical_video_dialog = False
|
||||
self.add_info_message("球形视频创建功能开发中...")
|
||||
|
||||
|
||||
def _draw_virtual_screen_dialog(self):
|
||||
"""绘制虚拟屏幕创建对话框"""
|
||||
if not self.show_virtual_screen_dialog:
|
||||
return
|
||||
self.show_virtual_screen_dialog = False
|
||||
self.add_info_message("虚拟屏幕创建功能开发中...")
|
||||
|
||||
|
||||
def _draw_spot_light_dialog(self):
|
||||
"""绘制聚光灯创建对话框"""
|
||||
@ -1457,5 +1023,3 @@ class DialogPanels:
|
||||
except Exception as e:
|
||||
print(f"刷新高度图浏览器时出错: {e}")
|
||||
self.heightmap_browser_items = []
|
||||
|
||||
# ==================== 导入功能实现 ====================
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
222
ui/panels/editor_panels_center.py
Normal file
222
ui/panels/editor_panels_center.py
Normal file
@ -0,0 +1,222 @@
|
||||
from imgui_bundle import imgui, imgui_ctx
|
||||
import io
|
||||
import tempfile
|
||||
import time
|
||||
from pathlib import Path
|
||||
from panda3d.core import Filename
|
||||
try:
|
||||
from PIL import Image
|
||||
except Exception: # pragma: no cover - pillow may be missing in minimal env
|
||||
Image = None
|
||||
|
||||
class EditorPanelsCenterMixin:
|
||||
"""Auto-split mixin from editor_panels.py."""
|
||||
|
||||
def _on_create_web_panel(self):
|
||||
"""创建或激活 ImGui Web 面板。"""
|
||||
self._ensure_web_panel_state()
|
||||
self.app.showWebPanel = True
|
||||
|
||||
webview = getattr(self.app, "_imgui_webview", None)
|
||||
if webview and getattr(webview, "_running", False):
|
||||
return True
|
||||
|
||||
return self._start_imgui_webview(self.app.web_panel_url_input)
|
||||
|
||||
def _ensure_web_panel_state(self):
|
||||
if not hasattr(self.app, "web_panel_url_input") or not self.app.web_panel_url_input:
|
||||
self.app.web_panel_url_input = "https://www.baidu.com"
|
||||
if not hasattr(self.app, "_imgui_webview"):
|
||||
self.app._imgui_webview = None
|
||||
if not hasattr(self.app, "_imgui_webview_tex_id"):
|
||||
self.app._imgui_webview_tex_id = None
|
||||
if not hasattr(self.app, "_imgui_webview_texture_path"):
|
||||
self.app._imgui_webview_texture_path = None
|
||||
|
||||
def _start_imgui_webview(self, url):
|
||||
self._ensure_web_panel_state()
|
||||
self._stop_imgui_webview()
|
||||
try:
|
||||
target_url = (url or "").strip()
|
||||
if not target_url:
|
||||
target_url = "https://www.example.com"
|
||||
if not target_url.startswith(("http://", "https://", "file://")):
|
||||
target_url = "https://" + target_url
|
||||
|
||||
from core.imgui_webview import ImGuiWebView
|
||||
webview = ImGuiWebView(width=1280, height=720)
|
||||
webview.start(target_url)
|
||||
self.app._imgui_webview = webview
|
||||
return True
|
||||
except Exception as e:
|
||||
self.app.add_error_message(f"启动Web视图失败: {e}")
|
||||
return False
|
||||
|
||||
def _stop_imgui_webview(self):
|
||||
webview = getattr(self.app, "_imgui_webview", None)
|
||||
if webview:
|
||||
try:
|
||||
webview.stop()
|
||||
except Exception:
|
||||
pass
|
||||
self.app._imgui_webview = None
|
||||
|
||||
tex_id = getattr(self.app, "_imgui_webview_tex_id", None)
|
||||
if tex_id:
|
||||
try:
|
||||
self.app.imgui.removeTexture(tex_id)
|
||||
except Exception:
|
||||
pass
|
||||
self.app._imgui_webview_tex_id = None
|
||||
texture_path = getattr(self.app, "_imgui_webview_texture_path", None)
|
||||
if texture_path:
|
||||
try:
|
||||
Path(texture_path).unlink(missing_ok=True)
|
||||
except Exception:
|
||||
pass
|
||||
self.app._imgui_webview_texture_path = None
|
||||
|
||||
def _navigate_web_panel(self):
|
||||
self._ensure_web_panel_state()
|
||||
url = (self.app.web_panel_url_input or "").strip()
|
||||
if not url:
|
||||
return
|
||||
|
||||
webview = getattr(self.app, "_imgui_webview", None)
|
||||
if not webview or not getattr(webview, "_running", False):
|
||||
self._start_imgui_webview(url)
|
||||
return
|
||||
|
||||
webview.navigate(url)
|
||||
|
||||
def _update_web_panel_texture(self):
|
||||
webview = getattr(self.app, "_imgui_webview", None)
|
||||
if not webview:
|
||||
return
|
||||
if not webview.tex_dirty:
|
||||
return
|
||||
|
||||
jpeg_bytes = webview.get_screenshot_bytes()
|
||||
if not jpeg_bytes:
|
||||
return
|
||||
|
||||
try:
|
||||
if Image is None:
|
||||
self.app.add_warning_message("缺少 Pillow,无法更新Web纹理")
|
||||
return
|
||||
img = Image.open(io.BytesIO(jpeg_bytes)).convert("RGBA")
|
||||
# p3dimgui 纹理坐标系与网页截图存在Y轴方向差异,先在像素层修正
|
||||
img = img.transpose(Image.FLIP_TOP_BOTTOM)
|
||||
|
||||
temp_dir = Path(tempfile.gettempdir()) / "eg_imgui_webpanel"
|
||||
temp_dir.mkdir(parents=True, exist_ok=True)
|
||||
temp_path = temp_dir / f"webview_{time.time_ns()}.png"
|
||||
img.save(temp_path, format="PNG")
|
||||
|
||||
panda_path = Filename.fromOsSpecific(str(temp_path)).getFullpath()
|
||||
new_tex_id = self.app.imgui.loadTexture(panda_path)
|
||||
old_tex_id = getattr(self.app, "_imgui_webview_tex_id", None)
|
||||
if old_tex_id:
|
||||
try:
|
||||
self.app.imgui.removeTexture(old_tex_id)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
old_texture_path = getattr(self.app, "_imgui_webview_texture_path", None)
|
||||
if old_texture_path:
|
||||
try:
|
||||
Path(old_texture_path).unlink(missing_ok=True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self.app._imgui_webview_tex_id = new_tex_id
|
||||
self.app._imgui_webview_texture_path = str(temp_path)
|
||||
except Exception as e:
|
||||
self.app.add_warning_message(f"Web纹理更新失败: {e}")
|
||||
finally:
|
||||
webview.tex_dirty = False
|
||||
|
||||
@staticmethod
|
||||
def _clamp01(value):
|
||||
return 0.0 if value < 0.0 else 1.0 if value > 1.0 else value
|
||||
|
||||
def _draw_web_panel(self):
|
||||
"""绘制 Web 面板(ImGui + 后台浏览器截图)。"""
|
||||
self._ensure_web_panel_state()
|
||||
if not self.app.showWebPanel:
|
||||
self._stop_imgui_webview()
|
||||
return
|
||||
|
||||
flags = self.app.style_manager.get_window_flags("panel")
|
||||
with self.app.style_manager.begin_styled_window("Web面板", self.app.showWebPanel, flags) as (_, opened):
|
||||
if not opened:
|
||||
self.app.showWebPanel = False
|
||||
self._stop_imgui_webview()
|
||||
return
|
||||
|
||||
self.app.showWebPanel = True
|
||||
|
||||
changed, self.app.web_panel_url_input = imgui.input_text(
|
||||
"URL", self.app.web_panel_url_input, 1024
|
||||
)
|
||||
if changed:
|
||||
self.app.web_panel_url_input = self.app.web_panel_url_input.strip()
|
||||
|
||||
imgui.same_line()
|
||||
if imgui.button("访问"):
|
||||
self._navigate_web_panel()
|
||||
|
||||
webview = getattr(self.app, "_imgui_webview", None)
|
||||
if not webview:
|
||||
if not self._start_imgui_webview(self.app.web_panel_url_input):
|
||||
imgui.text_colored((1.0, 0.4, 0.4, 1.0), "Web视图启动失败")
|
||||
return
|
||||
webview = self.app._imgui_webview
|
||||
|
||||
imgui.same_line()
|
||||
if imgui.button("后退"):
|
||||
webview.go_back()
|
||||
imgui.same_line()
|
||||
if imgui.button("前进"):
|
||||
webview.go_forward()
|
||||
imgui.same_line()
|
||||
if imgui.button("刷新"):
|
||||
webview.reload()
|
||||
|
||||
current_url = webview.current_url or self.app.web_panel_url_input
|
||||
if current_url:
|
||||
imgui.text_colored((0.7, 0.7, 0.7, 1.0), current_url)
|
||||
if webview.error:
|
||||
imgui.text_colored((1.0, 0.4, 0.4, 1.0), webview.error)
|
||||
|
||||
imgui.separator()
|
||||
|
||||
self._update_web_panel_texture()
|
||||
tex_id = getattr(self.app, "_imgui_webview_tex_id", None)
|
||||
available = imgui.get_content_region_avail()
|
||||
display_w = max(float(available.x), 64.0)
|
||||
display_h = max(float(available.y), 64.0)
|
||||
|
||||
if tex_id:
|
||||
imgui.image(tex_id, (display_w, display_h))
|
||||
|
||||
if imgui.is_item_hovered():
|
||||
mouse_wheel = imgui.get_io().mouse_wheel
|
||||
if abs(mouse_wheel) > 1e-4:
|
||||
webview.scroll(-mouse_wheel * 120.0)
|
||||
|
||||
if imgui.is_mouse_clicked(0):
|
||||
item_min = imgui.get_item_rect_min()
|
||||
item_max = imgui.get_item_rect_max()
|
||||
item_w = max(float(item_max.x - item_min.x), 1.0)
|
||||
item_h = max(float(item_max.y - item_min.y), 1.0)
|
||||
mouse_pos = imgui.get_mouse_pos()
|
||||
|
||||
x_ratio = self._clamp01((float(mouse_pos.x) - float(item_min.x)) / item_w)
|
||||
y_ratio = self._clamp01((float(mouse_pos.y) - float(item_min.y)) / item_h)
|
||||
webview.click(x_ratio, y_ratio)
|
||||
elif webview.is_loading:
|
||||
imgui.text("网页加载中...")
|
||||
else:
|
||||
imgui.text("正在初始化Web视图...")
|
||||
|
||||
600
ui/panels/editor_panels_left.py
Normal file
600
ui/panels/editor_panels_left.py
Normal file
@ -0,0 +1,600 @@
|
||||
from imgui_bundle import imgui, imgui_ctx
|
||||
from pathlib import Path
|
||||
|
||||
class EditorPanelsLeftMixin:
|
||||
"""Auto-split mixin from editor_panels.py."""
|
||||
|
||||
def _draw_scene_tree(self):
|
||||
"""绘制场景树面板"""
|
||||
# 使用更少的限制性标志,允许docking
|
||||
flags = (imgui.WindowFlags_.no_collapse)
|
||||
|
||||
with self.app.style_manager.begin_styled_window("场景树", self.app.showSceneTree, flags):
|
||||
self.app.showSceneTree = True # 确保窗口保持打开
|
||||
window_pos = imgui.get_window_pos()
|
||||
window_size = imgui.get_window_size()
|
||||
self.app._scene_tree_window_rect = (
|
||||
float(window_pos.x),
|
||||
float(window_pos.y),
|
||||
float(window_size.x),
|
||||
float(window_size.y),
|
||||
)
|
||||
|
||||
imgui.text("场景层级")
|
||||
imgui.separator()
|
||||
|
||||
# 构建动态场景树
|
||||
self._build_scene_tree()
|
||||
|
||||
def _build_scene_tree(self):
|
||||
"""构建动态场景树"""
|
||||
# 渲染节点
|
||||
if imgui.tree_node("渲染"):
|
||||
# 环境光
|
||||
if hasattr(self.app, 'ambient_light') and self.app.ambient_light:
|
||||
self._draw_scene_node(self.app.ambient_light, "环境光", "light")
|
||||
|
||||
# 聚光灯
|
||||
if hasattr(self.app, 'scene_manager') and self.app.scene_manager:
|
||||
if hasattr(self.app.scene_manager, 'Spotlight') and self.app.scene_manager.Spotlight:
|
||||
for i, spotlight in enumerate(self.app.scene_manager.Spotlight):
|
||||
self._draw_scene_node(spotlight, f"聚光灯_{i+1}", "light")
|
||||
if hasattr(self.app.scene_manager, 'Pointlight') and self.app.scene_manager.Pointlight:
|
||||
for i, pointlight in enumerate(self.app.scene_manager.Pointlight):
|
||||
self._draw_scene_node(pointlight, f"点光源_{i+1}", "light")
|
||||
|
||||
# 地板
|
||||
if hasattr(self.app, 'ground') and self.app.ground:
|
||||
self._draw_scene_node(self.app.ground, "地板", "geometry")
|
||||
|
||||
imgui.tree_pop()
|
||||
|
||||
# 相机节点
|
||||
if imgui.tree_node("相机"):
|
||||
if hasattr(self.app, 'camera') and self.app.camera:
|
||||
self._draw_scene_node(self.app.camera, "主相机", "camera")
|
||||
imgui.tree_pop()
|
||||
|
||||
# 3D模型节点
|
||||
if imgui.tree_node("模型"):
|
||||
models = []
|
||||
if hasattr(self.app, 'scene_manager') and self.app.scene_manager and hasattr(self.app.scene_manager, 'models'):
|
||||
models.extend([m for m in self.app.scene_manager.models if m and not m.isEmpty()])
|
||||
|
||||
# SSBO模式下,模型可能不在 scene_manager.models 中,补充显示 ssbo_editor.model
|
||||
ssbo_editor = getattr(self.app, "ssbo_editor", None)
|
||||
ssbo_model = getattr(ssbo_editor, "model", None) if ssbo_editor else None
|
||||
if ssbo_model and not ssbo_model.isEmpty() and ssbo_model.hasParent() and ssbo_model not in models:
|
||||
models.append(ssbo_model)
|
||||
|
||||
if models:
|
||||
for i, model in enumerate(models):
|
||||
self._draw_scene_node(model, model.getName() or f"模型_{i+1}", "model")
|
||||
else:
|
||||
imgui.text("(空)")
|
||||
imgui.tree_pop()
|
||||
|
||||
# if imgui.tree_node("GUI元素"):
|
||||
# if hasattr(self,'gui_manager') and self.app.gui_manager and hasattr(self.app.gui_manager,'gui_elements'):
|
||||
# if self.app.gui_manager.gui_elements:
|
||||
# for gui_element in self.app.gui_manager.gui_elements:
|
||||
# if gui_element and hasattr(gui_element,'node'):
|
||||
# gui_type = getattr(gui_element,'gui_type','GUI_UNKNOWN')
|
||||
# display_name = getattr(gui_element,'name',gui_type)
|
||||
# self._draw_scene_node(gui_element.node,display_name,"gui",gui_type)
|
||||
# else:
|
||||
# imgui.text("(空)")
|
||||
# else:
|
||||
# imgui.text("(空)")
|
||||
# imgui.tree_pop()
|
||||
|
||||
# LUI元素节点
|
||||
if imgui.tree_node("GUI元素"):
|
||||
if hasattr(self.app, 'lui_manager') and self.app.lui_manager.lui_enabled:
|
||||
self.app.lui_manager.draw_component_tree()
|
||||
imgui.tree_pop()
|
||||
# if imgui.tree_node("LUI元素"):
|
||||
# if hasattr(self.app, 'lui_manager') and self.app.lui_manager.lui_enabled:
|
||||
# if self.app.lui_manager.components:
|
||||
# for comp in self.app.lui_manager.components:
|
||||
# if 'node' in comp:
|
||||
# self._draw_scene_node(comp['node'], comp['name'], "ui")
|
||||
|
||||
# if self.app.lui_manager.canvases:
|
||||
# for canvas in self.app.lui_manager.canvases:
|
||||
# if imgui.tree_node(f"Canvas: {canvas['name']}"):
|
||||
# # 实际上组件已经在 node 下了,可以通过 _draw_scene_node 递归显示
|
||||
# # 但为了清晰,我们可以手动列出或者依赖递归
|
||||
# self._draw_scene_node(canvas['node'], canvas['name'], "geometry")
|
||||
# imgui.tree_pop()
|
||||
# else:
|
||||
# imgui.text("(空)")
|
||||
# else:
|
||||
# imgui.text("(空)")
|
||||
# imgui.tree_pop()
|
||||
|
||||
def _draw_scene_node(self, node, name, node_type, gui_subtype=None):
|
||||
"""绘制单个场景节点"""
|
||||
if not node or node.isEmpty():
|
||||
return
|
||||
|
||||
# 检查是否被选中
|
||||
is_selected = (hasattr(self.app, 'selection') and self.app.selection and
|
||||
hasattr(self.app.selection, 'selectedNode') and
|
||||
self.app.selection.selectedNode == node)
|
||||
|
||||
# 节点可见性
|
||||
is_visible = node.is_hidden() == False
|
||||
|
||||
# 设置选择颜色
|
||||
if is_selected:
|
||||
imgui.push_style_color(imgui.Col_.text, (0.2, 0.6, 1.0, 1.0))
|
||||
|
||||
node_open = False
|
||||
try:
|
||||
# 显示节点
|
||||
node_open = imgui.tree_node(name)
|
||||
|
||||
# 处理节点选择
|
||||
if imgui.is_item_clicked():
|
||||
# In SSBO mode, clicking model root should finally bind gizmo to
|
||||
# SSBO group proxy (not legacy model root). So run legacy
|
||||
# selection first, then force SSBO root selection at the end.
|
||||
force_ssbo_root_key = None
|
||||
ssbo_editor_ref = None
|
||||
try:
|
||||
ssbo_editor = getattr(self.app, "ssbo_editor", None)
|
||||
controller = getattr(ssbo_editor, "controller", None) if ssbo_editor else None
|
||||
ssbo_model = getattr(ssbo_editor, "model", None) if ssbo_editor else None
|
||||
root_key = getattr(controller, "tree_root_key", None) if controller else None
|
||||
if (
|
||||
ssbo_editor and controller and ssbo_model and node == ssbo_model
|
||||
and root_key and root_key in controller.tree_nodes
|
||||
):
|
||||
force_ssbo_root_key = root_key
|
||||
ssbo_editor_ref = ssbo_editor
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if hasattr(self.app, 'selection') and self.app.selection:
|
||||
self.app.selection.updateSelection(node)
|
||||
|
||||
if force_ssbo_root_key and ssbo_editor_ref:
|
||||
try:
|
||||
ssbo_editor_ref.select_node(force_ssbo_root_key)
|
||||
except Exception:
|
||||
pass
|
||||
# Clear LUI selection when a scene node is selected
|
||||
if hasattr(self.app, 'lui_manager'):
|
||||
self.app.lui_manager.selected_index = -1
|
||||
|
||||
if self.app.is_dragging and imgui.is_item_hovered():
|
||||
self.app._drag_scene_tree_hover_node = node
|
||||
|
||||
# 右键菜单
|
||||
if imgui.is_item_hovered() and imgui.is_mouse_clicked(1):
|
||||
self._show_node_context_menu(node, name, node_type)
|
||||
|
||||
# 显示节点属性
|
||||
imgui.same_line()
|
||||
if is_visible:
|
||||
imgui.text_colored((0.5, 1.0, 0.5, 1.0), "可见")
|
||||
else:
|
||||
imgui.text_colored((0.5, 0.5, 0.5, 1.0), "隐藏")
|
||||
|
||||
if node_open:
|
||||
# SSBO模型使用虚拟层级显示(避免 flatten 后真实子级丢失)
|
||||
if self._draw_ssbo_virtual_children(node):
|
||||
pass
|
||||
elif node.getNumChildren() > 0:
|
||||
for i in range(node.getNumChildren()):
|
||||
child = node.getChild(i)
|
||||
if not child or child.isEmpty():
|
||||
continue
|
||||
child_name = child.getName() or f"child_{i+1}"
|
||||
# 过滤碰撞辅助节点,避免污染场景树
|
||||
if child_name.startswith("modelCollision_"):
|
||||
continue
|
||||
self._draw_scene_node(child, child_name, node_type)
|
||||
# tree_pop moved to finally
|
||||
except Exception as e:
|
||||
print(f"绘制场景节点时出错: {e}")
|
||||
finally:
|
||||
if node_open:
|
||||
imgui.tree_pop()
|
||||
# Ensure style stack is balanced.
|
||||
if is_selected:
|
||||
imgui.pop_style_color()
|
||||
|
||||
def _draw_ssbo_virtual_children(self, node):
|
||||
"""Draw SSBO controller tree_nodes as virtual children for scene tree."""
|
||||
ssbo_editor = getattr(self.app, "ssbo_editor", None)
|
||||
if not ssbo_editor:
|
||||
return False
|
||||
model = getattr(ssbo_editor, "model", None)
|
||||
controller = getattr(ssbo_editor, "controller", None)
|
||||
if not model or model != node or not controller:
|
||||
return False
|
||||
|
||||
root_key = getattr(controller, "tree_root_key", None)
|
||||
if not root_key or root_key not in controller.tree_nodes:
|
||||
imgui.text_disabled("(无可用子节点)")
|
||||
return True
|
||||
|
||||
root_node = controller.tree_nodes[root_key]
|
||||
if not root_node["children"]:
|
||||
imgui.text_disabled("(无可用子节点)")
|
||||
return True
|
||||
|
||||
for child_key in root_node["children"]:
|
||||
self._draw_ssbo_virtual_tree_node(ssbo_editor, controller, child_key)
|
||||
return True
|
||||
|
||||
def _draw_ssbo_virtual_tree_node(self, ssbo_editor, controller, key):
|
||||
"""Recursively draw SSBO tree_nodes hierarchy in scene tree."""
|
||||
node_data = controller.tree_nodes.get(key)
|
||||
if not node_data:
|
||||
return
|
||||
|
||||
# Skip redundant wrapper nodes (e.g. ROOT), show their children instead.
|
||||
if controller.should_hide_tree_node(key):
|
||||
for child_key in node_data["children"]:
|
||||
self._draw_ssbo_virtual_tree_node(ssbo_editor, controller, child_key)
|
||||
return
|
||||
|
||||
display = controller.display_names.get(key, key)
|
||||
obj_count = len(controller.name_to_ids.get(key, []))
|
||||
children = node_data["children"]
|
||||
is_selected = (getattr(ssbo_editor, "selected_name", None) == key)
|
||||
|
||||
if not children:
|
||||
# Leaf node: selectable
|
||||
label = f"{display} ({obj_count})##{key}"
|
||||
if imgui.selectable(label, is_selected)[0]:
|
||||
ssbo_editor.select_node(key)
|
||||
if hasattr(self.app, "lui_manager"):
|
||||
self.app.lui_manager.selected_index = -1
|
||||
else:
|
||||
# Branch node: tree node
|
||||
flags = imgui.TreeNodeFlags_.open_on_arrow
|
||||
if is_selected:
|
||||
flags |= imgui.TreeNodeFlags_.selected
|
||||
label = f"{display} ({obj_count})##{key}"
|
||||
opened = imgui.tree_node_ex(label, flags)
|
||||
if imgui.is_item_clicked(0):
|
||||
ssbo_editor.select_node(key)
|
||||
if hasattr(self.app, "lui_manager"):
|
||||
self.app.lui_manager.selected_index = -1
|
||||
if opened:
|
||||
for child_key in children:
|
||||
self._draw_ssbo_virtual_tree_node(ssbo_editor, controller, child_key)
|
||||
imgui.tree_pop()
|
||||
|
||||
def _show_node_context_menu(self, node, name, node_type):
|
||||
"""显示节点右键菜单"""
|
||||
self.app._context_menu_node = True
|
||||
self.app._context_menu_target = node
|
||||
|
||||
def _draw_resource_manager(self):
|
||||
"""绘制资源管理器面板"""
|
||||
flags = self.app.style_manager.get_window_flags("panel")
|
||||
|
||||
with self.app.style_manager.begin_styled_window("资源管理器", self.app.showResourceManager, flags):
|
||||
self.app.showResourceManager = True
|
||||
rm = self.app.resource_manager
|
||||
|
||||
self._update_resource_manager_window_rect(rm)
|
||||
|
||||
imgui.text("文件浏览器")
|
||||
imgui.separator()
|
||||
|
||||
self._draw_resource_toolbar_and_filters(rm)
|
||||
|
||||
imgui.separator()
|
||||
|
||||
rm.refresh_if_needed()
|
||||
|
||||
dirs, files = rm.get_directory_contents(rm.current_path)
|
||||
self._draw_resource_directory_entries(rm, dirs)
|
||||
self._draw_resource_file_entries(rm, files)
|
||||
self._draw_resource_context_menu(rm)
|
||||
|
||||
def _update_resource_manager_window_rect(self, rm):
|
||||
"""更新资源管理器窗口矩形与根拖拽目标。"""
|
||||
window_pos = imgui.get_window_pos()
|
||||
window_size = imgui.get_window_size()
|
||||
self.app._resource_manager_window_rect = (
|
||||
float(window_pos.x),
|
||||
float(window_pos.y),
|
||||
float(window_size.x),
|
||||
float(window_size.y),
|
||||
)
|
||||
self.app._resource_drop_targets.append((
|
||||
float(window_pos.x),
|
||||
float(window_pos.y),
|
||||
float(window_size.x),
|
||||
float(window_size.y),
|
||||
str(rm.current_path),
|
||||
))
|
||||
|
||||
def _draw_resource_directory_entries(self, rm, dirs):
|
||||
"""绘制当前目录下的文件夹列表。"""
|
||||
for dir_path in dirs:
|
||||
if not rm.should_show_file(dir_path):
|
||||
continue
|
||||
self._draw_resource_directory_entry(rm, dir_path)
|
||||
|
||||
def _draw_resource_directory_entry(self, rm, dir_path: Path):
|
||||
"""绘制单个文件夹节点(含一层展开内容)。"""
|
||||
icon_name = rm.get_file_icon(dir_path.name, is_folder=True)
|
||||
is_selected = dir_path in rm.selected_files
|
||||
|
||||
if is_selected:
|
||||
imgui.push_style_color(imgui.Col_.header, (100 / 255, 150 / 255, 200 / 255, 1.0))
|
||||
|
||||
icon_texture = self._load_resource_icon(icon_name)
|
||||
if icon_texture:
|
||||
imgui.image(icon_texture, (16, 16))
|
||||
imgui.same_line()
|
||||
node_open = imgui.tree_node(f"{dir_path.name}##dir_{dir_path}")
|
||||
else:
|
||||
node_open = imgui.tree_node(f"[{icon_name.upper()}]{dir_path.name}##dir_{dir_path}")
|
||||
|
||||
if is_selected:
|
||||
imgui.pop_style_color()
|
||||
|
||||
self._append_drop_target_from_last_item(dir_path)
|
||||
self._start_resource_drag_if_needed(rm, dir_path)
|
||||
|
||||
if imgui.is_item_clicked():
|
||||
self._handle_resource_item_selection(rm, dir_path, is_selected)
|
||||
if imgui.is_item_hovered() and imgui.is_mouse_double_clicked(0):
|
||||
rm.navigate_to(dir_path)
|
||||
if imgui.is_item_hovered() and imgui.is_mouse_clicked(1):
|
||||
self._open_resource_item_context_menu(rm, dir_path)
|
||||
|
||||
if node_open:
|
||||
self._draw_resource_directory_children(rm, dir_path)
|
||||
imgui.tree_pop()
|
||||
|
||||
def _draw_resource_directory_children(self, rm, parent_dir: Path):
|
||||
"""绘制文件夹展开后的子目录与子文件(保持原有顺序)。"""
|
||||
subdirs, subfiles = rm.get_directory_contents(parent_dir)
|
||||
|
||||
for subdir in subdirs:
|
||||
if not rm.should_show_file(subdir):
|
||||
continue
|
||||
self._draw_resource_subdir_entry(rm, subdir)
|
||||
|
||||
for subfile in subfiles:
|
||||
if not rm.should_show_file(subfile):
|
||||
continue
|
||||
self._draw_resource_subfile_entry(rm, subfile)
|
||||
|
||||
def _draw_resource_subdir_entry(self, rm, subdir: Path):
|
||||
"""绘制一级子目录节点。"""
|
||||
subicon_name = rm.get_file_icon(subdir.name, is_folder=True)
|
||||
sub_is_selected = subdir in rm.selected_files
|
||||
subicon_texture = self._load_resource_icon(subicon_name)
|
||||
|
||||
if subicon_texture:
|
||||
imgui.image(subicon_texture, (16, 16))
|
||||
imgui.same_line()
|
||||
sub_node_open = imgui.tree_node(f" {subdir.name}##dir_{subdir}")
|
||||
else:
|
||||
sub_node_open = imgui.tree_node(f" [{subicon_name.upper()}]{subdir.name}##dir_{subdir}")
|
||||
|
||||
self._append_drop_target_from_last_item(subdir)
|
||||
self._start_resource_drag_if_needed(rm, subdir)
|
||||
|
||||
if imgui.is_item_clicked():
|
||||
self._handle_resource_item_selection(rm, subdir, sub_is_selected)
|
||||
if imgui.is_item_hovered() and imgui.is_mouse_double_clicked(0):
|
||||
rm.navigate_to(subdir)
|
||||
if imgui.is_item_hovered() and imgui.is_mouse_clicked(1):
|
||||
self._open_resource_item_context_menu(rm, subdir)
|
||||
|
||||
if sub_node_open:
|
||||
imgui.tree_pop()
|
||||
|
||||
def _draw_resource_subfile_entry(self, rm, subfile: Path):
|
||||
"""绘制一级子文件项。"""
|
||||
subicon_name = rm.get_file_icon(subfile.name)
|
||||
sub_is_selected = subfile in rm.selected_files
|
||||
subicon_texture = self._load_resource_icon(subicon_name)
|
||||
|
||||
if subicon_texture:
|
||||
imgui.image(subicon_texture, (16, 16))
|
||||
imgui.same_line()
|
||||
selected = imgui.selectable(f" {subfile.name}##file_{subfile}", sub_is_selected)
|
||||
else:
|
||||
selected = imgui.selectable(f" [{subicon_name.upper()}] {subfile.name}##file_{subfile}", sub_is_selected)
|
||||
|
||||
selected_clicked = selected[0] if isinstance(selected, tuple) else bool(selected)
|
||||
self._start_resource_drag_if_needed(rm, subfile)
|
||||
|
||||
if selected_clicked:
|
||||
self._handle_resource_item_selection(rm, subfile, sub_is_selected)
|
||||
if imgui.is_item_hovered() and imgui.is_mouse_double_clicked(0):
|
||||
self._handle_resource_file_double_click(rm, subfile)
|
||||
if imgui.is_item_hovered() and imgui.is_mouse_clicked(1):
|
||||
self._open_resource_item_context_menu(rm, subfile)
|
||||
|
||||
def _draw_resource_file_entries(self, rm, files):
|
||||
"""绘制当前目录下的文件列表。"""
|
||||
for file_path in files:
|
||||
if not rm.should_show_file(file_path):
|
||||
continue
|
||||
self._draw_resource_file_entry(rm, file_path)
|
||||
|
||||
def _draw_resource_file_entry(self, rm, file_path: Path):
|
||||
"""绘制顶层文件项。"""
|
||||
icon_name = rm.get_file_icon(file_path.name)
|
||||
is_selected = file_path in rm.selected_files
|
||||
icon_texture = self._load_resource_icon(icon_name)
|
||||
|
||||
if icon_texture:
|
||||
imgui.image(icon_texture, (16, 16))
|
||||
imgui.same_line()
|
||||
selected = imgui.selectable(f"{file_path.name}##file_{file_path}", is_selected)
|
||||
else:
|
||||
selected = imgui.selectable(f"[{icon_name.upper()}] {file_path.name}##file_{file_path}", is_selected)
|
||||
|
||||
selected_clicked = selected[0] if isinstance(selected, tuple) else bool(selected)
|
||||
self._start_resource_drag_if_needed(rm, file_path)
|
||||
|
||||
if selected_clicked:
|
||||
self._handle_resource_item_selection(rm, file_path, is_selected)
|
||||
if imgui.is_item_hovered() and imgui.is_mouse_double_clicked(0):
|
||||
self._handle_resource_file_double_click(rm, file_path)
|
||||
if imgui.is_item_hovered() and imgui.is_mouse_clicked(1):
|
||||
self._open_resource_item_context_menu(rm, file_path)
|
||||
|
||||
def _draw_resource_toolbar_and_filters(self, rm):
|
||||
"""绘制资源管理器顶部工具条与筛选输入。保持原有按钮顺序与文案。"""
|
||||
if imgui.button("◀"):
|
||||
rm.navigate_back()
|
||||
imgui.same_line()
|
||||
if imgui.button("▶"):
|
||||
rm.navigate_forward()
|
||||
imgui.same_line()
|
||||
if imgui.button("▲"):
|
||||
rm.navigate_up()
|
||||
imgui.same_line()
|
||||
if imgui.button("主页"):
|
||||
rm.navigate_to(rm.project_root / "Resources")
|
||||
imgui.same_line()
|
||||
if imgui.button("刷新"):
|
||||
rm.force_refresh()
|
||||
|
||||
# 自动刷新开关
|
||||
imgui.same_line()
|
||||
changed, rm.auto_refresh_enabled = imgui.checkbox("自动刷新", rm.auto_refresh_enabled)
|
||||
if changed:
|
||||
rm.set_auto_refresh(rm.auto_refresh_enabled)
|
||||
|
||||
imgui.same_line()
|
||||
imgui.text(" ")
|
||||
imgui.same_line()
|
||||
|
||||
# 路径输入框
|
||||
changed, new_path = imgui.input_text("路径", str(rm.current_path), 256)
|
||||
if changed:
|
||||
try:
|
||||
rm.navigate_to(Path(new_path))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 搜索框
|
||||
changed, rm.search_filter = imgui.input_text("搜索", rm.search_filter, 256)
|
||||
|
||||
def _load_resource_icon(self, icon_name: str):
|
||||
"""加载资源图标;失败时返回 None。"""
|
||||
try:
|
||||
return self.app.style_manager.load_icon(f"file_types/{icon_name}")
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _handle_resource_item_selection(self, rm, target_path: Path, is_selected: bool):
|
||||
"""处理资源项选中逻辑(支持 Ctrl 多选)。"""
|
||||
if imgui.get_io().key_ctrl:
|
||||
if is_selected:
|
||||
rm.selected_files.discard(target_path)
|
||||
else:
|
||||
rm.selected_files.add(target_path)
|
||||
else:
|
||||
rm.selected_files.clear()
|
||||
rm.selected_files.add(target_path)
|
||||
rm.focused_file = target_path
|
||||
|
||||
def _open_resource_item_context_menu(self, rm, target_path: Path):
|
||||
"""打开资源项右键菜单。"""
|
||||
rm.show_context_menu = True
|
||||
rm.context_menu_file = target_path
|
||||
rm.context_menu_position = (imgui.get_mouse_pos().x, imgui.get_mouse_pos().y)
|
||||
imgui.open_popup("resource_context_menu")
|
||||
|
||||
def _handle_resource_file_double_click(self, rm, file_path: Path):
|
||||
"""处理文件双击:模型导入,其他文件打开。"""
|
||||
if self._is_model_file(file_path):
|
||||
self.app.add_info_message(f"正在导入模型: {file_path.name}")
|
||||
self.app._import_model_for_runtime(str(file_path))
|
||||
else:
|
||||
rm.open_file(file_path)
|
||||
|
||||
def _draw_resource_context_menu(self, rm):
|
||||
"""绘制资源管理器右键菜单。保持原有菜单项与顺序。"""
|
||||
if rm.context_menu_file:
|
||||
imgui.set_next_window_pos((rm.context_menu_position[0], rm.context_menu_position[1]))
|
||||
|
||||
if imgui.begin_popup("resource_context_menu"):
|
||||
if rm.context_menu_file and rm.context_menu_file.is_dir():
|
||||
if imgui.menu_item("打开")[1]:
|
||||
rm.navigate_to(rm.context_menu_file)
|
||||
imgui.separator()
|
||||
if imgui.menu_item("重命名")[1]:
|
||||
print(f"重命名文件夹: {rm.context_menu_file.name}")
|
||||
if imgui.menu_item("删除")[1]:
|
||||
print(f"删除文件夹: {rm.context_menu_file.name}")
|
||||
elif rm.context_menu_file:
|
||||
if imgui.menu_item("打开")[1]:
|
||||
rm.open_file(rm.context_menu_file)
|
||||
imgui.separator()
|
||||
if imgui.menu_item("导入到场景")[1]:
|
||||
if self._is_model_file(rm.context_menu_file):
|
||||
self.app.add_info_message(f"正在导入模型: {rm.context_menu_file.name}")
|
||||
self.app._import_model_for_runtime(str(rm.context_menu_file))
|
||||
if imgui.menu_item("重命名")[1]:
|
||||
print(f"重命名文件: {rm.context_menu_file.name}")
|
||||
if imgui.menu_item("删除")[1]:
|
||||
print(f"删除文件: {rm.context_menu_file.name}")
|
||||
|
||||
if rm.context_menu_file:
|
||||
imgui.separator()
|
||||
if imgui.menu_item("复制路径")[1]:
|
||||
imgui.set_clipboard_text(str(rm.context_menu_file))
|
||||
self.app.add_info_message("路径已复制到剪贴板")
|
||||
if imgui.menu_item("在文件管理器中显示")[1]:
|
||||
import platform
|
||||
import subprocess
|
||||
if platform.system() == "Windows":
|
||||
subprocess.run(["explorer", "/select,", str(rm.context_menu_file)])
|
||||
elif platform.system() == "Darwin":
|
||||
subprocess.run(["open", "-R", str(rm.context_menu_file)])
|
||||
else:
|
||||
subprocess.run(["xdg-open", str(rm.context_menu_file.parent)])
|
||||
|
||||
if imgui.is_mouse_clicked(0) and not imgui.is_window_hovered():
|
||||
rm.context_menu_file = None
|
||||
rm.show_context_menu = False
|
||||
imgui.close_current_popup()
|
||||
|
||||
imgui.end_popup()
|
||||
|
||||
@staticmethod
|
||||
def _is_model_file(path: Path) -> bool:
|
||||
return path.suffix.lower() in ['.gltf', '.glb', '.fbx', '.bam', '.egg', '.obj']
|
||||
|
||||
def _append_drop_target_from_last_item(self, target_path: Path):
|
||||
item_min = imgui.get_item_rect_min()
|
||||
item_max = imgui.get_item_rect_max()
|
||||
width = float(item_max.x - item_min.x)
|
||||
height = float(item_max.y - item_min.y)
|
||||
if width <= 0 or height <= 0:
|
||||
return
|
||||
self.app._resource_drop_targets.append((
|
||||
float(item_min.x),
|
||||
float(item_min.y),
|
||||
width,
|
||||
height,
|
||||
str(target_path),
|
||||
))
|
||||
|
||||
def _start_resource_drag_if_needed(self, rm, fallback_path: Path):
|
||||
if not imgui.is_item_active() or not imgui.is_mouse_dragging(0):
|
||||
return
|
||||
drag_files = list(rm.selected_files) if rm.selected_files else [fallback_path]
|
||||
rm.start_drag(drag_files)
|
||||
self.app.is_dragging = True
|
||||
self.app.show_drag_overlay = True
|
||||
|
||||
766
ui/panels/editor_panels_right.py
Normal file
766
ui/panels/editor_panels_right.py
Normal file
@ -0,0 +1,766 @@
|
||||
from imgui_bundle import imgui, imgui_ctx
|
||||
|
||||
from ui.panels.editor_panels_right_collision import EditorPanelsRightCollisionMixin
|
||||
from ui.panels.editor_panels_right_material import EditorPanelsRightMaterialMixin
|
||||
from ui.panels.editor_panels_right_transform import EditorPanelsRightTransformMixin
|
||||
|
||||
|
||||
class EditorPanelsRightMixin(
|
||||
EditorPanelsRightTransformMixin,
|
||||
EditorPanelsRightMaterialMixin,
|
||||
EditorPanelsRightCollisionMixin,
|
||||
):
|
||||
"""Right panel aggregator mixin."""
|
||||
|
||||
def _draw_property_panel(self):
|
||||
"""绘制属性面板"""
|
||||
# 使用面板类型的窗口标志,支持docking
|
||||
flags = self.app.style_manager.get_window_flags("panel")
|
||||
|
||||
with self.app.style_manager.begin_styled_window("属性面板", self.app.showPropertyPanel, flags):
|
||||
self.app.showPropertyPanel = True # 确保窗口保持打开
|
||||
|
||||
# --- LUI Component Properties ---
|
||||
# 优先检查 LUI 组件选择
|
||||
if hasattr(self.app, 'lui_manager'):
|
||||
lui_selected_index = getattr(self.app.lui_manager, "selected_index", -1)
|
||||
if lui_selected_index >= 0 and self.app.lui_manager.luiFunction:
|
||||
self.app.lui_manager.luiFunction._draw_component_properties(
|
||||
self.app.lui_manager,
|
||||
lui_selected_index
|
||||
)
|
||||
return
|
||||
|
||||
# --- Scene Node Properties ---
|
||||
# 获取当前选中的节点
|
||||
selected_node = None
|
||||
if hasattr(self.app, 'selection') and self.app.selection and hasattr(self.app.selection, 'selectedNode'):
|
||||
selected_node = self.app.selection.selectedNode
|
||||
|
||||
# SSBO mode may select a proxy node for gizmo operations.
|
||||
# Resolve proxy back to a real scene node so property panel stays meaningful.
|
||||
try:
|
||||
if (selected_node and not selected_node.isEmpty() and
|
||||
selected_node.hasTag("is_ssbo_proxy")):
|
||||
ssbo_editor = getattr(self.app, "ssbo_editor", None)
|
||||
controller = getattr(ssbo_editor, "controller", None) if ssbo_editor else None
|
||||
if ssbo_editor and controller:
|
||||
resolved = None
|
||||
if getattr(ssbo_editor, "selected_ids", None):
|
||||
first_gid = ssbo_editor.selected_ids[0]
|
||||
key = controller.id_to_name.get(first_gid)
|
||||
if key:
|
||||
resolved = controller.key_to_node.get(key)
|
||||
if (resolved is None or resolved.isEmpty()) and getattr(ssbo_editor, "selected_name", None):
|
||||
resolved = controller.key_to_node.get(ssbo_editor.selected_name)
|
||||
if resolved and not resolved.isEmpty():
|
||||
selected_node = resolved
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if selected_node and not selected_node.isEmpty():
|
||||
self._draw_node_properties(selected_node)
|
||||
else:
|
||||
# 无选中对象时显示提示(模仿Qt版本的空状态样式)
|
||||
imgui.spacing()
|
||||
imgui.spacing()
|
||||
|
||||
# 居中显示提示信息
|
||||
window_width = imgui.get_window_width()
|
||||
text_width = 200 # 估算文本宽度
|
||||
text_pos_x = (window_width - text_width) / 2
|
||||
|
||||
imgui.set_cursor_pos_x(text_pos_x)
|
||||
imgui.text_colored((0.5, 0.5, 0.5, 1.0), "🔍 未选择任何对象")
|
||||
|
||||
imgui.set_cursor_pos_x(text_pos_x - 20)
|
||||
imgui.text("请从场景树中选择一个对象")
|
||||
|
||||
imgui.set_cursor_pos_x(text_pos_x + 10)
|
||||
imgui.text("以查看其属性")
|
||||
|
||||
imgui.spacing()
|
||||
imgui.spacing()
|
||||
|
||||
# 添加一些分隔线和装饰
|
||||
imgui.separator()
|
||||
|
||||
# 显示快速提示
|
||||
imgui.text("💡 快速提示:")
|
||||
imgui.bullet_text("单击场景树中的对象进行选择")
|
||||
imgui.bullet_text("使用 F 键快速聚焦到选中对象")
|
||||
imgui.bullet_text("使用 Delete 键删除选中对象")
|
||||
|
||||
def _draw_node_properties(self, node):
|
||||
"""绘制节点属性"""
|
||||
if not node or node.isEmpty():
|
||||
# 停止变换监控
|
||||
self.app.stop_transform_monitoring()
|
||||
return
|
||||
|
||||
# 检查是否需要重新启动变换监控
|
||||
if self.app._monitored_node != node:
|
||||
self.app.stop_transform_monitoring()
|
||||
self.app.start_transform_monitoring(node)
|
||||
|
||||
# 获取节点基本信息
|
||||
node_name = node.getName() or "未命名节点"
|
||||
node_type = self.app._get_node_type_from_node(node)
|
||||
|
||||
# 添加一些间距,模仿Qt版本的布局
|
||||
imgui.spacing()
|
||||
|
||||
# 物体名称组(使用Qt版本的样式)
|
||||
if imgui.collapsing_header("物体名称"):
|
||||
# 第一行:可见性复选框和名称输入
|
||||
user_visible = node.getPythonTag("user_visible")
|
||||
if user_visible is None:
|
||||
user_visible = True
|
||||
node.setPythonTag("user_visible", True)
|
||||
|
||||
# 可见性复选框(模仿Qt版本的样式)
|
||||
changed, is_visible = imgui.checkbox("##visibility", user_visible)
|
||||
if changed:
|
||||
node.setPythonTag("user_visible", is_visible)
|
||||
if is_visible:
|
||||
node.show()
|
||||
else:
|
||||
node.hide()
|
||||
|
||||
imgui.same_line()
|
||||
imgui.text("可见")
|
||||
imgui.same_line()
|
||||
imgui.spacing()
|
||||
imgui.same_line()
|
||||
|
||||
# 名称输入框(模仿Qt版本的样式)
|
||||
imgui.text("名称:")
|
||||
imgui.same_line()
|
||||
changed, new_name = imgui.input_text("##name", node_name, 256)
|
||||
if changed and hasattr(self.app, 'selection'):
|
||||
# 更新场景树中的名称
|
||||
self.app._update_node_name(node, new_name)
|
||||
|
||||
# 添加分隔线
|
||||
imgui.separator()
|
||||
|
||||
# 状态徽章(模仿Qt版本的徽章样式)
|
||||
self.app._draw_status_badges(node)
|
||||
|
||||
imgui.spacing()
|
||||
|
||||
# 变换属性组
|
||||
if imgui.collapsing_header("变换 Transform"):
|
||||
self.app._draw_transform_properties(node)
|
||||
|
||||
# 根据节点类型显示特定属性组
|
||||
if node_type == "GUI元素":
|
||||
if imgui.collapsing_header("GUI信息"):
|
||||
self.app._draw_gui_properties(node)
|
||||
elif node_type == "光源":
|
||||
if imgui.collapsing_header("光源属性"):
|
||||
self.app._draw_light_properties(node)
|
||||
elif node_type == "模型":
|
||||
if imgui.collapsing_header("模型属性"):
|
||||
self.app._draw_model_properties(node)
|
||||
|
||||
# 动画控制组(只对模型显示)
|
||||
if imgui.collapsing_header("动画控制"):
|
||||
self.app._draw_animation_properties(node)
|
||||
|
||||
# 外观属性组(通用)
|
||||
if imgui.collapsing_header("外观属性"):
|
||||
self.app._draw_appearance_properties(node)
|
||||
|
||||
# 碰撞检测组
|
||||
if imgui.collapsing_header("碰撞检测"):
|
||||
self.app._draw_collision_properties(node)
|
||||
|
||||
# 操作按钮组
|
||||
if imgui.collapsing_header("操作"):
|
||||
self.app._draw_property_actions(node)
|
||||
|
||||
def _draw_status_badges(self, node):
|
||||
"""绘制状态徽章(模仿Qt版本的徽章样式)"""
|
||||
imgui.text("状态标签: ")
|
||||
|
||||
# 可见性状态徽章
|
||||
is_visible = not node.is_hidden()
|
||||
visibility_color = (0.176, 1.0, 0.769, 1.0) if is_visible else (0.953, 0.616, 0.471, 1.0)
|
||||
visibility_text = "可见" if is_visible else "隐藏"
|
||||
|
||||
imgui.same_line()
|
||||
imgui.text_colored(visibility_color, f"[{visibility_text}]")
|
||||
|
||||
# 节点类型徽章
|
||||
node_type = self._get_node_type_from_node(node)
|
||||
type_colors = {
|
||||
"GUI元素": (0.188, 0.404, 0.753, 1.0), # 主题蓝色
|
||||
"光源": (1.0, 0.8, 0.2, 1.0), # 黄色
|
||||
"模型": (0.6, 0.8, 1.0, 1.0), # 浅蓝色
|
||||
"相机": (0.8, 0.8, 0.2, 1.0), # 橙色
|
||||
"几何体": (0.5, 0.5, 0.5, 1.0), # 灰色
|
||||
}
|
||||
|
||||
if node_type in type_colors:
|
||||
imgui.same_line()
|
||||
imgui.text_colored(type_colors[node_type], f"[{node_type}]")
|
||||
|
||||
# 功能性徽章
|
||||
badges = []
|
||||
|
||||
# 碰撞体徽章
|
||||
has_collision = hasattr(node, 'getChild') and any('Collision' in child.getName() for child in node.getChildren() if child.getName())
|
||||
if has_collision:
|
||||
badges.append(("碰撞", (0.2, 0.4, 0.8, 1.0))) # 蓝色
|
||||
|
||||
# 脚本徽章
|
||||
has_script = hasattr(node, 'getPythonTag') and node.getPythonTag('script')
|
||||
if has_script:
|
||||
badges.append(("脚本", (0.8, 0.4, 0.8, 1.0))) # 紫色
|
||||
|
||||
# 动画徽章(优化检测逻辑,避免重复创建Actor)
|
||||
has_animation = False
|
||||
if node_type == "模型": # 只对模型类型进行动画检测
|
||||
model_path = node.getTag("model_path") if node.hasTag("model_path") else ""
|
||||
likely_anim_format = bool(model_path and model_path.lower().endswith(('.glb', '.gltf', '.fbx', '.bam', '.egg')))
|
||||
|
||||
# 优先使用场景标签(导入/加载时会写入)
|
||||
if node.hasTag("has_animations"):
|
||||
has_animation = node.getTag("has_animations").lower() == "true"
|
||||
|
||||
# 再做轻量结构检测(不依赖 Actor)
|
||||
if not has_animation:
|
||||
try:
|
||||
has_character = node.findAllMatches("**/+Character").getNumPaths() > 0
|
||||
has_bundle = node.findAllMatches("**/+AnimBundleNode").getNumPaths() > 0
|
||||
has_animation = has_character or has_bundle
|
||||
if has_animation:
|
||||
node.setTag("has_animations", "true")
|
||||
node.setTag("can_create_actor_from_memory", "true")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 最后才尝试 Actor 检测(只缓存“有动画”,避免把失败结果永久缓存)
|
||||
cached_result = node.getPythonTag('animation')
|
||||
if cached_result is True:
|
||||
has_animation = True
|
||||
elif not has_animation and likely_anim_format:
|
||||
try:
|
||||
actor = self._getActor(node)
|
||||
if actor and actor.getAnimNames():
|
||||
has_animation = True
|
||||
node.setTag("has_animations", "true")
|
||||
node.setPythonTag('animation', True)
|
||||
print(f"[动画检测] {node.getName()}: 有动画")
|
||||
except Exception as e:
|
||||
print(f"动画检测失败: {e}")
|
||||
elif cached_result is False and not likely_anim_format:
|
||||
has_animation = False
|
||||
else:
|
||||
# 对于非模型类型,检查已有的动画标签
|
||||
has_animation = hasattr(node, 'getPythonTag') and node.getPythonTag('animation')
|
||||
|
||||
if has_animation:
|
||||
badges.append(("动画", (0.4, 0.8, 0.4, 1.0))) # 绿色
|
||||
|
||||
# 材质徽章
|
||||
has_material = hasattr(node, 'getMaterial') and node.getMaterial()
|
||||
if has_material:
|
||||
badges.append(("材质", (0.8, 0.6, 0.2, 1.0))) # 金色
|
||||
|
||||
# 绘制功能性徽章
|
||||
for badge_text, badge_color in badges:
|
||||
imgui.same_line()
|
||||
imgui.text_colored(badge_color, f"[{badge_text}]")
|
||||
|
||||
# 如果没有特殊徽章,显示默认状态
|
||||
if not badges:
|
||||
imgui.same_line()
|
||||
imgui.text_colored((0.5, 0.5, 0.5, 1.0), "[标准对象]")
|
||||
|
||||
def _draw_gui_properties(self, node):
|
||||
"""绘制GUI元素属性"""
|
||||
# 获取GUI元素
|
||||
gui_element = None
|
||||
if hasattr(node, 'getPythonTag'):
|
||||
gui_element = node.getPythonTag('gui_element')
|
||||
|
||||
if not gui_element:
|
||||
imgui.text("无GUI元素数据")
|
||||
return
|
||||
|
||||
# GUI类型信息
|
||||
gui_type = getattr(gui_element, 'gui_type', 'UNKNOWN')
|
||||
imgui.text(f"GUI类型: {gui_type}")
|
||||
|
||||
# 基本属性
|
||||
if imgui.collapsing_header("基本属性"):
|
||||
# 文本内容 (适用于按钮、标签等)
|
||||
if hasattr(gui_element, 'text'):
|
||||
changed, new_text = imgui.input_text("文本内容", gui_element.text, 256)
|
||||
if changed and hasattr(self, 'gui_manager'):
|
||||
self.gui_manager.editGUIElement(gui_element, 'text', new_text)
|
||||
|
||||
# GUI ID
|
||||
gui_id = getattr(gui_element, 'id', '')
|
||||
changed, new_id = imgui.input_text("GUI ID", gui_id, 64)
|
||||
if changed and hasattr(self, 'gui_manager'):
|
||||
gui_element.id = new_id
|
||||
|
||||
# 变换属性
|
||||
if imgui.collapsing_header("变换属性"):
|
||||
# 位置
|
||||
pos = gui_element.getPos()
|
||||
imgui.text("位置")
|
||||
|
||||
if gui_type in ["button", "label", "entry", "2d_image"]:
|
||||
# 2D GUI组件使用屏幕坐标
|
||||
imgui.text("屏幕坐标")
|
||||
logical_x = pos.getX() / 0.1
|
||||
logical_z = pos.getZ() / 0.1
|
||||
|
||||
changed, new_x = imgui.input_float("X##gui_pos_x", logical_x, 1.0, 10.0, "%.1f")
|
||||
if changed and hasattr(self, 'gui_manager'):
|
||||
gui_element.setX(new_x * 0.1)
|
||||
|
||||
changed, new_z = imgui.input_float("Y##gui_pos_y", logical_z, 1.0, 10.0, "%.1f")
|
||||
if changed and hasattr(self, 'gui_manager'):
|
||||
gui_element.setZ(new_z * 0.1)
|
||||
else:
|
||||
# 3D GUI组件使用世界坐标
|
||||
imgui.text("世界坐标")
|
||||
changed, new_x = imgui.input_float("X##gui_world_x", pos.getX(), 0.1, 1.0, "%.3f")
|
||||
if changed and hasattr(self, 'gui_manager'):
|
||||
gui_element.setX(new_x)
|
||||
|
||||
changed, new_y = imgui.input_float("Y##gui_world_y", pos.getY(), 0.1, 1.0, "%.3f")
|
||||
if changed and hasattr(self, 'gui_manager'):
|
||||
gui_element.setY(new_y)
|
||||
|
||||
changed, new_z = imgui.input_float("Z##gui_world_z", pos.getZ(), 0.1, 1.0, "%.3f")
|
||||
if changed and hasattr(self, 'gui_manager'):
|
||||
gui_element.setZ(new_z)
|
||||
|
||||
# 缩放
|
||||
scale = gui_element.getScale()
|
||||
imgui.text("缩放")
|
||||
changed, new_sx = imgui.input_float("X##gui_scale_x", scale.getX(), 0.1, 1.0, "%.3f")
|
||||
if changed and hasattr(self, 'gui_manager'):
|
||||
gui_element.setSx(new_sx)
|
||||
|
||||
changed, new_sy = imgui.input_float("Y##gui_scale_y", scale.getY(), 0.1, 1.0, "%.3f")
|
||||
if changed and hasattr(self, 'gui_manager'):
|
||||
gui_element.setSy(new_sy)
|
||||
|
||||
changed, new_sz = imgui.input_float("Z##gui_scale_z", scale.getZ(), 0.1, 1.0, "%.3f")
|
||||
if changed and hasattr(self, 'gui_manager'):
|
||||
gui_element.setSz(new_sz)
|
||||
|
||||
# 旋转
|
||||
hpr = gui_element.getHpr()
|
||||
imgui.text("旋转")
|
||||
changed, new_h = imgui.input_float("H##gui_rot_h", hpr.getX(), 1.0, 10.0, "%.1f")
|
||||
if changed and hasattr(self, 'gui_manager'):
|
||||
gui_element.setH(new_h)
|
||||
|
||||
changed, new_p = imgui.input_float("P##gui_rot_p", hpr.getY(), 1.0, 10.0, "%.1f")
|
||||
if changed and hasattr(self, 'gui_manager'):
|
||||
gui_element.setP(new_p)
|
||||
|
||||
changed, new_r = imgui.input_float("R##gui_rot_r", hpr.getZ(), 1.0, 10.0, "%.1f")
|
||||
if changed and hasattr(self, 'gui_manager'):
|
||||
gui_element.setR(new_r)
|
||||
|
||||
# 外观属性
|
||||
if imgui.collapsing_header("外观属性"):
|
||||
# 大小
|
||||
if hasattr(gui_element, 'size'):
|
||||
size = gui_element.size
|
||||
imgui.text("大小")
|
||||
changed, new_w = imgui.input_float("宽度", size[0], 1.0, 10.0, "%.1f")
|
||||
if changed and hasattr(self, 'gui_manager'):
|
||||
new_size = (new_w, size[1])
|
||||
self.gui_manager.editGUIElement(gui_element, 'size', new_size)
|
||||
|
||||
changed, new_h = imgui.input_float("高度", size[1], 1.0, 10.0, "%.1f")
|
||||
if changed and hasattr(self, 'gui_manager'):
|
||||
new_size = (size[0], new_h)
|
||||
self.gui_manager.editGUIElement(gui_element, 'size', new_size)
|
||||
|
||||
# 颜色
|
||||
if hasattr(gui_element, 'getColor'):
|
||||
try:
|
||||
color = gui_element.getColor()
|
||||
# 确保颜色是有效的
|
||||
if not color or (hasattr(color, '__len__') and len(color) < 3):
|
||||
color = (1.0, 1.0, 1.0, 1.0) # 默认白色
|
||||
except:
|
||||
color = (1.0, 1.0, 1.0, 1.0) # 默认白色
|
||||
|
||||
imgui.text("颜色")
|
||||
# 获取颜色值
|
||||
if hasattr(color, 'getX'):
|
||||
# 如果是Panda3D的Vec4对象
|
||||
r, g, b = color.getX(), color.getY(), color.getZ()
|
||||
else:
|
||||
# 如果是元组或其他格式
|
||||
r, g, b = color[0], color[1], color[2]
|
||||
|
||||
changed, new_r = imgui.slider_float("R##gui_color_r", r, 0.0, 1.0)
|
||||
if changed: gui_element.setColor(new_r, g, b, 1.0)
|
||||
|
||||
changed, new_g = imgui.slider_float("G##gui_color_g", g, 0.0, 1.0)
|
||||
if changed: gui_element.setColor(r, new_g, b, 1.0)
|
||||
|
||||
changed, new_b = imgui.slider_float("B##gui_color_b", b, 0.0, 1.0)
|
||||
if changed: gui_element.setColor(r, g, new_b, 1.0)
|
||||
# 透明度
|
||||
imgui.text("透明度")
|
||||
current_alpha = getattr(gui_element, 'alpha', 1.0)
|
||||
changed, new_alpha = imgui.slider_float("Alpha", current_alpha, 0.0, 1.0)
|
||||
if changed:
|
||||
gui_element.alpha = new_alpha
|
||||
if hasattr(gui_element, 'setTransparency'):
|
||||
# 将0.0-1.0范围转换为Panda3D的透明度格式
|
||||
panda_transparency = int((1.0 - new_alpha) * 255)
|
||||
gui_element.setTransparency(panda_transparency)
|
||||
|
||||
# 渲染顺序
|
||||
imgui.text("渲染顺序")
|
||||
current_sort = getattr(gui_element, 'sort', 0)
|
||||
changed, new_sort = imgui.input_int("Sort Order", current_sort)
|
||||
if changed:
|
||||
gui_element.sort = new_sort
|
||||
if hasattr(gui_element, 'setBin'):
|
||||
gui_element.setBin('fixed', new_sort)
|
||||
|
||||
# 字体设置(适用于文本类型的GUI元素)
|
||||
if gui_type in ["button", "label", "entry"]:
|
||||
imgui.text("字体设置")
|
||||
|
||||
# 字体选择
|
||||
current_font = getattr(gui_element, 'font_path', '')
|
||||
if imgui.button(f"字体: {Path(current_font).name if current_font else '默认'}##font_select"):
|
||||
self.show_font_selector(
|
||||
gui_element,
|
||||
'font_path',
|
||||
current_font,
|
||||
lambda font_path: self._apply_gui_font(gui_element, font_path)
|
||||
)
|
||||
|
||||
# 字体大小
|
||||
current_size = getattr(gui_element, 'font_size', 12)
|
||||
changed, new_size = imgui.slider_float("字体大小", current_size, 8.0, 72.0)
|
||||
if changed:
|
||||
gui_element.font_size = new_size
|
||||
self._apply_gui_font_size(gui_element, new_size)
|
||||
|
||||
# 字体样式
|
||||
imgui.text("字体样式")
|
||||
is_bold = getattr(gui_element, 'font_bold', False)
|
||||
changed, new_bold = imgui.checkbox("粗体", is_bold)
|
||||
if changed:
|
||||
gui_element.font_bold = new_bold
|
||||
self._apply_gui_font_style(gui_element)
|
||||
|
||||
imgui.same_line()
|
||||
is_italic = getattr(gui_element, 'font_italic', False)
|
||||
changed, new_italic = imgui.checkbox("斜体", is_italic)
|
||||
if changed:
|
||||
gui_element.font_italic = new_italic
|
||||
self._apply_gui_font_style(gui_element)
|
||||
|
||||
def _draw_light_properties(self, node):
|
||||
"""绘制光源属性"""
|
||||
imgui.text("光源属性")
|
||||
|
||||
# 光源颜色
|
||||
if hasattr(node, 'getColor'):
|
||||
try:
|
||||
color = node.getColor()
|
||||
# 确保颜色是有效的
|
||||
if not color or len(color) < 3:
|
||||
color = (1.0, 1.0, 1.0, 1.0) # 默认白色
|
||||
except:
|
||||
color = (1.0, 1.0, 1.0, 1.0) # 默认白色
|
||||
|
||||
changed, new_r = imgui.drag_float("颜色 R", color[0], 0.01, 0.0, 1.0)
|
||||
if changed: node.setColor(new_r, color[1], color[2], color[3] if len(color) > 3 else 1.0)
|
||||
|
||||
changed, new_g = imgui.drag_float("颜色 G", color[1], 0.01, 0.0, 1.0)
|
||||
if changed: node.setColor(color[0], new_g, color[2], color[3] if len(color) > 3 else 1.0)
|
||||
|
||||
changed, new_b = imgui.drag_float("颜色 B", color[2], 0.01, 0.0, 1.0)
|
||||
if changed: node.setColor(color[0], color[1], new_b, color[3] if len(color) > 3 else 1.0)
|
||||
|
||||
# 光源强度
|
||||
imgui.text("光源强度: (暂不支持编辑)")
|
||||
|
||||
def _draw_model_properties(self, node):
|
||||
"""绘制模型属性"""
|
||||
# 获取模型信息
|
||||
model_path = node.getTag("model_path") if node.hasTag("model_path") else "未知"
|
||||
|
||||
imgui.text("模型路径:")
|
||||
imgui.same_line()
|
||||
imgui.text_colored((0.7, 0.7, 0.7, 1.0), model_path)
|
||||
|
||||
# 模型基本信息
|
||||
imgui.text("模型名称:")
|
||||
imgui.same_line()
|
||||
model_name = node.getName() or "未命名模型"
|
||||
imgui.text_colored((0.7, 0.7, 0.7, 1.0), model_name)
|
||||
|
||||
# 模型位置信息
|
||||
imgui.text("位置:")
|
||||
imgui.same_line()
|
||||
pos = node.getPos()
|
||||
imgui.text_colored((0.7, 0.7, 0.7, 1.0), f"X:{pos.x:.2f} Y:{pos.y:.2f} Z:{pos.z:.2f}")
|
||||
|
||||
# 模型缩放信息
|
||||
imgui.text("缩放:")
|
||||
imgui.same_line()
|
||||
scale = node.getScale()
|
||||
imgui.text_colored((0.7, 0.7, 0.7, 1.0), f"X:{scale.x:.2f} Y:{scale.y:.2f} Z:{scale.z:.2f}")
|
||||
|
||||
# 模型旋转信息
|
||||
imgui.text("旋转:")
|
||||
imgui.same_line()
|
||||
hpr = node.getHpr()
|
||||
imgui.text_colored((0.7, 0.7, 0.7, 1.0), f"H:{hpr.x:.1f}° P:{hpr.y:.1f}° R:{hpr.z:.1f}°")
|
||||
|
||||
def _draw_animation_properties(self, node):
|
||||
"""绘制动画控制属性面板(优化版本,使用缓存避免重复计算)"""
|
||||
anim_node = node
|
||||
try:
|
||||
if hasattr(self, "_resolve_animation_owner_model"):
|
||||
resolved = self._resolve_animation_owner_model(node)
|
||||
if resolved and not resolved.isEmpty():
|
||||
anim_node = resolved
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 路径兜底:当 anim_node 缺少路径时,从 scene_manager.models 中反查祖先模型
|
||||
try:
|
||||
needs_path = (not anim_node.hasTag("model_path")) or (not anim_node.getTag("model_path"))
|
||||
if needs_path and hasattr(self, "scene_manager") and self.scene_manager:
|
||||
models = getattr(self.scene_manager, "models", [])
|
||||
for model in list(models):
|
||||
try:
|
||||
if not model or model.isEmpty():
|
||||
continue
|
||||
if (model == anim_node or model.isAncestorOf(anim_node) or anim_node.isAncestorOf(model)):
|
||||
if model.hasTag("model_path") and model.getTag("model_path"):
|
||||
anim_node.setTag("model_path", model.getTag("model_path"))
|
||||
if model.hasTag("original_path") and model.getTag("original_path"):
|
||||
anim_node.setTag("original_path", model.getTag("original_path"))
|
||||
break
|
||||
if model.hasTag("saved_model_path") and model.getTag("saved_model_path"):
|
||||
anim_node.setTag("model_path", model.getTag("saved_model_path"))
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 先刷新一次模型动画标签,避免“导入后未初始化”导致误判
|
||||
try:
|
||||
if hasattr(self, "scene_manager") and self.scene_manager and hasattr(self.scene_manager, "_processModelAnimations"):
|
||||
self.scene_manager._processModelAnimations(anim_node)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
has_animation_tag = anim_node.hasTag("has_animations") and anim_node.getTag("has_animations").lower() == "true"
|
||||
has_animation_nodes = False
|
||||
try:
|
||||
has_animation_nodes = (
|
||||
anim_node.findAllMatches("**/+Character").getNumPaths() > 0 or
|
||||
anim_node.findAllMatches("**/+AnimBundleNode").getNumPaths() > 0
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 检查是否已经缓存了动画信息
|
||||
cached_anim_info = anim_node.getPythonTag("cached_anim_info")
|
||||
cached_processed_names = anim_node.getPythonTag("cached_processed_names")
|
||||
|
||||
# 如果之前缓存的是“格式未知”,但现在已有路径,强制重建缓存
|
||||
try:
|
||||
now_has_path = anim_node.hasTag("model_path") and bool(anim_node.getTag("model_path"))
|
||||
if now_has_path and isinstance(cached_anim_info, str) and "格式: 未知" in cached_anim_info:
|
||||
cached_anim_info = None
|
||||
cached_processed_names = None
|
||||
anim_node.setPythonTag("cached_anim_info", None)
|
||||
anim_node.setPythonTag("cached_processed_names", None)
|
||||
anim_node.setPythonTag("animation", None)
|
||||
self._clear_animation_cache(anim_node)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 如果节点已被检测为有动画,但缓存是“无动画”,强制重新检测一次
|
||||
if (has_animation_tag or has_animation_nodes) and (cached_anim_info == "无动画" or cached_processed_names == []):
|
||||
cached_anim_info = None
|
||||
cached_processed_names = None
|
||||
anim_node.setPythonTag("cached_anim_info", None)
|
||||
anim_node.setPythonTag("cached_processed_names", None)
|
||||
anim_node.setPythonTag("animation", None)
|
||||
|
||||
# 只有在没有缓存时才进行完整的动画检测和处理
|
||||
if cached_anim_info is None or cached_processed_names is None:
|
||||
# 获取Actor
|
||||
actor = self._getActor(anim_node)
|
||||
if not actor:
|
||||
if has_animation_tag or has_animation_nodes:
|
||||
imgui.text_colored((1.0, 0.7, 0.3, 1.0), "检测到动画结构,但当前未成功绑定Actor")
|
||||
else:
|
||||
imgui.text_colored((0.7, 0.7, 0.7, 1.0), "此模型不包含动画")
|
||||
return
|
||||
|
||||
# 获取和分析动画名称
|
||||
anim_names = actor.getAnimNames()
|
||||
processed_names = self._processAnimationNames(anim_node, anim_names)
|
||||
|
||||
if not processed_names:
|
||||
imgui.text_colored((0.7, 0.7, 0.7, 1.0), "未检测到动画序列")
|
||||
# 只在明确无动画时缓存空结果,避免误缓存导致后续无法重试
|
||||
if not (has_animation_tag or has_animation_nodes):
|
||||
anim_node.setPythonTag("cached_processed_names", [])
|
||||
anim_node.setPythonTag("cached_anim_info", "无动画")
|
||||
return
|
||||
|
||||
anim_node.setTag("has_animations", "true")
|
||||
anim_node.setPythonTag("animation", True)
|
||||
|
||||
# 计算并缓存动画信息
|
||||
format_info = self._getModelFormat(anim_node)
|
||||
animation_info = self._analyzeAnimationQuality(actor, anim_names, format_info)
|
||||
info_text = f"格式: {format_info} | 动画数量: {len(processed_names)}"
|
||||
if animation_info:
|
||||
info_text += f" | {animation_info}"
|
||||
|
||||
# 缓存结果
|
||||
anim_node.setPythonTag("cached_anim_info", info_text)
|
||||
anim_node.setPythonTag("cached_processed_names", processed_names)
|
||||
|
||||
else:
|
||||
# 使用缓存的数据
|
||||
info_text = cached_anim_info
|
||||
processed_names = cached_processed_names
|
||||
|
||||
# 如果缓存的空结果,直接返回
|
||||
if not processed_names:
|
||||
imgui.text_colored((0.7, 0.7, 0.7, 1.0), "未检测到动画序列")
|
||||
return
|
||||
|
||||
# 显示动画信息(使用缓存的数据)
|
||||
imgui.text("信息:")
|
||||
imgui.same_line()
|
||||
imgui.text_colored((0.7, 0.7, 0.7, 1.0), info_text)
|
||||
|
||||
imgui.spacing()
|
||||
|
||||
# 动画选择下拉框
|
||||
imgui.text("动画名称:")
|
||||
imgui.same_line()
|
||||
|
||||
# 获取当前选中的动画
|
||||
current_anim = anim_node.getPythonTag("selected_animation")
|
||||
valid_original_names = [original_name for _, original_name in processed_names]
|
||||
if current_anim is None or current_anim not in valid_original_names:
|
||||
current_anim = processed_names[0][1] if processed_names else ""
|
||||
anim_node.setPythonTag("selected_animation", current_anim)
|
||||
|
||||
# 查找当前动画的索引
|
||||
current_index = 0
|
||||
for i, (display_name, original_name) in enumerate(processed_names):
|
||||
if original_name == current_anim:
|
||||
current_index = i
|
||||
break
|
||||
|
||||
# 创建下拉框选项
|
||||
animation_options = [display_name for display_name, _ in processed_names]
|
||||
changed, new_index = imgui.combo("##animation_combo", current_index, animation_options)
|
||||
|
||||
if changed and new_index < len(processed_names):
|
||||
selected_display, selected_original = processed_names[new_index]
|
||||
anim_node.setPythonTag("selected_animation", selected_original)
|
||||
print(f"选择动画: {selected_display} (原始名称: {selected_original})")
|
||||
|
||||
imgui.spacing()
|
||||
|
||||
# 控制按钮组
|
||||
imgui.text("控制:")
|
||||
|
||||
# 播放按钮
|
||||
if imgui.button("播放##play_animation"):
|
||||
self._playAnimation(anim_node)
|
||||
imgui.same_line()
|
||||
|
||||
# 暂停按钮
|
||||
if imgui.button("暂停##pause_animation"):
|
||||
self._pauseAnimation(anim_node)
|
||||
imgui.same_line()
|
||||
|
||||
# 停止按钮
|
||||
if imgui.button("停止##stop_animation"):
|
||||
self._stopAnimation(anim_node)
|
||||
imgui.same_line()
|
||||
|
||||
# 循环按钮
|
||||
if imgui.button("循环##loop_animation"):
|
||||
self._loopAnimation(anim_node)
|
||||
|
||||
imgui.spacing()
|
||||
|
||||
# 播放速度控制
|
||||
imgui.text("播放速度:")
|
||||
imgui.same_line()
|
||||
|
||||
# 获取当前速度
|
||||
current_speed = anim_node.getPythonTag("anim_speed")
|
||||
if current_speed is None:
|
||||
current_speed = 1.0
|
||||
anim_node.setPythonTag("anim_speed", current_speed)
|
||||
|
||||
# 速度滑块
|
||||
changed, new_speed = imgui.slider_float("##anim_speed", current_speed, 0.1, 5.0, "%.1f")
|
||||
if changed:
|
||||
anim_node.setPythonTag("anim_speed", new_speed)
|
||||
self._setAnimationSpeed(anim_node, new_speed)
|
||||
|
||||
imgui.same_line()
|
||||
imgui.text_colored((0.7, 0.7, 0.7, 1.0), "倍速")
|
||||
|
||||
def _draw_property_actions(self, node):
|
||||
"""绘制属性操作按钮"""
|
||||
# 重置变换
|
||||
if imgui.button("重置变换"):
|
||||
node.setPos(0, 0, 0)
|
||||
node.setHpr(0, 0, 0)
|
||||
node.setScale(1, 1, 1)
|
||||
|
||||
imgui.same_line()
|
||||
|
||||
# 切换可见性
|
||||
is_visible = not node.is_hidden()
|
||||
visibility_text = "隐藏" if is_visible else "显示"
|
||||
if imgui.button(visibility_text):
|
||||
if is_visible:
|
||||
node.hide()
|
||||
else:
|
||||
node.show()
|
||||
|
||||
imgui.same_line()
|
||||
|
||||
# 聚焦到对象
|
||||
if imgui.button("聚焦"):
|
||||
if hasattr(self, 'selection') and self.selection:
|
||||
self.selection.focusCameraOnSelectedNodeAdvanced()
|
||||
|
||||
# 删除对象
|
||||
imgui.same_line()
|
||||
if imgui.button("删除"):
|
||||
if hasattr(self, 'selection') and self.selection:
|
||||
self.selection.deleteSelectedNode()
|
||||
|
||||
218
ui/panels/editor_panels_right_collision.py
Normal file
218
ui/panels/editor_panels_right_collision.py
Normal file
@ -0,0 +1,218 @@
|
||||
from imgui_bundle import imgui, imgui_ctx
|
||||
|
||||
class EditorPanelsRightCollisionMixin:
|
||||
"""Auto-split mixin from editor_panels_right.py."""
|
||||
|
||||
def _draw_collision_properties(self, node):
|
||||
"""绘制碰撞检测属性"""
|
||||
if not node or node.isEmpty():
|
||||
return
|
||||
|
||||
try:
|
||||
# 检查节点是否已有碰撞
|
||||
has_collision = self._has_collision(node)
|
||||
|
||||
# 碰撞状态徽章
|
||||
imgui.text("状态:")
|
||||
imgui.same_line()
|
||||
if has_collision:
|
||||
imgui.text_colored((0.0, 0.8, 0.0, 1.0), "🟢 已启用")
|
||||
else:
|
||||
imgui.text_colored((0.8, 0.0, 0.0, 1.0), "🔴 未启用")
|
||||
|
||||
imgui.separator()
|
||||
|
||||
# 碰撞形状选择
|
||||
imgui.text("碰撞形状:")
|
||||
imgui.same_line()
|
||||
|
||||
# 碰撞形状选项
|
||||
collision_shapes = ["球形 (Sphere)", "盒型 (Box)", "胶囊体 (Capsule)", "平面 (Plane)", "自动选择 (Auto)"]
|
||||
|
||||
# 获取当前形状
|
||||
current_shape = self._get_current_collision_shape(node) if has_collision else getattr(self.app, '_selected_collision_shape', "球形 (Sphere)")
|
||||
if has_collision: self.app._selected_collision_shape = current_shape
|
||||
|
||||
# 形状选择下拉框
|
||||
current_index = collision_shapes.index(current_shape) if current_shape in collision_shapes else 0
|
||||
changed, selected_index = imgui.combo("##collision_shape", current_index, collision_shapes)
|
||||
if changed:
|
||||
# 始终更新选择的形状
|
||||
selected_shape = collision_shapes[selected_index]
|
||||
self.app._selected_collision_shape = selected_shape
|
||||
# 如果已经有碰撞体,询问用户是否要重新创建
|
||||
if has_collision:
|
||||
self._remove_collision_from_node(node)
|
||||
self._add_collision_to_node(node)
|
||||
|
||||
imgui.separator()
|
||||
|
||||
# 位置偏移控件
|
||||
imgui.text("位置偏移:")
|
||||
|
||||
# 获取当前位置偏移
|
||||
pos_offset = self._get_collision_position_offset(node)
|
||||
|
||||
# X位置
|
||||
changed, new_x = imgui.drag_float("X##collision_pos_x", pos_offset[0], 0.1, -100.0, 100.0, "%.2f")
|
||||
if changed and has_collision:
|
||||
self._update_collision_position(node, 'x', new_x)
|
||||
|
||||
# Y位置
|
||||
changed, new_y = imgui.drag_float("Y##collision_pos_y", pos_offset[1], 0.1, -100.0, 100.0, "%.2f")
|
||||
if changed and has_collision:
|
||||
self._update_collision_position(node, 'y', new_y)
|
||||
|
||||
# Z位置
|
||||
changed, new_z = imgui.drag_float("Z##collision_pos_z", pos_offset[2], 0.1, -100.0, 100.0, "%.2f")
|
||||
if changed and has_collision:
|
||||
self._update_collision_position(node, 'z', new_z)
|
||||
|
||||
# 形状特定参数(始终显示,但根据状态启用/禁用)
|
||||
shape_type = self._get_current_collision_shape_type(node)
|
||||
self._draw_shape_specific_parameters(node, shape_type, has_collision)
|
||||
|
||||
imgui.separator()
|
||||
|
||||
# 操作按钮
|
||||
if has_collision:
|
||||
# 显示/隐藏碰撞体按钮
|
||||
is_visible = self._is_collision_visible(node)
|
||||
visibility_text = "隐藏碰撞体" if is_visible else "显示碰撞体"
|
||||
if imgui.button(visibility_text):
|
||||
self._toggle_collision_visibility(node)
|
||||
|
||||
imgui.same_line()
|
||||
|
||||
# 移除碰撞按钮
|
||||
if imgui.button("移除碰撞"):
|
||||
self._remove_collision_from_node(node)
|
||||
else:
|
||||
# 添加碰撞按钮
|
||||
if imgui.button("添加碰撞"):
|
||||
self._add_collision_to_node(node)
|
||||
|
||||
imgui.separator()
|
||||
|
||||
# 碰撞检测触发模式
|
||||
imgui.text("触发模式:")
|
||||
|
||||
# 自动检测开关
|
||||
auto_enabled = self.collision_manager.model_collision_enabled if hasattr(self, 'collision_manager') else False
|
||||
changed, new_auto = imgui.checkbox("自动检测", auto_enabled)
|
||||
if changed and hasattr(self, 'collision_manager'):
|
||||
self.collision_manager.enableModelCollisionDetection(new_auto, 0.1, 0.5)
|
||||
|
||||
imgui.same_line()
|
||||
|
||||
# 手动检测按钮
|
||||
if imgui.button("立即检测"):
|
||||
if hasattr(self, 'collision_manager'):
|
||||
self._manual_collision_detection()
|
||||
|
||||
except Exception as e:
|
||||
print(f"绘制碰撞属性失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
def _draw_shape_specific_parameters(self, node, shape_type, has_collision=True):
|
||||
"""绘制形状特定参数"""
|
||||
try:
|
||||
if shape_type == "sphere":
|
||||
self._draw_sphere_parameters(node, has_collision)
|
||||
elif shape_type == "box":
|
||||
self._draw_box_parameters(node, has_collision)
|
||||
elif shape_type == "capsule":
|
||||
self._draw_capsule_parameters(node, has_collision)
|
||||
elif shape_type == "plane":
|
||||
self._draw_plane_parameters(node, has_collision)
|
||||
except Exception as e:
|
||||
print(f"绘制形状参数失败: {e}")
|
||||
|
||||
def _draw_sphere_parameters(self, node, has_collision=True):
|
||||
"""绘制球形参数"""
|
||||
try:
|
||||
imgui.text("球形参数:")
|
||||
imgui.same_line()
|
||||
|
||||
# 获取当前半径
|
||||
radius = self._get_sphere_radius(node)
|
||||
|
||||
# 半径调整
|
||||
changed, new_radius = imgui.drag_float("半径##sphere_radius", radius, 0.1, 0.1, 100.0, "%.2f")
|
||||
if changed and has_collision:
|
||||
self._update_sphere_radius(node, new_radius)
|
||||
|
||||
except Exception as e:
|
||||
print(f"绘制球形参数失败: {e}")
|
||||
|
||||
def _draw_box_parameters(self, node, has_collision=True):
|
||||
"""绘制盒型参数"""
|
||||
try:
|
||||
imgui.text("盒型参数:")
|
||||
|
||||
# 获取当前尺寸
|
||||
size = self._get_box_size(node)
|
||||
|
||||
# 尺寸调整
|
||||
changed, new_x = imgui.drag_float("长度##box_length", size[0], 0.1, 0.1, 100.0, "%.2f")
|
||||
if changed and has_collision:
|
||||
self._update_box_size(node, 'x', new_x)
|
||||
|
||||
changed, new_y = imgui.drag_float("宽度##box_width", size[1], 0.1, 0.1, 100.0, "%.2f")
|
||||
if changed and has_collision:
|
||||
self._update_box_size(node, 'y', new_y)
|
||||
|
||||
changed, new_z = imgui.drag_float("高度##box_height", size[2], 0.1, 0.1, 100.0, "%.2f")
|
||||
if changed and has_collision:
|
||||
self._update_box_size(node, 'z', new_z)
|
||||
|
||||
except Exception as e:
|
||||
print(f"绘制盒型参数失败: {e}")
|
||||
|
||||
def _draw_capsule_parameters(self, node, has_collision=True):
|
||||
"""绘制胶囊体参数"""
|
||||
try:
|
||||
imgui.text("胶囊体参数:")
|
||||
|
||||
# 获取当前参数
|
||||
radius = self._get_capsule_radius(node)
|
||||
height = self._get_capsule_height(node)
|
||||
|
||||
# 半径调整
|
||||
changed, new_radius = imgui.drag_float("半径##capsule_radius", radius, 0.1, 0.1, 100.0, "%.2f")
|
||||
if changed and has_collision:
|
||||
self._update_capsule_radius(node, new_radius)
|
||||
|
||||
# 高度调整
|
||||
changed, new_height = imgui.drag_float("高度##capsule_height", height, 0.1, 0.1, 100.0, "%.2f")
|
||||
if changed and has_collision:
|
||||
self._update_capsule_height(node, new_height)
|
||||
|
||||
except Exception as e:
|
||||
print(f"绘制胶囊体参数失败: {e}")
|
||||
|
||||
def _draw_plane_parameters(self, node, has_collision=True):
|
||||
"""绘制平面参数"""
|
||||
try:
|
||||
imgui.text("平面参数:")
|
||||
|
||||
# 获取当前法向量
|
||||
normal = self._get_plane_normal(node)
|
||||
|
||||
# 法向量调整
|
||||
changed, new_x = imgui.drag_float("法向量 X##plane_normal_x", normal[0], 0.1, -1.0, 1.0, "%.2f")
|
||||
if changed and has_collision:
|
||||
self._update_plane_normal(node, 'x', new_x)
|
||||
|
||||
changed, new_y = imgui.drag_float("法向量 Y##plane_normal_y", normal[1], 0.1, -1.0, 1.0, "%.2f")
|
||||
if changed and has_collision:
|
||||
self._update_plane_normal(node, 'y', new_y)
|
||||
|
||||
changed, new_z = imgui.drag_float("法向量 Z##plane_normal_z", normal[2], 0.1, -1.0, 1.0, "%.2f")
|
||||
if changed and has_collision:
|
||||
self._update_plane_normal(node, 'z', new_z)
|
||||
|
||||
except Exception as e:
|
||||
print(f"绘制平面参数失败: {e}")
|
||||
|
||||
227
ui/panels/editor_panels_right_material.py
Normal file
227
ui/panels/editor_panels_right_material.py
Normal file
@ -0,0 +1,227 @@
|
||||
from imgui_bundle import imgui, imgui_ctx
|
||||
|
||||
class EditorPanelsRightMaterialMixin:
|
||||
"""Auto-split mixin from editor_panels_right.py."""
|
||||
|
||||
def _draw_appearance_properties(self, node):
|
||||
"""绘制外观属性"""
|
||||
# 颜色属性
|
||||
if hasattr(node, 'getColor'):
|
||||
imgui.text("颜色")
|
||||
try:
|
||||
color = node.getColor()
|
||||
# 确保颜色是有效的
|
||||
if not color or len(color) < 3:
|
||||
color = (1.0, 1.0, 1.0, 1.0) # 默认白色
|
||||
except:
|
||||
color = (1.0, 1.0, 1.0, 1.0) # 默认白色
|
||||
|
||||
# 颜色滑块
|
||||
changed, new_r = imgui.slider_float("R##color_r", color[0], 0.0, 1.0)
|
||||
if changed:
|
||||
new_color = (new_r, color[1], color[2], color[3] if len(color) > 3 else 1.0)
|
||||
node.setColor(new_color)
|
||||
color = new_color
|
||||
|
||||
changed, new_g = imgui.slider_float("G##color_g", color[1], 0.0, 1.0)
|
||||
if changed:
|
||||
new_color = (color[0], new_g, color[2], color[3] if len(color) > 3 else 1.0)
|
||||
node.setColor(new_color)
|
||||
color = new_color
|
||||
|
||||
changed, new_b = imgui.slider_float("B##color_b", color[2], 0.0, 1.0)
|
||||
if changed:
|
||||
new_color = (color[0], color[1], new_b, color[3] if len(color) > 3 else 1.0)
|
||||
node.setColor(new_color)
|
||||
color = new_color
|
||||
|
||||
# 只有当颜色有alpha通道时才显示alpha滑块
|
||||
if len(color) > 3:
|
||||
changed, new_a = imgui.slider_float("A##color_a", color[3], 0.0, 1.0)
|
||||
if changed:
|
||||
new_color = (color[0], color[1], color[2], new_a)
|
||||
node.setColor(new_color)
|
||||
color = new_color
|
||||
|
||||
# 颜色预览和选择器
|
||||
imgui.text("颜色预览")
|
||||
color_with_alpha = (color[0], color[1], color[2], color[3] if len(color) > 3 else 1.0)
|
||||
if imgui.color_button("颜色预览##preview", color_with_alpha, 0, (100, 30)):
|
||||
# 点击颜色按钮打开颜色选择器
|
||||
self.show_color_picker(node, 'color', color_with_alpha)
|
||||
|
||||
imgui.same_line()
|
||||
if imgui.button("选择颜色##color_picker_btn"):
|
||||
self.show_color_picker(node, 'color', (color.x, color.y, color.z, color.w))
|
||||
|
||||
# 透明度
|
||||
if hasattr(node, 'setTransparency') and hasattr(node, 'getTransparency'):
|
||||
imgui.text("透明度")
|
||||
current_transparency = node.getTransparency()
|
||||
# 将当前的透明度值转换为0.0-1.0范围用于显示
|
||||
display_transparency = 1.0 - current_transparency if current_transparency <= 1 else 0.0
|
||||
changed, new_transparency = imgui.slider_float("透明度", display_transparency, 0.0, 1.0)
|
||||
if changed:
|
||||
# 将0.0-1.0范围转换回Panda3D的透明度格式
|
||||
panda_transparency = int((1.0 - new_transparency) * 255)
|
||||
node.setTransparency(panda_transparency)
|
||||
|
||||
# 材质属性
|
||||
self._draw_material_properties(node)
|
||||
|
||||
# 渲染状态
|
||||
imgui.text("渲染状态")
|
||||
if imgui.button("应用材质"):
|
||||
self._apply_material_to_node(node)
|
||||
|
||||
imgui.same_line()
|
||||
if imgui.button("重置材质"):
|
||||
self._reset_material(node)
|
||||
|
||||
def _draw_material_properties(self, node):
|
||||
"""绘制材质属性"""
|
||||
materials = node.find_all_materials()
|
||||
|
||||
if not materials:
|
||||
imgui.text_colored((0.5, 0.5, 0.5, 1.0), "无材质")
|
||||
return
|
||||
|
||||
for i, material in enumerate(materials):
|
||||
material_name = material.get_name() if hasattr(material, 'get_name') and material.get_name() else f"材质{i + 1}"
|
||||
|
||||
if imgui.collapsing_header(f"材质: {material_name}"):
|
||||
# 材质基础颜色
|
||||
base_color = self._get_material_base_color(material)
|
||||
if base_color:
|
||||
imgui.text("基础颜色")
|
||||
changed, new_r = imgui.slider_float(f"R##mat_r_{i}", base_color[0], 0.0, 1.0)
|
||||
if changed:
|
||||
self._update_material_base_color(material, 'r', new_r)
|
||||
base_color = (new_r, base_color[1], base_color[2], base_color[3])
|
||||
|
||||
changed, new_g = imgui.slider_float(f"G##mat_g_{i}", base_color[1], 0.0, 1.0)
|
||||
if changed:
|
||||
self._update_material_base_color(material, 'g', new_g)
|
||||
base_color = (base_color[0], new_g, base_color[2], base_color[3])
|
||||
|
||||
changed, new_b = imgui.slider_float(f"B##mat_b_{i}", base_color[2], 0.0, 1.0)
|
||||
if changed:
|
||||
self._update_material_base_color(material, 'b', new_b)
|
||||
base_color = (base_color[0], base_color[1], new_b, base_color[3])
|
||||
|
||||
changed, new_a = imgui.slider_float(f"A##mat_a_{i}", base_color[3], 0.0, 1.0)
|
||||
if changed:
|
||||
self._update_material_base_color(material, 'a', new_a)
|
||||
base_color = (base_color[0], base_color[1], base_color[2], new_a)
|
||||
|
||||
# PBR属性
|
||||
if hasattr(material, 'roughness') and material.roughness is not None:
|
||||
imgui.text("PBR属性")
|
||||
try:
|
||||
roughness_value = float(material.roughness)
|
||||
changed, new_roughness = imgui.slider_float(f"粗糙度##rough_{i}", roughness_value, 0.0, 1.0)
|
||||
if changed:
|
||||
self._update_material_roughness(material, new_roughness)
|
||||
except:
|
||||
imgui.text_colored((0.7, 0.7, 0.7, 1.0), "粗糙度: 不可用")
|
||||
|
||||
if hasattr(material, 'metallic') and material.metallic is not None:
|
||||
try:
|
||||
metallic_value = float(material.metallic)
|
||||
changed, new_metallic = imgui.slider_float(f"金属性##metal_{i}", metallic_value, 0.0, 1.0)
|
||||
if changed:
|
||||
self._update_material_metallic(material, new_metallic)
|
||||
except:
|
||||
imgui.text_colored((0.7, 0.7, 0.7, 1.0), "金属性: 不可用")
|
||||
|
||||
if hasattr(material, 'refractive_index') and material.refractive_index is not None:
|
||||
try:
|
||||
ior_value = float(material.refractive_index)
|
||||
changed, new_ior = imgui.slider_float(f"折射率##ior_{i}", ior_value, 1.0, 3.0)
|
||||
if changed:
|
||||
self._update_material_ior(material, new_ior)
|
||||
except:
|
||||
imgui.text_colored((0.7, 0.7, 0.7, 1.0), "折射率: 不可用")
|
||||
|
||||
# 材质预设
|
||||
imgui.text("材质预设")
|
||||
presets = ["默认", "金属", "塑料", "玻璃", "木材", "混凝土"]
|
||||
current_preset = 0 # 默认选择
|
||||
|
||||
if imgui.begin_combo(f"预设##preset_{i}", presets[current_preset]):
|
||||
for j, preset_name in enumerate(presets):
|
||||
if imgui.selectable(preset_name, j == current_preset):
|
||||
self._apply_material_preset(material, preset_name)
|
||||
imgui.end_combo()
|
||||
|
||||
# 纹理信息
|
||||
imgui.text("纹理贴图")
|
||||
if imgui.button(f"选择漫反射贴图##diffuse_{i}"):
|
||||
self._select_texture_for_material(node, material, "diffuse")
|
||||
|
||||
imgui.same_line()
|
||||
if imgui.button(f"选择法线贴图##normal_{i}"):
|
||||
self._select_texture_for_material(node, material, "normal")
|
||||
|
||||
imgui.same_line()
|
||||
if imgui.button(f"选择粗糙度贴图##roughness_{i}"):
|
||||
self._select_texture_for_material(node, material, "roughness")
|
||||
|
||||
if imgui.button(f"选择金属性贴图##metallic_{i}"):
|
||||
self._select_texture_for_material(node, material, "metallic")
|
||||
|
||||
imgui.same_line()
|
||||
if imgui.button(f"选择自发光贴图##emission_{i}"):
|
||||
self._select_texture_for_material(node, material, "emission")
|
||||
|
||||
imgui.same_line()
|
||||
if imgui.button(f"清除所有贴图##clear_{i}"):
|
||||
self._clear_all_textures(node)
|
||||
|
||||
# 着色模型选择
|
||||
self._draw_shading_model_panel(material, i)
|
||||
|
||||
# 显示当前纹理信息
|
||||
self._display_current_textures(node, material)
|
||||
|
||||
def _draw_shading_model_panel(self, material, material_index):
|
||||
"""绘制着色模型选择面板"""
|
||||
try:
|
||||
imgui.text("着色模型")
|
||||
|
||||
# RenderPipeline支持的着色模型
|
||||
shading_models = ["默认", "自发光", "透明"]
|
||||
current_model = 0 # 默认选择
|
||||
|
||||
# 安全地获取当前着色模型
|
||||
try:
|
||||
if hasattr(material, 'emission') and material.emission is not None:
|
||||
current_model = int(material.emission.x)
|
||||
except:
|
||||
current_model = 0
|
||||
|
||||
# 着色模型选择
|
||||
if imgui.begin_combo(f"着色模型##shading_{material_index}", shading_models[current_model]):
|
||||
for j, model_name in enumerate(shading_models):
|
||||
if imgui.selectable(model_name, j == current_model):
|
||||
self._update_shading_model(material, j)
|
||||
imgui.end_combo()
|
||||
|
||||
# 如果是透明着色模型,添加透明度控制
|
||||
if current_model == 3: # 透明着色模型
|
||||
imgui.text("透明度设置")
|
||||
try:
|
||||
if hasattr(material, 'shading_model_param0'):
|
||||
current_opacity = material.shading_model_param0
|
||||
else:
|
||||
current_opacity = 1.0
|
||||
|
||||
changed, new_opacity = imgui.slider_float(f"不透明度##opacity_{material_index}", current_opacity, 0.0, 1.0)
|
||||
if changed:
|
||||
self._update_transparency(material, new_opacity)
|
||||
except:
|
||||
imgui.text_colored((0.7, 0.7, 0.7, 1.0), "透明度控制不可用")
|
||||
|
||||
except Exception as e:
|
||||
print(f"绘制着色模型面板失败: {e}")
|
||||
|
||||
92
ui/panels/editor_panels_right_transform.py
Normal file
92
ui/panels/editor_panels_right_transform.py
Normal file
@ -0,0 +1,92 @@
|
||||
from imgui_bundle import imgui, imgui_ctx
|
||||
|
||||
class EditorPanelsRightTransformMixin:
|
||||
"""Auto-split mixin from editor_panels_right.py."""
|
||||
|
||||
def _draw_transform_properties(self, node):
|
||||
"""绘制变换属性"""
|
||||
# 位置组
|
||||
if imgui.collapsing_header("位置 Position"):
|
||||
# 相对位置
|
||||
imgui.text("相对位置")
|
||||
pos = node.getPos()
|
||||
|
||||
# X坐标
|
||||
changed, new_x = imgui.input_float("X##pos_x", pos.x, 0.1, 1.0, "%.3f")
|
||||
if changed: node.setX(new_x)
|
||||
|
||||
# Y坐标
|
||||
changed, new_y = imgui.input_float("Y##pos_y", pos.y, 0.1, 1.0, "%.3f")
|
||||
if changed: node.setY(new_y)
|
||||
|
||||
# Z坐标
|
||||
changed, new_z = imgui.input_float("Z##pos_z", pos.z, 0.1, 1.0, "%.3f")
|
||||
if changed: node.setZ(new_z)
|
||||
|
||||
# 世界位置
|
||||
imgui.text("世界位置")
|
||||
world_pos = node.getPos(self.render)
|
||||
|
||||
imgui.text(f"世界 X: {world_pos.x:.3f}")
|
||||
imgui.text(f"世界 Y: {world_pos.y:.3f}")
|
||||
imgui.text(f"世界 Z: {world_pos.z:.3f}")
|
||||
|
||||
# 位置操作按钮
|
||||
if imgui.button("重置位置##reset_pos"):
|
||||
node.setPos(0, 0, 0)
|
||||
imgui.same_line()
|
||||
if imgui.button("复制位置##copy_pos"):
|
||||
self._clipboard_pos = (pos.x, pos.y, pos.z)
|
||||
imgui.same_line()
|
||||
if imgui.button("粘贴位置##paste_pos") and hasattr(self, '_clipboard_pos'):
|
||||
node.setPos(self._clipboard_pos[0], self._clipboard_pos[1], self._clipboard_pos[2])
|
||||
|
||||
# 旋转组
|
||||
if imgui.collapsing_header("旋转 Rotation"):
|
||||
hpr = node.getHpr()
|
||||
|
||||
# HPR旋转
|
||||
imgui.text("HPR 旋转 (度)")
|
||||
changed, new_h = imgui.input_float("H##rot_h", hpr.x, 1.0, 10.0, "%.1f")
|
||||
if changed: node.setH(new_h)
|
||||
|
||||
changed, new_p = imgui.input_float("P##rot_p", hpr.y, 1.0, 10.0, "%.1f")
|
||||
if changed: node.setP(new_p)
|
||||
|
||||
changed, new_r = imgui.input_float("R##rot_r", hpr.z, 1.0, 10.0, "%.1f")
|
||||
if changed: node.setR(new_r)
|
||||
|
||||
# 旋转操作按钮
|
||||
if imgui.button("重置旋转##reset_rot"):
|
||||
node.setHpr(0, 0, 0)
|
||||
imgui.same_line()
|
||||
if imgui.button("随机旋转##random_rot"):
|
||||
import random
|
||||
node.setHpr(random.randint(0, 360), random.randint(0, 360), random.randint(0, 360))
|
||||
|
||||
# 缩放组
|
||||
if imgui.collapsing_header("缩放 Scale"):
|
||||
scale = node.getScale()
|
||||
|
||||
# XYZ缩放
|
||||
imgui.text("XYZ 缩放")
|
||||
changed, new_sx = imgui.input_float("X##scale_x", scale.x, 0.1, 1.0, "%.3f")
|
||||
if changed: node.setSx(new_sx)
|
||||
|
||||
changed, new_sy = imgui.input_float("Y##scale_y", scale.y, 0.1, 1.0, "%.3f")
|
||||
if changed: node.setSy(new_sy)
|
||||
|
||||
changed, new_sz = imgui.input_float("Z##scale_z", scale.z, 0.1, 1.0, "%.3f")
|
||||
if changed: node.setSz(new_sz)
|
||||
|
||||
# 统一缩放
|
||||
if imgui.button("统一缩放##uniform_scale"):
|
||||
uniform_scale = (scale.x + scale.y + scale.z) / 3.0
|
||||
node.setScale(uniform_scale, uniform_scale, uniform_scale)
|
||||
imgui.same_line()
|
||||
if imgui.button("重置缩放##reset_scale"):
|
||||
node.setScale(1, 1, 1)
|
||||
imgui.same_line()
|
||||
if imgui.button("翻倍##double_scale"):
|
||||
node.setScale(scale.x * 2, scale.y * 2, scale.z * 2)
|
||||
|
||||
323
ui/panels/editor_panels_top.py
Normal file
323
ui/panels/editor_panels_top.py
Normal file
@ -0,0 +1,323 @@
|
||||
from imgui_bundle import imgui, imgui_ctx
|
||||
|
||||
class EditorPanelsTopMixin:
|
||||
"""Auto-split mixin from editor_panels.py."""
|
||||
|
||||
def draw_menu_bar(self):
|
||||
"""绘制菜单栏"""
|
||||
with imgui_ctx.begin_main_menu_bar() as main_menu:
|
||||
if main_menu:
|
||||
# 文件菜单
|
||||
with imgui_ctx.begin_menu("文件") as file_menu:
|
||||
if file_menu:
|
||||
if imgui.menu_item("新建项目", "Ctrl+N", False, True)[1]:
|
||||
self.app._on_new_project()
|
||||
if imgui.menu_item("打开项目", "Ctrl+O", False, True)[1]:
|
||||
self.app._on_open_project()
|
||||
imgui.separator()
|
||||
if imgui.menu_item("保存", "Ctrl+S", False, True)[1]:
|
||||
self.app._on_save_project()
|
||||
if imgui.menu_item("另存为", "", False, True)[1]:
|
||||
self.app._on_save_as_project()
|
||||
imgui.separator()
|
||||
if imgui.menu_item("退出", "Alt+F4", False, True)[1]:
|
||||
self.app._on_exit()
|
||||
|
||||
# 编辑菜单
|
||||
with imgui_ctx.begin_menu("编辑") as edit_menu:
|
||||
if edit_menu:
|
||||
if imgui.menu_item("撤销", "Ctrl+Z", False, True)[1]:
|
||||
self.app._on_undo()
|
||||
if imgui.menu_item("重做", "Ctrl+Y", False, True)[1]:
|
||||
self.app._on_redo()
|
||||
imgui.separator()
|
||||
if imgui.menu_item("剪切", "Ctrl+X", False, True)[1]:
|
||||
self.app._on_cut()
|
||||
if imgui.menu_item("复制", "Ctrl+C", False, True)[1]:
|
||||
self.app._on_copy()
|
||||
if imgui.menu_item("粘贴", "Ctrl+V", False, True)[1]:
|
||||
self.app._on_paste()
|
||||
imgui.separator()
|
||||
if imgui.menu_item("删除", "Del", False, True)[1]:
|
||||
self.app._on_delete()
|
||||
|
||||
# 创建菜单
|
||||
with imgui_ctx.begin_menu("创建") as create_menu:
|
||||
if create_menu:
|
||||
if imgui.menu_item("导入模型", "", False, True)[1]:
|
||||
self.app._on_import_model()
|
||||
|
||||
imgui.separator()
|
||||
|
||||
if imgui.menu_item("空对象", "", False, True)[1]:
|
||||
self.app._on_create_empty_object()
|
||||
|
||||
# 3D对象子菜单
|
||||
with imgui_ctx.begin_menu("3D对象") as three_d_menu:
|
||||
if three_d_menu:
|
||||
if imgui.menu_item("立方体", "", False, True)[1]:
|
||||
self.app._on_create_cube()
|
||||
if imgui.menu_item("球体", "", False, True)[1]:
|
||||
self.app._on_create_sphere()
|
||||
if imgui.menu_item("圆柱体", "", False, True)[1]:
|
||||
self.app._on_create_cylinder()
|
||||
if imgui.menu_item("平面", "", False, True)[1]:
|
||||
self.app._on_create_plane()
|
||||
|
||||
# 3D GUI子菜单
|
||||
# with imgui_ctx.begin_menu("3D GUI") as three_d_gui_menu:
|
||||
# if three_d_gui_menu:
|
||||
# if imgui.menu_item("3D文本", "", False, True)[1]:
|
||||
# self.app._on_create_3d_text()
|
||||
# if imgui.menu_item("3D图片", "", False, True)[1]:
|
||||
# self.app._on_create_3d_image()
|
||||
|
||||
# # GUI子菜单
|
||||
# with imgui_ctx.begin_menu("GUI") as gui_menu:
|
||||
# if gui_menu:
|
||||
# if imgui.menu_item("创建按钮", "", False, True)[1]:
|
||||
# self.app._on_create_gui_button()
|
||||
# if imgui.menu_item("创建标签", "", False, True)[1]:
|
||||
# self.app._on_create_gui_label()
|
||||
# if imgui.menu_item("创建输入框", "", False, True)[1]:
|
||||
# self.app._on_create_gui_entry()
|
||||
# if imgui.menu_item("创建图片", "", False, True)[1]:
|
||||
# self.app._on_create_gui_image()
|
||||
# imgui.separator()
|
||||
# if imgui.menu_item("创建视频屏幕", "", False, True)[1]:
|
||||
# self.app._on_create_video_screen()
|
||||
# if imgui.menu_item("创建2D视频屏幕", "", False, True)[1]:
|
||||
# self.app._on_create_2d_video_screen()
|
||||
# if imgui.menu_item("创建球形视频", "", False, True)[1]:
|
||||
# self.app._on_create_spherical_video()
|
||||
# if imgui.menu_item("创建虚拟屏幕", "", False, True)[1]:
|
||||
# self.app._on_create_virtual_screen()
|
||||
|
||||
# 光源子菜单
|
||||
with imgui_ctx.begin_menu("光源") as light_menu:
|
||||
if light_menu:
|
||||
if imgui.menu_item("聚光灯", "", False, True)[1]:
|
||||
self.app._on_create_spot_light()
|
||||
if imgui.menu_item("点光源", "", False, True)[1]:
|
||||
self.app._on_create_point_light()
|
||||
|
||||
# 地形子菜单
|
||||
with imgui_ctx.begin_menu("地形") as terrain_menu:
|
||||
if terrain_menu:
|
||||
if imgui.menu_item("创建平面地形", "", False, True)[1]:
|
||||
self.app._on_create_flat_terrain()
|
||||
if imgui.menu_item("从高度图创建地形", "", False, True)[1]:
|
||||
self.app._on_create_heightmap_terrain()
|
||||
|
||||
# 脚本子菜单
|
||||
with imgui_ctx.begin_menu("脚本") as script_menu:
|
||||
if script_menu:
|
||||
if imgui.menu_item("创建脚本...", "", False, True)[1]:
|
||||
self.app._on_create_script()
|
||||
if imgui.menu_item("加载脚本文件...", "", False, True)[1]:
|
||||
self.app._on_load_script()
|
||||
imgui.separator()
|
||||
if imgui.menu_item("重载所有脚本", "", False, True)[1]:
|
||||
self.app._on_reload_all_scripts()
|
||||
_, self.app.hotReloadEnabled = imgui.menu_item("启用热重载", "", self.app.hotReloadEnabled, True)
|
||||
if imgui.menu_item("脚本管理器", "", False, True)[1]:
|
||||
self.app._on_open_scripts_manager()
|
||||
|
||||
# 信息面板子菜单
|
||||
with imgui_ctx.begin_menu("信息面板") as info_panel_menu:
|
||||
if info_panel_menu:
|
||||
# if imgui.menu_item("创建2D示例面板", "", False, True)[1]:
|
||||
# self.app._on_create_2d_sample_panel()
|
||||
# if imgui.menu_item("创建3D实例面板", "", False, True)[1]:
|
||||
# self.app._on_create_3d_sample_panel()
|
||||
if imgui.menu_item("Web面板", "", False, True)[1]:
|
||||
self.app._on_create_web_panel()
|
||||
|
||||
# 视图菜单
|
||||
with imgui_ctx.begin_menu("视图") as view_menu:
|
||||
if view_menu:
|
||||
_, self.app.showToolbar = imgui.menu_item("工具栏", "", self.app.showToolbar, True)
|
||||
_, self.app.showSceneTree = imgui.menu_item("场景树", "", self.app.showSceneTree, True)
|
||||
_, self.app.showResourceManager = imgui.menu_item("资源管理器", "", self.app.showResourceManager, True)
|
||||
_, self.app.showPropertyPanel = imgui.menu_item("属性面板", "", self.app.showPropertyPanel, True)
|
||||
_, self.app.showConsole = imgui.menu_item("控制台", "", self.app.showConsole, True)
|
||||
_, self.app.showScriptPanel = imgui.menu_item("脚本管理", "", self.app.showScriptPanel, True)
|
||||
_, self.app.showLUIEditor = imgui.menu_item("LUI编辑器", "", self.app.showLUIEditor, True)
|
||||
prev_show_web_panel = self.app.showWebPanel
|
||||
_, self.app.showWebPanel = imgui.menu_item("Web面板", "", self.app.showWebPanel, True)
|
||||
if prev_show_web_panel and not self.app.showWebPanel:
|
||||
self._stop_imgui_webview()
|
||||
elif (not prev_show_web_panel) and self.app.showWebPanel:
|
||||
self._on_create_web_panel()
|
||||
|
||||
# 工具菜单
|
||||
with imgui_ctx.begin_menu("工具") as tools_menu:
|
||||
if tools_menu:
|
||||
# 工具切换选项
|
||||
if imgui.menu_item("选择工具", "", False, True)[1]:
|
||||
self.app.tool_manager.setCurrentTool("选择")
|
||||
if imgui.menu_item("移动工具", "", False, True)[1]:
|
||||
self.app.tool_manager.setCurrentTool("移动")
|
||||
if imgui.menu_item("旋转工具", "", False, True)[1]:
|
||||
self.app.tool_manager.setCurrentTool("旋转")
|
||||
if imgui.menu_item("缩放工具", "", False, True)[1]:
|
||||
self.app.tool_manager.setCurrentTool("缩放")
|
||||
|
||||
imgui.separator()
|
||||
|
||||
# 编辑工具
|
||||
# if imgui.menu_item("光照编辑", "", False, True)[1]:
|
||||
# self.app.tool_manager.setCurrentTool("光照编辑")
|
||||
# if imgui.menu_item("图形编辑", "", False, True)[1]:
|
||||
# self.app.tool_manager.setCurrentTool("图形编辑")
|
||||
|
||||
imgui.separator()
|
||||
|
||||
# VR子菜单
|
||||
with imgui_ctx.begin_menu("VR") as vr_menu:
|
||||
if vr_menu:
|
||||
if imgui.menu_item("进入VR模式", "", False, True)[1]:
|
||||
self.app._toggle_vr_mode()
|
||||
if imgui.menu_item("退出VR模式", "", False, True)[1]:
|
||||
self.app._exit_vr_mode()
|
||||
|
||||
imgui.separator()
|
||||
|
||||
if imgui.menu_item("VR状态", "", False, True)[1]:
|
||||
self.app._show_vr_status()
|
||||
if imgui.menu_item("VR设置", "", False, True)[1]:
|
||||
self.app._show_vr_settings()
|
||||
|
||||
imgui.separator()
|
||||
|
||||
# VR调试子菜单
|
||||
with imgui_ctx.begin_menu("VR调试") as vr_debug_menu:
|
||||
if vr_debug_menu:
|
||||
_, self.app.vr_debug_enabled = imgui.menu_item("启用调试输出", "", self.app.vr_debug_enabled, True)
|
||||
|
||||
if imgui.menu_item("立即显示性能报告", "", False, True)[1]:
|
||||
self.app._show_vr_performance_report()
|
||||
|
||||
imgui.separator()
|
||||
|
||||
# 输出模式
|
||||
with imgui_ctx.begin_menu("输出模式") as output_menu:
|
||||
if output_menu:
|
||||
if imgui.menu_item("简短模式", "", not self.app.vr_detailed_mode, True)[1]:
|
||||
self.app.vr_detailed_mode = False
|
||||
if imgui.menu_item("详细模式", "", self.app.vr_detailed_mode, True)[1]:
|
||||
self.app.vr_detailed_mode = True
|
||||
|
||||
imgui.separator()
|
||||
|
||||
_, self.app.vr_performance_monitor = imgui.menu_item("启用性能监控", "", self.app.vr_performance_monitor, True)
|
||||
|
||||
# 窗口菜单 - 已隐藏
|
||||
# with imgui_ctx.begin_menu("窗口") as window_menu:
|
||||
# if window_menu:
|
||||
# _, self.app.showDemoWindow = imgui.menu_item("ImGui演示", "", self.app.showDemoWindow, True)
|
||||
# if self.app.testTexture:
|
||||
# imgui.menu_item("关闭纹理测试", "", False, True)
|
||||
# else:
|
||||
# imgui.menu_item("显示纹理测试", "", False, True)
|
||||
|
||||
# 帮助菜单
|
||||
with imgui_ctx.begin_menu("帮助") as help_menu:
|
||||
if help_menu:
|
||||
imgui.menu_item("关于", "", False, True)
|
||||
imgui.menu_item("文档", "", False, True)
|
||||
|
||||
# 右侧显示FPS
|
||||
imgui.set_cursor_pos_x(imgui.get_window_size().x - 140)
|
||||
imgui.text("%.2f FPS (%.2f ms)" % (imgui.get_io().framerate, 1000.0 / imgui.get_io().framerate))
|
||||
|
||||
def draw_toolbar(self):
|
||||
"""绘制工具栏"""
|
||||
# 工具栏可以保持无标题栏,但允许移动和调整大小
|
||||
flags = self.app.style_manager.get_window_flags("toolbar")
|
||||
|
||||
with self.app.style_manager.begin_styled_window("工具栏", self.app.showToolbar, flags):
|
||||
self.app.showToolbar = True # 确保窗口保持打开
|
||||
|
||||
# 选择工具按钮
|
||||
select_active = self.app.tool_manager.isSelectionTool()
|
||||
if self.app.icons.get('select'):
|
||||
tint_col = (1.0, 1.0, 0.0, 1.0) if select_active else (1.0, 1.0, 1.0, 1.0)
|
||||
if self.app.style_manager.image_button(self.app.icons['select'], (24, 24), tint_col=tint_col):
|
||||
self.app.tool_manager.setCurrentTool("选择")
|
||||
if imgui.is_item_hovered():
|
||||
imgui.set_tooltip("选择工具 (Q)")
|
||||
imgui.same_line()
|
||||
else:
|
||||
if imgui.button("选择##select_tool"):
|
||||
self.app.tool_manager.setCurrentTool("选择")
|
||||
if select_active:
|
||||
draw_list = imgui.get_window_draw_list()
|
||||
button_min = imgui.get_item_rect_min()
|
||||
button_max = imgui.get_item_rect_max()
|
||||
draw_list.add_rect_filled(button_min, button_max, imgui.get_color_u32((0.3, 0.6, 1.0, 0.3)))
|
||||
imgui.same_line()
|
||||
|
||||
# 移动工具按钮
|
||||
move_active = self.app.tool_manager.isMoveTool()
|
||||
if self.app.icons.get('move'):
|
||||
# 使用不同颜色表示活动状态
|
||||
tint_col = (1.0, 1.0, 0.0, 1.0) if move_active else (1.0, 1.0, 1.0, 1.0) # 活动时显示黄色
|
||||
if self.app.style_manager.image_button(self.app.icons['move'], (24, 24), tint_col=tint_col):
|
||||
self.app.tool_manager.setCurrentTool("移动")
|
||||
if imgui.is_item_hovered():
|
||||
imgui.set_tooltip("移动工具 (W)")
|
||||
imgui.same_line()
|
||||
else:
|
||||
if imgui.button("移动##move_tool"):
|
||||
self.app.tool_manager.setCurrentTool("移动")
|
||||
if move_active:
|
||||
# 为活动按钮添加背景色
|
||||
draw_list = imgui.get_window_draw_list()
|
||||
button_min = imgui.get_item_rect_min()
|
||||
button_max = imgui.get_item_rect_max()
|
||||
draw_list.add_rect_filled(button_min, button_max, imgui.get_color_u32((0.3, 0.6, 1.0, 0.3)))
|
||||
imgui.same_line()
|
||||
|
||||
# 旋转工具按钮
|
||||
rotate_active = self.app.tool_manager.isRotateTool()
|
||||
if self.app.icons.get('rotate'):
|
||||
tint_col = (1.0, 1.0, 0.0, 1.0) if rotate_active else (1.0, 1.0, 1.0, 1.0)
|
||||
if self.app.style_manager.image_button(self.app.icons['rotate'], (24, 24), tint_col=tint_col):
|
||||
self.app.tool_manager.setCurrentTool("旋转")
|
||||
if imgui.is_item_hovered():
|
||||
imgui.set_tooltip("旋转工具 (E)")
|
||||
imgui.same_line()
|
||||
else:
|
||||
if imgui.button("旋转##rotate_tool"):
|
||||
self.app.tool_manager.setCurrentTool("旋转")
|
||||
if rotate_active:
|
||||
draw_list = imgui.get_window_draw_list()
|
||||
button_min = imgui.get_item_rect_min()
|
||||
button_max = imgui.get_item_rect_max()
|
||||
draw_list.add_rect_filled(button_min, button_max, imgui.get_color_u32((0.3, 0.6, 1.0, 0.3)))
|
||||
imgui.same_line()
|
||||
|
||||
# 缩放工具按钮
|
||||
scale_active = self.app.tool_manager.isScaleTool()
|
||||
if self.app.icons.get('scale'):
|
||||
tint_col = (1.0, 1.0, 0.0, 1.0) if scale_active else (1.0, 1.0, 1.0, 1.0)
|
||||
if self.app.style_manager.image_button(self.app.icons['scale'], (24, 24), tint_col=tint_col):
|
||||
self.app.tool_manager.setCurrentTool("缩放")
|
||||
if imgui.is_item_hovered():
|
||||
imgui.set_tooltip("缩放工具 (R)")
|
||||
else:
|
||||
if imgui.button("缩放##scale_tool"):
|
||||
self.app.tool_manager.setCurrentTool("缩放")
|
||||
if scale_active:
|
||||
draw_list = imgui.get_window_draw_list()
|
||||
button_min = imgui.get_item_rect_min()
|
||||
button_max = imgui.get_item_rect_max()
|
||||
draw_list.add_rect_filled(button_min, button_max, imgui.get_color_u32((0.3, 0.6, 1.0, 0.3)))
|
||||
|
||||
imgui.same_line()
|
||||
imgui.separator()
|
||||
imgui.same_line()
|
||||
|
||||
# 工具按钮已移除(导入、保存、播放)
|
||||
|
||||
748
ui/panels/panel_delegates.py
Normal file
748
ui/panels/panel_delegates.py
Normal file
@ -0,0 +1,748 @@
|
||||
"""Delegated panel/runtime methods for MyWorld."""
|
||||
|
||||
|
||||
class PanelDelegates:
|
||||
def _draw_menu_bar(self):
|
||||
self.editor_panels.draw_menu_bar()
|
||||
|
||||
def _draw_toolbar(self):
|
||||
self.editor_panels.draw_toolbar()
|
||||
|
||||
def _draw_scene_tree(self, *args, **kwargs):
|
||||
return self.editor_panels._draw_scene_tree(*args, **kwargs)
|
||||
|
||||
def _build_scene_tree(self, *args, **kwargs):
|
||||
return self.editor_panels._build_scene_tree(*args, **kwargs)
|
||||
|
||||
def _draw_scene_node(self, *args, **kwargs):
|
||||
return self.editor_panels._draw_scene_node(*args, **kwargs)
|
||||
|
||||
def _show_node_context_menu(self, *args, **kwargs):
|
||||
return self.editor_panels._show_node_context_menu(*args, **kwargs)
|
||||
|
||||
def _draw_resource_manager(self, *args, **kwargs):
|
||||
return self.editor_panels._draw_resource_manager(*args, **kwargs)
|
||||
|
||||
def _draw_property_panel(self, *args, **kwargs):
|
||||
return self.editor_panels._draw_property_panel(*args, **kwargs)
|
||||
|
||||
def _draw_web_panel(self, *args, **kwargs):
|
||||
return self.editor_panels._draw_web_panel(*args, **kwargs)
|
||||
|
||||
def _draw_node_properties(self, *args, **kwargs):
|
||||
return self.editor_panels._draw_node_properties(*args, **kwargs)
|
||||
|
||||
def _getActor(self, *args, **kwargs):
|
||||
return self.animation_tools._getActor(*args, **kwargs)
|
||||
|
||||
def _getModelFormat(self, *args, **kwargs):
|
||||
return self.animation_tools._getModelFormat(*args, **kwargs)
|
||||
|
||||
def _processAnimationNames(self, *args, **kwargs):
|
||||
return self.animation_tools._processAnimationNames(*args, **kwargs)
|
||||
|
||||
def _isLikelyBoneGroup(self, *args, **kwargs):
|
||||
return self.animation_tools._isLikelyBoneGroup(*args, **kwargs)
|
||||
|
||||
def _analyzeAnimationQuality(self, *args, **kwargs):
|
||||
return self.animation_tools._analyzeAnimationQuality(*args, **kwargs)
|
||||
|
||||
def _playAnimation(self, *args, **kwargs):
|
||||
return self.animation_tools._playAnimation(*args, **kwargs)
|
||||
|
||||
def _pauseAnimation(self, *args, **kwargs):
|
||||
return self.animation_tools._pauseAnimation(*args, **kwargs)
|
||||
|
||||
def _stopAnimation(self, *args, **kwargs):
|
||||
return self.animation_tools._stopAnimation(*args, **kwargs)
|
||||
|
||||
def _loopAnimation(self, *args, **kwargs):
|
||||
return self.animation_tools._loopAnimation(*args, **kwargs)
|
||||
|
||||
def _setAnimationSpeed(self, *args, **kwargs):
|
||||
return self.animation_tools._setAnimationSpeed(*args, **kwargs)
|
||||
|
||||
def _clear_animation_cache(self, *args, **kwargs):
|
||||
return self.animation_tools._clear_animation_cache(*args, **kwargs)
|
||||
def _get_node_type_from_node(self, node):
|
||||
"""从节点判断其类型"""
|
||||
# 检查是否为GUI元素
|
||||
if hasattr(node, 'getPythonTag') and node.getPythonTag('gui_element'):
|
||||
return "GUI元素"
|
||||
|
||||
# 检查是否为光源
|
||||
node_name = node.getName() or ""
|
||||
if "light" in node_name.lower() or "Light" in node_name:
|
||||
return "光源"
|
||||
|
||||
# 检查是否为相机
|
||||
if "camera" in node_name.lower() or "Camera" in node_name:
|
||||
return "相机"
|
||||
|
||||
# 检查是否为模型
|
||||
if hasattr(self, 'scene_manager') and self.scene_manager:
|
||||
if hasattr(self.scene_manager, 'models') and node in self.scene_manager.models:
|
||||
return "模型"
|
||||
|
||||
# 默认为几何体
|
||||
return "几何体"
|
||||
|
||||
def _draw_status_badges(self, *args, **kwargs):
|
||||
return self.editor_panels._draw_status_badges(*args, **kwargs)
|
||||
|
||||
def _draw_transform_properties(self, *args, **kwargs):
|
||||
return self.editor_panels._draw_transform_properties(*args, **kwargs)
|
||||
|
||||
def _draw_gui_properties(self, *args, **kwargs):
|
||||
return self.editor_panels._draw_gui_properties(*args, **kwargs)
|
||||
|
||||
def _draw_light_properties(self, *args, **kwargs):
|
||||
return self.editor_panels._draw_light_properties(*args, **kwargs)
|
||||
|
||||
def _draw_model_properties(self, *args, **kwargs):
|
||||
return self.editor_panels._draw_model_properties(*args, **kwargs)
|
||||
|
||||
def _draw_animation_properties(self, *args, **kwargs):
|
||||
return self.editor_panels._draw_animation_properties(*args, **kwargs)
|
||||
|
||||
def _draw_collision_properties(self, *args, **kwargs):
|
||||
return self.editor_panels._draw_collision_properties(*args, **kwargs)
|
||||
|
||||
def _draw_shape_specific_parameters(self, *args, **kwargs):
|
||||
return self.editor_panels._draw_shape_specific_parameters(*args, **kwargs)
|
||||
|
||||
def _draw_sphere_parameters(self, *args, **kwargs):
|
||||
return self.editor_panels._draw_sphere_parameters(*args, **kwargs)
|
||||
|
||||
def _draw_box_parameters(self, *args, **kwargs):
|
||||
return self.editor_panels._draw_box_parameters(*args, **kwargs)
|
||||
|
||||
def _draw_capsule_parameters(self, *args, **kwargs):
|
||||
return self.editor_panels._draw_capsule_parameters(*args, **kwargs)
|
||||
|
||||
def _draw_plane_parameters(self, *args, **kwargs):
|
||||
return self.editor_panels._draw_plane_parameters(*args, **kwargs)
|
||||
|
||||
def _draw_property_actions(self, *args, **kwargs):
|
||||
return self.editor_panels._draw_property_actions(*args, **kwargs)
|
||||
|
||||
def _draw_appearance_properties(self, *args, **kwargs):
|
||||
return self.editor_panels._draw_appearance_properties(*args, **kwargs)
|
||||
|
||||
def _draw_material_properties(self, *args, **kwargs):
|
||||
return self.editor_panels._draw_material_properties(*args, **kwargs)
|
||||
|
||||
def _draw_shading_model_panel(self, *args, **kwargs):
|
||||
return self.editor_panels._draw_shading_model_panel(*args, **kwargs)
|
||||
|
||||
|
||||
def _apply_gui_font(self, *args, **kwargs):
|
||||
return self.property_helpers._apply_gui_font(*args, **kwargs)
|
||||
|
||||
def _apply_gui_font_size(self, *args, **kwargs):
|
||||
return self.property_helpers._apply_gui_font_size(*args, **kwargs)
|
||||
|
||||
def _apply_gui_font_style(self, *args, **kwargs):
|
||||
return self.property_helpers._apply_gui_font_style(*args, **kwargs)
|
||||
|
||||
def _has_collision(self, *args, **kwargs):
|
||||
return self.property_helpers._has_collision(*args, **kwargs)
|
||||
|
||||
def _get_current_collision_shape(self, *args, **kwargs):
|
||||
return self.property_helpers._get_current_collision_shape(*args, **kwargs)
|
||||
|
||||
def _get_current_collision_shape_type(self, *args, **kwargs):
|
||||
return self.property_helpers._get_current_collision_shape_type(*args, **kwargs)
|
||||
|
||||
def _get_collision_position_offset(self, *args, **kwargs):
|
||||
return self.property_helpers._get_collision_position_offset(*args, **kwargs)
|
||||
|
||||
def _is_collision_visible(self, *args, **kwargs):
|
||||
return self.property_helpers._is_collision_visible(*args, **kwargs)
|
||||
|
||||
def _add_collision_to_node(self, *args, **kwargs):
|
||||
return self.property_helpers._add_collision_to_node(*args, **kwargs)
|
||||
|
||||
def _remove_collision_from_node(self, *args, **kwargs):
|
||||
return self.property_helpers._remove_collision_from_node(*args, **kwargs)
|
||||
|
||||
def _toggle_collision_visibility(self, *args, **kwargs):
|
||||
return self.property_helpers._toggle_collision_visibility(*args, **kwargs)
|
||||
|
||||
def _update_collision_position(self, *args, **kwargs):
|
||||
return self.property_helpers._update_collision_position(*args, **kwargs)
|
||||
|
||||
def _get_shape_type_from_name(self, *args, **kwargs):
|
||||
return self.property_helpers._get_shape_type_from_name(*args, **kwargs)
|
||||
|
||||
def _get_sphere_radius(self, *args, **kwargs):
|
||||
return self.property_helpers._get_sphere_radius(*args, **kwargs)
|
||||
|
||||
def _update_sphere_radius(self, *args, **kwargs):
|
||||
return self.property_helpers._update_sphere_radius(*args, **kwargs)
|
||||
|
||||
def _get_box_size(self, *args, **kwargs):
|
||||
return self.property_helpers._get_box_size(*args, **kwargs)
|
||||
|
||||
def _update_box_size(self, *args, **kwargs):
|
||||
return self.property_helpers._update_box_size(*args, **kwargs)
|
||||
|
||||
def _get_capsule_radius(self, *args, **kwargs):
|
||||
return self.property_helpers._get_capsule_radius(*args, **kwargs)
|
||||
|
||||
def _update_capsule_radius(self, *args, **kwargs):
|
||||
return self.property_helpers._update_capsule_radius(*args, **kwargs)
|
||||
|
||||
def _get_capsule_height(self, *args, **kwargs):
|
||||
return self.property_helpers._get_capsule_height(*args, **kwargs)
|
||||
|
||||
def _update_capsule_height(self, *args, **kwargs):
|
||||
return self.property_helpers._update_capsule_height(*args, **kwargs)
|
||||
|
||||
def _get_plane_normal(self, *args, **kwargs):
|
||||
return self.property_helpers._get_plane_normal(*args, **kwargs)
|
||||
|
||||
def _update_plane_normal(self, *args, **kwargs):
|
||||
return self.property_helpers._update_plane_normal(*args, **kwargs)
|
||||
|
||||
def _manual_collision_detection(self, *args, **kwargs):
|
||||
return self.property_helpers._manual_collision_detection(*args, **kwargs)
|
||||
|
||||
def _update_node_name(self, *args, **kwargs):
|
||||
return self.property_helpers._update_node_name(*args, **kwargs)
|
||||
|
||||
def _get_material_base_color(self, *args, **kwargs):
|
||||
return self.property_helpers._get_material_base_color(*args, **kwargs)
|
||||
|
||||
def _update_material_base_color(self, *args, **kwargs):
|
||||
return self.property_helpers._update_material_base_color(*args, **kwargs)
|
||||
|
||||
def _update_material_roughness(self, *args, **kwargs):
|
||||
return self.property_helpers._update_material_roughness(*args, **kwargs)
|
||||
|
||||
def _update_material_metallic(self, *args, **kwargs):
|
||||
return self.property_helpers._update_material_metallic(*args, **kwargs)
|
||||
|
||||
def _update_material_ior(self, *args, **kwargs):
|
||||
return self.property_helpers._update_material_ior(*args, **kwargs)
|
||||
|
||||
def _apply_material_preset(self, *args, **kwargs):
|
||||
return self.property_helpers._apply_material_preset(*args, **kwargs)
|
||||
|
||||
def _apply_material_to_node(self, *args, **kwargs):
|
||||
return self.property_helpers._apply_material_to_node(*args, **kwargs)
|
||||
|
||||
def _reset_material(self, *args, **kwargs):
|
||||
return self.property_helpers._reset_material(*args, **kwargs)
|
||||
|
||||
def _select_texture_for_material(self, *args, **kwargs):
|
||||
return self.property_helpers._select_texture_for_material(*args, **kwargs)
|
||||
|
||||
def _apply_texture_to_material(self, *args, **kwargs):
|
||||
return self.property_helpers._apply_texture_to_material(*args, **kwargs)
|
||||
|
||||
def _clear_all_textures(self, *args, **kwargs):
|
||||
return self.property_helpers._clear_all_textures(*args, **kwargs)
|
||||
|
||||
def _display_current_textures(self, *args, **kwargs):
|
||||
return self.property_helpers._display_current_textures(*args, **kwargs)
|
||||
|
||||
def _update_shading_model(self, *args, **kwargs):
|
||||
return self.property_helpers._update_shading_model(*args, **kwargs)
|
||||
|
||||
def _update_transparency(self, *args, **kwargs):
|
||||
return self.property_helpers._update_transparency(*args, **kwargs)
|
||||
|
||||
def _draw_texture_file_dialog(self, *args, **kwargs):
|
||||
return self.property_helpers._draw_texture_file_dialog(*args, **kwargs)
|
||||
|
||||
def start_transform_monitoring(self, *args, **kwargs):
|
||||
return self.property_helpers.start_transform_monitoring(*args, **kwargs)
|
||||
|
||||
def stop_transform_monitoring(self, *args, **kwargs):
|
||||
return self.property_helpers.stop_transform_monitoring(*args, **kwargs)
|
||||
|
||||
def _update_last_transform_values(self, *args, **kwargs):
|
||||
return self.property_helpers._update_last_transform_values(*args, **kwargs)
|
||||
|
||||
def _check_transform_changes(self, *args, **kwargs):
|
||||
return self.property_helpers._check_transform_changes(*args, **kwargs)
|
||||
|
||||
def update_transform_monitoring(self, *args, **kwargs):
|
||||
return self.property_helpers.update_transform_monitoring(*args, **kwargs)
|
||||
|
||||
def show_color_picker(self, *args, **kwargs):
|
||||
return self.property_helpers.show_color_picker(*args, **kwargs)
|
||||
|
||||
def _draw_color_picker(self, *args, **kwargs):
|
||||
return self.property_helpers._draw_color_picker(*args, **kwargs)
|
||||
|
||||
def _apply_color_selection(self, *args, **kwargs):
|
||||
return self.property_helpers._apply_color_selection(*args, **kwargs)
|
||||
|
||||
def _draw_color_button(self, *args, **kwargs):
|
||||
return self.property_helpers._draw_color_button(*args, **kwargs)
|
||||
|
||||
def _refresh_available_fonts(self, *args, **kwargs):
|
||||
return self.property_helpers._refresh_available_fonts(*args, **kwargs)
|
||||
|
||||
def show_font_selector(self, *args, **kwargs):
|
||||
return self.property_helpers.show_font_selector(*args, **kwargs)
|
||||
|
||||
def _draw_font_selector(self, *args, **kwargs):
|
||||
return self.property_helpers._draw_font_selector(*args, **kwargs)
|
||||
|
||||
def _apply_font_selection(self, *args, **kwargs):
|
||||
return self.property_helpers._apply_font_selection(*args, **kwargs)
|
||||
|
||||
def _draw_font_selector_button(self, *args, **kwargs):
|
||||
return self.property_helpers._draw_font_selector_button(*args, **kwargs)
|
||||
def _draw_console(self, *args, **kwargs):
|
||||
return self.script_panels._draw_console(*args, **kwargs)
|
||||
|
||||
def _draw_script_panel(self, *args, **kwargs):
|
||||
return self.script_panels._draw_script_panel(*args, **kwargs)
|
||||
|
||||
def _draw_script_status_group(self, *args, **kwargs):
|
||||
return self.script_panels._draw_script_status_group(*args, **kwargs)
|
||||
|
||||
def _draw_create_script_group(self, *args, **kwargs):
|
||||
return self.script_panels._draw_create_script_group(*args, **kwargs)
|
||||
|
||||
def _draw_available_scripts_group(self, *args, **kwargs):
|
||||
return self.script_panels._draw_available_scripts_group(*args, **kwargs)
|
||||
|
||||
def _draw_script_mounting_group(self, *args, **kwargs):
|
||||
return self.script_panels._draw_script_mounting_group(*args, **kwargs)
|
||||
|
||||
|
||||
def _toggle_hot_reload(self, *args, **kwargs):
|
||||
return self.app_actions._toggle_hot_reload(*args, **kwargs)
|
||||
|
||||
def _create_new_script(self, *args, **kwargs):
|
||||
return self.app_actions._create_new_script(*args, **kwargs)
|
||||
|
||||
def _refresh_scripts_list(self, *args, **kwargs):
|
||||
return self.app_actions._refresh_scripts_list(*args, **kwargs)
|
||||
|
||||
def _reload_all_scripts(self, *args, **kwargs):
|
||||
return self.app_actions._reload_all_scripts(*args, **kwargs)
|
||||
|
||||
def _on_script_selected(self, *args, **kwargs):
|
||||
return self.app_actions._on_script_selected(*args, **kwargs)
|
||||
|
||||
def _edit_script(self, *args, **kwargs):
|
||||
return self.app_actions._edit_script(*args, **kwargs)
|
||||
|
||||
def _mount_script_to_selected(self, *args, **kwargs):
|
||||
return self.app_actions._mount_script_to_selected(*args, **kwargs)
|
||||
|
||||
def _unmount_script_from_selected(self, *args, **kwargs):
|
||||
return self.app_actions._unmount_script_from_selected(*args, **kwargs)
|
||||
|
||||
def _on_new_project(self, *args, **kwargs):
|
||||
return self.app_actions._on_new_project(*args, **kwargs)
|
||||
|
||||
def _on_open_project(self, *args, **kwargs):
|
||||
return self.app_actions._on_open_project(*args, **kwargs)
|
||||
|
||||
def _on_save_project(self, *args, **kwargs):
|
||||
return self.app_actions._on_save_project(*args, **kwargs)
|
||||
|
||||
def _on_save_as_project(self, *args, **kwargs):
|
||||
return self.app_actions._on_save_as_project(*args, **kwargs)
|
||||
|
||||
def _on_exit(self, *args, **kwargs):
|
||||
return self.app_actions._on_exit(*args, **kwargs)
|
||||
|
||||
def _on_ctrl_pressed(self, *args, **kwargs):
|
||||
return self.app_actions._on_ctrl_pressed(*args, **kwargs)
|
||||
|
||||
def _on_ctrl_released(self, *args, **kwargs):
|
||||
return self.app_actions._on_ctrl_released(*args, **kwargs)
|
||||
|
||||
def _on_alt_pressed(self, *args, **kwargs):
|
||||
return self.app_actions._on_alt_pressed(*args, **kwargs)
|
||||
|
||||
def _on_alt_released(self, *args, **kwargs):
|
||||
return self.app_actions._on_alt_released(*args, **kwargs)
|
||||
|
||||
def _on_n_pressed(self, *args, **kwargs):
|
||||
return self.app_actions._on_n_pressed(*args, **kwargs)
|
||||
|
||||
def _on_o_pressed(self, *args, **kwargs):
|
||||
return self.app_actions._on_o_pressed(*args, **kwargs)
|
||||
|
||||
def _on_f4_pressed(self, *args, **kwargs):
|
||||
return self.app_actions._on_f4_pressed(*args, **kwargs)
|
||||
|
||||
def _on_delete_pressed(self, *args, **kwargs):
|
||||
return self.app_actions._on_delete_pressed(*args, **kwargs)
|
||||
|
||||
def _on_escape_pressed(self, *args, **kwargs):
|
||||
return self.app_actions._on_escape_pressed(*args, **kwargs)
|
||||
|
||||
def _on_wheel_up(self, *args, **kwargs):
|
||||
return self.app_actions._on_wheel_up(*args, **kwargs)
|
||||
|
||||
def _on_wheel_down(self, *args, **kwargs):
|
||||
return self.app_actions._on_wheel_down(*args, **kwargs)
|
||||
|
||||
def _is_mouse_over_imgui(self, *args, **kwargs):
|
||||
return self.app_actions._is_mouse_over_imgui(*args, **kwargs)
|
||||
|
||||
def processImGuiMouseClick(self, *args, **kwargs):
|
||||
return self.app_actions.processImGuiMouseClick(*args, **kwargs)
|
||||
|
||||
def add_message(self, *args, **kwargs):
|
||||
return self.app_actions.add_message(*args, **kwargs)
|
||||
|
||||
def add_success_message(self, *args, **kwargs):
|
||||
return self.app_actions.add_success_message(*args, **kwargs)
|
||||
|
||||
def add_error_message(self, *args, **kwargs):
|
||||
return self.app_actions.add_error_message(*args, **kwargs)
|
||||
|
||||
def add_warning_message(self, *args, **kwargs):
|
||||
return self.app_actions.add_warning_message(*args, **kwargs)
|
||||
|
||||
def add_info_message(self, *args, **kwargs):
|
||||
return self.app_actions.add_info_message(*args, **kwargs)
|
||||
|
||||
def _on_undo(self, *args, **kwargs):
|
||||
return self.app_actions._on_undo(*args, **kwargs)
|
||||
|
||||
def _on_redo(self, *args, **kwargs):
|
||||
return self.app_actions._on_redo(*args, **kwargs)
|
||||
|
||||
def _on_copy(self, *args, **kwargs):
|
||||
return self.app_actions._on_copy(*args, **kwargs)
|
||||
|
||||
def _on_cut(self, *args, **kwargs):
|
||||
return self.app_actions._on_cut(*args, **kwargs)
|
||||
|
||||
def _on_paste(self, *args, **kwargs):
|
||||
return self.app_actions._on_paste(*args, **kwargs)
|
||||
|
||||
def _on_delete(self, *args, **kwargs):
|
||||
return self.app_actions._on_delete(*args, **kwargs)
|
||||
|
||||
def _delete_node(self, *args, **kwargs):
|
||||
return self.app_actions._delete_node(*args, **kwargs)
|
||||
|
||||
def _perform_node_cleanup(self, *args, **kwargs):
|
||||
return self.app_actions._perform_node_cleanup(*args, **kwargs)
|
||||
|
||||
def _create_new_project(self, *args, **kwargs):
|
||||
return self.app_actions._create_new_project(*args, **kwargs)
|
||||
|
||||
def _open_project_path(self, *args, **kwargs):
|
||||
return self.app_actions._open_project_path(*args, **kwargs)
|
||||
|
||||
def _save_project_impl(self, *args, **kwargs):
|
||||
return self.app_actions._save_project_impl(*args, **kwargs)
|
||||
|
||||
def _open_project_impl(self, *args, **kwargs):
|
||||
return self.app_actions._open_project_impl(*args, **kwargs)
|
||||
|
||||
def _create_new_project_impl(self, *args, **kwargs):
|
||||
return self.app_actions._create_new_project_impl(*args, **kwargs)
|
||||
|
||||
def _update_window_title(self, *args, **kwargs):
|
||||
return self.app_actions._update_window_title(*args, **kwargs)
|
||||
|
||||
def _import_model_for_runtime(self, *args, **kwargs):
|
||||
return self.app_actions._import_model_for_runtime(*args, **kwargs)
|
||||
|
||||
def _on_import_model(self, *args, **kwargs):
|
||||
return self.app_actions._on_import_model(*args, **kwargs)
|
||||
|
||||
def _import_model(self, *args, **kwargs):
|
||||
return self.app_actions._import_model(*args, **kwargs)
|
||||
|
||||
def _import_model_with_menu_logic(self, *args, **kwargs):
|
||||
return self.app_actions._import_model_with_menu_logic(*args, **kwargs)
|
||||
|
||||
def _draw_new_project_dialog(self, *args, **kwargs):
|
||||
return self.dialog_panels._draw_new_project_dialog(*args, **kwargs)
|
||||
|
||||
def _draw_open_project_dialog(self, *args, **kwargs):
|
||||
return self.dialog_panels._draw_open_project_dialog(*args, **kwargs)
|
||||
|
||||
def _draw_path_browser(self, *args, **kwargs):
|
||||
return self.dialog_panels._draw_path_browser(*args, **kwargs)
|
||||
|
||||
def _draw_import_dialog(self, *args, **kwargs):
|
||||
return self.dialog_panels._draw_import_dialog(*args, **kwargs)
|
||||
|
||||
def _refresh_path_browser(self, *args, **kwargs):
|
||||
return self.dialog_panels._refresh_path_browser(*args, **kwargs)
|
||||
|
||||
def _apply_selected_path(self, *args, **kwargs):
|
||||
return self.dialog_panels._apply_selected_path(*args, **kwargs)
|
||||
|
||||
def _draw_spot_light_dialog(self, *args, **kwargs):
|
||||
return self.dialog_panels._draw_spot_light_dialog(*args, **kwargs)
|
||||
|
||||
def _draw_point_light_dialog(self, *args, **kwargs):
|
||||
return self.dialog_panels._draw_point_light_dialog(*args, **kwargs)
|
||||
|
||||
def _draw_terrain_dialog(self, *args, **kwargs):
|
||||
return self.dialog_panels._draw_terrain_dialog(*args, **kwargs)
|
||||
|
||||
def _draw_script_dialog(self, *args, **kwargs):
|
||||
return self.dialog_panels._draw_script_dialog(*args, **kwargs)
|
||||
|
||||
def _draw_script_browser(self, *args, **kwargs):
|
||||
return self.dialog_panels._draw_script_browser(*args, **kwargs)
|
||||
|
||||
def _refresh_script_browser(self, *args, **kwargs):
|
||||
return self.dialog_panels._refresh_script_browser(*args, **kwargs)
|
||||
|
||||
def _draw_heightmap_browser(self, *args, **kwargs):
|
||||
return self.dialog_panels._draw_heightmap_browser(*args, **kwargs)
|
||||
|
||||
def _refresh_heightmap_browser(self, *args, **kwargs):
|
||||
return self.dialog_panels._refresh_heightmap_browser(*args, **kwargs)
|
||||
|
||||
def setup_drag_drop_support(self):
|
||||
"""初始化拖拽支持。"""
|
||||
return self.model_drag_drop.setup_drag_drop_support()
|
||||
|
||||
def _is_point_in_resource_manager(self, drop_pos):
|
||||
"""判断投放坐标是否命中资源管理器窗口。"""
|
||||
return self.model_drag_drop.is_point_in_resource_manager(drop_pos)
|
||||
|
||||
def _resolve_resource_drop_target_dir(self, drop_pos):
|
||||
"""根据投放坐标解析资源管理器中的目标目录。"""
|
||||
return self.model_drag_drop.resolve_resource_drop_target_dir(drop_pos)
|
||||
|
||||
def _process_external_drop_events(self):
|
||||
"""处理外部拖入(系统文件拖拽)事件。"""
|
||||
return self.model_drag_drop.process_external_drop_events()
|
||||
|
||||
def _handle_external_drop(self, file_paths, drop_pos=None):
|
||||
"""把外部拖入文件分发到资源管理器或场景。"""
|
||||
return self.model_drag_drop.handle_external_drop(file_paths, drop_pos)
|
||||
|
||||
def add_dragged_file(self, file_path):
|
||||
"""添加拖拽的文件"""
|
||||
return self.model_drag_drop.add_dragged_file(file_path)
|
||||
|
||||
def clear_dragged_files(self):
|
||||
"""清空拖拽文件列表"""
|
||||
return self.model_drag_drop.clear_dragged_files()
|
||||
|
||||
def process_dragged_files(self):
|
||||
"""处理拖拽的文件"""
|
||||
return self.model_drag_drop.process_dragged_files()
|
||||
|
||||
def _import_model_from_path(self, file_path):
|
||||
"""从路径导入模型的内部方法"""
|
||||
return self.model_drag_drop.import_model_from_path(file_path)
|
||||
|
||||
def _draw_drag_drop_interface(self, *args, **kwargs):
|
||||
return self.interaction_panels._draw_drag_drop_interface(*args, **kwargs)
|
||||
|
||||
def _handle_drag_drop_completion(self, *args, **kwargs):
|
||||
return self.interaction_panels._handle_drag_drop_completion(*args, **kwargs)
|
||||
|
||||
def _draw_drag_overlay(self, *args, **kwargs):
|
||||
return self.interaction_panels._draw_drag_overlay(*args, **kwargs)
|
||||
|
||||
def _draw_drag_status(self, *args, **kwargs):
|
||||
return self.interaction_panels._draw_drag_status(*args, **kwargs)
|
||||
|
||||
def _draw_context_menus(self, *args, **kwargs):
|
||||
return self.interaction_panels._draw_context_menus(*args, **kwargs)
|
||||
|
||||
def _delete_node_simple(self, *args, **kwargs):
|
||||
return self.interaction_panels._delete_node_simple(*args, **kwargs)
|
||||
|
||||
def _copy_node(self, *args, **kwargs):
|
||||
return self.interaction_panels._copy_node(*args, **kwargs)
|
||||
|
||||
|
||||
def _on_create_empty_object(self, *args, **kwargs):
|
||||
return self.create_actions._on_create_empty_object(*args, **kwargs)
|
||||
|
||||
def _on_create_cube(self, *args, **kwargs):
|
||||
return self.create_actions._on_create_cube(*args, **kwargs)
|
||||
|
||||
def _on_create_sphere(self, *args, **kwargs):
|
||||
return self.create_actions._on_create_sphere(*args, **kwargs)
|
||||
|
||||
def _on_create_cylinder(self, *args, **kwargs):
|
||||
return self.create_actions._on_create_cylinder(*args, **kwargs)
|
||||
|
||||
def _on_create_plane(self, *args, **kwargs):
|
||||
return self.create_actions._on_create_plane(*args, **kwargs)
|
||||
|
||||
def _on_create_3d_text(self, *args, **kwargs):
|
||||
return self.create_actions._on_create_3d_text(*args, **kwargs)
|
||||
|
||||
def _on_create_3d_image(self, *args, **kwargs):
|
||||
return self.create_actions._on_create_3d_image(*args, **kwargs)
|
||||
|
||||
def _on_create_gui_button(self, *args, **kwargs):
|
||||
return self.create_actions._on_create_gui_button(*args, **kwargs)
|
||||
|
||||
def _on_create_gui_label(self, *args, **kwargs):
|
||||
return self.create_actions._on_create_gui_label(*args, **kwargs)
|
||||
|
||||
def _on_create_gui_entry(self, *args, **kwargs):
|
||||
return self.create_actions._on_create_gui_entry(*args, **kwargs)
|
||||
|
||||
def _on_create_gui_image(self, *args, **kwargs):
|
||||
return self.create_actions._on_create_gui_image(*args, **kwargs)
|
||||
|
||||
def _on_create_video_screen(self, *args, **kwargs):
|
||||
return self.create_actions._on_create_video_screen(*args, **kwargs)
|
||||
|
||||
def _on_create_2d_video_screen(self, *args, **kwargs):
|
||||
return self.create_actions._on_create_2d_video_screen(*args, **kwargs)
|
||||
|
||||
def _on_create_spherical_video(self, *args, **kwargs):
|
||||
return self.create_actions._on_create_spherical_video(*args, **kwargs)
|
||||
|
||||
def _on_create_virtual_screen(self, *args, **kwargs):
|
||||
return self.create_actions._on_create_virtual_screen(*args, **kwargs)
|
||||
|
||||
def _on_create_spot_light(self, *args, **kwargs):
|
||||
return self.create_actions._on_create_spot_light(*args, **kwargs)
|
||||
|
||||
def _on_create_point_light(self, *args, **kwargs):
|
||||
return self.create_actions._on_create_point_light(*args, **kwargs)
|
||||
|
||||
def _on_create_flat_terrain(self, *args, **kwargs):
|
||||
return self.create_actions._on_create_flat_terrain(*args, **kwargs)
|
||||
|
||||
def _on_create_heightmap_terrain(self, *args, **kwargs):
|
||||
return self.create_actions._on_create_heightmap_terrain(*args, **kwargs)
|
||||
|
||||
def _on_create_script(self, *args, **kwargs):
|
||||
return self.create_actions._on_create_script(*args, **kwargs)
|
||||
|
||||
def _on_load_script(self, *args, **kwargs):
|
||||
return self.create_actions._on_load_script(*args, **kwargs)
|
||||
|
||||
def _on_reload_all_scripts(self, *args, **kwargs):
|
||||
return self.create_actions._on_reload_all_scripts(*args, **kwargs)
|
||||
|
||||
def _on_open_scripts_manager(self, *args, **kwargs):
|
||||
return self.create_actions._on_open_scripts_manager(*args, **kwargs)
|
||||
|
||||
def _on_create_2d_sample_panel(self, *args, **kwargs):
|
||||
return self.create_actions._on_create_2d_sample_panel(*args, **kwargs)
|
||||
|
||||
def _on_create_3d_sample_panel(self, *args, **kwargs):
|
||||
return self.create_actions._on_create_3d_sample_panel(*args, **kwargs)
|
||||
|
||||
def _on_create_web_panel(self, *args, **kwargs):
|
||||
return self.create_actions._on_create_web_panel(*args, **kwargs)
|
||||
|
||||
|
||||
def createEmptyObject(self, *args, **kwargs):
|
||||
return self.object_factory.createEmptyObject(*args, **kwargs)
|
||||
|
||||
def create3DText(self, *args, **kwargs):
|
||||
return self.object_factory.create3DText(*args, **kwargs)
|
||||
|
||||
def create3DImage(self, *args, **kwargs):
|
||||
return self.object_factory.create3DImage(*args, **kwargs)
|
||||
|
||||
def createCube(self, *args, **kwargs):
|
||||
return self.object_factory.createCube(*args, **kwargs)
|
||||
|
||||
def createSphere(self, *args, **kwargs):
|
||||
return self.object_factory.createSphere(*args, **kwargs)
|
||||
|
||||
def createCylinder(self, *args, **kwargs):
|
||||
return self.object_factory.createCylinder(*args, **kwargs)
|
||||
|
||||
def createPlane(self, *args, **kwargs):
|
||||
return self.object_factory.createPlane(*args, **kwargs)
|
||||
|
||||
def create2DSamplePanel(self, *args, **kwargs):
|
||||
return self.object_factory.create2DSamplePanel(*args, **kwargs)
|
||||
|
||||
def create3DSamplePanel(self, *args, **kwargs):
|
||||
return self.object_factory.create3DSamplePanel(*args, **kwargs)
|
||||
|
||||
def createWebPanel(self, *args, **kwargs):
|
||||
return self.object_factory.createWebPanel(*args, **kwargs)
|
||||
def createGUIButton(self, pos=(0, 0, 0), text="按钮", size=0.1):
|
||||
return self.runtime_actions.createGUIButton(pos, text, size)
|
||||
|
||||
def _create_simple_gui_button(self, pos=(0, 0, 0), text="按钮", size=0.1):
|
||||
return self.runtime_actions._create_simple_gui_button(pos, text, size)
|
||||
|
||||
def _on_gui_button_click(self):
|
||||
return self.runtime_actions._on_gui_button_click()
|
||||
|
||||
def _toggle_vr_mode(self):
|
||||
return self.runtime_actions._toggle_vr_mode()
|
||||
|
||||
def _exit_vr_mode(self):
|
||||
return self.runtime_actions._exit_vr_mode()
|
||||
|
||||
def _show_vr_status(self):
|
||||
return self.runtime_actions._show_vr_status()
|
||||
|
||||
def _show_vr_settings(self):
|
||||
return self.runtime_actions._show_vr_settings()
|
||||
|
||||
def _show_vr_performance_report(self):
|
||||
return self.runtime_actions._show_vr_performance_report()
|
||||
|
||||
def _get_chinese_font(self):
|
||||
return self.runtime_actions._get_chinese_font()
|
||||
|
||||
def createGUILabel(self, pos=(0, 0, 0), text="标签", size=0.08):
|
||||
return self.runtime_actions.createGUILabel(pos, text, size)
|
||||
|
||||
def _create_simple_gui_label(self, pos=(0, 0, 0), text="标签", size=0.08):
|
||||
return self.runtime_actions._create_simple_gui_label(pos, text, size)
|
||||
|
||||
def createGUIEntry(self, pos=(0, 0, 0), placeholder="输入文本...", size=0.08):
|
||||
return self.runtime_actions.createGUIEntry(pos, placeholder, size)
|
||||
|
||||
def _create_simple_gui_entry(self, pos=(0, 0, 0), placeholder="输入文本...", size=0.08):
|
||||
return self.runtime_actions._create_simple_gui_entry(pos, placeholder, size)
|
||||
|
||||
def createGUIImage(self, pos=(0, 0, 0), image_path=None, size=2):
|
||||
return self.runtime_actions.createGUIImage(pos, image_path, size)
|
||||
|
||||
def _create_simple_gui_image(self, pos=(0, 0, 0), image_path=None, size=2):
|
||||
return self.runtime_actions._create_simple_gui_image(pos, image_path, size)
|
||||
|
||||
def createVideoScreen(self, pos=(0, 0, 0), size=1, video_path=None):
|
||||
return self.runtime_actions.createVideoScreen(pos, size, video_path)
|
||||
|
||||
def create2DVideoScreen(self, pos=(0, 0, 0), size=0.2, video_path=None):
|
||||
return self.runtime_actions.create2DVideoScreen(pos, size, video_path)
|
||||
|
||||
def createSphericalVideo(self, pos=(0, 0, 0), radius=5.0, video_path=None):
|
||||
return self.runtime_actions.createSphericalVideo(pos, radius, video_path)
|
||||
|
||||
def createVirtualScreen(self, pos=(0, 0, 0), size=(2, 1), text="虚拟屏幕"):
|
||||
return self.runtime_actions.createVirtualScreen(pos, size, text)
|
||||
|
||||
def createSpotLight(self, pos=(0, 0, 5)):
|
||||
return self.runtime_actions.createSpotLight(pos)
|
||||
|
||||
def createPointLight(self, pos=(0, 0, 5)):
|
||||
return self.runtime_actions.createPointLight(pos)
|
||||
|
||||
def createFlatTerrain(self, size=(10, 10), resolution=129):
|
||||
return self.runtime_actions.createFlatTerrain(size, resolution)
|
||||
|
||||
def createTerrainFromHeightMap(self, heightmap_path, scale=(1.0, 1.0, 10.0)):
|
||||
return self.runtime_actions.createTerrainFromHeightMap(heightmap_path, scale)
|
||||
|
||||
def createScript(self, script_name, template="basic"):
|
||||
return self.runtime_actions.createScript(script_name, template)
|
||||
|
||||
def loadScript(self, script_path):
|
||||
return self.runtime_actions.loadScript(script_path)
|
||||
|
||||
425
ui/panels/runtime_actions.py
Normal file
425
ui/panels/runtime_actions.py
Normal file
@ -0,0 +1,425 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
from core.editor_context import get_editor_context
|
||||
|
||||
|
||||
class RuntimeActions:
|
||||
"""Runtime-facing creation and utility actions extracted from main world."""
|
||||
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
object.__setattr__(self, "_editor_context", get_editor_context(app))
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self.app, name)
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
if name == "app" or name.startswith("_") or name in self.__dict__ or hasattr(type(self), name):
|
||||
object.__setattr__(self, name, value)
|
||||
else:
|
||||
setattr(self.app, name, value)
|
||||
|
||||
def _get_editor_context(self):
|
||||
context = getattr(self, "_editor_context", None)
|
||||
if context is None:
|
||||
context = get_editor_context(self.app)
|
||||
object.__setattr__(self, "_editor_context", context)
|
||||
return context
|
||||
|
||||
def _get_gui_manager(self):
|
||||
"""统一获取 GUI 管理器,避免散落的 hasattr 判空。"""
|
||||
return self._get_editor_context().get_gui_manager()
|
||||
|
||||
def _append_gui_element(self, element_wrapper):
|
||||
"""统一维护 gui_elements 列表。"""
|
||||
gui_manager = self._get_gui_manager()
|
||||
if not gui_manager:
|
||||
return False
|
||||
self._get_editor_context().append_gui_element(element_wrapper)
|
||||
return True
|
||||
|
||||
def createGUIButton(self, pos=(0, 0, 0), text="按钮", size=0.1):
|
||||
"""创建2D GUI按钮"""
|
||||
try:
|
||||
if self._get_gui_manager():
|
||||
return self._create_simple_gui_button(pos, text, size)
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"创建GUI按钮失败: {e}")
|
||||
return None
|
||||
|
||||
def _create_simple_gui_button(self, pos=(0, 0, 0), text="按钮", size=0.1):
|
||||
"""创建简单的GUI按钮,不依赖QT树形控件"""
|
||||
try:
|
||||
from direct.gui.DirectGui import DirectButton
|
||||
|
||||
gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1)
|
||||
font = self._get_chinese_font()
|
||||
|
||||
if isinstance(size, (list, tuple)) and len(size) >= 2:
|
||||
scale_value = size[0]
|
||||
else:
|
||||
scale_value = size
|
||||
|
||||
button = DirectButton(
|
||||
text=text,
|
||||
pos=gui_pos,
|
||||
scale=scale_value,
|
||||
text_font=font,
|
||||
command=self._on_gui_button_click
|
||||
)
|
||||
|
||||
button_wrapper = type("GUIElement", (), {})()
|
||||
button_wrapper.node = button
|
||||
button_wrapper.name = text
|
||||
button_wrapper.gui_type = "GUI_BUTTON"
|
||||
button_wrapper.position = pos
|
||||
button_wrapper.size = size
|
||||
|
||||
if not self._append_gui_element(button_wrapper):
|
||||
return None
|
||||
|
||||
print(f"✓ GUI按钮创建成功: {text}")
|
||||
return button_wrapper
|
||||
except Exception as e:
|
||||
print(f"✗ 创建简单GUI按钮失败: {e}")
|
||||
return None
|
||||
|
||||
def _on_gui_button_click(self):
|
||||
"""GUI按钮点击事件处理"""
|
||||
print("GUI按钮被点击了")
|
||||
|
||||
def _toggle_vr_mode(self):
|
||||
"""切换VR模式"""
|
||||
if self.vr_manager:
|
||||
if self.vr_manager.is_enabled():
|
||||
self._exit_vr_mode()
|
||||
else:
|
||||
self.vr_manager.enable()
|
||||
self.add_info_message("已进入VR模式")
|
||||
else:
|
||||
self.add_error_message("VR管理器未初始化")
|
||||
|
||||
def _exit_vr_mode(self):
|
||||
"""退出VR模式"""
|
||||
if self.vr_manager:
|
||||
self.vr_manager.disable()
|
||||
self.add_info_message("已退出VR模式")
|
||||
|
||||
def _show_vr_status(self):
|
||||
"""显示VR状态"""
|
||||
if self.vr_manager:
|
||||
status = "已启用" if self.vr_manager.is_enabled() else "未启用"
|
||||
self.add_info_message(f"VR状态: {status}")
|
||||
|
||||
if self.vr_manager.is_enabled():
|
||||
devices = self.vr_manager.get_connected_devices()
|
||||
if devices:
|
||||
self.add_info_message(f"连接的设备: {', '.join(devices)}")
|
||||
else:
|
||||
self.add_info_message("未检测到VR设备")
|
||||
else:
|
||||
self.add_error_message("VR管理器未初始化")
|
||||
|
||||
def _show_vr_settings(self):
|
||||
"""显示VR设置"""
|
||||
if self.vr_manager:
|
||||
self.add_info_message("VR设置对话框待实现")
|
||||
else:
|
||||
self.add_error_message("VR管理器未初始化")
|
||||
|
||||
def _show_vr_performance_report(self):
|
||||
"""显示VR性能报告"""
|
||||
if self.vr_manager and self.vr_manager.is_enabled():
|
||||
report = self.vr_manager.get_performance_report()
|
||||
self.add_info_message(f"VR性能报告: {report}")
|
||||
else:
|
||||
self.add_info_message("VR未启用或管理器未初始化")
|
||||
|
||||
def _get_chinese_font(self):
|
||||
"""获取中文字体"""
|
||||
try:
|
||||
from panda3d.core import TextNode
|
||||
|
||||
font_paths = [
|
||||
"/usr/share/fonts/truetype/wqy/wqy-microhei.ttc",
|
||||
"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
|
||||
"/System/Library/Fonts/PingFang.ttc",
|
||||
"C:/Windows/Fonts/simhei.ttf",
|
||||
"C:/Windows/Fonts/msyh.ttc",
|
||||
]
|
||||
|
||||
for font_path in font_paths:
|
||||
if Path(font_path).exists():
|
||||
try:
|
||||
font = self.loader.loadFont(font_path)
|
||||
print(f"✓ 为GUI加载中文字体成功: {font_path}")
|
||||
return font
|
||||
except Exception:
|
||||
print(f"⚠️ 字体加载失败,尝试下一个: {font_path}")
|
||||
continue
|
||||
|
||||
print("⚠️ 无法加载中文字体,使用默认字体")
|
||||
return TextNode.getDefaultFont()
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ 获取中文字体失败: {e}")
|
||||
from panda3d.core import TextNode
|
||||
return TextNode.getDefaultFont()
|
||||
|
||||
def createGUILabel(self, pos=(0, 0, 0), text="标签", size=0.08):
|
||||
"""创建2D GUI标签"""
|
||||
try:
|
||||
if self._get_gui_manager():
|
||||
return self._create_simple_gui_label(pos, text, size)
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"创建GUI标签失败: {e}")
|
||||
return None
|
||||
|
||||
def _create_simple_gui_label(self, pos=(0, 0, 0), text="标签", size=0.08):
|
||||
"""创建简单的GUI标签,不依赖QT树形控件"""
|
||||
try:
|
||||
from direct.gui.DirectGui import DirectLabel
|
||||
|
||||
gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1)
|
||||
font = self._get_chinese_font()
|
||||
|
||||
if isinstance(size, (list, tuple)) and len(size) >= 2:
|
||||
scale_value = size[0]
|
||||
else:
|
||||
scale_value = size
|
||||
|
||||
label = DirectLabel(
|
||||
text=text,
|
||||
pos=gui_pos,
|
||||
scale=scale_value,
|
||||
text_font=font,
|
||||
text_fg=(1, 1, 1, 1)
|
||||
)
|
||||
|
||||
label_wrapper = type("GUIElement", (), {})()
|
||||
label_wrapper.node = label
|
||||
label_wrapper.name = text
|
||||
label_wrapper.gui_type = "GUI_LABEL"
|
||||
label_wrapper.position = pos
|
||||
label_wrapper.size = size
|
||||
|
||||
if not self._append_gui_element(label_wrapper):
|
||||
return None
|
||||
|
||||
print(f"✓ GUI标签创建成功: {text}")
|
||||
return label_wrapper
|
||||
except Exception as e:
|
||||
print(f"✗ 创建简单GUI标签失败: {e}")
|
||||
return None
|
||||
|
||||
def createGUIEntry(self, pos=(0, 0, 0), placeholder="输入文本...", size=0.08):
|
||||
"""创建2D GUI文本输入框"""
|
||||
try:
|
||||
if self._get_gui_manager():
|
||||
return self._create_simple_gui_entry(pos, placeholder, size)
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"创建GUI输入框失败: {e}")
|
||||
return None
|
||||
|
||||
def _create_simple_gui_entry(self, pos=(0, 0, 0), placeholder="输入文本...", size=0.08):
|
||||
"""创建简单的GUI输入框,不依赖QT树形控件"""
|
||||
try:
|
||||
from direct.gui.DirectGui import DirectEntry
|
||||
|
||||
gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1)
|
||||
font = self._get_chinese_font()
|
||||
|
||||
if isinstance(size, (list, tuple)) and len(size) >= 2:
|
||||
scale_value = size[0]
|
||||
else:
|
||||
scale_value = size
|
||||
|
||||
entry = DirectEntry(
|
||||
text=placeholder,
|
||||
pos=gui_pos,
|
||||
scale=scale_value,
|
||||
text_font=font,
|
||||
width=20,
|
||||
numLines=1,
|
||||
focus=1
|
||||
)
|
||||
|
||||
entry_wrapper = type("GUIElement", (), {})()
|
||||
entry_wrapper.node = entry
|
||||
entry_wrapper.name = placeholder
|
||||
entry_wrapper.gui_type = "GUI_ENTRY"
|
||||
entry_wrapper.position = pos
|
||||
entry_wrapper.size = size
|
||||
|
||||
if not self._append_gui_element(entry_wrapper):
|
||||
return None
|
||||
|
||||
print(f"✓ GUI输入框创建成功: {placeholder}")
|
||||
return entry_wrapper
|
||||
except Exception as e:
|
||||
print(f"✗ 创建简单GUI输入框失败: {e}")
|
||||
return None
|
||||
|
||||
def createGUIImage(self, pos=(0, 0, 0), image_path=None, size=2):
|
||||
"""创建2D GUI图片"""
|
||||
try:
|
||||
if self._get_gui_manager():
|
||||
return self._create_simple_gui_image(pos, image_path, size)
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"创建GUI图片失败: {e}")
|
||||
return None
|
||||
|
||||
def _create_simple_gui_image(self, pos=(0, 0, 0), image_path=None, size=2):
|
||||
"""创建简单的GUI图片,不依赖QT树形控件"""
|
||||
try:
|
||||
from direct.gui.DirectGui import DirectFrame
|
||||
from panda3d.core import Filename
|
||||
|
||||
gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1)
|
||||
|
||||
if isinstance(size, (list, tuple)) and len(size) >= 2:
|
||||
scale_value = size[0]
|
||||
else:
|
||||
scale_value = size
|
||||
|
||||
if image_path and os.path.exists(image_path):
|
||||
tex = self.loader.loadTexture(Filename.fromOsSpecific(image_path))
|
||||
image = DirectFrame(
|
||||
image=tex,
|
||||
pos=gui_pos,
|
||||
scale=scale_value
|
||||
)
|
||||
image_name = os.path.basename(image_path)
|
||||
else:
|
||||
image = DirectFrame(
|
||||
frameColor=(0.5, 0.5, 0.5, 1.0),
|
||||
frameSize=(-scale_value, scale_value, -scale_value, scale_value),
|
||||
pos=gui_pos
|
||||
)
|
||||
image_name = "占位符图片"
|
||||
|
||||
image_wrapper = type("GUIElement", (), {})()
|
||||
image_wrapper.node = image
|
||||
image_wrapper.name = image_name
|
||||
image_wrapper.gui_type = "GUI_IMAGE"
|
||||
image_wrapper.position = pos
|
||||
image_wrapper.size = size
|
||||
image_wrapper.image_path = image_path
|
||||
|
||||
if not self._append_gui_element(image_wrapper):
|
||||
return None
|
||||
|
||||
print(f"✓ GUI图片创建成功: {image_name}")
|
||||
return image_wrapper
|
||||
except Exception as e:
|
||||
print(f"✗ 创建简单GUI图片失败: {e}")
|
||||
return None
|
||||
|
||||
def createVideoScreen(self, pos=(0, 0, 0), size=1, video_path=None):
|
||||
"""创建视频屏幕"""
|
||||
try:
|
||||
gui_manager = self._get_gui_manager()
|
||||
if gui_manager:
|
||||
return gui_manager.createVideoScreen(pos, size, video_path)
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"创建视频屏幕失败: {e}")
|
||||
return None
|
||||
|
||||
def create2DVideoScreen(self, pos=(0, 0, 0), size=0.2, video_path=None):
|
||||
"""创建2D视频屏幕"""
|
||||
try:
|
||||
gui_manager = self._get_gui_manager()
|
||||
if gui_manager:
|
||||
return gui_manager.createGUI2DVideoScreen(pos, size, video_path)
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"创建2D视频屏幕失败: {e}")
|
||||
return None
|
||||
|
||||
def createSphericalVideo(self, pos=(0, 0, 0), radius=5.0, video_path=None):
|
||||
"""创建360度视频"""
|
||||
try:
|
||||
gui_manager = self._get_gui_manager()
|
||||
if gui_manager:
|
||||
return gui_manager.createSphericalVideo(pos, radius, video_path)
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"创建球形视频失败: {e}")
|
||||
return None
|
||||
|
||||
def createVirtualScreen(self, pos=(0, 0, 0), size=(2, 1), text="虚拟屏幕"):
|
||||
"""创建虚拟屏幕"""
|
||||
try:
|
||||
gui_manager = self._get_gui_manager()
|
||||
if gui_manager:
|
||||
return gui_manager.createGUIVirtualScreen(pos, size, text)
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"创建虚拟屏幕失败: {e}")
|
||||
return None
|
||||
|
||||
def createSpotLight(self, pos=(0, 0, 5)):
|
||||
"""创建聚光灯"""
|
||||
try:
|
||||
if hasattr(self, "scene_manager") and self.scene_manager:
|
||||
return self.scene_manager.createSpotLight(pos)
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"创建聚光灯失败: {e}")
|
||||
return None
|
||||
|
||||
def createPointLight(self, pos=(0, 0, 5)):
|
||||
"""创建点光源"""
|
||||
try:
|
||||
if hasattr(self, "scene_manager") and self.scene_manager:
|
||||
return self.scene_manager.createPointLight(pos)
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"创建点光源失败: {e}")
|
||||
return None
|
||||
|
||||
def createFlatTerrain(self, size=(10, 10), resolution=129):
|
||||
"""创建平面地形"""
|
||||
try:
|
||||
if hasattr(self, "terrain_manager") and self.terrain_manager:
|
||||
return self.terrain_manager.createFlatTerrain(size, resolution)
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"创建平面地形失败: {e}")
|
||||
return None
|
||||
|
||||
def createTerrainFromHeightMap(self, heightmap_path, scale=(1.0, 1.0, 10.0)):
|
||||
"""从高度图创建地形"""
|
||||
try:
|
||||
if hasattr(self, "terrain_manager") and self.terrain_manager:
|
||||
return self.terrain_manager.createTerrainFromHeightMap(heightmap_path, scale)
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"创建高度图地形失败: {e}")
|
||||
return None
|
||||
|
||||
def createScript(self, script_name, template="basic"):
|
||||
"""创建脚本"""
|
||||
try:
|
||||
if hasattr(self, "script_manager") and self.script_manager:
|
||||
return self.script_manager.createScript(script_name, template)
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"创建脚本失败: {e}")
|
||||
return None
|
||||
|
||||
def loadScript(self, script_path):
|
||||
"""加载脚本"""
|
||||
try:
|
||||
if hasattr(self, "script_manager") and self.script_manager:
|
||||
return self.script_manager.loadScript(script_path)
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"加载脚本失败: {e}")
|
||||
return None
|
||||
|
||||
4330
ui/widgets.py
4330
ui/widgets.py
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user