refactor: 完成_getActor首轮拆分并更新项目任务文档

This commit is contained in:
ayuan9957 2026-03-01 11:48:38 +08:00
parent d140552779
commit 280d6f8b2a
6 changed files with 584 additions and 520 deletions

View File

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

View File

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

View File

@ -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 B1 天
### Task B已完成
- 重构 `scene_manager_io_mixin.loadScene`
- `preflight`
@ -202,11 +199,36 @@
- `rebuild_scene_tree`
- `post_load_sync`
### Task C1 天)
### 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 本轮深入分析(非 VRP1 准备)
### 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
View 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`

View File

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

View File

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