refactor: 二轮拆分loadScene节点处理流程

This commit is contained in:
ayuan9957 2026-02-28 17:42:09 +08:00
parent 4f2c545b88
commit ffdad6f099
2 changed files with 247 additions and 315 deletions

View File

@ -21,7 +21,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`
- Task B 第轮已落地(`scene_manager_io_mixin.loadScene`
- 已抽出流程 helper
- `_preflight_load_scene`
- `_cleanup_after_failed_load`
@ -30,7 +30,12 @@
- `_bootstrap_scene_tree_for_loaded_root`
- `_load_scene_gui_metadata`
- `_retry_load_scene`
- `loadScene` 行数:`556 -> 366`
- `_walk_loaded_scene`
- `_process_loaded_scene_node`
- `_restore_node_scripts`
- `_parse_vec3_string`
- `_parse_color_string`
- `loadScene` 行数:`556 -> 74`
- 已清理重复异常分支:`_rebuildParentChildRelationships` 内重复 `except` 已移除
## 1. 总体画像
@ -64,16 +69,16 @@
7. `TransformGizmo/rotate_gizmo.py` (`1587` 行)
8. `ui/panels/animation_tools.py` (`1579` 行)
### 2.2 长函数 Top优先拆分
### 2.2 长函数 Top优先拆分,非 VR 当前状态
1. `ui/LUI/lui_function_properties.py::_draw_component_properties` (`1441` 行)
2. `scene/scene_manager_io_mixin.py::loadScene` (`556` 行)
3. `ui/panels/animation_tools.py::_getActor` (`510` 行)
4. `main.py::__init__` (`375` 行)
5. `ui/LUI/lui_manager_interaction.py::_update_drag` (`348` 行)
6. `ui/panels/editor_panels_left.py::_draw_resource_manager` (`310` 行)
7. `scene/scene_manager_io_mixin.py::processNode` (`281` 行)
8. `core/selection.py::updateGizmoDrag` (`278` 行)
2. `ui/panels/animation_tools.py::_getActor` (`510` 行)
3. `main.py::__init__` (`375` 行)
4. `ui/LUI/lui_manager_interaction.py::_update_drag` (`348` 行)
5. `ui/panels/editor_panels_left.py::_draw_resource_manager` (`310` 行)
6. `core/selection.py::updateGizmoDrag` (`278` 行)
7. `ui/LUI/lui_manager_interaction.py::_handle_resize_drag` (`257` 行)
8. `scene/scene_manager_io_mixin.py::saveScene` (`222` 行)
### 2.3 异常处理密度高(可观测性风险)
@ -191,16 +196,14 @@
## 4.1 本轮深入分析(非 VRP1 准备)
### A) `scene/scene_manager_io_mixin.py::loadScene`(核心优先)
### A) `scene/scene_manager_io_mixin.py::loadScene`(核心优先,已完成
- 函数规模: `556` 行
- 近似圈复杂度: `114`
- 函数规模: `74` 行(重构前 `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 重建入口目前仍注释,行为边界不清晰。
- `processNode` 内联逻辑已外提为 `_walk_loaded_scene/_process_loaded_scene_node`
- 重复异常分支已清理。
- 仍存在较多调试输出(`print` 与注释 `print`),后续可做日志分级。
- 建议拆分(保持外部 API `loadScene` 不变):
- `_preflight_scene_file(filename) -> (ok, normalized_path, reason)`
- `_cleanup_before_load(tree_widget, retry_count)`
@ -215,8 +218,8 @@
- 节点恢复行为(位置/材质/脚本/可见性)与当前一致。
- 失败重试逻辑保持语义一致。
- 当前状态2026-02-28:
- 第一轮已完成(预检/清理/加载/树初始化/GUI元数据/重试)。
- 剩余主要体积来自内嵌 `processNode`,下一轮应继续提取为独立方法
- 第二轮已完成(`loadScene` 已达验收目标:`74` 行)。
- 下一步应转向 `main.py::__init__``ui/panels/animation_tools.py::_getActor`
### B) `main.py::__init__`(第二优先)
@ -255,8 +258,8 @@
## 4.2 Task B 执行顺序建议(非 VR
1. 先拆 `loadScene`(收益最大,且与 VR 无关)。
2. 整理 `main.__init__`(降低后续模块接入冲突)。
1. `loadScene` 已完成两轮拆分(当前建议冻结并转入回归观察)。
2. 下一步整理 `main.__init__`(降低后续模块接入冲突)。
3. 最后处理 `_getActor`(风险最高,建议独立提交并做手工回归)。
## 5. 与现有文档关系

View File

@ -485,301 +485,9 @@ class SceneManagerIOMixin:
scene.reparentTo(self.world.render)
self._bootstrap_scene_tree_for_loaded_root(tree_widget, scene)
# 遍历场景中的所有模型节点
# 用于存储处理后的灯光节点,避免重复处理
processed_lights = []
# 用于存储处理后的GUI元素避免重复处理
#存储所有加载的节点,用于后续处理父子关系
loaded_nodes = {} #name->nodePath映射
# 遍历场景中的所有节点
def processNode(nodePath, depth=0):
indent = " " * depth
# print(f"{indent}处理节点: {nodePath.getName()} (类型: {type(nodePath.node()).__name__})")
# 检查节点是否有效
if nodePath.isEmpty():
# print(f"{indent}[DEBUG] 节点为空,跳过处理")
return
#存储节点以便后续处理父子关系
loaded_nodes[nodePath.getName()] = nodePath
if nodePath.getName().startswith('ground'):
print(f"{indent}跳过ground节点: {nodePath.getName()}")
return
# 跳过render节点的递归
if nodePath.getName() == "render" and depth > 0:
print(f"{indent}跳过重复的render节点")
return
# 跳过光源节点
if nodePath.getName() in ["alight", "dlight"]:
print(f"{indent}跳过光源节点: {nodePath.getName()}")
return
# 跳过相机节点
if nodePath.getName() in ["camera", "cam"]:
print(f"{indent}跳过相机节点: {nodePath.getName()}")
return
# 跳过辅助节点
if nodePath.getName().startswith(("gizmo", "selectionBox")):
print(f"{indent}跳过辅助节点: {nodePath.getName()}")
return
if nodePath.getName() in ['SceneRoot'] or \
any(keyword in nodePath.getName() for keyword in ["Skybox", "skybox"]):
print(f"{indent}跳过环境节点:{nodePath.getName()}")
return
# 检查是否是用户创建的场景元素
is_scene_element = (
nodePath.hasTag("is_scene_element") or
nodePath.hasTag("is_model_root") or
nodePath.hasTag("light_type") or
nodePath.hasTag("gui_type") or # 检查gui_type标签
nodePath.hasTag("is_gui_element") or
nodePath.hasTag("saved_gui_type") or
(nodePath.hasTag("element_type") and nodePath.getTag("element_type") == "info_panel")
)
# 特殊处理检查节点名称是否包含GUI相关关键词
is_potential_gui = any(keyword in nodePath.getName().lower() for keyword in
["gui", "button", "label", "entry", "image", "video", "screen", "text"])
if is_scene_element or is_potential_gui:
print(f"{indent}找到场景元素节点: {nodePath.getName()}")
# 如果是潜在的GUI元素但没有标签添加基本标签
if is_potential_gui and not (nodePath.hasTag("gui_type") or nodePath.hasTag("is_gui_element")):
print(f"{indent}为潜在GUI元素添加标签: {nodePath.getName()}")
nodePath.setTag("is_gui_element", "1")
nodePath.setTag("is_scene_element", "1")
# 尝试从名称推断类型
name_lower = nodePath.getName().lower()
if "button" in name_lower:
nodePath.setTag("gui_type", "button")
elif "label" in name_lower:
nodePath.setTag("gui_type", "label")
elif "entry" in name_lower:
nodePath.setTag("gui_type", "entry")
elif "image" in name_lower:
nodePath.setTag("gui_type", "image")
elif "video" in name_lower or "screen" in name_lower:
nodePath.setTag("gui_type", "video_screen")
else:
nodePath.setTag("gui_type", "unknown")
# 清除现有材质状态
nodePath.clearMaterial()
nodePath.clearColor()
# 恢复变换信息
def parseVec3(vec_str):
"""解析向量字符串为Vec3"""
try:
vec_str = vec_str.replace('LVecBase3f', '').replace('LPoint3f', '').strip('()')
x, y, z = map(float, vec_str.split(','))
return Vec3(x, y, z)
except Exception as e:
print(f"解析向量失败: {vec_str}, 错误: {e}")
return Vec3(0, 0, 0)
if nodePath.hasTag("transform_pos"):
pos = parseVec3(nodePath.getTag("transform_pos"))
nodePath.setPos(pos)
print(f"{indent}恢复位置: {pos}")
if nodePath.hasTag("transform_hpr"):
hpr = parseVec3(nodePath.getTag("transform_hpr"))
nodePath.setHpr(hpr)
print(f"{indent}恢复旋转: {hpr}")
if nodePath.hasTag("transform_scale"):
scale = parseVec3(nodePath.getTag("transform_scale"))
nodePath.setScale(scale)
print(f"{indent}恢复缩放: {scale}")
# 恢复可见性状态
user_visible = True
if nodePath.hasTag("user_visible"):
user_visible = nodePath.getTag("user_visible").lower() == "true"
# 设置用户可见性标记
nodePath.setPythonTag("user_visible", user_visible)
# 应用可见性状态
if hasattr(self.world, 'property_panel'):
self.world.property_panel._syncEffectiveVisibility(nodePath)
else:
# 如果没有属性面板,直接应用可见性
if user_visible:
nodePath.show()
else:
nodePath.hide()
if nodePath.hasTag("has_scripts") and nodePath.getTag("has_scripts") == "true":
if hasattr(self.world,'script_manager') and self.world.script_manager:
try:
import json
scripts_info = json.loads(nodePath.getTag("scripts_info"))
print(f"节点 {nodePath.getName()} 需要重新挂载 {len(scripts_info)} 个脚本")
script_manager = self.world.script_manager
for script_info in scripts_info:
script_name = script_info["name"]
script_file = script_info.get("file","")
print(f"尝试重新挂载脚本{script_name}from {script_file}")
if script_name not in script_manager.loader.script_classes:
if script_file and os.path.exists(script_file):
print(f"从文件加载脚本:{script_file}")
loaded_class = script_manager.load_script_from_file(script_file)
if loaded_class is None:
print(f"从文件加载脚本失败{script_file}")
script_path = self._find_script_in_directory(script_name)
if script_path:
print(f"从目录找到脚本并加载{script_path}")
script_manager.load_script_from_file(script_path)
else:
script_path = self._find_script_in_directory(script_name)
if script_path:
print(f"从目录找到脚本并加载: {script_path}")
script_manager.load_script_from_file(script_path)
else:
print(f"找不到脚本文件: {script_name}")
if script_name in script_manager.loader.script_classes:
script_component = script_manager.add_script_to_object(nodePath,script_name)
if script_component:
print(f"成功为 {nodePath.getName()} 添加脚本: {script_name}")
else:
print(f"{nodePath.getName()} 添加脚本失败: {script_name}")
else:
print(f"脚本 {script_name} 不可用,跳过挂载")
except Exception as e:
print(f"重新挂载脚本失败: {e}")
import traceback
traceback.print_exc()
# 恢复材质属性
def parseColor(color_str):
"""解析颜色字符串为Vec4"""
try:
color_str = color_str.replace('LVecBase4f', '').strip('()')
r, g, b, a = map(float, color_str.split(','))
return Vec4(r, g, b, a)
except:
return Vec4(1, 1, 1, 1)
# 创建并恢复材质
material = Material()
material_changed = False
if nodePath.hasTag("material_ambient"):
material.setAmbient(parseColor(nodePath.getTag("material_ambient")))
material_changed = True
if nodePath.hasTag("material_diffuse"):
material.setDiffuse(parseColor(nodePath.getTag("material_diffuse")))
material_changed = True
if nodePath.hasTag("material_specular"):
material.setSpecular(parseColor(nodePath.getTag("material_specular")))
material_changed = True
if nodePath.hasTag("material_emission"):
material.setEmission(parseColor(nodePath.getTag("material_emission")))
material_changed = True
if nodePath.hasTag("material_shininess"):
material.setShininess(float(nodePath.getTag("material_shininess")))
material_changed = True
if nodePath.hasTag("material_basecolor"):
material.setBaseColor(parseColor(nodePath.getTag("material_basecolor")))
material_changed = True
if material_changed:
nodePath.setMaterial(material)
# 恢复颜色属性
if nodePath.hasTag("color"):
nodePath.setColor(parseColor(nodePath.getTag("color")))
# 处理特定类型的节点
if nodePath.hasTag("light_type"):
light_type = nodePath.getTag("light_type")
print(f"{indent}检测到光源类型: {light_type}")
# 检查是否已经处理过这个灯光
if nodePath not in processed_lights:
# 重新创建RP光源对象
if light_type == "spot_light":
self._recreateSpotLight(nodePath)
elif light_type == "point_light":
self._recreatePointLight(nodePath)
# 标记为已处理
processed_lights.append(nodePath)
elif nodePath.hasTag("element_type"):
element_type = nodePath.getTag("element_type")
if element_type == "cesium_tileset":
tileset_url = nodePath.getTag("saved_tileset_url") if nodePath.hasTag(
"saved_tileset_url") else ""
tileset_info = {
'url': tileset_url,
'node': nodePath,
'position': nodePath.getPos(),
'tiles': {}
}
# 将节点重新挂载到render下如果需要
# 注意GUI元素可能需要挂载到特定的父节点上
if nodePath.hasTag("gui_type") or nodePath.hasTag("is_gui_element"):
# GUI元素通常应该挂载到aspect2d或特定的GUI父节点上
# 这里我们先保持原挂载关系
pass
else:
# 其他节点确保挂载到render下
if nodePath.getParent() != self.world.render and not nodePath.getName() in ["render",
"aspect2d",
"render2d"]:
nodePath.wrtReparentTo(self.world.render)
# 为模型节点设置碰撞检测
if nodePath.hasTag("is_model_root"):
print(f"J{indent}处理模型节点{nodePath.getName()}")
#self._validateAndFixAllTransforms(nodePath)
self._fixModelStructure(nodePath)
# 恢复模型动画信息
self._restoreModelAnimationInfo(nodePath)
# 检测并处理模型动画类似property_panel.py中的逻辑
self._processModelAnimations(nodePath)
# if self.world.property_panel._hasCollision(nodePath):
# print(f"{indent}模型{nodePath.getName()}已有碰撞体,跳过碰撞体设置")
# else:
# print(f"{indent}为模型{nodePath.getName()}设置碰撞检测")
# self.setupCollision(nodePath)
self.models.append(nodePath)
# 递归处理子节点
for child in nodePath.getChildren():
processNode(child, depth + 1)
print("\n开始处理场景节点...")
processNode(scene)
loaded_nodes = self._walk_loaded_scene(scene)
#处理父子关系 - 在所有节点加载完成后设置正确的父子关系
print("\n开始重建父子关系...")
self._rebuildParentChildRelationships(loaded_nodes)
@ -810,6 +518,227 @@ class SceneManagerIOMixin:
traceback.print_exc()
return self._retry_load_scene(filename, retry_count, max_retries)
def _walk_loaded_scene(self, scene):
"""遍历并恢复加载后的场景节点状态。"""
processed_lights = []
loaded_nodes = {}
self._process_loaded_scene_node(scene, loaded_nodes, processed_lights, depth=0)
return loaded_nodes
def _process_loaded_scene_node(self, nodePath, loaded_nodes, processed_lights, depth=0):
indent = " " * depth
if nodePath.isEmpty():
return
loaded_nodes[nodePath.getName()] = nodePath
if nodePath.getName().startswith('ground'):
print(f"{indent}跳过ground节点: {nodePath.getName()}")
return
if nodePath.getName() == "render" and depth > 0:
print(f"{indent}跳过重复的render节点")
return
if nodePath.getName() in ["alight", "dlight"]:
print(f"{indent}跳过光源节点: {nodePath.getName()}")
return
if nodePath.getName() in ["camera", "cam"]:
print(f"{indent}跳过相机节点: {nodePath.getName()}")
return
if nodePath.getName().startswith(("gizmo", "selectionBox")):
print(f"{indent}跳过辅助节点: {nodePath.getName()}")
return
if nodePath.getName() in ['SceneRoot'] or any(keyword in nodePath.getName() for keyword in ["Skybox", "skybox"]):
print(f"{indent}跳过环境节点:{nodePath.getName()}")
return
is_scene_element = (
nodePath.hasTag("is_scene_element") or
nodePath.hasTag("is_model_root") or
nodePath.hasTag("light_type") or
nodePath.hasTag("gui_type") or
nodePath.hasTag("is_gui_element") or
nodePath.hasTag("saved_gui_type") or
(nodePath.hasTag("element_type") and nodePath.getTag("element_type") == "info_panel")
)
is_potential_gui = any(
keyword in nodePath.getName().lower()
for keyword in ["gui", "button", "label", "entry", "image", "video", "screen", "text"]
)
if is_scene_element or is_potential_gui:
print(f"{indent}找到场景元素节点: {nodePath.getName()}")
if is_potential_gui and not (nodePath.hasTag("gui_type") or nodePath.hasTag("is_gui_element")):
print(f"{indent}为潜在GUI元素添加标签: {nodePath.getName()}")
nodePath.setTag("is_gui_element", "1")
nodePath.setTag("is_scene_element", "1")
name_lower = nodePath.getName().lower()
if "button" in name_lower:
nodePath.setTag("gui_type", "button")
elif "label" in name_lower:
nodePath.setTag("gui_type", "label")
elif "entry" in name_lower:
nodePath.setTag("gui_type", "entry")
elif "image" in name_lower:
nodePath.setTag("gui_type", "image")
elif "video" in name_lower or "screen" in name_lower:
nodePath.setTag("gui_type", "video_screen")
else:
nodePath.setTag("gui_type", "unknown")
nodePath.clearMaterial()
nodePath.clearColor()
if nodePath.hasTag("transform_pos"):
pos = self._parse_vec3_string(nodePath.getTag("transform_pos"))
nodePath.setPos(pos)
print(f"{indent}恢复位置: {pos}")
if nodePath.hasTag("transform_hpr"):
hpr = self._parse_vec3_string(nodePath.getTag("transform_hpr"))
nodePath.setHpr(hpr)
print(f"{indent}恢复旋转: {hpr}")
if nodePath.hasTag("transform_scale"):
scale = self._parse_vec3_string(nodePath.getTag("transform_scale"))
nodePath.setScale(scale)
print(f"{indent}恢复缩放: {scale}")
user_visible = True
if nodePath.hasTag("user_visible"):
user_visible = nodePath.getTag("user_visible").lower() == "true"
nodePath.setPythonTag("user_visible", user_visible)
if hasattr(self.world, 'property_panel'):
self.world.property_panel._syncEffectiveVisibility(nodePath)
else:
if user_visible:
nodePath.show()
else:
nodePath.hide()
if nodePath.hasTag("has_scripts") and nodePath.getTag("has_scripts") == "true":
self._restore_node_scripts(nodePath)
material = Material()
material_changed = False
if nodePath.hasTag("material_ambient"):
material.setAmbient(self._parse_color_string(nodePath.getTag("material_ambient")))
material_changed = True
if nodePath.hasTag("material_diffuse"):
material.setDiffuse(self._parse_color_string(nodePath.getTag("material_diffuse")))
material_changed = True
if nodePath.hasTag("material_specular"):
material.setSpecular(self._parse_color_string(nodePath.getTag("material_specular")))
material_changed = True
if nodePath.hasTag("material_emission"):
material.setEmission(self._parse_color_string(nodePath.getTag("material_emission")))
material_changed = True
if nodePath.hasTag("material_shininess"):
material.setShininess(float(nodePath.getTag("material_shininess")))
material_changed = True
if nodePath.hasTag("material_basecolor"):
material.setBaseColor(self._parse_color_string(nodePath.getTag("material_basecolor")))
material_changed = True
if material_changed:
nodePath.setMaterial(material)
if nodePath.hasTag("color"):
nodePath.setColor(self._parse_color_string(nodePath.getTag("color")))
if nodePath.hasTag("light_type"):
light_type = nodePath.getTag("light_type")
print(f"{indent}检测到光源类型: {light_type}")
if nodePath not in processed_lights:
if light_type == "spot_light":
self._recreateSpotLight(nodePath)
elif light_type == "point_light":
self._recreatePointLight(nodePath)
processed_lights.append(nodePath)
if not (nodePath.hasTag("gui_type") or nodePath.hasTag("is_gui_element")):
if nodePath.getParent() != self.world.render and nodePath.getName() not in ["render", "aspect2d", "render2d"]:
nodePath.wrtReparentTo(self.world.render)
if nodePath.hasTag("is_model_root"):
print(f"J{indent}处理模型节点{nodePath.getName()}")
self._fixModelStructure(nodePath)
self._restoreModelAnimationInfo(nodePath)
self._processModelAnimations(nodePath)
self.models.append(nodePath)
for child in nodePath.getChildren():
self._process_loaded_scene_node(child, loaded_nodes, processed_lights, depth + 1)
def _restore_node_scripts(self, nodePath):
"""恢复节点挂载脚本。"""
if not hasattr(self.world, 'script_manager') or not self.world.script_manager:
return
try:
scripts_info = json.loads(nodePath.getTag("scripts_info"))
print(f"节点 {nodePath.getName()} 需要重新挂载 {len(scripts_info)} 个脚本")
script_manager = self.world.script_manager
for script_info in scripts_info:
script_name = script_info["name"]
script_file = script_info.get("file", "")
print(f"尝试重新挂载脚本{script_name}from {script_file}")
if script_name not in script_manager.loader.script_classes:
if script_file and os.path.exists(script_file):
print(f"从文件加载脚本:{script_file}")
loaded_class = script_manager.load_script_from_file(script_file)
if loaded_class is None:
print(f"从文件加载脚本失败{script_file}")
script_path = self._find_script_in_directory(script_name)
if script_path:
print(f"从目录找到脚本并加载{script_path}")
script_manager.load_script_from_file(script_path)
else:
script_path = self._find_script_in_directory(script_name)
if script_path:
print(f"从目录找到脚本并加载: {script_path}")
script_manager.load_script_from_file(script_path)
else:
print(f"找不到脚本文件: {script_name}")
if script_name in script_manager.loader.script_classes:
script_component = script_manager.add_script_to_object(nodePath, script_name)
if script_component:
print(f"成功为 {nodePath.getName()} 添加脚本: {script_name}")
else:
print(f"{nodePath.getName()} 添加脚本失败: {script_name}")
else:
print(f"脚本 {script_name} 不可用,跳过挂载")
except Exception as e:
print(f"重新挂载脚本失败: {e}")
import traceback
traceback.print_exc()
def _parse_vec3_string(self, vec_str):
"""解析向量字符串为 Vec3。"""
try:
vec_str = vec_str.replace('LVecBase3f', '').replace('LPoint3f', '').strip('()')
x, y, z = map(float, vec_str.split(','))
return Vec3(x, y, z)
except Exception as e:
print(f"解析向量失败: {vec_str}, 错误: {e}")
return Vec3(0, 0, 0)
def _parse_color_string(self, color_str):
"""解析颜色字符串为 Vec4。"""
try:
color_str = color_str.replace('LVecBase4f', '').strip('()')
r, g, b, a = map(float, color_str.split(','))
return Vec4(r, g, b, a)
except Exception:
return Vec4(1, 1, 1, 1)
def _preflight_load_scene(self, filename, retry_count):
"""预检加载参数并在重试前做预清理。"""
filename = os.path.normpath(filename)