From 280d6f8b2a19a842457497fc7bdcaeb5424f7380 Mon Sep 17 00:00:00 2001 From: ayuan9957 <107920784+ayuan9957@users.noreply.github.com> Date: Sun, 1 Mar 2026 11:48:38 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E5=AE=8C=E6=88=90=5FgetActor?= =?UTF-8?q?=E9=A6=96=E8=BD=AE=E6=8B=86=E5=88=86=E5=B9=B6=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E9=A1=B9=E7=9B=AE=E4=BB=BB=E5=8A=A1=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PROJECT_FULL_CATALOG.md | 2 +- PROJECT_MODULE_INDEX.md | 2 +- PROJECT_OPTIMIZATION_ANALYSIS.md | 216 +++++---- PROJECT_TASK_CHECKLIST.md | 93 ++++ QT_TO_IMGUI_MIGRATION_CHECKLIST.md | 62 --- ui/panels/animation_tools.py | 729 ++++++++++++++--------------- 6 files changed, 584 insertions(+), 520 deletions(-) create mode 100644 PROJECT_TASK_CHECKLIST.md delete mode 100644 QT_TO_IMGUI_MIGRATION_CHECKLIST.md diff --git a/PROJECT_FULL_CATALOG.md b/PROJECT_FULL_CATALOG.md index af962b5b..c938b374 100644 --- a/PROJECT_FULL_CATALOG.md +++ b/PROJECT_FULL_CATALOG.md @@ -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 diff --git a/PROJECT_MODULE_INDEX.md b/PROJECT_MODULE_INDEX.md index b658fce4..b3df0874 100644 --- a/PROJECT_MODULE_INDEX.md +++ b/PROJECT_MODULE_INDEX.md @@ -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) diff --git a/PROJECT_OPTIMIZATION_ANALYSIS.md b/PROJECT_OPTIMIZATION_ANALYSIS.md index d0bd3f17..436f0cea 100644 --- a/PROJECT_OPTIMIZATION_ANALYSIS.md +++ b/PROJECT_OPTIMIZATION_ANALYSIS.md @@ -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` 的第一轮拆分并保持行为不变。 diff --git a/PROJECT_TASK_CHECKLIST.md b/PROJECT_TASK_CHECKLIST.md new file mode 100644 index 00000000..6658498e --- /dev/null +++ b/PROJECT_TASK_CHECKLIST.md @@ -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` diff --git a/QT_TO_IMGUI_MIGRATION_CHECKLIST.md b/QT_TO_IMGUI_MIGRATION_CHECKLIST.md deleted file mode 100644 index a364c9f5..00000000 --- a/QT_TO_IMGUI_MIGRATION_CHECKLIST.md +++ /dev/null @@ -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) diff --git a/ui/panels/animation_tools.py b/ui/panels/animation_tools.py index 01300dc8..e4e1b159 100644 --- a/ui/panels/animation_tools.py +++ b/ui/panels/animation_tools.py @@ -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): """获取模型格式信息"""