refactor: 拆分loadScene流程并更新优化分析
This commit is contained in:
parent
e2544ea440
commit
4f2c545b88
@ -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`
|
||||
|
||||
@ -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:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user