1
0
forked from Rowland/EG

剪切复制粘贴

This commit is contained in:
Hector 2025-09-23 09:27:49 +08:00
parent b5545b4b60
commit ec21df8f54
10 changed files with 1393 additions and 93 deletions

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -145,7 +145,7 @@ class InfoPanelManager(DirectObject):
text_scale=0.045,
text_fg=content_color,
text_align=TextNode.ALeft,
text_wordwrap=500, # 设置一个非常大的值,几乎不自动换行
text_wordwrap=0, # 设置一个非常大的值,几乎不自动换行
pos=(-size[0] / 2 + 0.03, 0, size[1] / 2 - title_bar_height - 0.05),
parent=panel_node,
relief=None,
@ -529,9 +529,7 @@ class InfoPanelManager(DirectObject):
-size[0] / 2 + 0.03, 0, size[1] / 2 - title_bar_height - 0.05
)
# 设置一个非常大的换行值,几乎不自动换行
panel_data['content_label']['text_wordwrap'] = 500
print(f"更新面板换行: 设置为500几乎不换行")
panel_data['content_label']['text_wordwrap'] = 0
# 如果有背景图片,也需要更新其大小
if 'bg_image' in panel_data and panel_data['bg_image']:
@ -575,8 +573,9 @@ class InfoPanelManager(DirectObject):
if 'content_size' in properties:
panel_data['content_label']['text_scale'] = properties['content_size']
props['content_size'] = properties['content_size']
current_size = props.get('size',(1.0,0.6))
# 当字体大小改变时,仍然保持较大的换行值
panel_data['content_label']['text_wordwrap'] = 500
panel_data['content_label']['text_wordwrap'] = 0
# 更新背景图片
if 'bg_image' in properties:

View File

@ -2761,4 +2761,23 @@ class SelectionSystem:
# 清理其他资源
self.clearSelectionBox()
self.clearGizmo()
self.clearGizmo()
def clearSelection(self):
"""清除当前选择"""
try:
self.selectedNode = None
self.selectedObject = None
self.clearSelectionBox()
self.clearGizmo()
# 清除树形控件中的选择
if (hasattr(self.world, 'interface_manager') and
self.world.interface_manager and
hasattr(self.world.interface_manager, 'treeWidget') and
self.world.interface_manager.treeWidget):
self.world.interface_manager.treeWidget.setCurrentItem(None)
print("已清除选择")
except Exception as e:
print(f"清除选择失败: {e}")

View File

@ -24,80 +24,80 @@ except ImportError:
WEB_ENGINE_AVAILABLE = False
print("⚠️ QtWebEngineWidgets 不可用Cesium 集成功能将被禁用")
def createGUI3DImage(self, pos=(0, 0, 0), image_path=None, size=1.0):
from panda3d.core import CardMaker, Material, LColor,TransparencyAttrib
# 参数类型检查和转换
if isinstance(size, (list, tuple)):
if len(size) >= 2:
x_size, y_size = float(size[0]), float(size[1])
else:
x_size = y_size = float(size[0]) if size else 1.0
else:
x_size = y_size = float(size)
# 创建卡片
cm = CardMaker('gui_3d_image')
cm.setFrame(-x_size/2, x_size/2, -y_size/2, y_size/2)
# 创建3D图像节点
image_node = self.world.render.attachNewNode(cm.generate())
image_node.setPos(*pos)
# 为3D图像创建独立的材质
material = Material(f"image-material-{len(self.gui_elements)}")
material.setBaseColor(LColor(1, 1, 1, 1))
material.setDiffuse(LColor(1, 1, 1, 1))
material.setAmbient(LColor(0.5, 0.5, 0.5, 1))
material.setSpecular(LColor(0.1, 0.1, 0.1, 1.0))
material.setShininess(10.0)
material.setEmission(LColor(0, 0, 0, 1)) # 无自发光
image_node.setMaterial(material, 1)
image_node.setTransparency(TransparencyAttrib.MAlpha)
# 如果提供了图像路径,则加载纹理
if image_path:
self.update3DImageTexture(image_node, image_path)
# 应用PBR效果如果可用
try:
if hasattr(self, 'render_pipeline') and self.render_pipeline:
self.render_pipeline.set_effect(
image_node,
"effects/default.yaml",
{
"normal_mapping": True,
"render_gbuffer": True,
"alpha_testing": False,
"parallax_mapping": False,
"render_shadow": False,
"render_envmap": True,
"disable_children_effects": True
},
50
)
print("✓ GUI 3D图像PBR效果已应用")
except Exception as e:
print(f"⚠️ GUI 3D图像PBR效果应用失败: {e}")
# 为GUI元素添加标识效仿3D文本方法
image_node.setTag("gui_type", "3d_image")
image_node.setTag("gui_id", f"3d_image_{len(self.gui_elements)}")
image_node.setTag("is_scene_element", "1")
image_node.setTag("tree_item_type", "GUI_3DIMAGE")
if image_path:
image_node.setTag("gui_image_path", image_path)
image_node.setTag("is_gui_element", "1")
self.gui_elements.append(image_node)
# 更新场景树
if hasattr(self.world, 'updateSceneTree'):
self.world.updateSceneTree()
print(f"✓ 3D图像创建完成: {image_path or '无纹理'} (世界位置: {pos})")
return image_node
# def createGUI3DImage(self, pos=(0, 0, 0), image_path=None, size=1.0):
# from panda3d.core import CardMaker, Material, LColor,TransparencyAttrib
#
# # 参数类型检查和转换
# if isinstance(size, (list, tuple)):
# if len(size) >= 2:
# x_size, y_size = float(size[0]), float(size[1])
# else:
# x_size = y_size = float(size[0]) if size else 1.0
# else:
# x_size = y_size = float(size)
#
# # 创建卡片
# cm = CardMaker('gui_3d_image')
# cm.setFrame(-x_size/2, x_size/2, -y_size/2, y_size/2)
#
# # 创建3D图像节点
# image_node = self.world.render.attachNewNode(cm.generate())
# image_node.setPos(*pos)
#
# # 为3D图像创建独立的材质
# material = Material(f"image-material-{len(self.gui_elements)}")
# material.setBaseColor(LColor(1, 1, 1, 1))
# material.setDiffuse(LColor(1, 1, 1, 1))
# material.setAmbient(LColor(0.5, 0.5, 0.5, 1))
# material.setSpecular(LColor(0.1, 0.1, 0.1, 1.0))
# material.setShininess(10.0)
# material.setEmission(LColor(0, 0, 0, 1)) # 无自发光
# image_node.setMaterial(material, 1)
#
# image_node.setTransparency(TransparencyAttrib.MAlpha)
#
# # 如果提供了图像路径,则加载纹理
# if image_path:
# self.update3DImageTexture(image_node, image_path)
#
# # 应用PBR效果如果可用
# try:
# if hasattr(self, 'render_pipeline') and self.render_pipeline:
# self.render_pipeline.set_effect(
# image_node,
# "effects/default.yaml",
# {
# "normal_mapping": True,
# "render_gbuffer": True,
# "alpha_testing": False,
# "parallax_mapping": False,
# "render_shadow": False,
# "render_envmap": True,
# "disable_children_effects": True
# },
# 50
# )
# print("✓ GUI 3D图像PBR效果已应用")
# except Exception as e:
# print(f"⚠️ GUI 3D图像PBR效果应用失败: {e}")
#
# # 为GUI元素添加标识效仿3D文本方法
# image_node.setTag("gui_type", "3d_image")
# image_node.setTag("gui_id", f"3d_image_{len(self.gui_elements)}")
# image_node.setTag("is_scene_element", "1")
# image_node.setTag("tree_item_type", "GUI_3DIMAGE")
# if image_path:
# image_node.setTag("gui_image_path", image_path)
# image_node.setTag("is_gui_element", "1")
#
# self.gui_elements.append(image_node)
#
# # 更新场景树
# if hasattr(self.world, 'updateSceneTree'):
# self.world.updateSceneTree()
#
# print(f"✓ 3D图像创建完成: {image_path or '无纹理'} (世界位置: {pos})")
# return image_node
class GUIManager:
"""GUI元素管理系统类"""

