diff --git a/PROJECT_OPTIMIZATION_ANALYSIS.md b/PROJECT_OPTIMIZATION_ANALYSIS.md index e1695852..ed7a7cfe 100644 --- a/PROJECT_OPTIMIZATION_ANALYSIS.md +++ b/PROJECT_OPTIMIZATION_ANALYSIS.md @@ -18,7 +18,20 @@ - `scene/scene_manager_serialization_mixin.py` - `scene/scene_manager_model_mixin.py` - 效果(本地静态检索): - - 直接访问 `world.interface_manager` / `interface_manager.treeWidget` 已基本移除(仅剩注释或非本轮范围点位)。 + - 直接访问 `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` + - `_cleanup_after_failed_load` + - `_clear_current_scene_for_load` + - `_load_scene_root_from_file` + - `_bootstrap_scene_tree_for_loaded_root` + - `_load_scene_gui_metadata` + - `_retry_load_scene` + - `loadScene` 行数:`556 -> 366` + - 已清理重复异常分支:`_rebuildParentChildRelationships` 内重复 `except` 已移除 ## 1. 总体画像 @@ -82,7 +95,7 @@ - `ui/panels/editor_panels_right_material.py` (`6`) - `ui/panels/editor_panels_left.py` (`5`) -### 2.4 旧上下文耦合集中区 +### 2.4 旧上下文耦合集中区(历史基线,Task A 前) 1. `ui/panels/runtime_actions.py` (`gui_manager=29`) 2. `core/event_handler.py` (`interface_manager=11`, `gui_manager=11`) @@ -176,6 +189,76 @@ - 统一异常日志工具(轻量封装) - 首批替换 `animation_tools.py` 与 `property_helpers.py` +## 4.1 本轮深入分析(非 VR,P1 准备) + +### A) `scene/scene_manager_io_mixin.py::loadScene`(核心优先) + +- 函数规模: `556` 行 +- 近似圈复杂度: `114` +- 关键问题: + - 单函数同时承担 8 类职责(校验/清理/加载/树同步/节点递归处理/脚本恢复/材质恢复/重试)。 + - 内嵌 `processNode` 递归函数长达 `281` 行,可测试性差。 + - 调试输出密度高(`print` 与注释 `print` 很多),影响可读性和噪声控制。 + - `scene/scene_manager_io_mixin.py:1026` 与 `scene/scene_manager_io_mixin.py:1032` 存在重复 `except Exception` 分支(可合并)。 + - `scene/scene_manager_io_mixin.py:946` 的 GUI 重建入口目前仍注释,行为边界不清晰。 +- 建议拆分(保持外部 API `loadScene` 不变): + - `_preflight_scene_file(filename) -> (ok, normalized_path, reason)` + - `_cleanup_before_load(tree_widget, retry_count)` + - `_load_bam_scene(filename) -> scene_or_none` + - `_bootstrap_tree_items(scene, tree_widget)` + - `_walk_loaded_scene(scene, tree_widget) -> loaded_nodes` + - `_restore_loaded_nodes_state(node_path, processed_lights, loaded_nodes)`(从 `processNode` 中抽) + - `_post_load_finalize(scene, loaded_nodes, filename)` + - `_retry_load_scene(filename, retry_count, error) -> bool` +- 验收标准: + - `loadScene` 主体压缩到 `120` 行以内,只保留流程编排。 + - 节点恢复行为(位置/材质/脚本/可见性)与当前一致。 + - 失败重试逻辑保持语义一致。 +- 当前状态(2026-02-28): + - 第一轮已完成(预检/清理/加载/树初始化/GUI元数据/重试)。 + - 剩余主要体积来自内嵌 `processNode`,下一轮应继续提取为独立方法。 + +### B) `main.py::__init__`(第二优先) + +- 函数规模: `375` 行 +- 近似圈复杂度: `15` +- 问题性质: + - 复杂度不高,但“启动装配职责”过于集中,初始化顺序风险高。 +- 建议拆分: + - `_init_legacy_compat_fields()` + - `_init_core_services_non_vr()`(不含 VR) + - `_init_imgui_runtime()` + - `_init_panel_modules()` + - `_init_runtime_state_flags()` + - `_bind_input_shortcuts()` + - `_init_drag_drop_and_messages()` +- 约束: + - VR 初始化保持原样,不纳入本轮改造范围。 + +### C) `ui/panels/animation_tools.py::_getActor`(第三优先) + +- 函数规模: `510` 行 +- 近似圈复杂度: `143` +- 关键问题: + - 路径推断、缓存策略、Actor 构建、autoBind 回退、GLTF 特化混在一个函数中。 + - 局部嵌套函数层级深,行为分支很难覆盖测试。 +- 建议拆分: + - `_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”的策略不变。 + +## 4.2 Task B 执行顺序建议(非 VR) + +1. 先拆 `loadScene`(收益最大,且与 VR 无关)。 +2. 再整理 `main.__init__`(降低后续模块接入冲突)。 +3. 最后处理 `_getActor`(风险最高,建议独立提交并做手工回归)。 + ## 5. 与现有文档关系 - 模块总索引: `PROJECT_MODULE_INDEX.md` diff --git a/scene/scene_manager_io_mixin.py b/scene/scene_manager_io_mixin.py index 7ae30dae..4b0572c0 100644 --- a/scene/scene_manager_io_mixin.py +++ b/scene/scene_manager_io_mixin.py @@ -453,160 +453,23 @@ class SceneManagerIOMixin: # print(f"[DEBUG] 当前场景中的灯光数量: {len(self.Spotlight) + len(self.Pointlight)}") # print(f"[DEBUG] 当前GUI元素数量: {len(self.world.gui_elements) if hasattr(self.world, 'gui_elements') else 0}") - # 确保文件路径是规范化的 - filename = os.path.normpath(filename) - # print(f"[DEBUG] 规范化后的文件路径: {filename}") - - # 检查文件是否存在 - if not os.path.exists(filename): - print(f"场景文件不存在: {filename}") + filename = self._preflight_load_scene(filename, retry_count) + if not filename: return False - # print(f"[DEBUG] 文件大小: {os.path.getsize(filename)} bytes") - - # 检查文件是否损坏 - if os.path.getsize(filename) == 0: - # print(f"[DEBUG] 错误: 场景文件为空") - return False - - # 预防性清理:确保Panda3D处于稳定状态 - if retry_count > 0: - # print(f"[DEBUG] 执行预防性清理...") - try: - # 清理所有可能的残留状态 - from panda3d.core import ModelPool - ModelPool.releaseAllModels() - - # 强制垃圾回收 - import gc - gc.collect() - - # 清理渲染状态 - if hasattr(self.world, 'render') and self.world.render: - # 清理渲染节点的子节点(保留基本节点) - children_to_remove = [] - for i in range(self.world.render.getNumChildren()): - child = self.world.render.getChild(i) - if child.getName() not in ['camera', 'alight', 'dlight', 'ambient', 'render']: - children_to_remove.append(child) - - for child in children_to_remove: - if not child.isEmpty(): - child.removeNode() - - # print(f"[DEBUG] 清理了 {len(children_to_remove)} 个渲染子节点") - - except Exception as cleanup_error: - # print(f"[DEBUG] 预防性清理时发生异常: {cleanup_error}") - pass - # print(f"[DEBUG] 预加载检查完成,开始正式加载...") tree_widget = self._get_tree_widget() # print(f"[DEBUG] 获取树形控件: {tree_widget is not None}") - # 清除当前场景 print("\n清除当前场景...") - # print(f"[DEBUG] 需要清理的模型数量: {len(self.models)}") - - for i, model in enumerate(self.models): - # print(f"[DEBUG] 清理模型 {i+1}/{len(self.models)}: {model.getName() if model else 'None'}") - if tree_widget: - tree_widget.delete_item(model) - else: - # 直接移除节点 - if not model.isEmpty(): - model.removeNode() - - self.models.clear() - # print(f"[DEBUG] 模型清理完成") - - # 清除灯光 - for light_node in self.Spotlight: - if tree_widget: - tree_widget.delete_item(light_node) - else: - if not light_node.isEmpty(): - light_node.removeNode() - - for light_node in self.Pointlight: - if tree_widget: - tree_widget.delete_item(light_node) - else: - if not light_node.isEmpty(): - light_node.removeNode() - - if hasattr(self.world, 'terrain_manager') and self.world.terrain_manager and hasattr(self.world.terrain_manager, 'terrains'): - for terrain in self.world.terrain_manager.terrains: - if tree_widget: - tree_widget.delete_item(terrain) - else: - if terrain and not terrain.isEmpty(): - terrain.removeNode() - - for light in self.Spotlight: - if not light.isEmpty(): - light.removeNode() - self.Spotlight.clear() - - for light in self.Pointlight: - if not light.isEmpty(): - light.removeNode() - self.Pointlight.clear() - - for gui in self.world.gui_elements: - if not gui.isEmpty(): - gui.removeNode() - self.world.gui_elements.clear() - - if hasattr(self.world,'info_panel_manager'): - self.world.info_panel_manager.removeAllPanels() - - # 清理可能存在的辅助节点 - self._cleanupAuxiliaryNodes() + self._clear_current_scene_for_load(tree_widget) # 加载场景 # print(f"[DEBUG] 开始加载BAM文件...") - # 安全清理措施 - try: - # 1. 清理Panda3D模型缓存 - from panda3d.core import ModelPool - ModelPool.releaseAllModels() - # print(f"[DEBUG] 模型缓存已清理") - - # 2. 强制垃圾回收 - import gc - gc.collect() - # print(f"[DEBUG] 垃圾回收完成") - - # 3. 检查文件状态 - if not os.access(filename, os.R_OK): - # print(f"[DEBUG] 文件不可读: {filename}") - return False - - # 4. 使用更安全的加载方式 - # print(f"[DEBUG] 尝试加载BAM文件...") - panda_filename = Filename.fromOsSpecific(filename) - # print(f"[DEBUG] Panda3D文件名: {panda_filename}") - - # 设置加载选项 - from panda3d.core import LoaderOptions - loader_options = LoaderOptions() - loader_options.setFlags(loader_options.LFNoCache) # 禁用缓存 - - scene = self.world.loader.loadModel(panda_filename, loader_options) - - if not scene: - print("场景加载失败") - return False - - # print(f"[DEBUG] BAM文件加载成功") - - except Exception as e: - # print(f"[DEBUG] BAM加载过程中发生异常: {e}") - import traceback - traceback.print_exc() + scene = self._load_scene_root_from_file(filename) + if not scene: return False # print(f"[DEBUG] BAM文件加载成功,场景根节点: {scene.getName()}") @@ -621,18 +484,7 @@ class SceneManagerIOMixin: # print(f"[DEBUG] 将场景节点临时挂载到render") scene.reparentTo(self.world.render) - if tree_widget: - # print(f"[DEBUG] 创建模型项...") - try: - tree_widget.create_model_items(scene) - # print(f"[DEBUG] 模型项创建完成") - except Exception as e: - # print(f"[DEBUG] 创建模型项失败: {e}") - # 继续执行,不影响场景加载 - pass - else: - # print(f"[DEBUG] 树形控件为空,跳过创建模型项") - pass + self._bootstrap_scene_tree_for_loaded_root(tree_widget, scene) # 遍历场景中的所有模型节点 # 用于存储处理后的灯光节点,避免重复处理 processed_lights = [] @@ -931,30 +783,7 @@ class SceneManagerIOMixin: print("\n开始重建父子关系...") self._rebuildParentChildRelationships(loaded_nodes) - # 加载GUI信息并重新创建非3D的GUI元素 - gui_info_file = filename.replace('.bam', '_gui.json') - if os.path.exists(gui_info_file): - try: - with open(gui_info_file, 'r', encoding='utf-8') as f: - content = f.read().strip() - if content: # 检查文件是否为空 - import json - gui_data = json.loads(content) - print(f"✓ 成功加载GUI信息文件: {gui_info_file}") - print(f" 发现 {len(gui_data)} 个GUI元素需要重建") - - # 使用gui_manager重新创建GUI元素 - #self._recreateGUIElementsFromData(gui_data) - else: - print("ℹ️ GUI信息文件为空") - except json.JSONDecodeError as e: - print(f"✗ GUI信息文件格式错误: {e}") - except Exception as e: - print(f"✗ 加载GUI信息失败: {e}") - import traceback - traceback.print_exc() - else: - print("ℹ️ 未找到GUI信息文件") + self._load_scene_gui_metadata(filename) # 移除临时场景节点 if not scene.isEmpty(): @@ -979,25 +808,171 @@ class SceneManagerIOMixin: print(f"加载场景时发生错误: {str(e)}") import traceback traceback.print_exc() - - # 重试机制 - if retry_count < 3: - print(f"[DEBUG] 准备重试... ({retry_count + 1}/{3})") - try: - # 清理状态 - import gc - gc.collect() - from panda3d.core import ModelPool - ModelPool.releaseAllModels() - - # 等待一小段时间后重试 - import time - time.sleep(0.5) - - return self.loadScene(filename, retry_count + 1) - except Exception as retry_error: - print(f"[DEBUG] 重试失败: {retry_error}") - + return self._retry_load_scene(filename, retry_count, max_retries) + + def _preflight_load_scene(self, filename, retry_count): + """预检加载参数并在重试前做预清理。""" + filename = os.path.normpath(filename) + + if not os.path.exists(filename): + print(f"场景文件不存在: {filename}") + return None + + if os.path.getsize(filename) == 0: + print(f"场景文件为空: {filename}") + return None + + if retry_count > 0: + self._cleanup_after_failed_load() + + return filename + + def _cleanup_after_failed_load(self): + """重试加载前的稳态清理。""" + try: + from panda3d.core import ModelPool + ModelPool.releaseAllModels() + + import gc + gc.collect() + + if hasattr(self.world, 'render') and self.world.render: + children_to_remove = [] + for i in range(self.world.render.getNumChildren()): + child = self.world.render.getChild(i) + if child.getName() not in ['camera', 'alight', 'dlight', 'ambient', 'render']: + children_to_remove.append(child) + + for child in children_to_remove: + if not child.isEmpty(): + child.removeNode() + except Exception: + pass + + def _clear_current_scene_for_load(self, tree_widget): + """清理当前场景内容,准备加载新场景。""" + for model in self.models: + if tree_widget: + tree_widget.delete_item(model) + elif not model.isEmpty(): + model.removeNode() + self.models.clear() + + for light_node in self.Spotlight: + if tree_widget: + tree_widget.delete_item(light_node) + elif not light_node.isEmpty(): + light_node.removeNode() + + for light_node in self.Pointlight: + if tree_widget: + tree_widget.delete_item(light_node) + elif not light_node.isEmpty(): + light_node.removeNode() + + if hasattr(self.world, 'terrain_manager') and self.world.terrain_manager and hasattr(self.world.terrain_manager, 'terrains'): + for terrain in self.world.terrain_manager.terrains: + if tree_widget: + tree_widget.delete_item(terrain) + elif terrain and not terrain.isEmpty(): + terrain.removeNode() + + for light in self.Spotlight: + if not light.isEmpty(): + light.removeNode() + self.Spotlight.clear() + + for light in self.Pointlight: + if not light.isEmpty(): + light.removeNode() + self.Pointlight.clear() + + for gui in self.world.gui_elements: + if not gui.isEmpty(): + gui.removeNode() + self.world.gui_elements.clear() + + if hasattr(self.world, 'info_panel_manager'): + self.world.info_panel_manager.removeAllPanels() + + self._cleanupAuxiliaryNodes() + + def _load_scene_root_from_file(self, filename): + """从 BAM 文件安全加载场景根节点。""" + try: + from panda3d.core import ModelPool, LoaderOptions + ModelPool.releaseAllModels() + + import gc + gc.collect() + + if not os.access(filename, os.R_OK): + return None + + panda_filename = Filename.fromOsSpecific(filename) + loader_options = LoaderOptions() + loader_options.setFlags(loader_options.LFNoCache) + + scene = self.world.loader.loadModel(panda_filename, loader_options) + if not scene: + print("场景加载失败") + return None + + return scene + except Exception: + import traceback + traceback.print_exc() + return None + + def _bootstrap_scene_tree_for_loaded_root(self, tree_widget, scene): + """在有树控件时创建场景树模型项。""" + if not tree_widget: + return + try: + tree_widget.create_model_items(scene) + except Exception: + pass + + def _load_scene_gui_metadata(self, filename): + """读取 GUI 元数据文件(当前仅加载校验,不执行重建)。""" + gui_info_file = filename.replace('.bam', '_gui.json') + if not os.path.exists(gui_info_file): + print("ℹ️ 未找到GUI信息文件") + return + + try: + with open(gui_info_file, 'r', encoding='utf-8') as f: + content = f.read().strip() + if not content: + print("ℹ️ GUI信息文件为空") + return + gui_data = json.loads(content) + print(f"✓ 成功加载GUI信息文件: {gui_info_file}") + print(f" 发现 {len(gui_data)} 个GUI元素需要重建") + + # 使用gui_manager重新创建GUI元素 + # self._recreateGUIElementsFromData(gui_data) + except json.JSONDecodeError as e: + print(f"✗ GUI信息文件格式错误: {e}") + except Exception as e: + print(f"✗ 加载GUI信息失败: {e}") + import traceback + traceback.print_exc() + + def _retry_load_scene(self, filename, retry_count, max_retries): + """统一场景加载重试逻辑。""" + if retry_count >= max_retries: + return False + print(f"[DEBUG] 准备重试... ({retry_count + 1}/{max_retries})") + try: + import gc + gc.collect() + from panda3d.core import ModelPool + ModelPool.releaseAllModels() + time.sleep(0.5) + return self.loadScene(filename, retry_count + 1) + except Exception as retry_error: + print(f"[DEBUG] 重试失败: {retry_error}") return False def _rebuildParentChildRelationships(self, loaded_nodes): @@ -1028,12 +1003,6 @@ class SceneManagerIOMixin: import traceback traceback.print_exc() - - except Exception as e: - print(f"重建父子关系时出错: {e}") - import traceback - traceback.print_exc() - def _inferParentChildRelationships(self, loaded_nodes): """从场景结构推断父子关系""" try: