Compare commits

...

26 Commits
main ... geng

Author SHA1 Message Date
ayuan9957
c8eb4cef4d refactor(lui): complete batch split for interaction/editor/property flows 2026-03-01 19:39:13 +08:00
ayuan9957
a1f6fe6097 回补G任务:拆分LUI _update_drag 2026-03-01 13:20:51 +08:00
ayuan9957
ea6ff140e9 重启N1资源管理器面板拆分(保留布局) 2026-03-01 12:50:14 +08:00
ayuan9957
2c9f512996 refactor: 拆分updateGizmoDrag并同步任务文档 2026-03-01 12:16:08 +08:00
ayuan9957
280d6f8b2a refactor: 完成_getActor首轮拆分并更新项目任务文档 2026-03-01 11:48:38 +08:00
ayuan9957
d140552779 refactor: 拆分main初始化并更新优化分析 2026-02-28 17:48:42 +08:00
ayuan9957
ffdad6f099 refactor: 二轮拆分loadScene节点处理流程 2026-02-28 17:42:09 +08:00
ayuan9957
4f2c545b88 refactor: 拆分loadScene流程并更新优化分析 2026-02-28 17:31:28 +08:00
ayuan9957
e2544ea440 refactor: 收敛非VR编辑上下文并更新优化索引 2026-02-28 17:06:15 +08:00
ayuan9957
3f8617a1b6 refactor: 拆分右侧属性面板为变换/材质/碰撞子mixin 2026-02-28 16:35:40 +08:00
ayuan9957
036acfeaed 继续优化:拆分editor_panels并收敛旧接口 2026-02-28 16:24:39 +08:00
ayuan9957
5a9e31a195 清理qt 2026-02-28 15:11:22 +08:00
Hector
036b68ef41 合并更新 2026-02-28 11:10:43 +08:00
Hector
5752559cdd Merge remote-tracking branch 'origin/geng' into IMgui_hu 2026-02-28 11:06:14 +08:00
Your Name
7386d687da feat: 接入RenderPipeline选中描边后处理 2026-02-28 10:58:34 +08:00
Hector
28d2b124cc 合并更新 2026-02-28 10:49:03 +08:00
Hector
2ac08b0582 Merge remote-tracking branch 'origin/geng' into IMgui_hu
# Conflicts:
#	imgui.ini
#	ssbo_component/ssbo_controller.py
#	ssbo_component/ssbo_editor.py
2026-02-28 09:37:33 +08:00
Your Name
86aaa21ddd ssbo 视锥剔除优化提升效果一般 2026-02-27 16:57:53 +08:00
Hector
af898947b5 拆分 2026-02-27 16:52:00 +08:00
Your Name
f3f8da7b90 Optimize SSBO root selection to avoid full dynamic activation 2026-02-27 16:11:35 +08:00
Your Name
2183d3fc3e Fix SSBO root selection behavior and optimize group sync FPS 2026-02-27 16:02:40 +08:00
Your Name
756db5b010 Fix SSBO picking sync and deletion cleanup 2026-02-27 15:39:23 +08:00
Your Name
1fd7e1d7ac Update ssbo editor 2026-02-27 14:24:11 +08:00
Your Name
53e6a829e4 Fix gizmo selection: GPU picking, group proxy, parent-child transform, pick sync 2026-02-27 11:47:15 +08:00
Hector
e917f9019d 简单实现http通信 2026-02-27 11:14:08 +08:00
Hector
c93ab3edac 修复手柄选中以及移动异常,添加Imgui的web视图 2026-02-27 10:32:57 +08:00
72 changed files with 20963 additions and 35490 deletions

62
IMGUI_MODULE_ANALYSIS.md Normal file
View File

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

2465
PROJECT_FULL_CATALOG.md Normal file

File diff suppressed because it is too large Load Diff

352
PROJECT_MODULE_INDEX.md Normal file
View File

@ -0,0 +1,352 @@
# PROJECT_MODULE_INDEX
> 本文档只基于本地工作区扫描生成,不依赖 Git 远程信息。
- 生成时间: 2026-02-28 16:41:46
- 目标: 为后续优化/重构提供 功能模块 -> 文件 的快速定位索引
## 0. 关联分析文档
- 优化分析(本轮): `PROJECT_OPTIMIZATION_ANALYSIS.md`
- 项目任务清单: `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

View 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 本轮深入分析(非 VRP1 准备)
### 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
View 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`

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

View File

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

View File

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

View File

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

View File

@ -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&current=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
View 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

View File

@ -1,6 +1,7 @@
from panda3d.core import (Point3, Point2, CollisionTraverser, CollisionHandlerQueue,
CollisionNode, CollisionRay, GeomNode, LineSegs, RenderState,
DepthTestAttrib, ColorAttrib)
from core.editor_context import get_editor_context
class EventHandler:
@ -9,11 +10,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
View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -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
View File

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

View File

@ -3,29 +3,25 @@ import time
import urllib
from panda3d.core import GeoMipTerrain, PNMImage, Texture, Vec3, NodePath
from panda3d.core import Filename, Material, ColorAttrib, AmbientLight, DirectionalLight
import os
from scene import util
from panda3d.core import Filename, Material, ColorAttrib, AmbientLight, DirectionalLight
import os
from scene import util
from core.editor_context import get_editor_context
class TerrainManager:
"""地形管理类"""
def __init__(self, world):
self.world = world
self.terrains = []
def __init__(self, world):
self.world = world
self._editor_context = get_editor_context(world)
self.terrains = []
# core/terrain_manager.py
def _get_tree_widget(self):
"""安全获取树形控件"""
try:
if (hasattr(self.world, 'interface_manager') and
hasattr(self.world.interface_manager, 'treeWidget')):
return self.world.interface_manager.treeWidget
except AttributeError:
pass
return None
def _get_tree_widget(self):
"""安全获取树形控件"""
return self._editor_context.get_tree_widget()
def createTerrainFromHeightMap(self, heightmap_path, scale=(1, 1, 1)):
"""从高度图创建地形"""

View File

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

View File

@ -1893,12 +1893,11 @@ class VRManager(DirectObject):
print(" 注意Submit后立即调用WaitGetPoses是错误实现")
self.set_prediction_time(11.0) # 11ms预测时间 - OpenVR标准值平衡准确性和延迟
# 🚀 动态调整Qt Timer频率以支持VR
if hasattr(self.world, 'qtWidget') and self.world.qtWidget:
if hasattr(self.world.qtWidget, 'synchronizer'):
# 设置为144Hz让OpenVR控制实际渲染节奏
self.world.qtWidget.synchronizer.setInterval(int(1000/144))
print("✓ Qt Timer调整为144Hz让OpenVR控制VR渲染节奏")
# 🚀 动态调整宿主同步器频率以支持VR
host_widget = getattr(self.world, "host_widget", None)
if host_widget and hasattr(host_widget, "synchronizer"):
host_widget.synchronizer.setInterval(int(1000 / 144))
print("✓ 渲染同步器调整为144Hz让OpenVR控制VR渲染节奏")
# 🔧 关键修复检测并重建缺失的手柄visualizer
# 当渲染模式切换时visualizer可能被清理但控制器对象仍存在
@ -1952,11 +1951,11 @@ class VRManager(DirectObject):
# 恢复主相机
self._enable_main_cam()
# 恢复Qt Timer到60FPS
if hasattr(self.world, 'qtWidget') and self.world.qtWidget:
if hasattr(self.world.qtWidget, 'synchronizer'):
self.world.qtWidget.synchronizer.setInterval(int(1000/60))
print("Qt Timer恢复为60Hz")
# 恢复渲染同步器到60FPS
host_widget = getattr(self.world, "host_widget", None)
if host_widget and hasattr(host_widget, "synchronizer"):
host_widget.synchronizer.setInterval(int(1000 / 60))
print("渲染同步器恢复为60Hz")
print("✅ VR模式已禁用手柄模型已隐藏")

View File

@ -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)

File diff suppressed because it is too large Load Diff

View File

@ -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

1310
main.py

File diff suppressed because it is too large Load Diff

View File

@ -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+

View File

@ -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

View File

@ -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

View File

@ -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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View 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

File diff suppressed because it is too large Load Diff

View 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)) # 默认为0p3d_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
View File

@ -0,0 +1,5 @@
"""Shared tree data-role constants."""
# Keep this value stable for legacy tree-item data payloads.
TREE_USER_ROLE = 256

View File

@ -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]

View File

@ -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

View File

@ -2,8 +2,7 @@
# -*- coding: utf-8 -*-
"""
测试动画模型 - Panda3D应用程序
使用Panda3D引擎编辑器创建
打包
"""
from __future__ import print_function

1
ui/LUI/__init__.py Normal file
View File

@ -0,0 +1 @@
"""LUI split modules."""

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

1738
ui/LUI/lui_manager_editor.py Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

69
ui/LUI/lui_shared.py Normal file
View 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

View File

@ -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)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -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):
"""获取模型格式信息"""

View File

@ -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

View File

@ -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

View File

@ -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

View 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视图...")

View 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

View 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()

View 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}")

View 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}")

View 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)

View 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()
# 工具按钮已移除(导入、保存、播放)

View 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)

View 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

File diff suppressed because it is too large Load Diff