1001 lines
44 KiB
Python
1001 lines
44 KiB
Python
"""Scene manager load/save and GUI rebuild operations."""
|
||
|
||
import os
|
||
import shutil
|
||
import time
|
||
import json
|
||
import aiohttp
|
||
import asyncio
|
||
import inspect
|
||
from pathlib import Path
|
||
|
||
from panda3d.core import (
|
||
ModelPool, ModelRoot, Filename, NodePath, GeomNode, Material, Vec4, Vec3,
|
||
MaterialAttrib, ColorAttrib, Point3, CollisionNode, CollisionSphere, CollisionBox,
|
||
BitMask32, TransparencyAttrib, LColor, TransformState, RenderModeAttrib
|
||
)
|
||
from panda3d.egg import EggData, EggVertexPool
|
||
from direct.actor.Actor import Actor
|
||
from RenderPipelineFile.rpplugins.smaa.jitters import halton_seq
|
||
from scene import util
|
||
|
||
class SceneManagerIOMixin:
|
||
def _collectGUIElementInfo(self, gui_node):
|
||
"""收集GUI元素的信息用于保存"""
|
||
try:
|
||
# 获取GUI元素类型
|
||
gui_type = "unknown"
|
||
if hasattr(gui_node, 'hasTag') and gui_node.hasTag("gui_type"):
|
||
gui_type = gui_node.getTag("gui_type")
|
||
elif hasattr(gui_node, 'hasTag') and gui_node.hasTag("saved_gui_type"):
|
||
gui_type = gui_node.getTag("saved_gui_type")
|
||
else:
|
||
# 尝试从节点名称推断类型
|
||
name_lower = gui_node.getName().lower()
|
||
if "button" in name_lower:
|
||
gui_type = "button"
|
||
elif "label" in name_lower:
|
||
gui_type = "label"
|
||
elif "entry" in name_lower:
|
||
gui_type = "entry"
|
||
elif "image" in name_lower:
|
||
gui_type = "2d_image"
|
||
elif "videoscreen" in name_lower:
|
||
if "2d" in name_lower:
|
||
gui_type = "2d_video_screen"
|
||
else:
|
||
gui_type = "video_screen"
|
||
elif "info_panel" in name_lower:
|
||
if "3d" in name_lower:
|
||
gui_type = "info_panel_3d"
|
||
else:
|
||
gui_type = "info_panel"
|
||
else:
|
||
# 如果无法识别类型,跳过该元素
|
||
print(f"跳过无法识别类型的GUI元素: {gui_node.getName()}")
|
||
return None
|
||
|
||
gui_info = {
|
||
"name": gui_node.getName(),
|
||
"type": gui_type,
|
||
"position": list(gui_node.getPos()),
|
||
"rotation": list(gui_node.getHpr()),
|
||
"scale": list(gui_node.getScale()),
|
||
"tags": {},
|
||
"parent_name":None,
|
||
"video_path":gui_node.getTag("video_path") if gui_node.hasTag("video_path") else None,
|
||
"panel_id":gui_node.getTag("panel_id") if gui_node.hasTag("panel_id") else None,
|
||
}
|
||
|
||
parent = gui_node.getParent()
|
||
if parent and not parent.isEmpty():
|
||
parent_name = parent.getName()
|
||
if parent_name not in ["render","aspect2d","render2d"]:
|
||
gui_info["parent_name"] = parent_name
|
||
|
||
# 收集所有标签(仅对NodePath类型的对象)
|
||
if hasattr(gui_node, 'getTagNames'):
|
||
for tag in gui_node.getTagNames():
|
||
gui_info["tags"][tag] = gui_node.getTag(tag)
|
||
elif hasattr(gui_node, 'getTags'): # 对于DirectGUI对象
|
||
# DirectGUI对象使用不同的方法存储标签
|
||
if hasattr(gui_node, '_tags'):
|
||
gui_info["tags"] = gui_node._tags.copy()
|
||
|
||
# 根据类型收集特定信息
|
||
if gui_type == "button":
|
||
if hasattr(gui_node, 'get'): # DirectButton
|
||
gui_info["text"] = gui_node.get()
|
||
elif hasattr(gui_node, 'getText'): # 其他类型
|
||
gui_info["text"] = gui_node.getText()
|
||
elif hasattr(gui_node, 'hasTag') and gui_node.hasTag("gui_text"):
|
||
gui_info["text"] = gui_node.getTag("gui_text")
|
||
elif gui_type == "label":
|
||
if hasattr(gui_node, 'getText'):
|
||
gui_info["text"] = gui_node.getText()
|
||
elif hasattr(gui_node, 'hasTag') and gui_node.hasTag("gui_text"):
|
||
gui_info["text"] = gui_node.getTag("gui_text")
|
||
elif gui_type == "entry":
|
||
if hasattr(gui_node, 'get'):
|
||
gui_info["text"] = gui_node.get()
|
||
elif hasattr(gui_node, 'hasTag') and gui_node.hasTag("gui_text"):
|
||
gui_info["text"] = gui_node.getTag("gui_text")
|
||
elif gui_type == "2d_image":
|
||
if hasattr(gui_node, 'hasTag') and gui_node.hasTag("image_path"):
|
||
gui_info["image_path"] = gui_node.getTag("image_path")
|
||
elif hasattr(gui_node, 'hasTag') and gui_node.hasTag("gui_image_path"):
|
||
gui_info["image_path"] = gui_node.getTag("gui_image_path")
|
||
elif gui_type == "3d_text":
|
||
if hasattr(gui_node,'hasTag') and gui_node.hasTag("gui_text"):
|
||
gui_info["text"] = gui_node.getTag("gui_text")
|
||
elif hasattr(gui_node,'node') and hasattr(gui_node.node(),'getText'):
|
||
gui_info["text"] = gui_node.node().getText()
|
||
elif gui_type == "3d_image":
|
||
if hasattr(gui_node,'hasTag') and gui_node.hasTag("gui_image_path"):
|
||
gui_info["image_path"] = gui_node.getTag("gui_image_path")
|
||
elif gui_type == "video_screen":
|
||
if hasattr(gui_node, 'hasTag') and gui_node.hasTag("video_path"):
|
||
gui_info["video_path"] = gui_node.getTag("video_path")
|
||
elif gui_type == "2d_video_screen":
|
||
if hasattr(gui_node, 'hasTag') and gui_node.hasTag("video_path"):
|
||
gui_info["video_path"] = gui_node.getTag("video_path")
|
||
elif gui_type == "virtual_screen":
|
||
if hasattr(gui_node, 'hasTag') and gui_node.hasTag("gui_text"):
|
||
gui_info["text"] = gui_node.getTag("gui_text")
|
||
elif gui_type in ["info_panel", "info_panel_3d"]:
|
||
# 收集信息面板的特定信息
|
||
if hasattr(gui_node, 'hasTag') and gui_node.hasTag("panel_id"):
|
||
gui_info["panel_id"] = gui_node.getTag("panel_id")
|
||
|
||
# 收集背景图片信息
|
||
if hasattr(gui_node, 'hasTag') and gui_node.hasTag("image_path"):
|
||
gui_info["image_path"] = gui_node.getTag("image_path")
|
||
|
||
# 收集GUI元素的可见性信息
|
||
user_visible = gui_node.getPythonTag("user_visible")
|
||
if user_visible is not None:
|
||
gui_info["user_visible"] = user_visible
|
||
else:
|
||
# 默认为可见
|
||
gui_info["user_visible"] = True
|
||
|
||
# 收集挂载的脚本信息
|
||
if hasattr(self.world, 'script_manager') and self.world.script_manager:
|
||
try:
|
||
script_manager = self.world.script_manager
|
||
scripts = script_manager.get_scripts_on_object(gui_node) # 修复:使用 gui_node 而不是 node
|
||
if scripts:
|
||
gui_info["scripts"] = []
|
||
for script_component in scripts:
|
||
try:
|
||
script_name = script_component.script_name
|
||
# 获取脚本路径
|
||
script_class = script_component.script_instance.__class__
|
||
script_file = self._get_script_file_path(script_class, script_name)
|
||
# 只有当脚本文件存在时才保存
|
||
if script_file and os.path.exists(script_file):
|
||
gui_info["scripts"].append({
|
||
"name": script_name,
|
||
"file": script_file
|
||
})
|
||
print(f"收集脚本信息: {script_name} from {script_file}")
|
||
else:
|
||
print(f"警告: 脚本文件不存在: {script_file}")
|
||
except Exception as e:
|
||
print(f"收集单个脚本信息失败 {script_name}, 错误: {e}")
|
||
continue
|
||
except Exception as e:
|
||
print(f"收集脚本信息失败: {e}")
|
||
|
||
print(f"成功收集GUI元素信息: {gui_info}")
|
||
return gui_info
|
||
except Exception as e:
|
||
print(f"收集GUI元素信息失败: {e}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
return None
|
||
|
||
def saveScene(self, filename,project_path):
|
||
"""保存场景到BAM文件 - 完整版,支持GUI元素,地形"""
|
||
try:
|
||
print(f"\n=== 开始保存场景到: {filename} ===")
|
||
|
||
# 确保文件路径是规范化的
|
||
filename = os.path.normpath(filename)
|
||
|
||
# 确保目录存在
|
||
directory = os.path.dirname(filename)
|
||
if directory and not os.path.exists(directory):
|
||
os.makedirs(directory)
|
||
|
||
resources_dir = os.path.join(directory,"resources")
|
||
if not os.path.exists(resources_dir):
|
||
os.makedirs(resources_dir)
|
||
|
||
# 存储需要临时隐藏的节点,以便保存后恢复
|
||
nodes_to_restore = []
|
||
|
||
# 查找并隐藏所有坐标轴和选择框节点
|
||
gizmo_nodes = self.world.render.findAllMatches("**/gizmo*")
|
||
selection_box_nodes = self.world.render.findAllMatches("**/selectionBox*")
|
||
|
||
# 隐藏坐标轴节点
|
||
for node in gizmo_nodes:
|
||
if not node.isHidden():
|
||
nodes_to_restore.append((node, True)) # (节点, 原先是否可见)
|
||
node.hide()
|
||
print(f"临时隐藏坐标轴节点: {node.getName()}")
|
||
|
||
# 隐藏选择框节点
|
||
for node in selection_box_nodes:
|
||
if not node.isHidden():
|
||
nodes_to_restore.append((node, True))
|
||
node.hide()
|
||
print(f"临时隐藏选择框节点: {node.getName()}")
|
||
|
||
# 收集所有需要保存的节点
|
||
all_nodes = []
|
||
all_nodes.extend(self.models)
|
||
all_nodes.extend(self.Spotlight)
|
||
all_nodes.extend(self.Pointlight)
|
||
|
||
# 添加GUI元素节点
|
||
gui_elements = []
|
||
if hasattr(self.world, 'gui_elements'):
|
||
# 过滤掉空的或重复的GUI元素
|
||
unique_gui_elements = []
|
||
seen_names = set()
|
||
|
||
for elem in self.world.gui_elements:
|
||
if elem and not elem.isEmpty():
|
||
if not elem.isEmpty() and elem.getName() not in seen_names:
|
||
unique_gui_elements.append(elem)
|
||
seen_names.add(elem.getName())
|
||
gui_elements = unique_gui_elements
|
||
|
||
print(f"保存时GUI元素列表=>>>>>>>>>>>>{self.world.gui_elements}")
|
||
all_nodes.extend(gui_elements)
|
||
|
||
# 创建用于保存GUI信息的JSON文件路径
|
||
gui_info_file = filename.replace('.bam', '_gui.json')
|
||
|
||
# 保存所有节点的信息
|
||
for node in all_nodes:
|
||
if node.isEmpty():
|
||
continue
|
||
|
||
# 保存变换信息
|
||
node.setTag("transform_pos", str(node.getPos()))
|
||
node.setTag("transform_hpr", str(node.getHpr()))
|
||
node.setTag("transform_scale", str(node.getScale()))
|
||
print(f"保存节点 {node.getName()} 的变换信息")
|
||
|
||
# 保存可见性信息
|
||
user_visible = node.getPythonTag("user_visible")
|
||
if user_visible is not None:
|
||
node.setTag("user_visible", str(user_visible).lower())
|
||
print(f"保存节点 {node.getName()} 的可见性信息: {user_visible}")
|
||
|
||
# 保存父子关系信息 - 关键修改
|
||
parent = node.getParent()
|
||
if parent and not parent.isEmpty() and parent != self.world.render:
|
||
# 只有当父节点不是根节点且父节点是场景中的模型时才保存父子关系
|
||
if parent.getName() not in ["render", "aspect2d", "render2d"]:
|
||
# 检查父节点是否也是场景中的模型
|
||
is_parent_model = False
|
||
for model in self.models:
|
||
if model == parent:
|
||
is_parent_model = True
|
||
break
|
||
|
||
if is_parent_model:
|
||
node.setTag("parent_name", parent.getName())
|
||
print(f"保存节点 {node.getName()} 的父节点信息: {parent.getName()}")
|
||
|
||
# 获取当前状态
|
||
state = node.getState()
|
||
|
||
# 如果有材质属性,保存为标签
|
||
if state.hasAttrib(MaterialAttrib.getClassType()):
|
||
mat_attrib = state.getAttrib(MaterialAttrib.getClassType())
|
||
material = mat_attrib.getMaterial()
|
||
if material:
|
||
# 保存材质属性到标签
|
||
node.setTag("material_ambient", str(material.getAmbient()))
|
||
node.setTag("material_diffuse", str(material.getDiffuse()))
|
||
node.setTag("material_specular", str(material.getSpecular()))
|
||
node.setTag("material_emission", str(material.getEmission()))
|
||
node.setTag("material_shininess", str(material.getShininess()))
|
||
if material.hasBaseColor():
|
||
node.setTag("material_basecolor", str(material.getBaseColor()))
|
||
|
||
# 保存特定类型节点的额外信息
|
||
if node.hasTag("light_type"):
|
||
# 保存光源特定信息
|
||
light_obj = node.getPythonTag("rp_light_object")
|
||
if light_obj:
|
||
node.setTag("light_energy", str(light_obj.energy))
|
||
if node.hasTag("stored_energy"):
|
||
node.setTag("stored_energy", node.getTag("stored_energy"))
|
||
node.setTag("light_radius", str(getattr(light_obj, 'radius', 0)))
|
||
if hasattr(light_obj, 'fov'):
|
||
node.setTag("light_fov", str(light_obj.fov))
|
||
elif node.hasTag("element_type"):
|
||
element_type = node.getTag("element_type")
|
||
if element_type == "cesium_tileset":
|
||
# 保存tileset特定信息
|
||
if node.hasTag("tileset_url"):
|
||
node.setTag("saved_tileset_url", node.getTag("tileset_url"))
|
||
elif node.hasTag("gui_type") or node.hasTag("is_gui_element"):
|
||
# 保存GUI元素特定信息
|
||
gui_type = node.getTag("gui_type") if node.hasTag("gui_type") else \
|
||
node.getTag("saved_gui_type") if node.hasTag("saved_gui_type") else "unknown"
|
||
node.setTag("saved_gui_type", gui_type)
|
||
|
||
# 保存GUI元素的通用属性
|
||
if hasattr(node, 'getPythonTag'):
|
||
# 保存任何Python标签数据
|
||
for tag_name in node.getPythonTagKeys():
|
||
try:
|
||
tag_value = node.getPythonTag(tag_name)
|
||
node.setTag(f"python_tag_{tag_name}", str(tag_value))
|
||
except:
|
||
pass
|
||
elif node.hasTag("element_type") and node.getTag("element_type") == "info_panel":
|
||
# 保存信息面板特定信息
|
||
print(f"保存信息面板信息: {node.getName()}")
|
||
panel_id = node.getTag("panel_id") if node.hasTag("panel_id") else node.getName()
|
||
if hasattr(self.world, 'info_panel_manager'):
|
||
panel_data = self.world.info_panel_manager.serializePanelData(panel_id)
|
||
if panel_data:
|
||
import json
|
||
node.setTag("info_panel_data", json.dumps(panel_data, ensure_ascii=False))
|
||
|
||
# 保存模型动画信息
|
||
elif node.hasTag("is_model_root"):
|
||
# 保存模型动画相关信息
|
||
if node.hasTag("has_animations"):
|
||
node.setTag("saved_has_animations", node.getTag("has_animations"))
|
||
if node.hasTag("model_path"):
|
||
node.setTag("saved_model_path", node.getTag("model_path"))
|
||
if node.hasTag("can_create_actor_from_memory"):
|
||
node.setTag("saved_can_create_actor_from_memory", node.getTag("can_create_actor_from_memory"))
|
||
|
||
if hasattr(self.world,'script_manager') and self.world.script_manager:
|
||
script_manager = self.world.script_manager
|
||
scripts = script_manager.get_scripts_on_object(node)
|
||
if scripts:
|
||
node.setTag("has_scripts", "true")
|
||
script_info_list = []
|
||
for script_component in scripts:
|
||
script_name = script_component.script_name
|
||
print(f"保存脚本信息: {script_name}")
|
||
|
||
# 获取脚本类的文件路径
|
||
script_class = script_component.script_instance.__class__
|
||
script_file = self._get_script_file_path(script_class, script_name)
|
||
|
||
script_info_list.append({
|
||
"name": script_name,
|
||
"file": script_file
|
||
})
|
||
|
||
# 将脚本信息保存为JSON字符串
|
||
import json
|
||
node.setTag("scripts_info", json.dumps(script_info_list, ensure_ascii=False))
|
||
print(f"为节点 {node.getName()} 保存了 {len(script_info_list)} 个脚本")
|
||
|
||
try:
|
||
print("--- 打印当前场景图 (render) ---")
|
||
self.world.render.ls()
|
||
print("---------------------------------")
|
||
|
||
self.take_screenshot(project_path)
|
||
# 保存场景
|
||
success = self.world.render.writeBamFile(Filename.fromOsSpecific(filename))
|
||
|
||
if success:
|
||
print(f"✓ 场景保存成功: {filename}")
|
||
else:
|
||
print("✗ 场景保存失败")
|
||
|
||
return success
|
||
|
||
finally:
|
||
# 恢复之前隐藏的节点
|
||
for item in nodes_to_restore:
|
||
node, was_visible = item
|
||
if was_visible and not node.isEmpty():
|
||
node.show()
|
||
print(f"恢复显示节点: {node.getName()}")
|
||
|
||
if nodes_to_restore:
|
||
print(f"已恢复 {len(nodes_to_restore)} 个辅助节点的显示")
|
||
|
||
except Exception as e:
|
||
print(f"保存场景时发生错误: {str(e)}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
return False
|
||
|
||
def take_screenshot(self, projectpath):
|
||
"""
|
||
截图并保存到指定的完整路径
|
||
|
||
Args:
|
||
full_path (str): 完整的文件保存路径,包括文件名和扩展名
|
||
|
||
Returns:
|
||
bool: 截图是否成功
|
||
"""
|
||
try:
|
||
from panda3d.core import Filename
|
||
import os
|
||
|
||
print(f"\n=== 截图保存: {projectpath} ===")
|
||
|
||
# 确保目录存在
|
||
directory = os.path.dirname(projectpath)
|
||
if directory and not os.path.exists(directory):
|
||
os.makedirs(directory)
|
||
print(f"创建目录: {directory}")
|
||
|
||
# 规范化路径
|
||
filename = os.path.basename(os.path.normpath(projectpath))
|
||
filename = f'{filename}.png'
|
||
print(f'project_path: {projectpath}')
|
||
print(f'project_name: {filename}')
|
||
full_path = os.path.normpath(os.path.join(projectpath, filename))
|
||
p3d_filename = Filename.from_os_specific(full_path)
|
||
# 使用 Panda3D 的截图功能
|
||
success = self.world.win.saveScreenshot(p3d_filename)
|
||
|
||
if success:
|
||
print(f"✅ 成功截图并保存到: {full_path}")
|
||
return True
|
||
else:
|
||
print(f"❌ 截图保存失败: {full_path}")
|
||
return False
|
||
|
||
except Exception as e:
|
||
print(f"保存截图时发生错误: {str(e)}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
return False
|
||
|
||
def loadScene(self, filename, retry_count=0):
|
||
"""从BAM文件加载场景"""
|
||
max_retries = 3
|
||
try:
|
||
print(f"\n=== 开始加载场景: {filename} (尝试 {retry_count + 1}/{max_retries + 1}) ===")
|
||
# print(f"[DEBUG] loadScene 被调用,参数: {filename}")
|
||
# print(f"[DEBUG] 当前场景中的模型数量: {len(self.models)}")
|
||
# 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 = self._preflight_load_scene(filename, retry_count)
|
||
if not filename:
|
||
return False
|
||
|
||
# print(f"[DEBUG] 预加载检查完成,开始正式加载...")
|
||
|
||
tree_widget = self._get_tree_widget()
|
||
# print(f"[DEBUG] 获取树形控件: {tree_widget is not None}")
|
||
|
||
print("\n清除当前场景...")
|
||
self._clear_current_scene_for_load(tree_widget)
|
||
|
||
# 加载场景
|
||
# print(f"[DEBUG] 开始加载BAM文件...")
|
||
|
||
scene = self._load_scene_root_from_file(filename)
|
||
if not scene:
|
||
return False
|
||
|
||
# print(f"[DEBUG] BAM文件加载成功,场景根节点: {scene.getName()}")
|
||
# print(f"[DEBUG] 场景子节点数量: {scene.getNumChildren()}")
|
||
|
||
# 验证场景节点的有效性
|
||
if scene.isEmpty():
|
||
# print(f"[DEBUG] 警告: 加载的场景节点为空")
|
||
return False
|
||
|
||
if not scene.hasParent():
|
||
# print(f"[DEBUG] 将场景节点临时挂载到render")
|
||
scene.reparentTo(self.world.render)
|
||
|
||
self._bootstrap_scene_tree_for_loaded_root(tree_widget, scene)
|
||
print("\n开始处理场景节点...")
|
||
loaded_nodes = self._walk_loaded_scene(scene)
|
||
|
||
print("\n开始重建父子关系...")
|
||
self._rebuildParentChildRelationships(loaded_nodes)
|
||
|
||
self._load_scene_gui_metadata(filename)
|
||
|
||
# 移除临时场景节点
|
||
if not scene.isEmpty():
|
||
scene.removeNode()
|
||
|
||
# 更新场景树
|
||
#self.updateSceneTree()
|
||
#self._get_tree_widget().create_model_items(scene)
|
||
|
||
print(f"加载完成,GUI元素数量: {len(self.world.gui_elements)}")
|
||
if len(self.world.gui_elements) > 0:
|
||
print("GUI元素列表:")
|
||
for i, elem in enumerate(self.world.gui_elements):
|
||
print(
|
||
f" {i + 1}. {elem.getName()} (类型: {elem.getTag('gui_type') if elem.hasTag('gui_type') else 'unknown'})")
|
||
|
||
print("=== 场景加载完成 ===\n")
|
||
return True
|
||
|
||
except Exception as e:
|
||
print(f"=== 场景加载失败 ===")
|
||
print(f"加载场景时发生错误: {str(e)}")
|
||
import traceback
|
||
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)
|
||
|
||
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):
|
||
try:
|
||
parent_child_relations = []
|
||
for node_name, node in loaded_nodes.items():
|
||
if node.hasTag("parent_name"):
|
||
parent_name = node.getTag("parent_name")
|
||
if parent_name in loaded_nodes:
|
||
parent_child_relations.append((node, loaded_nodes[parent_name])) # 修复:应该是元组
|
||
print(f"发现父子关系:{parent_name}->{node_name}")
|
||
else:
|
||
print(f"警告:节点{node_name}的父节点{parent_name}不存在")
|
||
for child_node, parent_node in parent_child_relations:
|
||
try:
|
||
child_node.wrtReparentTo(parent_node)
|
||
print(f"成功设置父子关系:{parent_node.getName()}->{child_node.getName()}")
|
||
except Exception as e:
|
||
print(f"设置父子关系失败{parent_node.getName()}->{child_node.getName()}:{e}")
|
||
|
||
if not parent_child_relations:
|
||
print("尝试从场景结构推断父子关系")
|
||
self._inferParentChildRelationships(loaded_nodes)
|
||
|
||
print("父子关系重建完成")
|
||
except Exception as e:
|
||
print(f"重建父子关系时出错: {e}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
|
||
def _inferParentChildRelationships(self, loaded_nodes):
|
||
"""从场景结构推断父子关系"""
|
||
try:
|
||
# 这里可以添加更复杂的父子关系推断逻辑
|
||
# 例如,根据节点名称、位置关系等进行推断
|
||
# 目前保持简单,后续可以扩展
|
||
print("父子关系推断完成(当前为空实现)")
|
||
except Exception as e:
|
||
print(f"推断父子关系时出错: {e}")
|
||
|
||
def _shouldSkipNodeInTree(self, nodePath):
|
||
"""判断节点是否应该在场景树中跳过显示"""
|
||
|
||
if nodePath.getName().startswith('ground'):
|
||
return True
|
||
|
||
# 跳过render节点的递归
|
||
if nodePath.getName() == "render":
|
||
return True
|
||
|
||
# 跳过光源节点
|
||
if nodePath.getName() in ["alight", "dlight"]:
|
||
return True
|
||
|
||
# 跳过相机节点
|
||
if nodePath.getName() in ["camera", "cam"]:
|
||
return True
|
||
|
||
# 跳过3D文本和3D图像节点
|
||
if (hasattr(nodePath.node(), "hasTag") and
|
||
nodePath.node().hasTag("gui_type") and
|
||
nodePath.node().getTag("gui_type") in ["3d_text", "3d_image"]):
|
||
return True
|
||
|
||
# 跳过辅助节点
|
||
if nodePath.getName().startswith(("gizmo", "selectionBox")):
|
||
return True
|
||
|
||
return False
|
||
|
||
|
||
def _find_script_in_directory(self, script_name):
|
||
"""在脚本目录中查找脚本文件"""
|
||
try:
|
||
if hasattr(self.world, 'script_manager') and self.world.script_manager:
|
||
script_manager = self.world.script_manager
|
||
scripts_dir = script_manager.scripts_directory
|
||
|
||
if os.path.exists(scripts_dir):
|
||
# 首先精确匹配
|
||
for file_name in os.listdir(scripts_dir):
|
||
if file_name.endswith('.py'):
|
||
base_name = os.path.splitext(file_name)[0]
|
||
if base_name == script_name:
|
||
return os.path.join(scripts_dir, file_name)
|
||
|
||
# 如果没有精确匹配,尝试模糊匹配
|
||
for file_name in os.listdir(scripts_dir):
|
||
if file_name.endswith('.py'):
|
||
base_name = os.path.splitext(file_name)[0]
|
||
if script_name.lower() in base_name.lower() or base_name.lower() in script_name.lower():
|
||
return os.path.join(scripts_dir, file_name)
|
||
except Exception as e:
|
||
print(f"查找脚本文件时出错: {e}")
|
||
|
||
return None
|