refactor: 完成_getActor首轮拆分并更新项目任务文档
This commit is contained in:
parent
d140552779
commit
280d6f8b2a
@ -20,7 +20,7 @@ D:.
|
||||
| imgui.ini
|
||||
| IMGUI_MODULE_ANALYSIS.md
|
||||
| main.py
|
||||
| QT_TO_IMGUI_MIGRATION_CHECKLIST.md
|
||||
| PROJECT_TASK_CHECKLIST.md
|
||||
| Start_Run.py
|
||||
|
|
||||
+---.idea
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
## 0. 关联分析文档
|
||||
|
||||
- 优化分析(本轮): `PROJECT_OPTIMIZATION_ANALYSIS.md`
|
||||
- Qt 清理迁移清单: `QT_TO_IMGUI_MIGRATION_CHECKLIST.md`
|
||||
- 项目任务清单: `PROJECT_TASK_CHECKLIST.md`
|
||||
- 历史模块分析: `IMGUI_MODULE_ANALYSIS.md`
|
||||
|
||||
## 1. 一级目录规模(Python)
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
- 效果(本地静态检索):
|
||||
- 直接访问 `world.interface_manager`: `0`
|
||||
- 直接访问 `interface_manager.treeWidget`: `0`
|
||||
- `app.gui_manager` 仅剩注释引用(`ui/panels/editor_panels_left.py`)
|
||||
- 旧上下文引用已从“接口访问路径”转为“上下文适配层/兼容字段”模式
|
||||
- Task B 第二轮已落地(`scene_manager_io_mixin.loadScene`):
|
||||
- 已抽出流程 helper:
|
||||
- `_preflight_load_scene`
|
||||
@ -50,17 +50,21 @@
|
||||
- `_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`,暂不触碰 VR 模块
|
||||
|
||||
## 1. 总体画像
|
||||
|
||||
- Python 文件: `146`
|
||||
- 代码总行数: `58,371`
|
||||
- `except Exception` / `except:` 总计: `950`
|
||||
- 裸 `except:` 总计: `63`
|
||||
- Python 文件: `147`
|
||||
- 代码总行数: `58,341`
|
||||
- `except Exception` / `except:` 总计: `949`
|
||||
- 裸 `except:` 总计: `62`
|
||||
- 旧上下文关键词引用总量:
|
||||
- `interface_manager`: `35`
|
||||
- `treeWidget`: `10`
|
||||
- `gui_manager`: `77`
|
||||
- `interface_manager`: `20`
|
||||
- `treeWidget`: `2`
|
||||
- `gui_manager`: `90`
|
||||
|
||||
结论:
|
||||
|
||||
@ -71,40 +75,39 @@
|
||||
|
||||
## 2. 热点文件(按规模/风险)
|
||||
|
||||
### 2.1 超大文件 Top
|
||||
### 2.1 超大文件 Top(非 VR)
|
||||
|
||||
1. `core/vr_manager.py` (`3553` 行)
|
||||
2. `core/selection.py` (`2942` 行)
|
||||
3. `core/InfoPanelManager.py` (`1726` 行)
|
||||
4. `ui/LUI/lui_manager_editor.py` (`1724` 行)
|
||||
5. `ui/panels/property_helpers.py` (`1711` 行)
|
||||
6. `ui/LUI/lui_function_properties.py` (`1707` 行)
|
||||
7. `TransformGizmo/rotate_gizmo.py` (`1587` 行)
|
||||
8. `ui/panels/animation_tools.py` (`1579` 行)
|
||||
1. `core/selection.py` (`2941` 行)
|
||||
2. `core/InfoPanelManager.py` (`1725` 行)
|
||||
3. `ui/LUI/lui_manager_editor.py` (`1724` 行)
|
||||
4. `ui/panels/property_helpers.py` (`1711` 行)
|
||||
5. `ui/LUI/lui_function_properties.py` (`1707` 行)
|
||||
6. `TransformGizmo/rotate_gizmo.py` (`1587` 行)
|
||||
7. `ui/panels/animation_tools.py` (`1579` 行)
|
||||
8. `templates/main_template.py` (`1571` 行)
|
||||
|
||||
### 2.2 长函数 Top(优先拆分,非 VR 当前状态)
|
||||
|
||||
1. `ui/LUI/lui_function_properties.py::_draw_component_properties` (`1441` 行)
|
||||
2. `ui/panels/animation_tools.py::_getActor` (`510` 行)
|
||||
3. `ui/LUI/lui_manager_interaction.py::_update_drag` (`348` 行)
|
||||
4. `ui/panels/editor_panels_left.py::_draw_resource_manager` (`310` 行)
|
||||
5. `core/selection.py::updateGizmoDrag` (`278` 行)
|
||||
6. `ui/LUI/lui_manager_interaction.py::_handle_resize_drag` (`257` 行)
|
||||
7. `scene/scene_manager_io_mixin.py::saveScene` (`222` 行)
|
||||
8. `TransformGizmo/move_gizmo.py::_on_mouse_down` (`222` 行)
|
||||
2. `ui/LUI/lui_manager_interaction.py::_update_drag` (`348` 行)
|
||||
3. `ui/panels/editor_panels_left.py::_draw_resource_manager` (`310` 行)
|
||||
4. `core/selection.py::updateGizmoDrag` (`278` 行)
|
||||
5. `ui/LUI/lui_manager_interaction.py::_handle_resize_drag` (`257` 行)
|
||||
6. `ui/LUI/lui_manager_editor.py::_set_parent_child_relationship` (`234` 行)
|
||||
7. `ui/panels/editor_panels_top.py::draw_menu_bar` (`227` 行)
|
||||
8. `scene/scene_manager_io_mixin.py::saveScene` (`222` 行)
|
||||
|
||||
### 2.3 异常处理密度高(可观测性风险)
|
||||
|
||||
1. `ui/panels/animation_tools.py` (`except* = 85`)
|
||||
2. `core/vr_manager.py` (`71`)
|
||||
3. `core/selection.py` (`57`)
|
||||
4. `ui/panels/property_helpers.py` (`54`)
|
||||
5. `ui/panels/app_actions.py` (`46`)
|
||||
6. `scene/scene_manager_model_mixin.py` (`36`)
|
||||
7. `scene/scene_manager_serialization_mixin.py` (`27`)
|
||||
8. `scene/scene_manager_io_mixin.py` (`20`)
|
||||
9. `project/project_manager.py` (`20`)
|
||||
10. `ui/panels/runtime_actions.py` (`20`)
|
||||
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:` 集中区:
|
||||
|
||||
@ -113,36 +116,32 @@
|
||||
- `ui/panels/editor_panels_right_material.py` (`6`)
|
||||
- `ui/panels/editor_panels_left.py` (`5`)
|
||||
|
||||
### 2.4 旧上下文耦合集中区(历史基线,Task A 前)
|
||||
### 2.4 旧上下文耦合集中区(当前分布,非 VR)
|
||||
|
||||
1. `ui/panels/runtime_actions.py` (`gui_manager=29`)
|
||||
2. `core/event_handler.py` (`interface_manager=11`, `gui_manager=11`)
|
||||
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` (`interface_manager=6`, `treeWidget=2`, `gui_manager=5`)
|
||||
5. `core/selection.py` (`interface_manager=4`, `treeWidget=1`)
|
||||
6. `core/InfoPanelManager.py` (`interface_manager=4`, `treeWidget=1`)
|
||||
7. `core/terrain_manager.py` (`interface_manager=3`, `treeWidget=2`)
|
||||
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: 上下文收敛(先做)
|
||||
## P0: 上下文收敛(已完成主干,继续收尾)
|
||||
|
||||
目标: 统一 GUI/场景树访问边界,减少跨模块 `hasattr(..., 'interface_manager')` 与 `treeWidget` 语义残留。
|
||||
|
||||
建议动作:
|
||||
当前状态:
|
||||
|
||||
1. 引入 `EditorContext`(或 `UIContext`)统一提供:
|
||||
- `get_tree_adapter()`
|
||||
- `get_gui_service()`
|
||||
- `get_selection_service()`
|
||||
2. 在以下文件先改为调用上下文接口:
|
||||
- `core/event_handler.py`
|
||||
- `core/selection.py`
|
||||
- `core/InfoPanelManager.py`
|
||||
- `core/terrain_manager.py`
|
||||
- `scene/scene_manager_serialization_mixin.py`
|
||||
- `scene/scene_manager_convert_tiles_mixin.py`
|
||||
- `ui/panels/runtime_actions.py`
|
||||
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. 文档层明确“兼容字段仅保留读,不再新增写路径”。
|
||||
|
||||
预期收益:
|
||||
|
||||
@ -153,13 +152,12 @@
|
||||
|
||||
目标: 将核心长函数拆分成“流程编排 + 子步骤函数”,减少单函数认知负担。
|
||||
|
||||
建议拆分顺序:
|
||||
建议拆分顺序(非 VR 当前阶段):
|
||||
|
||||
1. `scene/scene_manager_io_mixin.py::loadScene`
|
||||
2. `main.py::__init__`
|
||||
3. `ui/panels/animation_tools.py::_getActor`
|
||||
4. `core/selection.py::updateGizmoDrag`
|
||||
5. `ui/LUI/lui_function_properties.py::_draw_component_properties`(可按“变换/布局/视觉/交互/脚本”分区)
|
||||
1. `core/selection.py::updateGizmoDrag`
|
||||
2. `ui/panels/editor_panels_left.py::_draw_resource_manager`
|
||||
3. `ui/LUI/lui_manager_interaction.py::_update_drag`
|
||||
4. `ui/LUI/lui_function_properties.py::_draw_component_properties`(可按“变换/布局/视觉/交互/脚本”分区)
|
||||
|
||||
预期收益:
|
||||
|
||||
@ -180,20 +178,19 @@
|
||||
优先文件:
|
||||
|
||||
- `ui/panels/animation_tools.py`
|
||||
- `core/vr_manager.py`
|
||||
- `core/selection.py`
|
||||
- `ui/panels/property_helpers.py`
|
||||
- `scene/scene_manager_io_mixin.py`
|
||||
|
||||
## 4. 下一步可直接执行的任务包
|
||||
|
||||
### Task A(推荐先做,1-2 天)
|
||||
### Task A(已完成)
|
||||
|
||||
- 建立 `core/editor_context.py`(或同级命名)
|
||||
- 给 `event_handler/selection/InfoPanelManager/runtime_actions` 接入上下文
|
||||
- 保持外部 API 不变,仅替换内部访问路径
|
||||
|
||||
### Task B(1 天)
|
||||
### Task B(已完成)
|
||||
|
||||
- 重构 `scene_manager_io_mixin.loadScene`:
|
||||
- `preflight`
|
||||
@ -202,11 +199,36 @@
|
||||
- `rebuild_scene_tree`
|
||||
- `post_load_sync`
|
||||
|
||||
### Task C(1 天)
|
||||
### 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(并行候选,1 天)
|
||||
|
||||
- 拆分 `core/selection.py::updateGizmoDrag`:
|
||||
- `_apply_scale_drag`
|
||||
- `_apply_rotation_drag`
|
||||
- `_compute_axis_projected_distance`
|
||||
- `_apply_translation_drag`
|
||||
- 验收:
|
||||
- 主函数长度降到 `<= 120` 行
|
||||
- 保持当前 gizmo 拖拽行为一致
|
||||
|
||||
## 4.1 本轮深入分析(非 VR,P1 准备)
|
||||
|
||||
### A) `scene/scene_manager_io_mixin.py::loadScene`(核心优先,已完成)
|
||||
@ -232,7 +254,7 @@
|
||||
- 失败重试逻辑保持语义一致。
|
||||
- 当前状态(2026-02-28):
|
||||
- 第二轮已完成(`loadScene` 已达验收目标:`74` 行)。
|
||||
- 下一步应转向 `main.py::__init__` 与 `ui/panels/animation_tools.py::_getActor`。
|
||||
- 后续建议转入回归观察,优先推进 `core/selection.py::updateGizmoDrag`。
|
||||
|
||||
### B) `main.py::__init__`(第二优先,已完成第一轮)
|
||||
|
||||
@ -242,36 +264,64 @@
|
||||
- 约束:
|
||||
- 后续仅建议做微调,不再进行结构性拆分。
|
||||
|
||||
### C) `ui/panels/animation_tools.py::_getActor`(第三优先)
|
||||
### C) `ui/panels/animation_tools.py::_getActor`(已完成第一轮)
|
||||
|
||||
- 函数规模: `510` 行
|
||||
- 近似圈复杂度: `143`
|
||||
- 函数规模: `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`(第四优先)
|
||||
|
||||
- 函数规模: `278` 行
|
||||
- 近似圈复杂度: `65`
|
||||
- 关键问题:
|
||||
- 路径推断、缓存策略、Actor 构建、autoBind 回退、GLTF 特化混在一个函数中。
|
||||
- 局部嵌套函数层级深,行为分支很难覆盖测试。
|
||||
- 缩放/旋转/平移三套逻辑混在同一函数,分支切换成本高。
|
||||
- 投影计算、坐标变换、UI 刷新混杂,异常边界不清晰。
|
||||
- 建议拆分:
|
||||
- `_resolve_actor_owner_and_paths(origin_model)`
|
||||
- `_load_actor_from_candidate_paths(owner_model, paths)`
|
||||
- `_load_actor_via_memory_fallback(owner_model, origin_model)`
|
||||
- `_load_actor_via_gltf_special(path)`
|
||||
- `_validate_actor_playable(actor_or_proxy)`
|
||||
- `_cache_actor(owner_model, actor)`
|
||||
- 验收标准:
|
||||
- `_getActor` 保留为流程入口,长度控制到 `150` 行以内。
|
||||
- 保持当前“优先路径加载,失败回退内存/autoBind”的策略不变。
|
||||
- `_validate_gizmo_drag_state()`
|
||||
- `_drag_scale(mouseDeltaX, mouseDeltaY)`
|
||||
- `_drag_rotate(mouseDeltaX, mouseDeltaY)`
|
||||
- `_project_mouse_to_axis_distance(mouseX, mouseY)`
|
||||
- `_drag_translate(projected_distance)`
|
||||
|
||||
## 4.2 Task B 执行顺序建议(非 VR)
|
||||
### E) `ui/panels/editor_panels_left.py::_draw_resource_manager`(第五优先)
|
||||
|
||||
- 函数规模: `310` 行
|
||||
- 近似圈复杂度: `88`
|
||||
- 关键问题:
|
||||
- 目录树与文件列表 UI 逻辑高度重复(图标、选择、多选、双击、右键菜单)。
|
||||
- 文件/目录条目处理缺少统一的 item 渲染与交互抽象。
|
||||
- 建议拆分:
|
||||
- `_draw_resource_toolbar(rm)`
|
||||
- `_draw_resource_path_and_search(rm)`
|
||||
- `_draw_directory_entry(rm, path, depth)`
|
||||
- `_draw_file_entry(rm, path, depth)`
|
||||
- `_draw_resource_context_popup(rm)`
|
||||
|
||||
## 4.2 执行顺序建议(非 VR)
|
||||
|
||||
1. `loadScene` 已完成两轮拆分(当前建议冻结并转入回归观察)。
|
||||
2. `main.__init__` 已完成第一轮拆分(建议转入回归观察)。
|
||||
3. 下一步处理 `_getActor`(风险最高,建议独立提交并做手工回归)。
|
||||
3. 下一步处理 `updateGizmoDrag`(输入与变换逻辑解耦)。
|
||||
4. 然后处理 `_draw_resource_manager`(提炼重复 UI 渲染分支)。
|
||||
5. 最后处理 `_update_drag`(LUI 交互链路收敛)。
|
||||
|
||||
## 5. 与现有文档关系
|
||||
|
||||
- 模块总索引: `PROJECT_MODULE_INDEX.md`
|
||||
- Qt 迁移状态: `QT_TO_IMGUI_MIGRATION_CHECKLIST.md`
|
||||
- 项目任务清单: `PROJECT_TASK_CHECKLIST.md`
|
||||
- 历史分析: `IMGUI_MODULE_ANALYSIS.md`
|
||||
|
||||
---
|
||||
|
||||
如果按此路线继续,建议下一轮直接从 **Task A** 开始,我可以先落地上下文适配层并改 4 个高耦合文件。
|
||||
如果按此路线继续,建议下一轮直接从 **Task E** 开始,我可以先落地 `updateGizmoDrag` 的第一轮拆分并保持行为不变。
|
||||
|
||||
93
PROJECT_TASK_CHECKLIST.md
Normal file
93
PROJECT_TASK_CHECKLIST.md
Normal file
@ -0,0 +1,93 @@
|
||||
# 项目任务清单(Project Task Checklist)
|
||||
|
||||
更新时间:2026-02-28
|
||||
适用范围:`d:\IMGUI\EG` 本地工作区(不含远程仓库内容)
|
||||
|
||||
## 目标
|
||||
|
||||
- 保持编辑器主路径稳定运行在 ImGui + Panda3D。
|
||||
- 持续收敛历史兼容耦合(`interface_manager/treeWidget/gui_manager`)。
|
||||
- 基于可执行任务包推进非 VR 优化重构。
|
||||
|
||||
## 当前状态总览
|
||||
|
||||
- Qt 运行依赖主路径已清理完成(排除 `RenderPipelineFile`)。
|
||||
- 项目已进入“非 VR 结构优化阶段”。
|
||||
- `_getActor` 第一轮拆分已完成,当前最高优先任务转为 `updateGizmoDrag` 拆分。
|
||||
|
||||
## 已完成(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 兜底
|
||||
|
||||
## 下一步(Next)
|
||||
|
||||
### N1 `updateGizmoDrag` 拆分(最高优先级,非 VR)
|
||||
|
||||
- 目标文件:`core/selection.py`
|
||||
- 当前规模:`278` 行
|
||||
- 验收目标:
|
||||
- 主函数压缩到 `<= 120` 行
|
||||
- 保持 gizmo 拖拽行为一致
|
||||
|
||||
### N2 `资源管理器面板` 拆分(高优先级,非 VR)
|
||||
|
||||
- 目标文件:`ui/panels/editor_panels_left.py::_draw_resource_manager`
|
||||
- 当前规模:`310` 行
|
||||
- 验收目标:
|
||||
- 提取目录/文件项渲染与右键菜单复用逻辑
|
||||
- 交互行为保持一致
|
||||
|
||||
## 验收标准(阶段)
|
||||
|
||||
- 不安装 PyQt/PySide 时,`python main.py` 可进入编辑器主界面。
|
||||
- 非 VR 主流程(导入/选择/变换/保存/加载)无 Qt 残余异常。
|
||||
- 每轮重构后可通过最小回归(启动、导入、播放动画、保存加载)。
|
||||
|
||||
## 关联文档
|
||||
|
||||
- 优化分析:`PROJECT_OPTIMIZATION_ANALYSIS.md`
|
||||
- 模块索引:`PROJECT_MODULE_INDEX.md`
|
||||
- 全量目录:`PROJECT_FULL_CATALOG.md`
|
||||
- 历史分析:`IMGUI_MODULE_ANALYSIS.md`
|
||||
@ -1,62 +0,0 @@
|
||||
# Qt -> ImGui 迁移清单(Qt 清理后状态)
|
||||
|
||||
更新时间:2026-02-28
|
||||
适用范围:`d:\IMGUI\EG` 本地工作区
|
||||
|
||||
## 目标
|
||||
|
||||
- 编辑器主路径稳定运行在 ImGui + Panda3D。
|
||||
- 项目主代码不再依赖 PyQt/PySide 运行。
|
||||
- 后续把历史命名与耦合(`interface_manager/treeWidget/gui_manager`)继续收敛。
|
||||
|
||||
## 当前状态总览
|
||||
|
||||
- `PyQt/PySide/Qt` 在项目代码主路径(排除 `RenderPipelineFile`)扫描结果:`0`。
|
||||
- 依赖清单已从 Qt 运行依赖切到 ImGui 运行依赖。
|
||||
- `ui/widgets.py` 已改为 legacy 占位模块,避免误用 Qt 路径。
|
||||
|
||||
## 已完成(Done)
|
||||
|
||||
- [x] `core/InfoPanelManager.py`:去除 Qt 直接导入。
|
||||
- [x] `core/selection.py`:光标逻辑改为 Panda3D 路径。
|
||||
- [x] `scene/scene_manager_convert_tiles_mixin.py`:去掉 `QProgressDialog`,改为非 Qt 进度反馈。
|
||||
- [x] `ui/widgets.py`:替换为 ImGui 迁移提示占位模块。
|
||||
- [x] `ui/icon_manager.py`:替换为无 Qt 的兼容层实现。
|
||||
- [x] `core/world.py` / `core/vr_manager.py` / `core/vr/testing/test_mode.py`:去除 `qtWidget` 语义依赖。
|
||||
- [x] `requirements/*`:清理 Qt/PySide 依赖项并同步部署文档。
|
||||
- [x] `main.py`:补齐兼容字段初始化(`gui_elements/gui_manager/interface_manager/guiEditMode/currentGUITool`)。
|
||||
- [x] `scene/scene_manager_io_mixin.py`:修复 `_find_scrip_in_directory` 拼写错误。
|
||||
- [x] `main.py`:入口改为 `if __name__ == "__main__":`,消除导入即运行副作用。
|
||||
- [x] `core/event_handler.py`:新增场景树访问 helper,替换 `interface_manager.treeWidget` 直连。
|
||||
- [x] `core/selection.py`:统一树控件清空逻辑(`_get_tree_widget/_clear_tree_selection`)。
|
||||
- [x] `ui/panels/runtime_actions.py`:统一 `gui_manager` 访问与 `gui_elements` 追加入口。
|
||||
- [x] `core/InfoPanelManager.py`:统一场景树控件访问 helper。
|
||||
- [x] `ui/panels/editor_panels.py`:按布局拆分为 4 个 mixin(`top/left/right/center`),保留门面类组合。
|
||||
|
||||
## 待完成(Next)
|
||||
|
||||
### N1 场景树接口收敛(高优先级)
|
||||
|
||||
- 目标:移除 `treeWidget` 语义,统一走 ImGui 场景树接口。
|
||||
- 影响路径:`core/event_handler.py`、`scene/scene_manager_*_mixin.py`、`core/terrain_manager.py` 等。
|
||||
|
||||
### N2 GUI 管理上下文收敛(高优先级)
|
||||
|
||||
- 目标:将 `interface_manager/gui_manager/gui_elements` 统一到单一上下文对象。
|
||||
- 影响路径:`project/project_manager.py`、`scene/scene_manager_io_mixin.py`、`ui/panels/*`。
|
||||
|
||||
### N3 入口规范化与回归脚本(中优先级)
|
||||
|
||||
- 目标:减少脚本式副作用,建立最小回归检查。
|
||||
- 建议覆盖:启动、导入模型、选择变换、保存加载、脚本挂载。
|
||||
|
||||
## 验收标准
|
||||
|
||||
- 不安装 PyQt/PySide 时,`python main.py` 能进入编辑器主界面。
|
||||
- 场景导入/保存/加载流程不触发 Qt 相关异常。
|
||||
- 关键交互(选择、变换、场景树)行为与清理前一致。
|
||||
|
||||
## 关联文档
|
||||
|
||||
- 模块分析: [IMGUI_MODULE_ANALYSIS.md](d:/IMGUI/EG/IMGUI_MODULE_ANALYSIS.md)
|
||||
- 全量目录与脚本清单: [PROJECT_FULL_CATALOG.md](d:/IMGUI/EG/PROJECT_FULL_CATALOG.md)
|
||||
@ -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,217 +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:
|
||||
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)
|
||||
# 无论是否已检测到动画名,都显式绑定一次,避免“有名字但无可播放控制”
|
||||
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
|
||||
|
||||
@ -920,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")
|
||||
@ -934,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:
|
||||
@ -1095,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:
|
||||
@ -1120,49 +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:
|
||||
model_source = p
|
||||
if isinstance(p, (str, os.PathLike)):
|
||||
model_source = Filename.from_os_specific(os.fspath(p))
|
||||
loaded_model = self.loader.loadModel(model_source)
|
||||
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):
|
||||
"""获取模型格式信息"""
|
||||
|
||||
Loading…
Reference in New Issue
Block a user