16
main.py
View File

@ -895,6 +895,22 @@ class MyWorld(CoreWorld):
except Exception as e:
print(f"创建默认自动朝向巡检路线失败: {e}")
def _serializeNode(self, node):
"""序列化节点数据"""
try:
return self.world.scene_manager.serializeNode(node)
except Exception as e:
print(f"序列化节点失败: {e}")
return None
def _deserializeNode(self, node_data, parent_node):
"""反序列化节点数据"""
try:
return self.world.scene_manager.deserializeNode(node_data, parent_node)
except Exception as e:
print(f"反序列化节点失败: {e}")
return None
# ==================== 项目管理功能代理 ====================
# 以下函数代理到project_manager模块的对应功能

View File

@ -7,6 +7,7 @@
"""
import os
import time
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QTreeWidgetItem
@ -165,16 +166,16 @@ class SceneManager:
model.setTag("converted_to_glb", "true")
# 特殊处理FBX模型
if filepath.lower().endswith('.fbx'):
print("检测到FBX模型应用特殊处理...")
# 将模型缩放设置为原来的1/100
model.setScale(0.01)
print("设置模型缩放为 0.01 (原始大小的1/100)")
# 设置模型旋转为 (0, 90, 0)
model.setHpr(0, 90, 0)
print("设置模型旋转为 (0, 90, 0)")
# if filepath.lower().endswith('.fbx'):
# print("检测到FBX模型应用特殊处理...")
#
# # 将模型缩放设置为原来的1/100
# model.setScale(0.01)
# print("设置模型缩放为 0.01 (原始大小的1/100)")
#
# # 设置模型旋转为 (0, 90, 0)
# model.setHpr(0, 90, 0)
# print("设置模型旋转为 (0, 90, 0)")
# # 可选的单位转换主要针对FBX
# if apply_unit_conversion and filepath.lower().endswith('.fbx'):
@ -2294,6 +2295,9 @@ class SceneManager:
light.casts_shadows = True
light.shadow_map_resolution = 256
light_pos = light_node.getPos()
light.setPos(light_pos)
# 添加到渲染管线
render_pipeline = get_render_pipeline()
render_pipeline.add_light(light)
@ -3456,4 +3460,729 @@ except Exception as e:
return None
def serializeNode(self, node):
"""序列化节点为字典数据"""
try:
node_data = {
'name': node.getName(),
'type': type(node.node()).__name__,
'pos': (node.getX(), node.getY(), node.getZ()),
'hpr': (node.getH(), node.getP(), node.getR()),
'scale': (node.getSx(), node.getSy(), node.getSz()),
'tags': {},
'children': []
}
# 保存所有标签
for tag_key in node.getTagKeys():
node_data['tags'][tag_key] = node.getTag(tag_key)
# 特殊处理不同类型的节点
if hasattr(node.node(), 'getClassType'):
node_class = node.node().getClassType().getName()
node_data['node_class'] = node_class
# 递归序列化子节点
for child in node.getChildren():
# 跳过辅助节点
if not child.getName().startswith(('gizmo', 'selectionBox', 'grid')):
child_data = self.serializeNode(child)
if child_data:
node_data['children'].append(child_data)
return node_data
except Exception as e:
print(f"序列化节点 {node.getName()} 失败: {e}")
import traceback
traceback.print_exc()
return None
def deserializeNode(self, node_data, parent_node):
"""从字典数据反序列化节点"""
try:
# 创建新节点
node_name = node_data.get('name', 'node')
new_node = parent_node.attachNewNode(node_name)
# 设置变换
pos = node_data.get('pos', (0, 0, 0))
hpr = node_data.get('hpr', (0, 0, 0))
scale = node_data.get('scale', (1, 1, 1))
new_node.setPos(*pos)
new_node.setHpr(*hpr)
new_node.setScale(*scale)
# 恢复标签
for tag_key, tag_value in node_data.get('tags', {}).items():
new_node.setTag(tag_key, tag_value)
# 根据节点类型进行特殊处理
node_type = node_data.get('type', '')
node_class = node_data.get('node_class', '')
# 特殊处理光源节点
if 'light_type' in node_data.get('tags', {}):
light_type = node_data['tags']['light_type']
if light_type == 'spot_light':
self._recreateSpotLight(new_node)
elif light_type == 'point_light':
self._recreatePointLight(new_node)
# 递归创建子节点
for child_data in node_data.get('children', []):
self.deserializeNode(child_data, new_node)
return new_node
except Exception as e:
print(f"反序列化节点 {node_data.get('name', 'unknown')} 失败: {e}")
import traceback
traceback.print_exc()
return None
def serializeNodeForCopy(self, node):
"""序列化节点用于复制操作,完整保存视觉属性"""
try:
if not node or node.isEmpty():
return None
node_data = {
'name': node.getName(),
'type': type(node.node()).__name__,
'pos': (node.getX(), node.getY(), node.getZ()),
'hpr': (node.getH(), node.getP(), node.getR()),
'scale': (node.getSx(), node.getSy(), node.getSz()),
'tags': {},
'children': []
}
# 保存所有标签
try:
if hasattr(node, 'getTagKeys'):
for tag_key in node.getTagKeys():
node_data['tags'][tag_key] = node.getTag(tag_key)
except Exception as e:
print(f"获取标签时出错: {e}")
# 保存视觉属性
try:
# 保存颜色属性
if hasattr(node, 'getColor'):
color = node.getColor()
node_data['color'] = (color.getX(), color.getY(), color.getZ(), color.getW())
# 保存材质属性
if hasattr(node, 'getMaterial'):
material = node.getMaterial()
if material:
material_data = {}
material_data['base_color'] = (
material.getBaseColor().getX(),
material.getBaseColor().getY(),
material.getBaseColor().getZ(),
material.getBaseColor().getW()
)
material_data['ambient'] = (
material.getAmbient().getX(),
material.getAmbient().getY(),
material.getAmbient().getZ(),
material.getAmbient().getW()
)
material_data['diffuse'] = (
material.getDiffuse().getX(),
material.getDiffuse().getY(),
material.getDiffuse().getZ(),
material.getDiffuse().getW()
)
material_data['specular'] = (
material.getSpecular().getX(),
material.getSpecular().getY(),
material.getSpecular().getZ(),
material.getSpecular().getW()
)
material_data['shininess'] = material.getShininess()
node_data['material'] = material_data
except Exception as e:
print(f"保存视觉属性时出错: {e}")
# 根据节点类型保存特定信息
if node.hasTag("tree_item_type"):
node_type = node.getTag("tree_item_type")
node_data['node_type'] = node_type
# 保存特定类型节点的额外信息
if node_type in ["LIGHT_NODE", "SPOT_LIGHT_NODE", "POINT_LIGHT_NODE"]:
# 保存光源特定信息
rp_light = node.getPythonTag("rp_light_object")
if rp_light:
node_data['light_data'] = {
'energy': getattr(rp_light, 'energy', 5000),
'radius': getattr(rp_light, 'radius', 1000),
'fov': getattr(rp_light, 'fov', 70) if hasattr(rp_light, 'fov') else None,
'inner_radius': getattr(rp_light, 'inner_radius', 0.4) if hasattr(rp_light,
'inner_radius') else None,
'casts_shadows': getattr(rp_light, 'casts_shadows', True) if hasattr(rp_light,
'casts_shadows') else True,
'shadow_map_resolution': getattr(rp_light, 'shadow_map_resolution', 256) if hasattr(
rp_light, 'shadow_map_resolution') else 256
}
elif node_type in ["GUI_BUTTON", "GUI_LABEL", "GUI_ENTRY", "GUI_IMAGE",
"GUI_3D_TEXT", "GUI_3D_IMAGE", "GUI_VIRTUAL_SCREEN"]:
# 保存GUI元素特定信息
node_data['gui_data'] = self._serializeGUIData(node)
elif node_type == "IMPORTED_MODEL_NODE":
# 保存模型特定信息
node_data['model_data'] = self._serializeModelData(node)
return node_data
except Exception as e:
print(f"序列化节点失败: {e}")
import traceback
traceback.print_exc()
return None
def _serializeGUIData(self, node):
"""序列化GUI元素数据"""
try:
gui_data = {}
# 保存GUI相关的通用属性
if node.hasTag("gui_type"):
gui_data['gui_type'] = node.getTag("gui_type")
# 保存文本内容(如果有的话)
if node.hasTag("text"):
gui_data['text'] = node.getTag("text")
# 保存其他GUI相关标签
gui_tags = ['font', 'font_size', 'text_color', 'bg_color', 'size']
for tag in gui_tags:
if node.hasTag(tag):
gui_data[tag] = node.getTag(tag)
return gui_data
except Exception as e:
print(f"序列化GUI数据失败: {e}")
return {}
def _serializeModelData(self, node):
"""序列化模型数据,包括材质信息"""
try:
model_data = {}
# 保存模型相关的标签
model_tags = ['model_path', 'file', 'element_type']
for tag in model_tags:
if node.hasTag(tag):
model_data[tag] = node.getTag(tag)
# 保存材质信息
try:
# 获取模型的材质信息
if hasattr(node, 'getState'):
state = node.getState()
if state:
# 保存基础颜色信息
from panda3d.core import ColorAttrib
color_attrib = state.getColor()
if color_attrib:
model_data['base_color'] = (
color_attrib.getColor().getX(),
color_attrib.getColor().getY(),
color_attrib.getColor().getZ(),
color_attrib.getColor().getW()
)
# 保存其他材质属性
from panda3d.core import MaterialAttrib
material_attrib = state.getAttrib(MaterialAttrib.getClassType())
if material_attrib:
material = material_attrib.getMaterial()
if material:
# 保存基础颜色
base_color = material.getBaseColor()
model_data['material_base_color'] = (
base_color.getX(), base_color.getY(), base_color.getZ(), base_color.getW()
)
# 保存环境光颜色
ambient_color = material.getAmbient()
model_data['material_ambient_color'] = (
ambient_color.getX(), ambient_color.getY(), ambient_color.getZ(),
ambient_color.getW()
)
# 保存漫反射颜色
diffuse_color = material.getDiffuse()
model_data['material_diffuse_color'] = (
diffuse_color.getX(), diffuse_color.getY(), diffuse_color.getZ(),
diffuse_color.getW()
)
# 保存高光颜色
specular_color = material.getSpecular()
model_data['material_specular_color'] = (
specular_color.getX(), specular_color.getY(), specular_color.getZ(),
specular_color.getW()
)
# 保存粗糙度和金属度等参数
model_data['material_roughness'] = material.getRoughness()
model_data['material metallic'] = material.getMetallic()
except Exception as e:
print(f"保存材质信息时出错: {e}")
return model_data
except Exception as e:
print(f"序列化模型数据失败: {e}")
return {}
def recreateNodeFromData(self, node_data, parent_node):
"""根据数据重建节点,并确保在场景树中显示"""
try:
if not node_data or not parent_node or parent_node.isEmpty():
return None
print(f"正在重建节点 {node_data}")
node_type = node_data.get('node_type', '')
original_name = node_data.get('name', 'node')
# 生成唯一名称
unique_name = self._generateUniqueName(original_name, parent_node)
# 根据节点类型调用相应的重建方法
new_node = None
if node_type in ["LIGHT_NODE", "SPOT_LIGHT_NODE", "POINT_LIGHT_NODE"]:
new_node = self._recreateLightFromData(node_data, parent_node, unique_name)
elif node_type == "CESIUM_TILESET_NODE":
new_node = self._recreateTilesetFromData(node_data, parent_node, unique_name)
elif node_type in ["GUI_BUTTON", "GUI_LABEL", "GUI_ENTRY", "GUI_IMAGE",
"GUI_3DTEXT", "GUI_3DIMAGE", "GUI_VIRTUAL_SCREEN"]:
new_node = self._recreateGUIFromData(node_data, parent_node, unique_name)
elif node_type == "IMPORTED_MODEL_NODE":
new_node = self._recreateModelFromData(node_data, parent_node, unique_name)
else:
# 创建普通节点
new_node = self._createBasicNodeFromData(node_data, parent_node, unique_name)
# 如果成功创建节点,确保它在场景树中显示
if new_node:
# 尝试更新场景树以显示新节点
try:
if hasattr(self.world, 'interface_manager') and self.world.interface_manager:
# 查找父节点在场景树中的对应项
parent_item = self._findTreeItemForNode(parent_node)
if parent_item:
# 添加新节点到场景树
tree_widget = self.world.interface_manager.treeWidget
if tree_widget:
tree_widget.add_node_to_tree_widget(new_node, parent_item, node_type or "NODE")
except Exception as e:
print(f"添加节点到场景树时出错: {e}")
return new_node
except Exception as e:
print(f"重建节点失败: {e}")
import traceback
traceback.print_exc()
return None
def _findTreeItemForNode(self, node):
"""根据节点查找对应的场景树项"""
try:
if hasattr(self.world, 'interface_manager') and self.world.interface_manager:
tree_widget = self.world.interface_manager.treeWidget
if tree_widget:
# 遍历场景树查找匹配的节点项
for i in range(tree_widget.topLevelItemCount()):
item = tree_widget.topLevelItem(i)
result = self._findTreeItemForNodeRecursive(item, node)
if result:
return result
return None
except Exception as e:
print(f"查找场景树项时出错: {e}")
return None
def _findTreeItemForNodeRecursive(self, item, target_node):
"""递归查找场景树项"""
try:
# 检查当前项是否匹配
item_node = getattr(item, 'node_path', None)
if not item_node:
item_node = getattr(item, 'node', None)
if item_node and item_node == target_node:
return item
# 递归检查子项
for i in range(item.childCount()):
child_item = item.child(i)
result = self._findTreeItemForNodeRecursive(child_item, target_node)
if result:
return result
return None
except Exception as e:
print(f"递归查找场景树项时出错: {e}")
return None
def _recreateLightFromData(self, node_data, parent_node, name):
"""根据数据重建光源"""
try:
light_type = node_data.get('tags', {}).get('light_type', 'spot_light')
# 创建光源
if light_type == 'spot_light':
light_node = self.createSpotLight(pos=node_data.get('pos', (0, 0, 0)))
else: # point_light
light_node = self.createPointLight(pos=node_data.get('pos', (0, 0, 0)))
if light_node:
# 设置名称
light_node.setName(name)
# 恢复其他属性
light_data = node_data.get('light_data', {})
rp_light = light_node.getPythonTag("rp_light_object")
if rp_light and light_data:
if 'energy' in light_data:
rp_light.energy = light_data['energy']
if 'radius' in light_data:
rp_light.radius = light_data['radius']
if 'fov' in light_data and hasattr(rp_light, 'fov'):
rp_light.fov = light_data['fov']
if 'inner_radius' in light_data and hasattr(rp_light, 'inner_radius'):
rp_light.inner_radius = light_data['inner_radius']
if 'casts_shadows' in light_data and hasattr(rp_light, 'casts_shadows'):
rp_light.casts_shadows = light_data['casts_shadows']
if 'shadow_map_resolution' in light_data and hasattr(rp_light, 'shadow_map_resolution'):
rp_light.shadow_map_resolution = light_data['shadow_map_resolution']
# 恢复其他标签
for tag_key, tag_value in node_data.get('tags', {}).items():
if tag_key not in ['name', 'light_type']:
light_node.setTag(tag_key, str(tag_value))
return light_node
except Exception as e:
print(f"重建光源失败: {e}")
return None
def _recreateTilesetFromData(self, node_data, parent_node, name):
"""根据数据重建Tileset"""
try:
tileset_url = node_data.get('tileset_url', '')
if not tileset_url:
return None
# 使用现有方法加载tileset
position = node_data.get('pos', (0, 0, 0))
tileset_node = self.load_cesium_tileset(tileset_url, position)
if tileset_node:
# 设置名称
tileset_node.setName(name)
# 恢复其他标签
for tag_key, tag_value in node_data.get('tags', {}).items():
if tag_key not in ['name']:
tileset_node.setTag(tag_key, str(tag_value))
return tileset_node
except Exception as e:
print(f"重建Tileset失败: {e}")
return None
def _recreateGUIFromData(self, node_data, parent_node, name):
"""根据数据重建GUI元素"""
try:
gui_data = node_data.get('gui_data', {})
#gui_type = gui_data.get('gui_type', '')
gui_type = node_data.get("tags").get("gui_type", "")
print(f"正在重建GUI元素: {gui_type}")
print(f"正在重建GUI元素: {node_data}")
# 根据GUI类型调用相应的创建方法
new_gui_element = None
if gui_type == "button" and hasattr(self.world, 'createGUIButton'):
pos = node_data.get('pos', (0, 0, 0))
text = node_data.get('tags').get('gui_text', '')
size = node_data.get('scale', 1)
print(pos,text,size)
new_gui_element = self.world.createGUIButton(pos,text,size)
elif gui_type == "label" and hasattr(self.world, 'createGUILabel'):
pos = node_data.get('pos', (0, 0, 0))
text = node_data.get('tags').get('gui_text', '')
size = node_data.get('scale', 1)
new_gui_element = self.world.createGUILabel(pos,text,size)
elif gui_type == "entry" and hasattr(self.world, 'createGUIEntry'):
pos = node_data.get('pos', (0, 0, 0))
text = node_data.get('tags').get('gui_text', '')
size = node_data.get('scale', 1)
new_gui_element = self.world.createGUIEntry(pos,text,size)
elif gui_type == "2d_image" and hasattr(self.world, 'createGUI2DImage'):
pos = node_data.get('pos', (0, 0, 0))
image_path = node_data.get('tags').get('image_path', '')
size = node_data.get('size', 1)
new_gui_element = self.world.createGUI2DImage(pos, image_path, size)
elif gui_type == "3d_text" and hasattr(self.world, 'createGUI3DText'):
print("正在创建3D文本!!!")
pos = node_data.get('pos', (0, 0, 0))
text = node_data.get('tags', {}).get('gui_text', '')
scale = node_data.get('scale', 1)
if isinstance(scale, (list, tuple)):
scale = scale[0] if len(scale) > 0 else 1
print(f"正在创建3D文本: 位置={pos}, 文本={text}, 大小={scale}")
new_gui_element = self.world.createGUI3DText(pos, text, scale)
elif gui_type == "3d_image" and hasattr(self.world, 'createGUI3DImage'):
pos = node_data.get('pos', (0, 0, 0))
image_path = node_data.get('tags').get('image_path', '')
scale = node_data.get('scale', (1, 1))
if isinstance(scale, (int, float)):
scale = (scale, scale)
elif isinstance(scale, (list, tuple)) and len(scale) >= 2:
scale = (scale[0], scale[1])
else:
scale = (1, 1)
print(f"正在创建3D图片: 位置={pos}, 路径={image_path}, 大小={scale}")
new_gui_element = self.world.createGUI3DImage(pos, image_path, scale)
if new_gui_element:
# 设置名称和变换
if hasattr(new_gui_element, 'setName'):
new_gui_element.setName(name)
# 设置位置、旋转、缩放
pos = node_data.get('pos', (0, 0, 0))
hpr = node_data.get('hpr', (0, 0, 0))
scale = node_data.get('scale', (1, 1, 1))
if hasattr(new_gui_element, 'setPos'):
new_gui_element.setPos(*pos)
if hasattr(new_gui_element, 'setHpr'):
new_gui_element.setHpr(*hpr)
if hasattr(new_gui_element, 'setScale'):
new_gui_element.setScale(*scale)
# 恢复文本内容
if 'text' in gui_data and hasattr(new_gui_element, 'setText'):
new_gui_element.setText(gui_data['text'])
# 恢复其他标签
for tag_key, tag_value in node_data.get('tags', {}).items():
if hasattr(new_gui_element, 'setTag') and tag_key not in ['name']:
new_gui_element.setTag(tag_key, str(tag_value))
print(f"GUI元素重建成功: {name}")
return new_gui_element
except Exception as e:
print(f"重建GUI元素失败: {e}")
import traceback
traceback.print_exc()
return None
def _recreateModelFromData(self, node_data, parent_node, name):
"""根据数据重建模型,保持材质"""
try:
model_data = node_data.get('model_data', {})
model_path = model_data.get('model_path', model_data.get('file', ''))
if not model_path or not os.path.exists(model_path):
# 如果原始模型文件不存在,创建一个基本节点
return self._createBasicNodeFromData(node_data, parent_node, name)
# 导入模型,保持原有参数
model = self.importModel(
model_path,
apply_unit_conversion=False, # 已经处理过的模型不需要再次转换
normalize_scales=False, # 保持原有缩放
auto_convert_to_glb=False # 已经处理过的模型不需要再次转换
)
if model:
# 设置名称
model.setName(name)
# 设置变换
pos = node_data.get('pos', (0, 0, 0))
hpr = node_data.get('hpr', (0, 0, 0))
scale = node_data.get('scale', (1, 1, 1))
model.setPos(*pos)
model.setHpr(*hpr)
model.setScale(*scale)
# 恢复材质信息
try:
self._restoreModelMaterial(model, model_data)
except Exception as e:
print(f"恢复模型材质时出错: {e}")
# 恢复标签
for tag_key, tag_value in node_data.get('tags', {}).items():
if tag_key not in ['name']:
model.setTag(tag_key, str(tag_value))
# 添加到模型列表
if model not in self.models:
self.models.append(model)
return model
except Exception as e:
print(f"重建模型失败: {e}")
# 出错时创建基本节点
return self._createBasicNodeFromData(node_data, parent_node, name)
def _restoreModelMaterial(self, model, model_data):
"""恢复模型材质"""
try:
# 恢复基础颜色
if 'base_color' in model_data:
from panda3d.core import ColorAttrib
base_color = model_data['base_color']
color = (base_color[0], base_color[1], base_color[2], base_color[3])
model.setColor(color)
# 恢复复杂材质属性
if any(key.startswith('material_') for key in model_data.keys()):
from panda3d.core import Material
# 创建新材质或获取现有材质
material = Material()
# 恢复基础颜色
if 'material_base_color' in model_data:
base_color = model_data['material_base_color']
material.setBaseColor((base_color[0], base_color[1], base_color[2], base_color[3]))
# 恢复环境光颜色
if 'material_ambient_color' in model_data:
ambient_color = model_data['material_ambient_color']
material.setAmbient((ambient_color[0], ambient_color[1], ambient_color[2], ambient_color[3]))
# 恢复漫反射颜色
if 'material_diffuse_color' in model_data:
diffuse_color = model_data['material_diffuse_color']
material.setDiffuse((diffuse_color[0], diffuse_color[1], diffuse_color[2], diffuse_color[3]))
# 恢复高光颜色
if 'material_specular_color' in model_data:
specular_color = model_data['material_specular_color']
material.setSpecular((specular_color[0], specular_color[1], specular_color[2], specular_color[3]))
# 恢复粗糙度和金属度
if 'material_roughness' in model_data:
material.setRoughness(model_data['material_roughness'])
if 'material_metallic' in model_data:
material.setMetallic(model_data['material_metallic'])
# 应用材质到模型
model.setMaterial(material)
except Exception as e:
print(f"恢复材质失败: {e}")
def _createBasicNodeFromData(self, node_data, parent_node, name):
"""创建基本节点,保持视觉属性"""
try:
new_node = parent_node.attachNewNode(name)
# 设置变换
pos = node_data.get('pos', (0, 0, 0))
hpr = node_data.get('hpr', (0, 0, 0))
scale = node_data.get('scale', (1, 1, 1))
new_node.setPos(*pos)
new_node.setHpr(*hpr)
new_node.setScale(*scale)
# 恢复视觉属性
try:
# 恢复颜色
if 'color' in node_data:
color_data = node_data['color']
new_node.setColor(color_data[0], color_data[1], color_data[2], color_data[3])
# 恢复材质
if 'material' in node_data:
from panda3d.core import Material
material_data = node_data['material']
material = Material()
if 'base_color' in material_data:
bc = material_data['base_color']
material.setBaseColor((bc[0], bc[1], bc[2], bc[3]))
if 'ambient' in material_data:
ac = material_data['ambient']
material.setAmbient((ac[0], ac[1], ac[2], ac[3]))
if 'diffuse' in material_data:
dc = material_data['diffuse']
material.setDiffuse((dc[0], dc[1], dc[2], dc[3]))
if 'specular' in material_data:
sc = material_data['specular']
material.setSpecular((sc[0], sc[1], sc[2], sc[3]))
if 'shininess' in material_data:
material.setShininess(material_data['shininess'])
new_node.setMaterial(material)
except Exception as e:
print(f"恢复视觉属性时出错: {e}")
# 恢复标签
for tag_key, tag_value in node_data.get('tags', {}).items():
if tag_key not in ['name']:
new_node.setTag(tag_key, str(tag_value))
return new_node
except Exception as e:
print(f"创建基本节点失败: {e}")
return None
def _generateUniqueName(self, base_name, parent_node):
"""生成唯一节点名称"""
try:
# 移除可能的数字后缀
import re
import time
name_base = re.sub(r'_\d+$', '', base_name)
# 查找现有同名节点
counter = 1
unique_name = base_name
while True:
# 检查父节点下是否已存在同名子节点
existing_node = parent_node.find(unique_name)
if existing_node.isEmpty():
break
unique_name = f"{name_base}_{counter}"
counter += 1
if counter > 1000: # 防止无限循环
unique_name = f"{name_base}_{int(time.time())}"
break
return unique_name
except:
return f"{base_name}_{int(time.time())}"

View File

@ -19,6 +19,7 @@ from PyQt5.QtWidgets import (QApplication, QMainWindow, QMenuBar, QMenu, QAction
QSpinBox, QFrame)
from PyQt5.QtCore import Qt, QDir, QTimer, QSize, QPoint, QUrl, QRect
from direct.showbase.ShowBaseGlobal import aspect2d
from panda3d.core import OrthographicLens
from ui.widgets import CustomPanda3DWidget, CustomFileView, CustomTreeWidget,CustomAssetsTreeWidget, CustomConsoleDockWidget
from ui.icon_manager import get_icon_manager, get_icon
@ -37,6 +38,10 @@ class MainWindow(QMainWindow):
self.world = world
self.world.main_window = self # 关键让world对象能访问主窗口
#剪切板相关属性
self.clipboard = []
self.clipboard_mode = None
# 初始化图标管理器并打印调试信息
self.icon_manager = get_icon_manager()
print("🔧 图标管理器初始化完成")
@ -808,6 +813,238 @@ class MainWindow(QMainWindow):
# 统一连接信号到处理方法
self.connectCreateMenuActions()
def setupViewMenuActions(self):
"""设置视图菜单动作"""
# 连接视图菜单事件
self.viewPerspectiveAction.triggered.connect(self.onViewPerspective)
self.viewTopAction.triggered.connect(self.onViewTop)
self.viewFrontAction.triggered.connect(self.onViewFront)
self.viewOrthographicAction = self.viewMenu.addAction('正交视图') # 添加正交视图动作
self.viewOrthographicAction.triggered.connect(self.onViewOrthographic)
self.viewGridAction.triggered.connect(self.onViewGrid) # 添加网格显示的信号连接
# 保存原始相机设置
self._original_camera_fov = 80
self._original_camera_pos = (0, -50, 20)
self._original_camera_lookat = (0, 0, 0)
self._grid_visible = False
def onViewPerspective(self):
"""切换到透视视图"""
try:
lens = self.world.cam.node().getLens()
lens.setFov(self._original_camera_fov)
except Exception as e:
print(f"切换到透视视图失败{e}")
def onViewOrthographic(self):
"""切换到正交视图"""
try:
# 保存当前相机设置(如果是透视模式)
lens = self.world.cam.node().getLens()
if not hasattr(self, '_saved_perspective_settings'):
self._saved_perspective_settings = {
'fov': lens.getFov()[0],
'pos': self.world.cam.getPos(),
'hpr': self.world.cam.getHpr()
}
# 获取窗口尺寸
win_width, win_height = self.world.getWindowSize()
aspect_ratio = win_width / win_height if win_height != 0 else 16 / 9
# 修改现有镜头为正交投影
if not isinstance(lens, OrthographicLens):
# 保存当前镜头类型
self._original_lens = lens
# 创建正交镜头并替换现有镜头
ortho_lens = OrthographicLens()
ortho_lens.setFilmSize(20 * aspect_ratio, 20) # 设置正交镜头大小
ortho_lens.setNearFar(-1000, 1000) # 设置较大的近远裁剪面
# 应用正交镜头
self.world.cam.node().setLens(ortho_lens)
else:
# 如果已经是正交镜头,则调整其参数
film_height = 20
film_width = film_height * aspect_ratio
lens.setFilmSize(film_width, film_height)
print("切换到正交视图")
except Exception as e:
print(f"切换正交视图失败: {e}")
def onViewTop(self):
"""切换到俯视图(正交)"""
try:
# 保存当前设置
self._saveCurrentCameraSettings()
# 设置正交投影
self._setupOrthographicLens()
# 设置摄像机位置(从上方俯视)
self.world.cam.setPos(0, 0, 30)
self.world.cam.lookAt(0, 0, 0)
self.world.cam.setHpr(0, -90, 0) # 朝下看
# 更新菜单项文本
self._updateViewMenuText()
print("切换到俯视图")
except Exception as e:
print(f"切换俯视图失败: {e}")
def onViewFront(self):
"""切换到前视图(正交)"""
try:
# 保存当前设置
self._saveCurrentCameraSettings()
# 设置正交投影
self._setupOrthographicLens()
# 设置摄像机位置(从前方向看)
self.world.cam.setPos(0, -30, 0)
self.world.cam.lookAt(0, 0, 0)
self.world.cam.setHpr(0, 0, 0) # 正面朝向
# 更新菜单项文本
self._updateViewMenuText()
print("切换到前视图")
except Exception as e:
print(f"切换前视图失败: {e}")
def onViewGrid(self):
"""切换网格显示/隐藏"""
try:
# 切换网格显示状态
self._grid_visible = not self._grid_visible
# 查找网格节点
grid_node = self.world.render.find("**/grid")
if grid_node.isEmpty():
# 如果网格不存在则创建
self._createGridView()
grid_node = self.world.render.find("**/grid")
# 设置网格可见性
if not grid_node.isEmpty():
if self._grid_visible:
grid_node.show()
self.viewGridAction.setText("隐藏网格")
print("网格已显示")
else:
grid_node.hide()
self.viewGridAction.setText("显示网格")
print("网格已隐藏")
else:
print("网格节点未找到")
except Exception as e:
print(f"切换网格显示失败: {e}")
def _createGridView(self):
"""创建网格视图"""
try:
from panda3d.core import LineSegs,Vec3
grid_node = self.world.render.attachNewNode("grid")
lines = LineSegs()
lines.setThickness(1.0)
lines.setColor(0.3,0.3,0.3,1.0)
grid_size = 20
grid_step = 1
for i in range(-grid_size,grid_size+1,grid_step):
lines.moveTo(Vec3(-grid_size,i,0))
lines.drawTo(Vec3(grid_size,i,0))
grid_node.attachNewNode(lines.create())
# 添加中心轴线红色X轴绿色Y轴
axis_lines = LineSegs()
axis_lines.setThickness(2.0)
# X轴红色
axis_lines.setColor(1.0,0.0,0.0,1.0)
axis_lines.moveTo(Vec3(0,0,0))
axis_lines.drawTo(Vec3(grid_size,0,0))
# Y轴绿色
axis_lines.setColor(0.0, 1.0, 0.0, 1.0)
axis_lines.moveTo(Vec3(0, 0, 0))
axis_lines.drawTo(Vec3(0, grid_size, 0))
grid_node.attachNewNode(axis_lines.create())
print("网格已创建")
except Exception as e:
print(f"创建网格失败{e}")
def _saveCurrentCameraSettings(self):
"""保存当前相机设置"""
try:
lens = self.world.cam.node().getLens()
self._saved_camera_settings = {
'lens_type': 'perspective' if not isinstance(lens, OrthographicLens) else 'orthographic',
'fov': lens.getFov()[0] if hasattr(lens, 'getFov') else None,
'film_size': (lens.getFilmSize()[0], lens.getFilmSize()[1]) if hasattr(lens, 'getFilmSize') else None,
'pos': self.world.cam.getPos(),
'hpr': self.world.cam.getHpr()
}
except Exception as e:
print(f"保存相机设置失败: {e}")
def _setupOrthographicLens(self):
"""设置正交镜头"""
try:
win_width, win_height = self.world.getWindowSize()
aspect_ratio = win_width / win_height if win_height != 0 else 16 / 9
from panda3d.core import OrthographicLens
ortho_lens = OrthographicLens()
ortho_lens.setFilmSize(20 * aspect_ratio, 20) # 设置正交镜头大小
ortho_lens.setNearFar(-1000, 1000) # 设置较大的近远裁剪面
self.world.cam.node().setLens(ortho_lens)
except Exception as e:
print(f"设置正交镜头失败: {e}")
def _updateViewMenuText(self):
"""更新视图菜单文本"""
try:
lens = self.world.cam.node().getLens()
from panda3d.core import OrthographicLens
# 更新正交/透视视图动作文本
if isinstance(lens, OrthographicLens):
self.viewOrthographicAction.setText("切换到透视视图")
self.viewOrthographicAction.triggered.disconnect()
self.viewOrthographicAction.triggered.connect(self.onViewPerspective)
else:
self.viewOrthographicAction.setText("切换到正交视图")
self.viewOrthographicAction.triggered.disconnect()
self.viewOrthographicAction.triggered.connect(self.onViewOrthographic)
except Exception as e:
print(f"更新视图菜单文本失败: {e}")
# 如果需要在窗口大小改变时调整正交镜头,可以添加以下方法
def _onWindowResized(self):
"""窗口大小改变时的处理"""
try:
lens = self.world.cam.node().getLens()
from panda3d.core import OrthographicLens
# 如果当前是正交镜头,需要根据新窗口大小调整
if isinstance(lens, OrthographicLens):
win_width, win_height = self.world.getWindowSize()
if win_height != 0:
aspect_ratio = win_width / win_height
film_height = 20
film_width = film_height * aspect_ratio
lens.setFilmSize(film_width, film_height)
except Exception as e:
print(f"窗口大小调整失败: {e}")
def connectCreateMenuActions(self):
"""统一连接创建菜单的信号到处理方法"""
# 连接到world对象的创建方法
@ -1564,6 +1801,9 @@ class MainWindow(QMainWindow):
self.buildAction.triggered.connect(lambda: buildPackage(self))
self.exitAction.triggered.connect(QApplication.instance().quit)
#添加保存项目快捷键盘
self.saveAction.setShortcut(QKeySequence.Save)
# 连接工具事件
self.sunsetAction.triggered.connect(lambda: self.world.setCurrentTool("光照编辑"))
self.pluginAction.triggered.connect(lambda: self.world.setCurrentTool("图形编辑"))
@ -1593,6 +1833,21 @@ class MainWindow(QMainWindow):
lambda: self.world.onTreeItemClicked(self.treeWidget.currentItem(), 0))
print("已连接点击信号")
self.undoAction.triggered.connect(self.onUndo)
self.redoAction.triggered.connect(self.onRedo)
self.cutAction.triggered.connect(self.onCut)
self.copyAction.triggered.connect(self.onCopy)
self.pasteAction.triggered.connect(self.onPaste)
self.undoAction.setShortcut(QKeySequence.Undo)
self.redoAction.setShortcut(QKeySequence.Redo)
self.cutAction.setShortcut(QKeySequence.Cut)
self.copyAction.setShortcut(QKeySequence.Copy)
self.pasteAction.setShortcut(QKeySequence.Paste)
#连接视图菜单事件
self.setupViewMenuActions()
# 连接工具切换信号
#self.toolGroup.buttonClicked.connect(self.onToolChanged)
@ -1603,6 +1858,288 @@ class MainWindow(QMainWindow):
# self.toggleHotReloadAction.triggered.connect(self.onToggleHotReload)
# self.openScriptsManagerAction.triggered.connect(self.onOpenScriptsManager)
def onCopy(self):
"""复制操作"""
try:
selected_item = self.treeWidget.currentItem()
if not selected_item:
QMessageBox.information(self, "提示", "请先选择要复制的节点")
return
# 获取选中的节点
selected_node = getattr(selected_item, 'node_path', None)
if not selected_node:
selected_node = getattr(selected_item, 'node', None)
if not selected_node and hasattr(self.world, 'selection'):
selected_node = getattr(self.world.selection, 'selectedNode', None)
if not selected_node or selected_node.isEmpty():
QMessageBox.warning(self, "错误", "无法获取选中节点")
return
# 检查是否是根节点
if selected_node.getName() == "render":
QMessageBox.warning(self, "错误", "不能复制根节点")
return
# 序列化节点数据
node_data = self.world.scene_manager.serializeNodeForCopy(selected_node)
if not node_data:
QMessageBox.warning(self, "错误", "无法序列化选中节点")
return
# 存储到剪切板
self.clipboard = [node_data]
self.clipboard_mode = "copy"
QMessageBox.information(self, "成功", "节点已复制到剪切板")
except Exception as e:
QMessageBox.critical(self, "错误", f"复制操作失败: {str(e)}")
def onCut(self):
"""剪切操作"""
try:
selected_item = self.treeWidget.currentItem()
if not selected_item:
QMessageBox.information(self, "提示", "请先选择要剪切的节点")
return
# 获取选中的节点
selected_node = getattr(selected_item, 'node_path', None)
if not selected_node:
selected_node = getattr(selected_item, 'node', None)
if not selected_node and hasattr(self.world, 'selection'):
selected_node = getattr(self.world.selection, 'selectedNode', None)
if not selected_node or selected_node.isEmpty():
QMessageBox.warning(self, "错误", "无法获取选中节点")
return
# 检查是否是根节点或特殊节点
if selected_node.getName() in ["render", "camera", "ambientLight", "directionalLight"]:
QMessageBox.warning(self, "错误", "不能剪切根节点或系统节点")
return
# 序列化节点数据
node_data = self.world.scene_manager.serializeNodeForCopy(selected_node)
if not node_data:
QMessageBox.warning(self, "错误", "无法序列化选中节点")
return
# 存储到剪切板
self.clipboard = [node_data]
self.clipboard_mode = "cut"
# 删除原节点
self.treeWidget.delete_items([selected_item])
QMessageBox.information(self, "成功", "节点已剪切到剪切板")
except Exception as e:
QMessageBox.critical(self, "错误", f"剪切操作失败: {str(e)}")
def onPaste(self):
"""粘贴操作"""
try:
if not self.clipboard:
QMessageBox.information(self, "提示", "剪切板为空")
return
# 获取粘贴目标节点
parent_item = self.treeWidget.currentItem()
parent_node = None
# 如果选中了节点,将其作为父节点
if parent_item:
parent_node = getattr(parent_item, 'node_path', None)
if not parent_node:
parent_node = getattr(parent_item, 'node', None)
# 确保获取到有效的父节点
if parent_node and not parent_node.isEmpty():
print(f"将粘贴到选中的节点: {parent_node.getName()}")
else:
parent_node = None
# 如果没有选中有效节点默认粘贴到render节点下
if not parent_node:
print("未选中有效节点,将粘贴到根节点下")
# 查找render节点
for i in range(self.treeWidget.topLevelItemCount()):
item = self.treeWidget.topLevelItem(i)
if item.text(0) == "render":
parent_item = item
break
# 如果找到了render节点项获取对应的节点
if parent_item:
parent_node = getattr(parent_item, 'node_path', None)
if not parent_node:
parent_node = getattr(parent_item, 'node', None)
# 如果仍然没有找到父节点项直接使用world.render
if not parent_node:
parent_node = self.world.render
# 检查父节点有效性
if not parent_node or parent_node.isEmpty():
QMessageBox.warning(self, "错误", "无法获取有效的父节点")
return
# 检查目标节点是否为允许的父节点类型
parent_name = parent_node.getName()
if parent_name in ["camera", "ambientLight", "directionalLight"]:
QMessageBox.warning(self, "错误", "不能粘贴到该类型节点下")
return
# 粘贴节点
pasted_nodes = []
for node_data in self.clipboard:
print(f"正在粘贴节点数据:{node_data.get('name','Unknown')}")
new_node = self.world.scene_manager.recreateNodeFromData(node_data, parent_node)
if new_node:
pasted_nodes.append(new_node)
print(f"成功粘贴节点: {new_node.getName()}")
else:
print(f"粘贴节点失败: {node_data.get('name', 'Unknown')}")
# 如果是剪切操作,清空剪切板
if self.clipboard_mode == "cut":
self.clipboard.clear()
self.clipboard_mode = None
QMessageBox.information(self, "成功", f"已粘贴 {len(pasted_nodes)} 个节点")
except Exception as e:
QMessageBox.critical(self, "错误", f"粘贴操作失败: {str(e)}")
def _serializeNode(self, node):
"""序列化节点数据"""
try:
if not node or node.isEmpty():
return None
node_data = {
'name': node.getName(),
'type': type(node.node()).__name__,
'pos': (node.getX(), node.getY(), node.getZ()),
'hpr': (node.getH(), node.getP(), node.getR()),
'scale': (node.getSx(), node.getSy(), node.getSz()),
'tags': {},
'children': []
}
# 保存所有标签
try:
# 使用更安全的方式获取标签
if hasattr(node, 'getTagKeys'):
for tag_key in node.getTagKeys():
node_data['tags'][tag_key] = node.getTag(tag_key)
except Exception as e:
print(f"获取标签时出错: {e}")
# 递归序列化子节点(跳过辅助节点)
try:
if hasattr(node, 'getChildren'):
for child in node.getChildren():
# 跳过辅助节点
child_name = child.getName() if hasattr(child, 'getName') else ""
if not child_name.startswith(('gizmo', 'selectionBox', 'grid')):
child_data = self._serializeNode(child)
if child_data:
node_data['children'].append(child_data)
except Exception as e:
print(f"序列化子节点时出错: {e}")
return node_data
except Exception as e:
print(f"序列化节点失败: {e}")
return None
def _deserializeNode(self, node_data, parent_node):
"""反序列化节点数据"""
try:
if not node_data or not parent_node or parent_node.isEmpty():
return None
# 创建新节点
node_name = node_data.get('name', 'node')
new_node = parent_node.attachNewNode(node_name)
# 设置变换
try:
pos = node_data.get('pos', (0, 0, 0))
hpr = node_data.get('hpr', (0, 0, 0))
scale = node_data.get('scale', (1, 1, 1))
new_node.setPos(*pos)
new_node.setHpr(*hpr)
new_node.setScale(*scale)
except Exception as e:
print(f"设置变换时出错: {e}")
# 恢复标签
try:
for tag_key, tag_value in node_data.get('tags', {}).items():
new_node.setTag(tag_key, str(tag_value)) # 确保标签值是字符串
except Exception as e:
print(f"恢复标签时出错: {e}")
# 递归创建子节点
try:
for child_data in node_data.get('children', []):
self._deserializeNode(child_data, new_node)
except Exception as e:
print(f"创建子节点时出错: {e}")
return new_node
except Exception as e:
print(f"反序列化节点失败: {e}")
return None
def _deleteNode(self, node, tree_item):
"""删除节点"""
try:
if not node or node.isEmpty():
return
# 特殊处理选中节点
if hasattr(self.world, 'selection') and self.world.selection.selectedNode == node:
self.world.selection.clearSelection()
# 从场景中删除节点
node.removeNode()
# 从树形控件中删除项目
if tree_item:
try:
parent = tree_item.parent()
if parent:
parent.removeChild(tree_item)
else:
index = self.treeWidget.indexOfTopLevelItem(tree_item)
if index >= 0:
self.treeWidget.takeTopLevelItem(index)
except Exception as e:
print(f"从树形控件删除项目时出错: {e}")
except Exception as e:
print(f"删除节点失败: {e}")
# 添加撤销/重做功能的基础实现
def onUndo(self):
"""撤销操作"""
QMessageBox.information(self, "提示", "撤销功能将在后续版本中实现")
def onRedo(self):
"""重做操作"""
QMessageBox.information(self, "提示", "重做功能将在后续版本中实现")
def onCreateCesiumView(self):
if hasattr(self.world,'gui_manager') and self.world.gui_manager: