EG/gui/gui_manager.py
2026-01-22 11:13:01 +08:00

4028 lines
173 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
GUI元素管理模块
负责2D/3D GUI元素的管理
- GUI元素的创建按钮、标签、输入框等
- GUI编辑模式
- GUI属性编辑
- GUI元素的复制和删除
"""
from direct.gui.DirectGui import *
from panda3d.core import *
from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QFormLayout, QLineEdit,
QDoubleSpinBox, QPushButton, QDialogButtonBox,
QColorDialog, QLabel, QWidget, QGroupBox, QHBoxLayout, QGridLayout, QSpinBox)
from PyQt5.QtGui import QColor
from PyQt5.QtCore import Qt
# 尝试导入 QtWebEngineWidgets如果失败则设置为 None
try:
from PyQt5.QtWebEngineWidgets import QWebEngineView
WEB_ENGINE_AVAILABLE = True
except ImportError:
QWebEngineView = None
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
class GUIManager:
"""GUI元素管理系统类"""
def __init__(self, world):
"""初始化GUI管理系统
Args:
world: 核心世界对象引用
"""
self.world = world
# GUI元素列表
self.gui_elements = []
#光源列表
self.light_elements = []
# GUI编辑模式状态
self.guiEditMode = False
self.guiEditPanel = None
self.guiPreviewWindow = None
self.currentGUITool = None
print("✓ GUI管理系统初始化完成")
def _get_chinese_font(self):
"""获取中文字体用于3D文本显示"""
try:
from panda3d.core import TextNode
from pathlib import Path
# 尝试不同的中文字体路径
font_paths = [
"/usr/share/fonts/truetype/wqy/wqy-microhei.ttc", # Linux 文泉驿
"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc", # Linux Noto
"/System/Library/Fonts/PingFang.ttc", # macOS
"C:/Windows/Fonts/msyh.ttc", # Windows 微软雅黑
"C:/Windows/Fonts/simhei.ttf", # Windows 黑体
]
for font_path in font_paths:
if Path(font_path).exists():
font = TextNode.getFont(font_path)
if font:
print(f"✓ 为3D文本加载中文字体成功: {font_path}")
return font
# 如果都失败了,返回默认字体
print("⚠️ 无法加载中文字体,使用默认字体")
return None
except Exception as e:
print(f"⚠️ 加载中文字体失败: {e}")
return None
# ==================== GUI元素创建方法 ====================
def createGUIButton(self, pos=(0, 0, 0), text="按钮", size=(0.1,0.1,0.1)):
"""创建2D GUI按钮 - 支持多选创建和GUI父子关系优化版本"""
try:
from direct.gui.DirectGui import DirectButton
from PyQt5.QtCore import Qt
print(f"🔘 开始创建GUI按钮位置: {pos}, 文本: {text}, 尺寸: {size}")
# 获取树形控件
tree_widget = self._get_tree_widget()
if not tree_widget:
print("❌ 无法访问树形控件")
return None
# 使用CustomTreeWidget的方法获取目标父节点列表
target_parents = tree_widget.get_target_parents_for_gui_creation()
if not target_parents:
print("❌ 没有找到有效的父节点")
return None
created_buttons = []
# 为每个有效的父节点创建GUI按钮
for parent_item, parent_node in target_parents:
try:
# 生成唯一名称
button_name = f"GUIButton_{len(self.gui_elements)}"
# 使用CustomTreeWidget的方法判断父节点类型并设置相应的挂载方式
if tree_widget.is_gui_element(parent_node):
# 父节点是GUI元素 - 作为子GUI挂载
gui_pos = tree_widget.calculate_relative_gui_position(pos, parent_node)
parent_gui_node = parent_node # 直接挂载到GUI元素
parent_scale=parent_node.getScale()
relative_scale = (
size/parent_scale[0] if parent_scale[0]!=0 else size,
size/parent_scale[1] if parent_scale[1]!=0 else size,
size/parent_scale[2] if parent_scale[2]!=0 else size
)
else:
# 父节点是普通3D节点 - 使用屏幕坐标
gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1)
parent_gui_node = None # 使用默认的aspect2d
relative_scale = size
print(f"📎 挂载到3D父节点: {parent_item.text(0)}")
button = DirectButton(
text=text,
pos=gui_pos,
scale=relative_scale,
command=self.onGUIButtonClick,
extraArgs=[f"button_{len(self.gui_elements)}"],
frameColor=(0.2, 0.6, 0.8, 1),
text_font=self.world.getChineseFont() if self.world.getChineseFont() else None,
rolloverSound=None,
clickSound=None,
parent=parent_gui_node
)
if not hasattr(button,'_tags'):
button._tags = {}
# button._tags["gui_type"] = "button"
# button._tags["gui_id"] = f"button_{len(self.gui_elements)}"
# button._tags["gui_text"] = text
# button._tags["is_gui_element"] = "1"
# button._tags["is_scene_element"] = "1"
# button._tags["saved_gui_type"] = "button"
# button._tags["gui_element_type"] = "button"
# button._tags["created_by_user"] = "1"
# button._tags["name"] = button_name
# button.setName(button_name)
# 设置节点标签
button.setTag("gui_type", "button")
button.setTag("gui_id", f"button_{len(self.gui_elements)}")
button.setTag("gui_text", text)
button.setTag("is_gui_element", "1")
button.setTag("is_scene_element", "1") # 确保这个标签被设置
button.setTag("tree_item_type", "GUI_BUTTON")
button.setTag("saved_gui_type", "button") # 添加这个标签以确保兼容性
button.setTag("gui_element_type", "button")
button.setTag("created_by_user", "1")
button.setTag("gui_parent_type", "gui" if parent_gui_node else "3d")
button.setTag("name", button_name)
button.setName(button_name)
# 如果有GUI父节点建立引用关系
if parent_gui_node:
parent_id = parent_gui_node.getTag("gui_id") if hasattr(parent_gui_node, 'getTag') else ""
button.setTag("gui_parent_id", parent_id)
# 添加到GUI元素列表
self.gui_elements.append(button)
button.reparentTo(self.world.aspect2d)
print(f"✅ 为 {parent_item.text(0)} 创建GUI按钮成功: {button_name}")
# 使用CustomTreeWidget的方法在Qt树形控件中添加对应节点
qt_item = tree_widget.add_node_to_tree_widget(button, parent_item, "GUI_BUTTON")
if qt_item:
created_buttons.append((button, qt_item))
else:
created_buttons.append((button, None))
print("⚠️ Qt树节点添加失败但GUI对象已创建")
except Exception as e:
print(f"❌ 为 {parent_item.text(0)} 创建GUI按钮失败: {str(e)}")
continue
# 处理创建结果
if not created_buttons:
print("❌ 没有成功创建任何GUI按钮")
return None
# 选中最后创建的按钮并更新场景树
# if created_buttons:
# last_button, last_qt_item = created_buttons[-1]
# if last_qt_item:
# tree_widget.setCurrentItem(last_qt_item)
# tree_widget.update_selection_and_properties(last_button, last_qt_item)
print(f"🎉 总共创建了 {len(created_buttons)} 个GUI按钮")
# 返回值处理
if len(created_buttons) == 1:
return created_buttons[0][0]
else:
return [button for button, _ in created_buttons]
except Exception as e:
print(f"❌ 创建GUI按钮过程失败: {str(e)}")
import traceback
traceback.print_exc()
return None
def createGUILabel(self, pos=(0, 0, 0), text="标签", size=0.08):
"""创建2D GUI标签 - 支持多选创建和GUI父子关系优化版本"""
try:
from direct.gui.DirectGui import DirectLabel
from PyQt5.QtCore import Qt
print(f"🏷️ 开始创建GUI标签位置: {pos}, 文本: {text}, 尺寸: {size}")
# 获取树形控件
tree_widget = self._get_tree_widget()
if not tree_widget:
print("❌ 无法访问树形控件")
return None
# 使用CustomTreeWidget的方法获取目标父节点列表
target_parents = tree_widget.get_target_parents_for_gui_creation()
if not target_parents:
print("❌ 没有找到有效的父节点")
return None
created_labels = []
# 为每个有效的父节点创建GUI标签
for parent_item, parent_node in target_parents:
try:
# 生成唯一名称
label_name = f"GUILabel_{len(self.gui_elements)}"
# 使用CustomTreeWidget的方法判断父节点类型并设置相应的挂载方式
if tree_widget.is_gui_element(parent_node):
# 父节点是GUI元素 - 作为子GUI挂载
gui_pos = tree_widget.calculate_relative_gui_position(pos, parent_node)
parent_gui_node = parent_node
parent_scale = parent_node.getScale()
relative_scale = (
size/parent_scale[0] if parent_scale[0]!= 0 else size,
size/parent_scale[1] if parent_scale[1]!= 0 else size,
size/parent_scale[2] if parent_scale[2]!= 0 else size
)
else:
# 父节点是普通3D节点 - 使用屏幕坐标
gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1)
parent_gui_node = None
relative_scale = size
label = DirectLabel(
text=text,
pos=gui_pos,
scale=relative_scale,
frameColor=(0, 0, 0, 0), # 透明背景
text_fg=(1, 1, 1, 1),
text_font=self.world.getChineseFont() if self.world.getChineseFont() else None,
parent=parent_gui_node # 设置GUI父节点
)
# 设置节点标签
label.setTag("gui_type", "label")
label.setTag("gui_id", f"label_{len(self.gui_elements)}")
label.setTag("gui_text", text)
label.setTag("is_gui_element", "1")
label.setTag("tree_item_type", "GUI_LABEL")
label.setTag("is_scene_element", "1")
label.setTag("created_by_user", "1")
label.setTag("gui_parent_type", "gui" if parent_gui_node else "3d")
label.setTag("name",label_name)
label.setName(label_name)
# 如果有GUI父节点建立引用关系
if parent_gui_node:
parent_id = parent_gui_node.getTag("gui_id") if hasattr(parent_gui_node, 'getTag') else ""
label.setTag("gui_parent_id", parent_id)
# 添加到GUI元素列表
self.gui_elements.append(label)
print(f"✅ 为 {parent_item.text(0)} 创建GUI标签成功: {label_name}")
# 使用CustomTreeWidget的方法在Qt树形控件中添加对应节点
qt_item = tree_widget.add_node_to_tree_widget(label, parent_item, "GUI_LABEL")
if qt_item:
created_labels.append((label, qt_item))
else:
created_labels.append((label, None))
print("⚠️ Qt树节点添加失败但GUI对象已创建")
except Exception as e:
print(f"❌ 为 {parent_item.text(0)} 创建GUI标签失败: {str(e)}")
continue
# 处理创建结果
if not created_labels:
print("❌ 没有成功创建任何GUI标签")
return None
# 选中最后创建的标签并更新场景树
# if created_labels:
# last_label, last_qt_item = created_labels[-1]
# if last_qt_item:
# tree_widget.setCurrentItem(last_qt_item)
# tree_widget.update_selection_and_properties(last_label, last_qt_item)
print(f"🎉 总共创建了 {len(created_labels)} 个GUI标签")
# 返回值处理
if len(created_labels) == 1:
return created_labels[0][0]
else:
return [label for label, _ in created_labels]
except Exception as e:
print(f"❌ 创建GUI标签过程失败: {str(e)}")
import traceback
traceback.print_exc()
return None
def createGUIEntry(self, pos=(0, 0, 0), placeholder="输入文本...", size=0.08):
"""创建2D GUI文本输入框 - 支持多选创建和GUI父子关系优化版本"""
try:
from direct.gui.DirectGui import DirectEntry
from PyQt5.QtCore import Qt
print(f"📝 开始创建GUI输入框位置: {pos}, 占位符: {placeholder}, 尺寸: {size}")
# 获取树形控件
tree_widget = self._get_tree_widget()
if not tree_widget:
print("❌ 无法访问树形控件")
return None
# 使用CustomTreeWidget的方法获取目标父节点列表
target_parents = tree_widget.get_target_parents_for_gui_creation()
if not target_parents:
print("❌ 没有找到有效的父节点")
return None
created_entries = []
# 为每个有效的父节点创建GUI输入框
for parent_item, parent_node in target_parents:
try:
# 生成唯一名称
entry_name = f"GUIEntry_{len(self.gui_elements)}"
# 使用CustomTreeWidget的方法判断父节点类型并设置相应的挂载方式
if tree_widget.is_gui_element(parent_node):
# 父节点是GUI元素 - 作为子GUI挂载
gui_pos = tree_widget.calculate_relative_gui_position(pos, parent_node)
parent_gui_node = parent_node
print(f"📎 挂载到GUI父节点: {parent_node.getName()}")
else:
# 父节点是普通3D节点 - 使用屏幕坐标
gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1)
parent_gui_node = None
print(f"📎 挂载到3D父节点: {parent_item.text(0)}")
font = None
if hasattr(self.world,'getChineseFont'):
font = self.world.getChineseFont()
entry = DirectEntry(
text="",
pos=(pos[0],pos[1],pos[2]),
scale=size,
command=self.onGUIEntrySubmit,
extraArgs=[f"entry_{len(self.gui_elements)}"],
initialText=placeholder,
numLines=1,
width=12,
focus=0,
frameColor = (0,0,0,0),
text_fg=(1,1,1,1),
text_align=TextNode.ACenter,
text_wordwrap=None,
rolloverSound=None,
clickSound=None,
parent=parent_gui_node, # 设置GUI父节点
text_font = font,
frameSize=(-0.1,0.1,-0.05,0.05)
)
# 设置节点标签
entry.setTag("gui_type", "entry")
entry.setTag("gui_id", f"entry_{len(self.gui_elements)}")
entry.setTag("gui_placeholder", placeholder)
entry.setTag("is_gui_element", "1")
entry.setTag("tree_item_type", "GUI_ENTRY")
entry.setTag("is_scene_element", "1")
entry.setTag("created_by_user", "1")
entry.setTag("gui_parent_type", "gui" if parent_gui_node else "3d")
entry.setTag("name",entry_name)
entry.setName(entry_name)
# 如果有GUI父节点建立引用关系
if parent_gui_node:
parent_id = parent_gui_node.getTag("gui_id") if hasattr(parent_gui_node, 'getTag') else ""
entry.setTag("gui_parent_id", parent_id)
# 添加到GUI元素列表
self.gui_elements.append(entry)
print(f"✅ 为 {parent_item.text(0)} 创建GUI输入框成功: {entry_name}")
# 使用CustomTreeWidget的方法在Qt树形控件中添加对应节点
qt_item = tree_widget.add_node_to_tree_widget(entry, parent_item, "GUI_ENTRY")
if qt_item:
created_entries.append((entry, qt_item))
else:
created_entries.append((entry, None))
print("⚠️ Qt树节点添加失败但GUI对象已创建")
except Exception as e:
print(f"❌ 为 {parent_item.text(0)} 创建GUI输入框失败: {str(e)}")
continue
# 处理创建结果
if not created_entries:
print("❌ 没有成功创建任何GUI输入框")
return None
# 选中最后创建的输入框并更新场景树
# if created_entries:
# last_entry, last_qt_item = created_entries[-1]
# if last_qt_item:
# tree_widget.setCurrentItem(last_qt_item)
# tree_widget.update_selection_and_properties(last_entry, last_qt_item)
print(f"🎉 总共创建了 {len(created_entries)} 个GUI输入框")
# 返回值处理
if len(created_entries) == 1:
return created_entries[0][0]
else:
return [entry for entry, _ in created_entries]
except Exception as e:
print(f"❌ 创建GUI输入框过程失败: {str(e)}")
import traceback
traceback.print_exc()
return None
def createGUI2DImage(self, pos=(0, 0, 0), image_path=None, size=(1,1,1)):
"""创建2D GUI图片"""
try:
from direct.gui.DirectGui import DirectButton
from PyQt5.QtCore import Qt
print(f"🔘 开始创建GUI按钮位置: {pos}, 图片路径: {image_path}, 尺寸: {size}")
# 获取树形控件
tree_widget = self._get_tree_widget()
if not tree_widget:
print("❌ 无法访问树形控件")
return None
# 使用CustomTreeWidget的方法获取目标父节点列表
target_parents = tree_widget.get_target_parents_for_gui_creation()
if not target_parents:
print("❌ 没有找到有效的父节点")
return None
created_2dimage = []
# 为每个有效的父节点创建GUI按钮
for parent_item, parent_node in target_parents:
try:
# 生成唯一名称
image_name = f"GUIImage_{len(self.gui_elements)}"
# 使用CustomTreeWidget的方法判断父节点类型并设置相应的挂载方式
if tree_widget.is_gui_element(parent_node):
# 父节点是GUI元素 - 作为子GUI挂载
gui_pos = tree_widget.calculate_relative_gui_position(pos, parent_node)
parent_gui_node = parent_node # 直接挂载到GUI元素
print(f"📎 挂载到GUI父节点: {parent_node.getName()}")
else:
# 父节点是普通3D节点 - 使用屏幕坐标
gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1)
parent_gui_node = None # 使用默认的aspect2d
print(f"📎 挂载到3D父节点: {parent_item.text(0)}")
if isinstance(size, (list, tuple)) and len(size) >= 2:
# 分别处理宽度和高度的缩放
width_scale = size[0] * 0.25
height_scale = size[2] * 0.25
else:
# 如果只提供了一个缩放值,则使用相同值
width_scale = size * 0.1 if isinstance(size, (int, float)) else 0.2
height_scale = width_scale
cm = CardMaker("gui-2d-image")
cm.setFrame(-width_scale, width_scale, -height_scale, height_scale)
# image_node = self.world.aspect2d.attachNewNode(cm.generate())
if parent_gui_node:
image_node = parent_gui_node.attachNewNode(cm.generate())
else:
image_node = self.world.aspect2d.attachNewNode(cm.generate())
image_node.setPos(gui_pos)
image_node.setBin('fixed', 0)
image_node.setDepthWrite(False)
image_node.setDepthTest(False)
image_node.setColor(1, 1, 1, 1)
# 设置透明度支持
image_node.setTransparency(TransparencyAttrib.MAlpha)
# 如果提供了图像路径,则加载纹理
if image_path:
try:
image_node.setTag("image_path", image_path)
texture = self.world.loader.loadTexture(image_path)
if texture:
image_node.setTexture(texture, 1)
texture.setWrapU(Texture.WM_clamp)
texture.setWrapV(Texture.WM_clamp)
texture.setMinfilter(Texture.FT_linear)
texture.setMagfilter(Texture.FT_linear)
image_node.setColor(1, 1, 1, 1)
else:
print(f"⚠️ 无法加载2D图片纹理: {image_path}")
except Exception as e:
print(f"❌ 加载2D图片纹理失败: {e}")
# 设置节点标签
image_node.setTag("gui_type", "2d_image")
image_node.setTag("gui_id", f"2d_image_{len(self.gui_elements)}")
image_node.setTag("gui_text", f"2D图片_{len(self.gui_elements)}")
image_node.setTag("is_gui_element", "1")
image_node.setTag("is_scene_element", "1")
image_node.setTag("tree_item_type", "GUI_IMAGE")
image_node.setTag("created_by_user", "1")
image_node.setTag("gui_parent_type", "gui" if parent_gui_node else "3d")
image_node.setTag("name",image_name)
image_node.setName(image_name)
# 如果有GUI父节点建立引用关系
if parent_gui_node:
parent_id = parent_gui_node.getTag("gui_id") if hasattr(parent_gui_node, 'getTag') else ""
image_node.setTag("gui_parent_id", parent_id)
# 添加到GUI元素列表
self.gui_elements.append(image_node)
print(f"✅ 为 {parent_item.text(0)} 创建GUI按钮成功: {image_name}")
# 使用CustomTreeWidget的方法在Qt树形控件中添加对应节点
qt_item = tree_widget.add_node_to_tree_widget(image_node, parent_item, "GUI_IMAGE")
if qt_item:
created_2dimage.append((image_node, qt_item))
else:
created_2dimage.append((image_node, None))
print("⚠️ Qt树节点添加失败但GUI对象已创建")
except Exception as e:
print(f"❌ 为 {parent_item.text(0)} 创建GUI按钮失败: {str(e)}")
continue
# 处理创建结果
if not created_2dimage:
print("❌ 没有成功创建任何GUI按钮")
return None
# 选中最后创建的按钮并更新场景树
# if created_2dimage:
# last_button, last_qt_item = created_2dimage[-1]
# if last_qt_item:
# tree_widget.setCurrentItem(last_qt_item)
# tree_widget.update_selection_and_properties(last_button, last_qt_item)
print(f"🎉 总共创建了 {len(created_2dimage)} 个GUI按钮")
# 返回值处理
if len(created_2dimage) == 1:
return created_2dimage[0][0]
else:
return [button for button, _ in created_2dimage]
except Exception as e:
print(f"❌ 创建GUI按钮过程失败: {str(e)}")
import traceback
traceback.print_exc()
return None
def constrain2DPosition(self,gui_element,new_x=None,new_z=None):
"""限制2dGUI元素位置在屏幕范围内"""
try:
from panda3d.core import Vec3
bounds = gui_element.getTightBounds()
element_width=0
element_height=0
if bounds:
min_point,max_point = bounds
element_width = (max_point.getX() - min_point.getX())/2
element_height = (max_point.getZ()-min_point.getZ())/2
#获取当前缩放
scale = gui_element.getScale()
if hasattr(scale,'getX'):
scale_x = scale.getX()
scale_z = scale.getZ() if hasattr(scale,'getZ') else scale_x
else:
scale_x = scale_z = scale if isinstance(scale,(int,float)) else 1.0
actual_width = element_width * scale_x
actual_height = element_height * scale_z
screen_width = 1.9
screen_height = 0.9
min_x = -screen_width + actual_width
max_x = screen_width - actual_width
min_z = -screen_height + actual_height
max_z = screen_height - actual_height
#获取当前位置
current_pos = gui_element.getPos()
x = new_x if new_x is not None else current_pos.getX()
z = new_z if new_z is not None else current_pos.getZ()
#应用边界限制
x = max(min_x,min(max_x,x))
z = max(min_z,min(max_z,z))
return Vec3(x,current_pos.getY(),z)
except Exception as e:
print(f"约束2D位置时出错: {e}")
# 出错时返回原始值
current_pos = gui_element.getPos()
x = new_x if new_x is not None else current_pos.getX()
z = new_z if new_z is not None else current_pos.getZ()
return Vec3(x, current_pos.getY(), z)
def createGUI3DText(self, pos=(0, 0, 0), text="3D文本", size=0.5):
"""创建3D空间文本 - 支持多选创建,优化版本"""
try:
from panda3d.core import TextNode
from PyQt5.QtCore import Qt
print(f"📄 开始创建3D文本位置: {pos}, 文本: {text}, 尺寸: {size}")
# 获取树形控件
tree_widget = self._get_tree_widget()
if not tree_widget:
print("❌ 无法访问树形控件")
return None
# 获取目标父节点列表
target_parents = tree_widget.get_target_parents_for_creation()
if not target_parents:
print("❌ 没有找到有效的父节点")
return None
created_texts = []
# 为每个有效的父节点创建3D文本
for parent_item, parent_node in target_parents:
try:
# 生成唯一名称
text_name = f"GUI3DText_{len(self.gui_elements)}"
textNode = TextNode(f'3d-text-{len(self.gui_elements)}')
textNode.setText(text)
textNode.setAlign(TextNode.ACenter)
# 设置中文字体
chinese_font = self._get_chinese_font()
if chinese_font:
textNode.setFont(chinese_font)
# 挂载到选中的父节点
textNodePath = parent_node.attachNewNode(textNode)
textNodePath.setPos(*pos)
textNodePath.setScale(size)
textNodePath.setColor(1, 1, 0, 1)
#textNodePath.setBillboardAxis() # 让文本总是面向相机
textNodePath.setName(text_name)
# 设置节点标签
textNodePath.setTag("gui_type", "3d_text")
textNodePath.setTag("gui_id", f"3d_text_{len(self.gui_elements)}")
textNodePath.setTag("gui_text", text)
textNodePath.setTag("is_gui_element", "1")
textNodePath.setTag("is_scene_element", "1")
textNodePath.setTag("tree_item_type", "GUI_3DTEXT")
textNodePath.setTag("created_by_user", "1")
textNodePath.setTag("name", text_name)
# 添加到GUI元素列表
self.gui_elements.append(textNodePath)
print(f"✅ 为 {parent_item.text(0)} 创建3D文本成功: {text_name}")
# 在Qt树形控件中添加对应节点
qt_item = tree_widget.add_node_to_tree_widget(textNodePath, parent_item, "GUI_3DTEXT")
if qt_item:
created_texts.append((textNodePath, qt_item))
else:
created_texts.append((textNodePath, None))
print("⚠️ Qt树节点添加失败但GUI对象已创建")
except Exception as e:
print(f"❌ 为 {parent_item.text(0)} 创建3D文本失败: {str(e)}")
continue
# 处理创建结果
if not created_texts:
print("❌ 没有成功创建任何3D文本")
return None
# 选中最后创建的文本并更新场景树
# if created_texts:
# last_text, last_qt_item = created_texts[-1]
# if last_qt_item:
# tree_widget.setCurrentItem(last_qt_item)
# tree_widget.update_selection_and_properties(last_text, last_qt_item)
print(f"🎉 总共创建了 {len(created_texts)} 个3D文本")
# 返回值处理
if len(created_texts) == 1:
return created_texts[0][0]
else:
return [text_np for text_np, _ in created_texts]
except Exception as e:
print(f"❌ 创建3D文本过程失败: {str(e)}")
import traceback
traceback.print_exc()
return None
def createGUI3DImage(self, pos=(0, 0, 0), image_path=None, size=(1.0,1.0,1.0)):
"""创建3D空间图片"""
try:
from panda3d.core import TextNode
from PyQt5.QtCore import Qt
print(f"📄 开始创建3D文本位置: {pos}, 3D图片位置: {image_path}, 尺寸: {size}")
# 获取树形控件
tree_widget = self._get_tree_widget()
if not tree_widget:
print("❌ 无法访问树形控件")
return None
# 获取目标父节点列表
target_parents = tree_widget.get_target_parents_for_creation()
if not target_parents:
print("❌ 没有找到有效的父节点")
return None
created_3dimage = []
# 为每个有效的父节点创建3D文本
for parent_item, parent_node in target_parents:
try:
# 生成唯一名称
image_name = f"GUI3DImage_{len(self.gui_elements)}"
# 参数类型检查和转换
if isinstance(size, (list, tuple)):
if len(size) >= 2:
x_size, y_size = float(size[0]), float(size[2])
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, x_size, -y_size, y_size)
# 创建3D图像节点
# image_node = self.world.render.attachNewNode(cm.generate())
image_node = parent_node.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}")
image_node.setName(image_name)
# 设置节点标签
image_node.setTag("gui_type", "3d_image")
image_node.setTag("gui_id", f"3d_image_{len(self.gui_elements)}")
if image_path:
image_node.setTag("image_path", image_path)
image_node.setTag("is_gui_element", "1")
image_node.setTag("is_scene_element", "1")
image_node.setTag("tree_item_type", "GUI_3DIMAGE")
image_node.setTag("created_by_user", "1")
image_node.setTag("name",image_name)
# 添加到GUI元素列表
self.gui_elements.append(image_node)
print(f"✅ 为 {parent_item.text(0)} 创建3D文本成功: {image_name}")
# 在Qt树形控件中添加对应节点
qt_item = tree_widget.add_node_to_tree_widget(image_node, parent_item, "GUI_3DIMAGE")
if qt_item:
created_3dimage.append((image_node, qt_item))
else:
created_3dimage.append((image_node, None))
print("⚠️ Qt树节点添加失败但GUI对象已创建")
except Exception as e:
print(f"❌ 为 {parent_item.text(0)} 创建3D文本失败: {str(e)}")
continue
# 处理创建结果
if not created_3dimage:
print("❌ 没有成功创建任何3D文本")
return None
# 选中最后创建的文本并更新场景树
# if created_3dimage:
# last_image, last_qt_item = created_3dimage[-1]
# if last_qt_item:
# tree_widget.setCurrentItem(last_qt_item)
# tree_widget.update_selection_and_properties(last_image, last_qt_item)
print(f"🎉 总共创建了 {len(created_3dimage)} 个3D文本")
# 返回值处理
if len(created_3dimage) == 1:
return created_3dimage[0][0]
else:
return [text_np for text_np, _ in created_3dimage]
except Exception as e:
print(f"❌ 创建3D文本过程失败: {str(e)}")
import traceback
traceback.print_exc()
return None
def createVideoScreen(self, pos=(0, 0, 0), size=1, video_path=None):
"""创建3D视频播放屏幕 - 添加占位符纹理支持"""
try:
from panda3d.core import CardMaker, TransparencyAttrib, Texture, TextureStage
import os
# 确保 pos 是有效的三维坐标元组
if not isinstance(pos, (tuple, list)) or len(pos) != 3:
print(f"⚠️ 位置参数无效,使用默认值 (0, 0, 0),原始值: {pos}")
pos = (0, 0, 0)
else:
# 确保所有坐标都是数值类型
try:
pos = (float(pos[0]), float(pos[1]), float(pos[2]))
except (ValueError, TypeError):
print(f"⚠️ 位置参数包含非数值,使用默认值 (0, 0, 0),原始值: {pos}")
pos = (0, 0, 0)
# 确保 size 是有效数值
try:
size = float(size)
except (ValueError, TypeError):
print(f"⚠️ 尺寸参数无效,使用默认值 0.2,原始值: {size}")
size = 0.2*5
print(f"📺 开始创建视频屏幕,位置: {pos}, 尺寸: {size}, 视频路径: {video_path}")
# 获取树形控件
tree_widget = self._get_tree_widget()
if not tree_widget:
print("❌ 无法访问树形控件")
return None
# 获取目标父节点列表
target_parents = tree_widget.get_target_parents_for_creation()
if not target_parents:
print("❌ 没有找到有效的父节点")
return None
created_videoscreens = []
# 为每个有效的父节点创建视频屏幕
for parent_item, parent_node in target_parents:
try:
# 生成唯一名称
screen_name = f"VideoScreen_{len(self.gui_elements)}"
# 使用CardMaker创建视频屏幕框架
cm = CardMaker('video-screen')
cm.setFrame(-size, size, -size, size)
# 创建挂载节点 - 挂载到选中的父节点
video_screen = parent_node.attachNewNode(cm.generate())
video_screen.setPos(*pos)
video_screen.setName(screen_name)
video_screen.setBin('fixed', 10)
# 设置透明度支持
video_screen.setTransparency(TransparencyAttrib.MAlpha)
# 设置初始颜色为白色,确保纹理能正确显示
video_screen.setColor(1, 1, 1, 1)
# 确保视频屏幕有正确的材质
self._ensureVideoScreenMaterial(video_screen)
# 设置节点标签
video_screen.setTag("gui_type", "video_screen")
video_screen.setTag("gui_id", f"video_screen_{len(self.gui_elements)}")
video_screen.setTag("gui_text", f"视频屏幕_{len(self.gui_elements)}")
video_screen.setTag("is_gui_element", "1")
video_screen.setTag("is_scene_element", "1")
video_screen.setTag("tree_item_type", "GUI_VIDEO_SCREEN")
video_screen.setTag("created_by_user", "1")
video_screen.setTag("name",screen_name)
# 设置视频路径标签
if video_path and os.path.exists(video_path):
video_screen.setTag("video_path", video_path)
else:
video_screen.setTag("video_path", "")
# 关键修改:预先创建一个占位符纹理,为后续视频播放做准备
placeholder_texture = Texture(f"placeholder_video_texture_3d_{len(self.gui_elements)}")
placeholder_texture.setup2dTexture(1, 1, Texture.TUnsignedByte, Texture.FRgb)
placeholder_data = b'\x19\x19\x4c' # 深蓝色占位符颜色 (25, 25, 76)
placeholder_texture.setRamImage(placeholder_data)
# 创建纹理阶段并应用占位符纹理到视频屏幕
texture_stage = TextureStage("video_placeholder")
texture_stage.setSort(0)
texture_stage.setMode(TextureStage.MModulate)
video_screen.setTexture(texture_stage, placeholder_texture)
# 保存占位符纹理引用
video_screen.setPythonTag("placeholder_texture", placeholder_texture)
print(f"🔧 为3D视频屏幕创建了占位符纹理环境: {screen_name}")
# 如果提供了视频路径,则加载视频纹理
movie_texture = None
if video_path:
if video_path.startswith(("http://","https://")):
try:
if hasattr(self.world,'property_panel'):
success = self.world.property_panel._loadVideoFromURLWithOpenCV_3D(video_screen,video_path)
if success:
print(f"✅ 视频流URL加载成功: {video_path}")
else:
print(f"⚠️ 视频流URL加载失败: {video_path}")
else:
print("⚠️ property_manager不可用无法加载视频流")
except Exception as e:
print(f"❌ 加载视频流URL失败: {e}")
import traceback
traceback.print_exc()
elif os.path.exists(video_path):
# 对于本地视频文件,使用原有方法
try:
print(f"🔍 尝试加载2D视频纹理: {video_path}")
# 加载视频纹理
movie_texture = self._loadMovieTexture(video_path)
if movie_texture:
# 应用纹理到视频屏幕(替换占位符)
video_screen["frameTexture"] = movie_texture
print(f"✅ 2D视频纹理加载成功: {video_path}")
# 保存视频纹理引用以便后续控制
video_screen.setPythonTag("movie_texture", movie_texture)
# 尝试自动播放视频(如果支持)
try:
if hasattr(movie_texture, 'play'):
movie_texture.play()
print("▶️ 2D视频已开始播放")
except Exception as play_error:
print(f"⚠️ 2D视频自动播放失败: {play_error}")
else:
print(f"⚠️ 无法加载2D视频纹理: {video_path}")
except Exception as e:
print(f"❌ 加载2D视频纹理失败: {e}")
import traceback
traceback.print_exc()
else:
print(f"⚠️ 2D视频文件不存在: {video_path}")
# if video_path and os.path.exists(video_path):
# try:
# print(f"🔍 尝试加载视频纹理: {video_path}")
# # 加载视频纹理
# movie_texture = self._loadMovieTexture(video_path)
# if movie_texture:
# # 创建纹理阶段
# texture_stage = TextureStage("video")
# texture_stage.setSort(0)
# texture_stage.setMode(TextureStage.MModulate)
# video_screen.setTexture(texture_stage, movie_texture)
#
# print(f"✅ 视频纹理加载成功: {video_path}")
#
# # 保存视频纹理引用以便后续控制
# video_screen.setPythonTag("movie_texture", movie_texture)
#
# # 尝试自动播放视频(如果支持)
# try:
# if hasattr(movie_texture, 'play'):
# movie_texture.play()
# print("▶️ 视频已开始播放")
# except Exception as play_error:
# print(f"⚠️ 视频自动播放失败: {play_error}")
# else:
# print(f"⚠️ 无法加载视频纹理: {video_path}")
# # 使用默认颜色作为占位符
# video_screen.setColor(0.1, 0.1, 0.3, 0.8)
# except Exception as e:
# print(f"❌ 加载视频纹理失败: {e}")
# import traceback
# traceback.print_exc()
# # 使用默认颜色作为占位符
# video_screen.setColor(0.1, 0.1, 0.3, 0.8)
# else:
# # 没有视频文件时显示默认颜色
# video_screen.setColor(0.1, 0.1, 0.3, 0.8)
# if video_path:
# print(f"⚠️ 视频文件不存在: {video_path}")
# else:
# print(" 未提供视频文件,显示默认占位符")
#
# # 保存视频纹理引用以便后续控制
# if movie_texture:
# video_screen.setPythonTag("movie_texture", movie_texture)
# 添加到GUI元素列表
self.gui_elements.append(video_screen)
print(f"✅ 为 {parent_item.text(0)} 创建视频屏幕成功: {screen_name}")
# 在Qt树形控件中添加对应节点
qt_item = tree_widget.add_node_to_tree_widget(video_screen, parent_item, "GUI_VIDEO_SCREEN")
if qt_item:
created_videoscreens.append((video_screen, qt_item))
else:
created_videoscreens.append((video_screen, None))
print("⚠️ Qt树节点添加失败但Panda3D对象已创建")
except Exception as e:
print(f"❌ 为 {parent_item.text(0)} 创建视频屏幕失败: {str(e)}")
import traceback
traceback.print_exc()
continue
# 处理创建结果
if not created_videoscreens:
print("❌ 没有成功创建任何视频屏幕")
return None
# 选中最后创建的视频屏幕
# if created_videoscreens:
# last_screen_np, last_qt_item = created_videoscreens[-1]
# if last_qt_item:
# tree_widget.setCurrentItem(last_qt_item)
# # 更新选择和属性面板
# tree_widget.update_selection_and_properties(last_screen_np, last_qt_item)
print(f"🎉 总共创建了 {len(created_videoscreens)} 个视频屏幕")
# 返回值处理
if len(created_videoscreens) == 1:
return created_videoscreens[0][0] # 单个屏幕返回NodePath
else:
return [screen_np for screen_np, _ in created_videoscreens] # 多个屏幕返回列表
except Exception as e:
print(f"❌ 创建视频屏幕过程失败: {str(e)}")
import traceback
traceback.print_exc()
return None
def _ensureVideoScreenMaterial(self, video_screen):
"""确保视频屏幕有正确的材质设置"""
try:
from panda3d.core import Material, LColor
# 如果还没有材质,则创建一个
if not video_screen.hasMaterial():
material = Material(f"video-material-{video_screen.getName()}")
material.setBaseColor(LColor(1, 1, 1, 1))
material.setDiffuse(LColor(1, 1, 1, 1))
material.setAmbient(LColor(1, 1, 1, 1)) # 确保环境光为白色
material.setEmission(LColor(0, 0, 0, 1))
material.setSpecular(LColor(0, 0, 0, 1))
material.setShininess(0)
video_screen.setMaterial(material, 1)
print(f"✅ 为视频屏幕创建了新材质: {video_screen.getName()}")
else:
# 更新现有材质确保正确设置
material = video_screen.getMaterial()
material.setBaseColor(LColor(1, 1, 1, 1))
material.setAmbient(LColor(1, 1, 1, 1)) # 确保环境光为白色
video_screen.setMaterial(material, 1)
print(f"✅ 更新了视频屏幕材质: {video_screen.getName()}")
except Exception as e:
print(f"⚠️ 设置视频屏幕材质时出错: {e}")
def _debugVideoScreenTextures(self, video_screen):
"""调试视频屏幕的纹理状态"""
try:
print(f"调试视频屏幕 {video_screen.getName()}:")
# 检查PythonTag
movie_texture = video_screen.getPythonTag("movie_texture")
if movie_texture:
print(f" - PythonTag movie_texture: {type(movie_texture)}")
if hasattr(movie_texture, 'is_playable'):
print(f" - is_playable: {movie_texture.is_playable()}")
else:
print(" - PythonTag movie_texture: None")
# 检查所有纹理阶段
texture_stages = video_screen.findAllTextureStages()
print(f" - 纹理阶段数: {texture_stages.getNumStages()}")
for i in range(texture_stages.getNumStages()):
stage = texture_stages.getStage(i)
texture = video_screen.getTexture(stage)
print(f" - 阶段 {i}: {stage.getName()}, 纹理: {texture.getName() if texture else 'None'}")
except Exception as e:
print(f"调试视频屏幕纹理时出错: {e}")
def playVideo(self, video_screen):
"""播放视频 - 改进版本,支持从暂停处继续播放"""
try:
# 获取视频纹理
movie_texture = self._getMovieTextureFromScreen(video_screen)
if movie_texture:
# 检查是否有播放方法
if hasattr(movie_texture, 'play'):
try:
self.loadVideoFile(video_screen, video_screen.getTag("video_path"))
movie_texture.play()
print(f"▶️ 继续播放视频: {video_screen.getName()}")
return True
except Exception as play_error:
print(f"⚠️ 播放视频时出错: {play_error}")
return False
else:
print(f"⚠️ 纹理对象没有播放方法: {video_screen.getName()}")
return False
else:
self.loadVideoFile(video_screen,video_screen.getTag("video_path"))
except Exception as e:
print(f"❌ 播放视频失败: {e}")
import traceback
traceback.print_exc()
return False
def _getMovieTextureFromScreen(self, video_screen):
"""从视频屏幕获取视频纹理"""
try:
# 方法1: 从PythonTag获取
movie_texture = video_screen.getPythonTag("movie_texture")
if movie_texture:
return movie_texture
# 方法2: 从纹理阶段获取
from panda3d.core import TextureStage
texture_stage = video_screen.findTextureStage("video")
if texture_stage:
movie_texture = video_screen.getTexture(texture_stage)
if movie_texture:
return movie_texture
# 方法3: 获取第一个纹理
if video_screen.hasTexture():
movie_texture = video_screen.getTexture()
if movie_texture:
return movie_texture
return None
except Exception as e:
print(f"获取视频纹理时出错: {e}")
return None
def pauseVideo(self, video_screen):
"""暂停视频"""
try:
movie_texture = self._getMovieTextureFromScreen(video_screen)
if movie_texture:
# 检查是否有暂停方法
if hasattr(movie_texture, 'stop'): # MovieTexture使用stop来暂停
try:
movie_texture.stop()
print(f"⏸️ 视频已暂停: {video_screen.getName()}")
return True
except Exception as stop_error:
print(f"⚠️ 暂停视频时出错: {stop_error}")
return False
elif hasattr(movie_texture, 'set play rate'): # 某些版本支持设置播放速率
try:
movie_texture.setPlayRate(0.0)
print(f"⏸️ 视频已暂停(播放速率设为0): {video_screen.getName()}")
return True
except Exception as rate_error:
print(f"⚠️ 设置播放速率时出错: {rate_error}")
return False
else:
print(f"⚠️ 纹理对象没有暂停方法: {video_screen.getName()}")
return False
else:
print(f"❌ 视频屏幕没有关联的视频纹理: {video_screen.getName()}")
return False
except Exception as e:
print(f"❌ 暂停视频失败: {e}")
import traceback
traceback.print_exc()
return False
def stopVideo(self, video_screen):
"""停止视频(回到开头)"""
try:
movie_texture = self._getMovieTextureFromScreen(video_screen)
if movie_texture:
# 停止并重置到开头
if hasattr(movie_texture, 'stop'):
try:
movie_texture.stop()
# 如果有重置方法,调用它
if hasattr(movie_texture, 'setTime'):
movie_texture.setTime(0.0)
print(f"⏹️ 视频已停止: {video_screen.getName()}")
return True
except Exception as stop_error:
print(f"⚠️ 停止视频时出错: {stop_error}")
return False
else:
print(f"⚠️ 纹理对象没有停止方法: {video_screen.getName()}")
return False
else:
print(f"❌ 视频屏幕没有关联的视频纹理: {video_screen.getName()}")
return False
except Exception as e:
print(f"❌ 停止视频失败: {e}")
import traceback
traceback.print_exc()
return False
def setVideoTime(self, video_screen, time_seconds):
"""设置视频播放时间"""
try:
movie_texture = video_screen.getPythonTag("movie_texture")
# 备用获取方法
if not movie_texture:
from panda3d.core import TextureStage
texture_stage = video_screen.findTextureStage("video")
if texture_stage:
movie_texture = video_screen.getTexture(texture_stage)
if not movie_texture and video_screen.hasTexture():
movie_texture = video_screen.getTexture()
if movie_texture and hasattr(movie_texture, 'set_time'):
movie_texture.set_time(time_seconds)
print(f"🕒 设置视频时间 {time_seconds}s: {video_screen.getName()}")
return True
else:
print(f"❌ 视频屏幕没有关联的视频纹理: {video_screen.getName()}")
return False
except Exception as e:
print(f"❌ 设置视频时间失败: {e}")
return False
def loadVideoFile(self, video_screen, video_path):
"""为视频屏幕加载新的视频文件"""
try:
import os
if not os.path.exists(video_path):
print(f"❌ 视频文件不存在: {video_path}")
return False
# Convert Windows path to Panda3D compatible path format
from panda3d.core import Filename
panda_path = Filename.fromOsSpecific(video_path)
converted_path = str(panda_path)
# 加载新的视频纹理
movie_texture = self._loadMovieTexture(video_path) # Pass original path for existence check
if movie_texture:
# 清除现有的纹理
video_screen.clearTexture()
# 设置视频纹理属性
movie_texture.setWrapU(Texture.WM_clamp)
movie_texture.setWrapV(Texture.WM_clamp)
movie_texture.setMinfilter(Texture.FT_linear)
movie_texture.setMagfilter(Texture.FT_linear)
# 如果视频纹理支持循环播放,设置循环
if hasattr(movie_texture, 'set_loop'):
movie_texture.set_loop(True)
# 如果视频纹理支持播放速率控制,设置正常速率
if hasattr(movie_texture, 'set_play_rate'):
movie_texture.set_play_rate(1.0)
# 重要:为视频纹理创建专用的纹理阶段
texture_stage = TextureStage("video")
texture_stage.setSort(0) # 使用第一个纹理槽
texture_stage.setMode(TextureStage.MModulate)
# 应用纹理到视频屏幕
video_screen.setTexture(texture_stage, movie_texture)
# 保存新的视频纹理引用到PythonTag
video_screen.setPythonTag("movie_texture", movie_texture)
video_screen.setTag("video_path", converted_path) # Store converted path
# 确保视频屏幕有正确的材质
self._ensureVideoScreenMaterial(video_screen)
print(f"✅ 成功加载新视频: {converted_path}")
return True
else:
print(f"❌ 无法加载视频文件: {converted_path}")
return False
except Exception as e:
print(f"❌ 加载视频文件失败: {e}")
import traceback
traceback.print_exc()
return False
def _loadMovieTexture(self, video_path):
"""加载视频纹理的兼容方法"""
try:
from panda3d.core import Texture, MovieTexture, Filename
import os
# 检查文件是否存在
if not os.path.exists(video_path):
print(f"❌ 视频文件不存在: {video_path}")
return None
# Convert Windows path to Panda3D compatible path format
panda_path = Filename.fromOsSpecific(video_path)
converted_path = str(panda_path)
print(f"🔍 尝试加载视频文件: {converted_path}")
# 方法1: 尝试使用 MovieTexture专门用于视频
try:
movie_texture = MovieTexture(converted_path)
if movie_texture.read(converted_path):
print("✅ 使用 MovieTexture 成功加载视频")
self._configureVideoTexture(movie_texture)
return movie_texture
else:
print("⚠️ MovieTexture.read() 返回失败")
except Exception as e:
print(f"⚠️ MovieTexture 方法失败: {e}")
# 方法2: 尝试使用 loader.loadTexture
try:
movie_texture = self.world.loader.loadTexture(converted_path)
if movie_texture and hasattr(movie_texture, 'is_playable') and movie_texture.is_playable():
print("✅ 使用 loader.loadTexture 成功加载视频纹理")
self._configureVideoTexture(movie_texture)
return movie_texture
else:
print("⚠️ loader.loadTexture 加载的不是可播放的视频纹理")
except Exception as e:
print(f"⚠️ loader.loadTexture 方法失败: {e}")
# 方法3: 尝试使用 Texture.read作为最后备选
try:
texture = Texture()
if texture.read(converted_path):
print("✅ 使用 Texture.read 成功加载(可能作为静态纹理)")
self._configureVideoTexture(texture)
return texture
except Exception as e:
print(f"⚠️ Texture.read 方法失败: {e}")
print("❌ 所有视频纹理加载方法都失败")
return None
except Exception as e:
print(f"❌ 加载视频纹理时发生未知错误: {e}")
import traceback
traceback.print_exc()
return None
def _configureVideoTexture(self, texture):
"""配置视频纹理属性"""
try:
from panda3d.core import Texture
# 设置纹理属性
texture.setWrapU(Texture.WM_clamp)
texture.setWrapV(Texture.WM_clamp)
texture.setMinfilter(Texture.FT_linear)
texture.setMagfilter(Texture.FT_linear)
# 如果是可播放的视频纹理,设置播放属性
if hasattr(texture, 'set_loop') and hasattr(texture, 'set_play_rate'):
texture.set_loop(True)
texture.set_play_rate(1.0)
print(f"✅ 视频纹理配置完成: {texture.getName()}")
except Exception as e:
print(f"⚠️ 配置视频纹理时出错: {e}")
def createGUI2DVideoScreen(self, pos=(0, 0), size=0.2, video_path=None):
"""创建2D视频播放屏幕 - 使用2D坐标"""
try:
from direct.gui.DirectGui import DirectFrame
from panda3d.core import TransparencyAttrib, Texture, TextureStage
from PyQt5.QtCore import Qt
import os
# 确保 pos 是有效的二维坐标元组
if pos is None or pos is False or not isinstance(pos, (tuple, list)) or len(pos) != 2:
print(f"⚠️ 位置参数无效,使用默认值 (0, 0),原始值: {pos}")
pos = (0, 0)
else:
# 确保所有坐标都是数值类型
try:
pos = (float(pos[0]), float(pos[1]))
except (ValueError, TypeError, IndexError) as e:
print(f"⚠️ 位置参数包含非数值或索引错误,使用默认值 (0, 0),原始值: {pos}, 错误: {e}")
pos = (0, 0)
# 确保 size 是有效数值
try:
size = float(size)
except (ValueError, TypeError) as e:
print(f"⚠️ 尺寸参数无效,使用默认值 0.2,原始值: {size}, 错误: {e}")
size = 0.2
# 获取树形控件
tree_widget = self._get_tree_widget()
if not tree_widget:
print("❌ 无法访问树形控件")
return None
# 获取目标父节点列表
target_parents = tree_widget.get_target_parents_for_gui_creation()
if not target_parents:
print("❌ 没有找到有效的父节点")
return None
created_videoscreens = []
# 为每个有效的父节点创建2D视频屏幕
for parent_item, parent_node in target_parents:
try:
# 生成唯一名称
screen_name = f"GUI2DVideoScreen_{len(self.gui_elements)}"
# 使用DirectFrame创建2D视频屏幕
video_screen = DirectFrame(
frameSize=(-size, size, -size, size),
frameColor=(1, 1, 1, 1), # 默认背景色
pos=(pos[0] * 0.1, 0, pos[1] * 0.1), # 转换为屏幕坐标
parent=parent_node if tree_widget.is_gui_element(parent_node) else self.world.aspect2d,
suppressMouse=True,
)
video_screen.setName(screen_name)
# 设置透明度支持
video_screen.setTransparency(TransparencyAttrib.MAlpha)
#设置2D视频屏幕特有的标签
video_screen.setTag("gui_type", "2d_video_screen")
video_screen.setTag("gui_id", f"2d_video_screen_{len(self.gui_elements)}")
video_screen.setTag("gui_text", f"2D视频屏幕_{len(self.gui_elements)}")
video_screen.setTag("is_gui_element", "1")
video_screen.setTag("is_scene_element", "1")
video_screen.setTag("tree_item_type", "GUI_2D_VIDEO_SCREEN")
video_screen.setTag("created_by_user", "1")
video_screen.setTag("name",screen_name)
video_screen.setTag("video_path",video_path or "")
# 关键修改:预先创建一个占位符纹理,为后续视频播放做准备
placeholder_texture = Texture(f"placeholder_video_texture_{len(self.gui_elements)}")
placeholder_texture.setup2dTexture(1, 1, Texture.TUnsignedByte, Texture.FRgb)
placeholder_data = b'\x19\x19\x4c' # 深蓝色占位符颜色 (25, 25, 76)
placeholder_texture.setRamImage(placeholder_data)
# 应用占位符纹理到视频屏幕
video_screen["frameTexture"] = placeholder_texture
# 保存占位符纹理引用
video_screen.setPythonTag("placeholder_texture", placeholder_texture)
print(f"🔧 为2D视频屏幕创建了占位符纹理环境: {screen_name}")
# 如果提供了视频路径,则加载视频纹理
movie_texture = None
if video_path:
if video_path.startswith(("http://","https://")):
try:
if hasattr(self.world,'property_panel'):
success = self.world.property_panel._loadVideoFromURLWithOpenCV(video_screen,video_path)
if success:
print(f"✅ 视频流URL加载成功: {video_path}")
else:
print(f"⚠️ 视频流URL加载失败: {video_path}")
else:
print("⚠️ property_manager不可用无法加载视频流")
except Exception as e:
print(f"❌ 加载视频流URL失败: {e}")
import traceback
traceback.print_exc()
elif os.path.exists(video_path):
try:
print(f"🔍 尝试加载2D视频纹理: {video_path}")
# 加载视频纹理
movie_texture = self._loadMovieTexture(video_path)
if movie_texture:
# 应用纹理到视频屏幕(替换占位符)
video_screen["frameTexture"] = movie_texture
print(f"✅ 2D视频纹理加载成功: {video_path}")
# 保存视频纹理引用以便后续控制
video_screen.setPythonTag("movie_texture", movie_texture)
# 尝试自动播放视频(如果支持)
try:
if hasattr(movie_texture, 'play'):
movie_texture.play()
print("▶️ 2D视频已开始播放")
except Exception as play_error:
print(f"⚠️ 2D视频自动播放失败: {play_error}")
else:
print(f"⚠️ 无法加载2D视频纹理: {video_path}")
except Exception as e:
print(f"❌ 加载2D视频纹理失败: {e}")
import traceback
traceback.print_exc()
else:
print(f"⚠️ 2D视频文件不存在: {video_path}")
# if video_path and os.path.exists(video_path):
# try:
# print(f"🔍 尝试加载2D视频纹理: {video_path}")
# # 加载视频纹理
# movie_texture = self._loadMovieTexture(video_path)
# if movie_texture:
# # 应用纹理到视频屏幕(替换占位符)
# video_screen["frameTexture"] = movie_texture
# print(f"✅ 2D视频纹理加载成功: {video_path}")
#
# # 保存视频纹理引用以便后续控制
# video_screen.setPythonTag("movie_texture", movie_texture)
#
# # 尝试自动播放视频(如果支持)
# try:
# if hasattr(movie_texture, 'play'):
# movie_texture.play()
# print("▶️ 2D视频已开始播放")
# except Exception as play_error:
# print(f"⚠️ 2D视频自动播放失败: {play_error}")
# else:
# print(f"⚠️ 无法加载2D视频纹理: {video_path}")
# except Exception as e:
# print(f"❌ 加载2D视频纹理失败: {e}")
# import traceback
# traceback.print_exc()
# else:
# if not video_path:
# print(f"⚠️ 2D视频文件不存在: {video_path}")
# 添加到GUI元素列表
self.gui_elements.append(video_screen)
print(f"✅ 为 {parent_item.text(0)} 创建2D视频屏幕成功: {screen_name}")
# 在Qt树形控件中添加对应节点
qt_item = tree_widget.add_node_to_tree_widget(video_screen, parent_item, "GUI_2D_VIDEO_SCREEN")
if qt_item:
created_videoscreens.append((video_screen, qt_item))
else:
created_videoscreens.append((video_screen, None))
print("⚠️ Qt树节点添加失败但Panda3D对象已创建")
except Exception as e:
print(f"❌ 为 {parent_item.text(0)} 创建2D视频屏幕失败: {str(e)}")
import traceback
traceback.print_exc()
continue
# 处理创建结果
if not created_videoscreens:
print("❌ 没有成功创建任何2D视频屏幕")
return None
# 选中最后创建的视频屏幕
# if created_videoscreens:
# last_screen_np, last_qt_item = created_videoscreens[-1]
# if last_qt_item:
# tree_widget.setCurrentItem(last_qt_item)
# # 更新选择和属性面板
# tree_widget.update_selection_and_properties(last_screen_np, last_qt_item)
print(f"🎉 总共创建了 {len(created_videoscreens)} 个2D视频屏幕")
# 返回值处理
if len(created_videoscreens) == 1:
return created_videoscreens[0][0] # 单个屏幕返回NodePath
else:
return [screen_np for screen_np, _ in created_videoscreens] # 多个屏幕返回列表
except Exception as e:
print(f"❌ 创建2D视频屏幕过程失败: {str(e)}")
import traceback
traceback.print_exc()
return None
def load2DVideoFile(self, video_screen, video_path):
"""为2D视频屏幕加载新的视频文件"""
try:
import os
video_screen.setTag("video_path",video_path)
path = video_screen.getTag("video_path")
print({video_screen.getTag("gui_type")})
print(f"🔧 更新2D视频屏幕标签 - video_path: {path}")
if not video_path or not os.path.exists(video_path):
print(f"❌ 2D视频文件不存在: {video_path}")
return False
# 加载新的视频纹理
movie_texture = self._loadMovieTexture(video_path)
if movie_texture:
# 应用纹理到2D视频屏幕
video_screen["frameTexture"] = movie_texture
# 保存视频纹理引用
video_screen.setPythonTag("movie_texture", movie_texture)
print(f"✅ 成功加载新2D视频: {video_path}")
return True
else:
print(f"❌ 无法加载2D视频文件: {video_path}")
return False
except Exception as e:
print(f"❌ 加载2D视频文件失败: {e}")
import traceback
traceback.print_exc()
return False
def play2DVideo(self, video_screen):
"""播放2D视频"""
try:
if video_screen.hasPythonTag("video_capture"):
print("视频已在播放中")
return True
# 获取视频路径并重新开始播放
video_path = video_screen.getTag("video_path")
if video_path:
return self.world.property_manager._loadVideoFromURL(video_screen, video_path)
else:
print("❌ 没有找到视频源")
return False
except Exception as e:
print(f"❌ 播放视频失败: {e}")
return False
def pause2DVideo(self, video_screen):
"""暂停2D视频"""
try:
# 在OpenCV中没有直接的暂停功能我们通过停止线程来模拟暂停
if video_screen.hasPythonTag("video_capture"):
cap = video_screen.getPythonTag("video_capture")
if cap:
cap.release()
video_screen.clearPythonTag("video_capture")
print("⏸️ 视频已暂停")
return True
return False
except Exception as e:
print(f"❌ 暂停视频失败: {e}")
return False
def stop2DVideo(self, video_screen):
"""停止2D视频"""
try:
# 停止视频捕获
if video_screen.hasPythonTag("video_capture"):
cap = video_screen.getPythonTag("video_capture")
if cap:
cap.release()
video_screen.clearPythonTag("video_capture")
# 清理纹理
if video_screen.hasPythonTag("video_texture"):
video_screen.clearPythonTag("video_texture")
# 清理视频路径标签
video_screen.clearTag("video_path")
print("⏹️ 视频已停止")
return True
except Exception as e:
print(f"❌ 停止视频失败: {e}")
return False
def createSphericalVideo(self, pos=(0, 0, 0), radius=5.0, video_path=None):
"""创建球形视频360度视频- 支持无初始视频文件"""
try:
from panda3d.core import GeomVertexFormat, GeomVertexData, GeomVertexWriter
from panda3d.core import Geom, GeomTriangles, GeomNode
from panda3d.core import TextureStage, Texture
import math
import os
# 确保 pos 是有效的三维坐标元组
if not isinstance(pos, (tuple, list)) or len(pos) != 3:
print(f"⚠️ 位置参数无效,使用默认值 (0, 0, 0),原始值: {pos}")
pos = (0, 0, 0)
else:
# 确保所有坐标都是数值类型
try:
pos = (float(pos[0]), float(pos[1]), float(pos[2]))
except (ValueError, TypeError):
print(f"⚠️ 位置参数包含非数值,使用默认值 (0, 0, 0),原始值: {pos}")
pos = (0, 0, 0)
# 确保 radius 是有效数值
try:
radius = float(radius)
except (ValueError, TypeError):
print(f"⚠️ 半径参数无效,使用默认值 5.0,原始值: {radius}")
radius = 5.0
print(f"🌍 开始创建球形视频,位置: {pos}, 半径: {radius}, 视频路径: {video_path}")
# 不再强制检查视频文件是否存在,允许创建空的球形视频
if video_path and not os.path.exists(video_path):
print(f"⚠️ 视频文件不存在,将创建空的球形视频: {video_path}")
# 获取树形控件
tree_widget = self._get_tree_widget()
if not tree_widget:
print("❌ 无法访问树形控件")
return None
# 获取目标父节点列表
target_parents = tree_widget.get_target_parents_for_creation()
if not target_parents:
print("❌ 没有找到有效的父节点")
return None
created_spherical_videos = []
# 为每个有效的父节点创建球形视频
for parent_item, parent_node in target_parents:
try:
# 生成唯一名称
sphere_name = f"SphericalVideo_{len(self.gui_elements)}"
# 创建球形几何体
sphere_geom = self._createSphereGeometry(radius, segments=32)
# 创建几何节点
sphere_node = GeomNode('sphere_video')
sphere_node.addGeom(sphere_geom)
# 创建节点路径并挂载
sphere_np = parent_node.attachNewNode(sphere_node)
sphere_np.setPos(*pos) # 现在 pos 已经是有效的元组
sphere_np.setName(sphere_name)
# 翻转法线,使视频在球体内部显示
sphere_np.setTwoSided(True)
sphere_np.setBin('background', 0) # 确保在背景层
sphere_np.setDepthWrite(False) # 不写入深度缓冲
# 设置初始颜色为占位符颜色
sphere_np.setColor(0.1, 0.1, 0.3, 0.8) # 深蓝色占位符
# 如果提供了视频路径且文件存在,则加载视频纹理
movie_texture = None
if video_path and os.path.exists(video_path):
try:
# 加载视频纹理
movie_texture = self._loadMovieTexture(video_path)
if movie_texture:
# 设置视频纹理属性
movie_texture.setWrapU(Texture.WM_clamp)
movie_texture.setWrapV(Texture.WM_clamp)
movie_texture.setMinfilter(Texture.FT_linear)
movie_texture.setMagfilter(Texture.FT_linear)
# 为视频纹理创建专用的纹理阶段
texture_stage = TextureStage("spherical_video")
texture_stage.setMode(TextureStage.MModulate)
# 应用纹理到球体
sphere_np.setTexture(texture_stage, movie_texture)
# 设置为白色以正确显示视频
sphere_np.setColor(1, 1, 1, 1)
print(f"✅ 视频纹理已应用到球形视频: {video_path}")
# 尝试自动播放
try:
if hasattr(movie_texture, 'play'):
movie_texture.play()
print("▶️ 球形视频已开始播放")
except Exception as play_error:
print(f"⚠️ 球形视频自动播放失败: {play_error}")
else:
print(f"⚠️ 无法加载视频纹理: {video_path}")
# 保持占位符颜色
except Exception as e:
print(f"❌ 加载视频纹理失败: {e}")
import traceback
traceback.print_exc()
# 保持占位符颜色
else:
if video_path:
print(f"⚠️ 视频文件不存在: {video_path}")
else:
print(" 未提供视频文件,创建空的球形视频")
# 设置标签以便识别和管理
sphere_np.setTag("gui_type", "spherical_video")
sphere_np.setTag("is_gui_element", "1")
sphere_np.setTag("tree_item_type", "GUI_SPHERICAL_VIDEO")
sphere_np.setTag("video_path", video_path or "")
sphere_np.setTag("original_radius", str(radius))
# 保存视频纹理引用
if movie_texture:
sphere_np.setPythonTag("movie_texture", movie_texture)
# 添加到GUI元素列表
self.gui_elements.append(sphere_np)
print(f"✅ 为 {parent_item.text(0)} 创建球形视频成功: {sphere_name}")
# 在Qt树形控件中添加对应节点
qt_item = tree_widget.add_node_to_tree_widget(sphere_np, parent_item, "GUI_SPHERICAL_VIDEO")
if qt_item:
created_spherical_videos.append((sphere_np, qt_item))
else:
created_spherical_videos.append((sphere_np, None))
print("⚠️ Qt树节点添加失败但Panda3D对象已创建")
except Exception as e:
print(f"❌ 为 {parent_item.text(0)} 创建球形视频失败: {str(e)}")
import traceback
traceback.print_exc()
continue
# 处理创建结果
if not created_spherical_videos:
print("❌ 没有成功创建任何球形视频")
return None
# 选中最后创建的球形视频
# if created_spherical_videos:
# last_sphere_np, last_qt_item = created_spherical_videos[-1]
# if last_qt_item:
# tree_widget.setCurrentItem(last_qt_item)
# # 更新选择和属性面板
# tree_widget.update_selection_and_properties(last_sphere_np, last_qt_item)
print(f"🎉 总共创建了 {len(created_spherical_videos)} 个球形视频")
# 返回值处理
if len(created_spherical_videos) == 1:
return created_spherical_videos[0][0] # 单个球形视频返回NodePath
else:
return [sphere_np for sphere_np, _ in created_spherical_videos] # 多个球形视频返回列表
except Exception as e:
print(f"❌ 创建球形视频过程失败: {str(e)}")
import traceback
traceback.print_exc()
return None
def _createSphereGeometry(self, radius=5.0, segments=32):
"""创建球形几何体"""
try:
from panda3d.core import GeomVertexFormat, GeomVertexData, GeomVertexWriter
from panda3d.core import Geom, GeomTriangles
import math
# 创建顶点数据
format = GeomVertexFormat.getV3n3t2()
vdata = GeomVertexData('sphere', format, Geom.UHStatic)
vertex = GeomVertexWriter(vdata, 'vertex')
normal = GeomVertexWriter(vdata, 'normal')
texcoord = GeomVertexWriter(vdata, 'texcoord')
# 生成球体顶点
vertices = []
for i in range(segments + 1):
phi = math.pi * i / segments # 从0到π
for j in range(segments * 2 + 1):
theta = 2 * math.pi * j / (segments * 2) # 从0到2π
x = radius * math.sin(phi) * math.cos(theta)
y = radius * math.cos(phi)
z = radius * math.sin(phi) * math.sin(theta)
# 纹理坐标
u = j / (segments * 2)
v = i / segments
# 修复:将坐标作为一个元组添加到列表中
vertices.append((x, y, z, u, v))
# 添加顶点数据
for vert in vertices:
vertex.addData3f(vert[0], vert[1], vert[2])
# 法线(标准化顶点位置)
length = math.sqrt(vert[0] ** 2 + vert[1] ** 2 + vert[2] ** 2)
if length > 0:
normal.addData3f(vert[0] / length, vert[1] / length, vert[2] / length)
else:
normal.addData3f(0, 1, 0)
texcoord.addData2f(vert[3], vert[4])
# 创建几何体
geom = Geom(vdata)
prim = GeomTriangles(Geom.UHStatic)
# 生成三角形面
for i in range(segments):
for j in range(segments * 2):
# 计算顶点索引
v1 = i * (segments * 2 + 1) + j
v2 = (i + 1) * (segments * 2 + 1) + j
v3 = (i + 1) * (segments * 2 + 1) + (j + 1)
v4 = i * (segments * 2 + 1) + (j + 1)
# 添加两个三角形
prim.addVertices(v1, v2, v3)
prim.addVertices(v1, v3, v4)
prim.closePrimitive()
geom.addPrimitive(prim)
return geom
except Exception as e:
print(f"❌ 创建球形几何体失败: {e}")
import traceback
traceback.print_exc()
raise
def playSphericalVideo(self,spherical_video_node):
try:
if not spherical_video_node:
return False
texture = spherical_video_node.getTexture()
if texture and hasattr(texture,'play'):
texture.play()
return True
else:
return False
except Exception as e:
return False
def pauseSphericalVideo(self,spherical_video_node):
try:
if not spherical_video_node:
return False
texture = spherical_video_node.getTexture()
if texture and hasattr(texture,'stop'):
texture.stop()
return True
else:
return False
except Exception as e:
return False
def setSphericalVideoTime(self,spherical_video_node,time_seconds):
try:
if not spherical_video_node:
return False
texture = spherical_video_node.getTexture()
if texture and hasattr(texture,'set_time'):
texture.setTime(time_seconds)
return True
else:
return False
except Exception as e:
return False
def createGUIVirtualScreen(self, pos=(0, 0, 0), size=(2, 1), text="虚拟屏幕"):
"""创建3D虚拟屏幕 - 支持多选创建,优化版本"""
try:
from panda3d.core import CardMaker, TransparencyAttrib, TextNode
from PyQt5.QtCore import Qt
print(f"🖥️ 开始创建虚拟屏幕,位置: {pos}, 尺寸: {size}, 文本: {text}")
# 获取树形控件
tree_widget = self._get_tree_widget()
if not tree_widget:
print("❌ 无法访问树形控件")
return None
# 获取目标父节点列表
target_parents = tree_widget.get_target_parents_for_creation()
if not target_parents:
print("❌ 没有找到有效的父节点")
return None
created_screens = []
# 为每个有效的父节点创建虚拟屏幕
for parent_item, parent_node in target_parents:
try:
# 生成唯一名称
screen_name = f"VirtualScreen_{len(self.gui_elements)}"
# 创建虚拟屏幕几何体
cm = CardMaker(f"virtual-screen-{len(self.gui_elements)}")
cm.setFrame(-size[0] / 2, size[0] / 2, -size[1] / 2, size[1] / 2)
# 创建挂载节点 - 挂载到选中的父节点
virtual_screen = parent_node.attachNewNode(cm.generate())
virtual_screen.setPos(*pos)
virtual_screen.setName(screen_name)
virtual_screen.setColor(0.2, 0.2, 0.2, 0.8)
virtual_screen.setTransparency(TransparencyAttrib.MAlpha)
# 在虚拟屏幕上添加文本
screen_text_node = self._create_screen_text(virtual_screen, text, len(self.gui_elements))
# 设置节点标签
virtual_screen.setTag("gui_type", "virtual_screen")
virtual_screen.setTag("gui_id", f"virtual_screen_{len(self.gui_elements)}")
virtual_screen.setTag("gui_text", text)
virtual_screen.setTag("is_gui_element", "1")
virtual_screen.setTag("is_scene_element", "1")
virtual_screen.setTag("tree_item_type", "GUI_VirtualScreen")
virtual_screen.setTag("created_by_user", "1")
# 添加到GUI元素列表
self.gui_elements.append(virtual_screen)
print(f"✅ 为 {parent_item.text(0)} 创建虚拟屏幕成功: {screen_name}")
# 在Qt树形控件中添加对应节点
qt_item = tree_widget.add_node_to_tree_widget(virtual_screen, parent_item, "GUI_VirtualScreen")
if qt_item:
created_screens.append((virtual_screen, qt_item))
else:
created_screens.append((virtual_screen, None))
print("⚠️ Qt树节点添加失败但Panda3D对象已创建")
except Exception as e:
print(f"❌ 为 {parent_item.text(0)} 创建虚拟屏幕失败: {str(e)}")
continue
# 处理创建结果
if not created_screens:
print("❌ 没有成功创建任何虚拟屏幕")
return None
# 选中最后创建的虚拟屏幕
# if created_screens:
# last_screen_np, last_qt_item = created_screens[-1]
# if last_qt_item:
# tree_widget.setCurrentItem(last_qt_item)
# # 更新选择和属性面板
# tree_widget.update_selection_and_properties(last_screen_np, last_qt_item)
print(f"🎉 总共创建了 {len(created_screens)} 个虚拟屏幕")
# 返回值处理
if len(created_screens) == 1:
return created_screens[0][0] # 单个屏幕返回NodePath
else:
return [screen_np for screen_np, _ in created_screens] # 多个屏幕返回列表
except Exception as e:
print(f"❌ 创建虚拟屏幕过程失败: {str(e)}")
import traceback
traceback.print_exc()
return None
def _create_screen_text(self, virtual_screen, text, screen_index):
"""为虚拟屏幕创建文本节点"""
try:
from panda3d.core import TextNode
screen_text = TextNode(f'screen-text-{screen_index}')
screen_text.setText(text)
screen_text.setAlign(TextNode.ACenter)
# 设置中文字体
if hasattr(self.world, 'getChineseFont') and self.world.getChineseFont():
screen_text.setFont(self.world.getChineseFont())
# 创建文本节点路径并设置属性
screen_text_np = virtual_screen.attachNewNode(screen_text)
screen_text_np.setPos(0, 0.01, 0) # 稍微向前偏移避免Z-fighting
screen_text_np.setScale(0.3)
screen_text_np.setColor(0, 1, 0, 1) # 绿色文本
print(f"✅ 虚拟屏幕文本创建成功: {text}")
return screen_text_np
except Exception as e:
print(f"❌ 创建虚拟屏幕文本失败: {str(e)}")
return None
def _get_tree_widget(self):
"""安全获取树形控件"""
try:
if (hasattr(self.world, 'interface_manager') and
hasattr(self.world.interface_manager, 'treeWidget')):
return self.world.interface_manager.treeWidget
except AttributeError:
pass
return None
# 暂无滑块功能
def createGUISlider(self, pos=(0, 0, 0), text="滑块", scale=0.3):
pass
"""创建2D GUI滑块"""
from direct.gui.DirectGui import DirectSlider
gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1)
slider = DirectSlider(
pos=gui_pos,
scale=scale,
range=(0, 100),
value=50,
frameColor=(0.6, 0.6, 0.6, 1),
thumbColor=(0.2, 0.8, 0.2, 1)
)
slider.setTag("gui_type", "slider")
slider.setTag("gui_id", f"slider_{len(self.gui_elements)}")
slider.setTag("gui_text", text)
slider.setTag("is_gui_element", "1")
self.gui_elements.append(slider)
# 安全地调用updateSceneTree
if hasattr(self.world, 'updateSceneTree'):
pass # CH
self.world.updateSceneTree()
print(f"✓ 创建GUI滑块: {text} (逻辑位置: {pos}, 屏幕位置: {gui_pos})")
return slider
# ==================== GUI元素管理方法 ====================
def deleteGUIElement(self, gui_element):
"""删除GUI元素"""
try:
if gui_element in self.gui_elements:
# # 移除GUI元素
# if hasattr(gui_element, 'removeNode'):
# gui_element.removeNode()
# elif hasattr(gui_element, 'destroy'):
# gui_element.destroy()
#
# # 从列表中移除
# self.gui_elements.remove(gui_element)
# 更新场景树
# 安全地调用updateSceneTree
tree_widget = self._get_tree_widget()
if tree_widget:
tree_widget.delete_items(tree_widget.selectedItems())
# if hasattr(self.world, 'updateSceneTree'):
# self.world.updateSceneTree()
print(f"删除GUI元素: {gui_element}")
return True
except Exception as e:
print(f"删除GUI元素失败: {str(e)}")
return False
# 在 gui_manager.py 中确保 editGUIElement 方法正确处理文本颜色
def editGUIElement(self, gui_element, property_name, value):
"""编辑GUI元素属性"""
try:
from panda3d.core import TextNode
gui_type = gui_element.getTag("gui_type") if hasattr(gui_element, 'getTag') else "unknown"
print(f"开始编辑GUI元素: 类型={gui_type}, 属性={property_name}, 值={value}")
if property_name == "text":
original_frame_size = None
if hasattr(gui_element,'getFrameSize'):
try:
original_frame_size = gui_element.getFrameSize()
except:
pass
if gui_type in ["button", "label"]:
gui_element['text'] = value
print(f"成功更新2D GUI文本: {value}")
# if gui_type == "button":
# self._resizeButtonToText(gui_element,value,original_frame_size)
elif gui_type == "entry":
gui_element.set(value)
print(f"成功更新输入框文本: {value}")
elif gui_type == "3d_text":
# 对于3D文本直接修改自身的TextNode
if isinstance(gui_element.node(), TextNode):
gui_element.node().setText(value)
print(f"成功更新3D文本: {value}")
else:
print(f"警告: {gui_type}节点类型为{type(gui_element.node())}不是TextNode类型")
elif gui_type == "virtual_screen":
# 对于虚拟屏幕需要找到TextNode子节点
print(f"虚拟屏幕有 {gui_element.getNumChildren()} 个子节点")
text_found = False
for i, child in enumerate(gui_element.getChildren()):
print(f"子节点 {i}: {child.getName()}, 类型: {type(child.node())}")
if isinstance(child.node(), TextNode):
child.node().setText(value)
text_found = True
print(f"成功更新虚拟屏幕文本: {value}")
break
if not text_found:
print(f"警告: 在{gui_type}中未找到TextNode子节点")
gui_element.setTag("gui_text", value)
elif property_name == "color": # 添加颜色处理
if isinstance(value, (list, tuple)) and len(value) >= 3:
# 对于2D GUI元素button和label使用frameColor属性
if gui_type in ["button", "label"]:
# 设置背景颜色
gui_element['frameColor'] = (value[0], value[1], value[2], value[3] if len(value) > 3 else 1.0)
print(f"成功更新2D GUI背景颜色: {gui_type} -> {value}")
# 对于3D元素使用材质颜色
elif gui_type in ["3d_text", "3d_image", "virtual_screen"]:
# 更新材质颜色
if not gui_element.hasMaterial():
material = Material(f"text-material-{gui_element.getName()}")
material.setBaseColor(Vec4(value[0], value[1], value[2], value[3] if len(value) > 3 else 1.0))
material.setDiffuse(Vec4(value[0], value[1], value[2], value[3] if len(value) > 3 else 1.0))
gui_element.setMaterial(material, 1)
else:
material = gui_element.getMaterial()
material.setBaseColor(Vec4(value[0], value[1], value[2], value[3] if len(value) > 3 else 1.0))
material.setDiffuse(Vec4(value[0], value[1], value[2], value[3] if len(value) > 3 else 1.0))
gui_element.setMaterial(material, 1)
# 更新 TextNode 的文本颜色
if isinstance(gui_element.node(), TextNode):
gui_element.node().setTextColor(
Vec4(value[0], value[1], value[2], value[3] if len(value) > 3 else 1.0))
print(f"成功更新3D GUI颜色: {gui_type} -> {value}")
else:
print(f"警告: 未知的GUI类型 {gui_type},无法设置颜色")
elif property_name == "position":
if isinstance(value, (list, tuple)) and len(value) >= 3:
gui_element.setPos(*value[:3])
elif property_name == "scale":
if isinstance(value, (int, float)):
gui_element.setScale(value)
elif isinstance(value, (list, tuple)) and len(value) >= 3:
gui_element.setScale(*value[:3])
print(f"编辑GUI元素 {gui_type}: {property_name} = {value}")
return True
except Exception as e:
print(f"编辑GUI元素失败: {str(e)}")
import traceback
traceback.print_exc()
return False
def _resizeButtonToText(self, button, text, original_frame_size=None):
"""
根据文本内容调整按钮大小
Args:
button: DirectButton 对象
text: 新的文本内容
original_frame_size: 原始frameSize用于计算合适的padding
"""
try:
# 获取按钮当前的文本缩放
text_scale = 0.03 # 默认文本缩放
padding = 0.2 # 默认边距
# 尝试从按钮获取实际的文本缩放值
if hasattr(button, 'getTextScale'):
text_scale = button.getTextScale()
elif hasattr(button, 'textScale'):
text_scale = button.textScale
# 根据原始frameSize计算合适的padding
if original_frame_size and len(original_frame_size) >= 4:
# 基于原始按钮宽度计算合适的padding
padding = max((original_frame_size[1] - original_frame_size[0]) * 0.15, 0.1)
# 更精确的文本尺寸估算
# 考虑中文字符和英文字符的不同宽度
chinese_chars = len([c for c in text if ord(c) > 127])
english_chars = len(text) - chinese_chars
# 中文字符通常比英文字符宽
char_width = text_scale * 0.6
text_width = (chinese_chars * char_width * 1.5) + (english_chars * char_width)
text_height = text_scale * 1.2 # 文本高度
# 计算新的frameSize确保有足够的边距
half_width = max(text_width * 0.5 + padding, 0.3) # 最小宽度0.3
half_height = max(text_height * 0.5 + padding * 0.5, 0.15) # 最小高度0.15
# 正确设置frameSize - 使用字典方式设置
new_frame = (-half_width, half_width, -half_height, half_height)
button['frameSize'] = new_frame # 使用字典方式设置
print(f"按钮大小已调整: {new_frame} (文本: {text})")
except Exception as e:
print(f"调整按钮大小失败: {e}")
# 如果自动调整失败,保持原有大小或设置一个合理的默认大小
try:
if original_frame_size:
# 保持原有大小
button['frameSize'] = original_frame_size
else:
# 设置一个合理的默认大小
button['frameSize'] = (-0.5, 0.5, -0.15, 0.15)
except:
pass
def duplicateGUIElement(self, gui_element):
"""复制GUI元素"""
try:
gui_type = gui_element.getTag("gui_type")
gui_text = gui_element.getTag("gui_text")
# 获取当前位置并偏移
pos = gui_element.getPos()
new_pos = (pos.getX() + 0.2, pos.getY(), pos.getZ() + 0.2)
# 根据类型创建新的GUI元素
if gui_type == "button":
self.createGUIButton(new_pos, gui_text + "_副本")
elif gui_type == "label":
self.createGUILabel(new_pos, gui_text + "_副本")
elif gui_type == "entry":
self.createGUIEntry(new_pos, gui_text + "_副本")
elif gui_type == "3d_text":
self.createGUI3DText(new_pos, gui_text + "_副本")
elif gui_type == "3d_image":
image_path = gui_element.getTag("image_path")
self.createGUI3DImage(new_pos,image_path,size=(2,2))
elif gui_type == "2d_image":
image_path = gui_element.getTag("image_path")
self.createGUI2DImage(new_pos, image_path, size=0.2)
elif gui_type == "virtual_screen":
self.createGUIVirtualScreen(new_pos, text=gui_text + "_副本")
print(f"复制GUI元素: {gui_type} - {gui_text}")
except Exception as e:
print(f"复制GUI元素失败: {str(e)}")
def editGUIElementDialog(self, gui_element):
"""显示GUI元素编辑对话框"""
dialog = QDialog()
dialog.setWindowTitle("编辑GUI元素")
dialog.setMinimumWidth(300)
layout = QVBoxLayout(dialog)
form = QFormLayout()
gui_type = gui_element.getTag("gui_type")
gui_text = gui_element.getTag("gui_text")
# 文本编辑
if gui_type in ["button", "label", "entry", "3d_text", "virtual_screen"]:
textEdit = QLineEdit(gui_text or "")
form.addRow("文本:", textEdit)
# 位置编辑
if hasattr(gui_element, 'getPos'):
pos = gui_element.getPos()
xEdit = QDoubleSpinBox()
xEdit.setRange(-1000, 1000)
xEdit.setValue(pos.getX())
form.addRow("位置 X:", xEdit)
yEdit = QDoubleSpinBox()
yEdit.setRange(-1000, 1000)
yEdit.setValue(pos.getY())
form.addRow("位置 Y:", yEdit)
zEdit = QDoubleSpinBox()
zEdit.setRange(-1000, 1000)
zEdit.setValue(pos.getZ())
form.addRow("位置 Z:", zEdit)
# 缩放编辑
if hasattr(gui_element, 'getScale'):
scale = gui_element.getScale()
scaleEdit = QDoubleSpinBox()
scaleEdit.setRange(0.01, 100)
scaleEdit.setSingleStep(0.1)
scaleEdit.setValue(scale.getX())
form.addRow("缩放:", scaleEdit)
layout.addLayout(form)
# 按钮
buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
buttonBox.accepted.connect(dialog.accept)
buttonBox.rejected.connect(dialog.reject)
layout.addWidget(buttonBox)
# 执行对话框
if dialog.exec_() == QDialog.Accepted:
try:
# 应用更改
if gui_type in ["button", "label", "entry", "3d_text", "virtual_screen"]:
self.editGUIElement(gui_element, "text", textEdit.text())
if hasattr(gui_element, 'getPos'):
self.editGUIElement(gui_element, "position", [xEdit.value(), yEdit.value(), zEdit.value()])
if hasattr(gui_element, 'getScale'):
self.editGUIElement(gui_element, "scale", scaleEdit.value())
# 更新属性面板
if self.world.treeWidget:
currentItem = self.world.treeWidget.currentItem()
if currentItem:
self.world.updatePropertyPanel(currentItem)
print("GUI元素编辑完成")
except Exception as e:
print(f"应用GUI编辑失败: {str(e)}")
# ==================== GUI事件处理方法 ====================
def onGUIButtonClick(self, button_id):
"""GUI按钮点击事件处理"""
print(f"GUI按钮被点击: {button_id}")
def onGUIEntrySubmit(self, text, entry_id):
"""GUI输入框提交事件处理"""
print(f"GUI输入框提交: {entry_id} = {text}")
# ==================== GUI编辑模式方法 ====================
def toggleGUIEditMode(self):
"""切换GUI编辑模式"""
self.guiEditMode = not self.guiEditMode
if self.guiEditMode:
self.enterGUIEditMode()
else:
self.exitGUIEditMode()
def enterGUIEditMode(self):
"""进入GUI编辑模式"""
print("\n=== 进入GUI编辑模式 ===")
# 打开GUI预览窗口
self.openGUIPreviewWindow()
# 创建GUI编辑面板
self.createGUIEditPanel()
# 改变当前工具为GUI选择工具
self.world.currentTool = "GUI编辑"
print("GUI编辑模式已激活")
print("- 使用右侧工具栏创建GUI元素")
print("- 在独立预览窗口中查看效果")
print("- 左键点击现有GUI元素选择和编辑")
def exitGUIEditMode(self):
"""退出GUI编辑模式"""
print("\n=== 退出GUI编辑模式 ===")
# 关闭GUI预览窗口
self.closeGUIPreviewWindow()
# 移除GUI编辑面板
if self.guiEditPanel:
self.guiEditPanel.destroy()
self.guiEditPanel = None
# 恢复普通工具
self.world.currentTool = "选择"
print("GUI编辑模式已关闭")
def createGUIEditPanel(self):
"""创建GUI编辑面板"""
from direct.gui.DirectGui import DirectFrame, DirectButton, DirectLabel
# 创建主面板
self.guiEditPanel = DirectFrame(
pos=(0.85, 0, 0),
frameSize=(-0.15, 0.15, -0.9, 0.9),
frameColor=(0.1, 0.1, 0.1, 0.8),
text="GUI编辑器",
text_pos=(0, 0.85),
text_scale=0.05,
text_fg=(1, 1, 1, 1),
text_font=self.world.getChineseFont() if self.world.getChineseFont() else None
)
# 创建工具按钮
y_pos = 0.7
spacing = 0.12
# 2D GUI工具
label_2d = DirectLabel(
parent=self.guiEditPanel,
text="2D GUI",
pos=(0, 0, y_pos),
scale=0.04,
text_fg=(1, 1, 0, 1),
frameColor=(0, 0, 0, 0),
text_font=self.world.getChineseFont() if self.world.getChineseFont() else None
)
y_pos -= 0.08
# 按钮工具
btn_button = DirectButton(
parent=self.guiEditPanel,
text="按钮",
pos=(0, 0, y_pos),
scale=0.04,
command=self.setGUICreateTool,
extraArgs=["button"],
frameColor=(0.2, 0.6, 0.8, 1),
text_font=self.world.getChineseFont() if self.world.getChineseFont() else None
)
y_pos -= spacing
# 标签工具
btn_label = DirectButton(
parent=self.guiEditPanel,
text="标签",
pos=(0, 0, y_pos),
scale=0.04,
command=self.setGUICreateTool,
extraArgs=["label"],
frameColor=(0.6, 0.8, 0.2, 1),
text_font=self.world.getChineseFont() if self.world.getChineseFont() else None
)
y_pos -= spacing
# 输入框工具
btn_entry = DirectButton(
parent=self.guiEditPanel,
text="输入框",
pos=(0, 0, y_pos),
scale=0.04,
command=self.setGUICreateTool,
extraArgs=["entry"],
frameColor=(0.8, 0.6, 0.2, 1),
text_font=self.world.getChineseFont() if self.world.getChineseFont() else None
)
y_pos -= spacing
# 3D GUI工具
label_3d = DirectLabel(
parent=self.guiEditPanel,
text="3D GUI",
pos=(0, 0, y_pos),
scale=0.04,
text_fg=(1, 1, 0, 1),
frameColor=(0, 0, 0, 0),
text_font=self.world.getChineseFont() if self.world.getChineseFont() else None
)
y_pos -= 0.08
# 3D文本工具
btn_3dtext = DirectButton(
parent=self.guiEditPanel,
text="3D文本",
pos=(0, 0, y_pos),
scale=0.04,
command=self.setGUICreateTool,
extraArgs=["3d_text"],
frameColor=(0.8, 0.2, 0.6, 1),
text_font=self.world.getChineseFont() if self.world.getChineseFont() else None
)
y_pos -= spacing
#3D图片工具
btn_image = DirectButton(
parent = self.guiEditPanel,
text="3D图片",
pos=(0,0,y_pos),
scale=0.04,
command=self.setGUICreateTool,
extraArgs=["3d_image"],
frameColor=(0.2,0.8,0.8,1),
text_font=self.world.getChineseFont() if self.world.getChineseFont() else None
)
y_pos -= spacing
# 2D图片工具
btn_2d_image = DirectButton(
parent=self.guiEditPanel,
text="2D图片",
pos=(0, 0, y_pos),
scale=0.04,
command=self.setGUICreateTool,
extraArgs=["2d_image"],
frameColor=(0.8, 0.6, 0.2, 1),
text_font=self.world.getChineseFont() if self.world.getChineseFont() else None
)
y_pos -= spacing
# 虚拟屏幕工具
btn_screen = DirectButton(
parent=self.guiEditPanel,
text="虚拟屏幕",
pos=(0, 0, y_pos),
scale=0.04,
command=self.setGUICreateTool,
extraArgs=["virtual_screen"],
frameColor=(0.6, 0.2, 0.8, 1),
text_font=self.world.getChineseFont() if self.world.getChineseFont() else None
)
y_pos -= spacing
#Cesium 集成工具 仅在Webengine 可用时显示)
if WEB_ENGINE_AVAILABLE:
label_cesium = DirectLabel(
parent=self.guiEditPanel,
text="Cesium 集成",
pos=(0, 0, y_pos),
scale=0.04,
text_fg=(1, 1, 0, 1),
frameColor=(0, 0, 0, 0),
text_font=self.world.getChineseFont() if self.world.getChineseFont() else None
)
y_pos -= 0.08
# 切换 Cesium 视图按钮
btn_toggle_cesium = DirectButton(
parent=self.guiEditPanel,
text="切换地图视图",
pos=(0, 0, y_pos),
scale=0.04,
command=self.toggleCesiumView,
frameColor=(0.2, 0.8, 0.6, 1),
text_font=self.world.getChineseFont() if self.world.getChineseFont() else None
)
y_pos -= spacing
# 刷新 Cesium 视图按钮
btn_refresh_cesium = DirectButton(
parent=self.guiEditPanel,
text="刷新地图",
pos=(0, 0, y_pos),
scale=0.04,
command=self.refreshCesiumView,
frameColor=(0.6, 0.8, 0.2, 1),
text_font=self.world.getChineseFont() if self.world.getChineseFont() else None
)
y_pos -= spacing
# 分隔线
y_pos -= 0.1
# 操作按钮
label_ops = DirectLabel(
parent=self.guiEditPanel,
text="操作",
pos=(0, 0, y_pos),
scale=0.04,
text_fg=(1, 1, 0, 1),
frameColor=(0, 0, 0, 0),
text_font=self.world.getChineseFont() if self.world.getChineseFont() else None
)
y_pos -= 0.08
# 删除工具
btn_delete = DirectButton(
parent=self.guiEditPanel,
text="删除",
pos=(0, 0, y_pos),
scale=0.04,
command=self.deleteSelectedGUI,
frameColor=(0.8, 0.2, 0.2, 1),
text_font=self.world.getChineseFont() if self.world.getChineseFont() else None
)
y_pos -= spacing
# 复制工具
btn_copy = DirectButton(
parent=self.guiEditPanel,
text="复制",
pos=(0, 0, y_pos),
scale=0.04,
command=self.copySelectedGUI,
frameColor=(0.2, 0.8, 0.2, 1),
text_font=self.world.getChineseFont() if self.world.getChineseFont() else None
)
y_pos -= spacing
# 退出GUI编辑模式
btn_exit = DirectButton(
parent=self.guiEditPanel,
text="退出",
pos=(0, 0, -0.8),
scale=0.04,
command=self.toggleGUIEditMode,
frameColor=(0.5, 0.5, 0.5, 1),
text_font=self.world.getChineseFont() if self.world.getChineseFont() else None
)
# 存储当前的GUI创建工具
self.currentGUITool = None
def openGUIPreviewWindow(self):
"""打开独立的GUI预览窗口"""
try:
from gui_preview_window import GUIPreviewWindow
self.guiPreviewWindow = GUIPreviewWindow()
self.guiPreviewWindow.set_main_world(self.world)
print("✓ GUI预览窗口已打开")
print("这个独立窗口会实时显示您创建的GUI元素")
except ImportError:
print("错误: 无法导入GUI预览窗口模块")
except Exception as e:
print(f"打开GUI预览窗口失败: {str(e)}")
def closeGUIPreviewWindow(self):
"""关闭GUI预览窗口"""
if self.guiPreviewWindow:
self.guiPreviewWindow.destroy()
self.guiPreviewWindow = None
print("GUI预览窗口已关闭")
# ==================== GUI工具和选择方法 ====================
def setGUICreateTool(self, tool_type):
"""设置GUI创建工具"""
self.currentGUITool = tool_type
print(f"选择GUI创建工具: {tool_type}")
def deleteSelectedGUI(self):
"""删除选中的GUI元素"""
if self.world.selection.selectedNode and hasattr(self.world.selection.selectedNode, 'getTag'):
gui_type = self.world.selection.selectedNode.getTag("gui_type")
if gui_type:
success = self.deleteGUIElement(self.world.selection.selectedNode)
if success:
self.world.selection.updateSelection(None)
print("GUI元素已删除")
else:
print("删除GUI元素失败")
else:
print("选中的不是GUI元素")
else:
print("没有选中的GUI元素")
def copySelectedGUI(self):
"""复制选中的GUI元素"""
if self.world.selection.selectedNode and hasattr(self.world.selection.selectedNode, 'getTag'):
gui_type = self.world.selection.selectedNode.getTag("gui_type")
if gui_type:
self.duplicateGUIElement(self.world.selection.selectedNode)
print("GUI元素已复制")
else:
print("选中的不是GUI元素")
else:
print("没有选中的GUI元素")
def handleGUIEditClick(self, hitPos):
"""处理GUI编辑模式下的点击"""
if not self.guiEditMode:
return False
if self.currentGUITool:
# 创建新的GUI元素
self.createGUIAtPosition(hitPos, self.currentGUITool)
return True
return False
def createGUIAtPosition(self, world_pos, gui_type):
"""在指定位置创建GUI元素"""
print(f"在位置 {world_pos} 创建 {gui_type}")
# 根据GUI类型选择合适的坐标转换
if gui_type in ["button", "label", "entry"]:
# 2D GUI - 将世界坐标转换为屏幕逻辑坐标
screen_x = world_pos.getX() * 2 # 缩放因子
screen_z = world_pos.getZ() * 2
pos = (screen_x, 0, screen_z)
else:
# 3D GUI - 直接使用世界坐标
pos = (world_pos.getX(), world_pos.getY(), world_pos.getZ())
# 创建不同类型的GUI元素
if gui_type == "button":
element = self.createGUIButton(pos, f"按钮_{len(self.gui_elements)}")
elif gui_type == "label":
element = self.createGUILabel(pos, f"标签_{len(self.gui_elements)}")
elif gui_type == "entry":
element = self.createGUIEntry(pos, f"输入框_{len(self.gui_elements)}")
elif gui_type == "3d_text":
element = self.createGUI3DText(pos, f"3D文本_{len(self.gui_elements)}")
elif gui_type == "3d_image":
element = self.createGUI3DImage(pos)
elif gui_type == "2d_image":
element = self.createGUI2DImage(pos)
elif gui_type == "virtual_screen":
element = self.createGUIVirtualScreen(pos, text=f"屏幕_{len(self.gui_elements)}")
else:
print(f"未知的GUI类型: {gui_type}")
return
# 自动选中新创建的元素
self.world.selection.updateSelection(element)
self.selectGUIInTree(element)
print(f"创建并选中了新的{gui_type}元素")
def findClickedGUI(self, hitNode):
"""查找被点击的GUI元素"""
# 检查点击的节点是否是GUI元素
current = hitNode
while current != self.world.render:
if hasattr(current, 'getTag') and current.getTag("gui_type"):
return current
current = current.getParent()
return None
def selectGUIInTree(self, gui_element):
"""在树形控件中选中GUI元素"""
if not self.world.treeWidget or not gui_element:
return
def findGUIItem(item):
"""递归查找GUI元素对应的树形项"""
if item.data(0, Qt.UserRole) == gui_element:
return item
for i in range(item.childCount()):
child = item.child(i)
result = findGUIItem(child)
if result:
return result
return None
# 从根开始查找
root = self.world.treeWidget.invisibleRootItem()
for i in range(root.childCount()):
sceneItem = root.child(i)
if sceneItem.text(0) == "场景":
for j in range(sceneItem.childCount()):
childItem = sceneItem.child(j)
if childItem.text(0) == "GUI元素":
foundItem = findGUIItem(childItem)
if foundItem:
self.world.treeWidget.setCurrentItem(foundItem)
self.world.updatePropertyPanel(foundItem)
return
def updateGUISelection(self, gui_element):
"""更新GUI元素选择状态"""
self.world.selection.updateSelection(gui_element)
if gui_element and hasattr(gui_element, 'getTag'):
gui_type = gui_element.getTag("gui_type")
gui_text = gui_element.getTag("gui_text")
print(f"选中GUI元素: {gui_type} - {gui_text}")
# 在树形控件中选中
self.selectGUIInTree(gui_element)
# ==================== GUI属性面板方法 ====================
def updateGUIPropertyPanel(self, gui_element, layout):
"""更新GUI元素属性面板"""
gui_type = gui_element.getTag("gui_type")
gui_text = gui_element.getTag("gui_text")
# GUI类型显示
typeLabel = QLabel("GUI类型:")
typeValue = QLabel(gui_type)
typeValue.setStyleSheet("color: #00AAFF; font-weight: bold;")
layout.addRow(typeLabel, typeValue)
# 文本属性(如果适用)
if gui_type in ["button", "label", "entry", "3d_text", "virtual_screen"]:
textLabel = QLabel("文本:")
textEdit = QLineEdit(gui_text or "")
# 创建一个更新函数来处理文本变化
def updateText(text):
success = self.editGUIElement(gui_element, "text", text)
if success:
# 更新场景树显示的名称
# 安全地调用updateSceneTree
if hasattr(self.world, 'updateSceneTree'):
self.world.updateSceneTree()
textEdit.textChanged.connect(updateText)
layout.addRow(textLabel, textEdit)
# 位置属性
if hasattr(gui_element, 'getPos'):
# 根据GUI类型设置组名
if gui_type in ["button", "label", "entry", "2d_image"]:
transform_group = QGroupBox("变换 Rect Transform")
else:
transform_group = QGroupBox("变换 Transform")
transform_layout = QGridLayout()
pos = gui_element.getPos()
# 根据GUI类型决定位置编辑方式
if gui_type in ["button", "label", "entry","2d_image"]:
# 2D GUI组件使用屏幕坐标
logical_x = pos.getX() / 0.1 # 反向转换为逻辑坐标
logical_z = pos.getZ() / 0.1
transform_layout.addWidget(QLabel("屏幕位置"), 0, 0)
x_label = QLabel("X")
z_label = QLabel("z")
x_label.setAlignment(Qt.Aligncenter)
z_label.setAlignment(Qt.AlignCenter)
transform_layout.addWidget(x_label, 0, 1)
transform_layout.addWidget(z_label, 0, 2)
xPos = QDoubleSpinBox()
xPos.setRange(-1000, 1000)
xPos.setValue(logical_x)
xPos.valueChanged.connect(lambda v: self.world.gui_manager.editGUI2DPosition(gui_element, "x", v))
transform_layout.addWidget(xPos, 1, 1)
zPos = QDoubleSpinBox()
zPos.setRange(-1000, 1000)
zPos.setValue(logical_z)
zPos.valueChanged.connect(lambda v: self.world.gui_manager.editGUI2DPosition(gui_element, "z", v))
transform_layout.addWidget(zPos, 1, 2)
# 显示实际屏幕坐标(只读)
transform_layout.addWidget(QLabel("实际坐标"), 2, 0)
actualXLabel = QLabel(f"{pos.getX():.3f}")
actualXLabel.setStyleSheet("color: gray; font-size: 10px;")
actualZLabel = QLabel(f"{pos.getZ():.3f}")
actualZLabel.setStyleSheet("color: gray; font-size: 10px;")
transform_layout.addWidget(actualXLabel, 3, 1)
transform_layout.addWidget(actualZLabel, 3, 2)
# 添加宽度和高度控件对于2D图像
if gui_type == "2d_image":
# 添加排序控制组
sort_group = QGroupBox("渲染顺序")
sort_layout = QGridLayout()
# 获取当前的sort值如果没有设置则默认为0
current_sort = int(gui_element.getTag("sort") or "0")
sort_layout.addWidget(QLabel("层级:"), 0, 0)
sort_spin = QSpinBox()
sort_spin.setRange(-1000, 1000) # 设置合理的范围
sort_spin.setValue(current_sort)
# 创建更新排序的函数
def updateSort(value):
try:
# 设置标签
gui_element.setTag("sort", str(value))
# 应用sort到节点 - 使用fixed bin和指定的值
gui_element.setBin("fixed", value)
print(f"✓ 更新2D图像渲染顺序: {value}")
except Exception as e:
print(f"✗ 更新2D图像渲染顺序失败: {e}")
sort_spin.valueChanged.connect(updateSort)
sort_layout.addWidget(sort_spin, 0, 1)
# 添加说明标签
sort_help = QLabel("(数值越大越靠前)")
sort_help.setStyleSheet("font-size: 10px; color: gray;")
sort_layout.addWidget(sort_help, 1, 0, 1, 2)
sort_group.setLayout(sort_layout)
layout.addWidget(sort_group)
scale = gui_element.getScale()
width = scale.getX() if hasattr(scale, 'getX') else scale[0] if isinstance(scale,
(tuple, list)) else scale
height = scale.getZ() if hasattr(scale, 'getZ') else scale[1] if isinstance(scale,
(tuple, list)) and len(
scale) > 1 else scale
# 宽度控件
transform_layout.addWidget(QLabel("宽度"), 4, 0)
widthSpinBox = QDoubleSpinBox()
widthSpinBox.setRange(0.01, 100)
widthSpinBox.setSingleStep(0.1)
widthSpinBox.setValue(width)
widthSpinBox.valueChanged.connect(
lambda v: self.world.gui_manager.editGUIScale(gui_element, "x", v))
transform_layout.addWidget(widthSpinBox, 4, 1)
# 高度控件
transform_layout.addWidget(QLabel("高度"), 4, 2)
heightSpinBox = QDoubleSpinBox()
heightSpinBox.setRange(0.01, 100)
heightSpinBox.setSingleStep(0.1)
heightSpinBox.setValue(height)
heightSpinBox.valueChanged.connect(
lambda v: self.world.gui_manager.editGUIScale(gui_element, "z", v))
transform_layout.addWidget(heightSpinBox, 4, 3)
else:
# 3D GUI组件使用世界坐标
transform_layout.addWidget(QLabel("位置"), 0, 0)
# X, Y, Z 标签居中
x_label = QLabel("X")
y_label = QLabel("Y")
z_label = QLabel("Z")
x_label.setAlignment(Qt.AlignCenter)
y_label.setAlignment(Qt.AlignCenter)
z_label.setAlignment(Qt.AlignCenter)
transform_layout.addWidget(x_label, 0, 1)
transform_layout.addWidget(y_label, 0, 2)
transform_layout.addWidget(z_label, 0, 3)
# 位置数值输入框
xPos = QDoubleSpinBox()
xPos.setRange(-100, 100)
xPos.setValue(pos.getX())
xPos.valueChanged.connect(lambda v: self.world.gui_manager.editGUI3DPosition(gui_element, "x", v))
transform_layout.addWidget(xPos, 1, 1)
yPos = QDoubleSpinBox()
yPos.setRange(-100, 100)
yPos.setValue(pos.getY())
yPos.valueChanged.connect(lambda v: self.world.gui_manager.editGUI3DPosition(gui_element, "y", v))
transform_layout.addWidget(yPos, 1, 2)
zPos = QDoubleSpinBox()
zPos.setRange(-100, 100)
zPos.setValue(pos.getZ())
zPos.valueChanged.connect(lambda v: self.world.gui_manager.editGUI3DPosition(gui_element, "z", v))
transform_layout.addWidget(zPos, 1, 3)
# 缩放属性
scale = gui_element.getScale()
transform_layout.addWidget(QLabel("缩放"), 2, 0)
# X, Y, Z 缩放标签居中
sx_label = QLabel("X")
sy_label = QLabel("Y")
sz_label = QLabel("Z")
sx_label.setAlignment(Qt.AlignCenter)
sy_label.setAlignment(Qt.AlignCenter)
sz_label.setAlignment(Qt.AlignCenter)
transform_layout.addWidget(sx_label, 2, 1)
transform_layout.addWidget(sy_label, 2, 2)
transform_layout.addWidget(sz_label, 2, 3)
# 缩放数值输入框
scale_x = QDoubleSpinBox()
scale_x.setRange(0.01, 100)
scale_x.setSingleStep(0.1)
scale_x.setValue(
scale.getX() if hasattr(scale, 'getX') else scale[0] if isinstance(scale, (tuple, list)) else scale)
scale_x.valueChanged.connect(lambda v: self.world.gui_manager.editGUIScale(gui_element, "x", v))
transform_layout.addWidget(scale_x, 3, 1)
scale_y = QDoubleSpinBox()
scale_y.setRange(0.01, 100)
scale_y.setSingleStep(0.1)
scale_y.setValue(
scale.getY() if hasattr(scale, 'getY') else scale[1] if isinstance(scale, (tuple, list)) and len(
scale) > 1 else scale)
scale_y.valueChanged.connect(lambda v: self.world.gui_manager.editGUIScale(gui_element, "y", v))
transform_layout.addWidget(scale_y, 3, 2)
scale_z = QDoubleSpinBox()
scale_z.setRange(0.01, 100)
scale_z.setSingleStep(0.1)
scale_z.setValue(
scale.getZ() if hasattr(scale, 'getZ') else scale[2] if isinstance(scale, (tuple, list)) and len(
scale) > 2 else scale)
scale_z.valueChanged.connect(lambda v: self.world.gui_manager.editGUIScale(gui_element, "z", v))
transform_layout.addWidget(scale_z, 3, 3)
transform_group.setLayout(transform_layout)
self._propertyLayout.addWidget(transform_group)
# 缩放属性
if hasattr(gui_element, 'getScale'):
scale = gui_element.getScale()
scaleSpinBox = QDoubleSpinBox()
scaleSpinBox.setRange(0.01, 100)
scaleSpinBox.setSingleStep(0.1)
scaleSpinBox.setValue(scale.getX())
scaleSpinBox.valueChanged.connect(lambda v: self.editGUIElement(gui_element, "scale", v))
layout.addRow("缩放:", scaleSpinBox)
# 颜色属性针对2D GUI
if gui_type in ["button", "label"]:
colorButton = QPushButton("选择颜色")
colorButton.clicked.connect(lambda: self.selectGUIColor(gui_element))
layout.addRow("背景颜色:", colorButton)
# 3D特有属性
if gui_type in ["3d_text", "virtual_screen"]:
# 旋转属性
if hasattr(gui_element, 'getHpr'):
hpr = gui_element.getHpr()
hRot = QDoubleSpinBox()
hRot.setRange(-180, 180)
hRot.setValue(hpr.getX())
hRot.valueChanged.connect(lambda v: gui_element.setH(v))
layout.addRow("旋转 H:", hRot)
pRot = QDoubleSpinBox()
pRot.setRange(-180, 180)
pRot.setValue(hpr.getY())
pRot.valueChanged.connect(lambda v: gui_element.setP(v))
layout.addRow("旋转 P:", pRot)
rRot = QDoubleSpinBox()
rRot.setRange(-180, 180)
rRot.setValue(hpr.getZ())
rRot.valueChanged.connect(lambda v: gui_element.setR(v))
layout.addRow("旋转 R:", rRot)
def selectGUIColor(self, gui_element):
"""选择GUI元素颜色"""
color = QColorDialog.getColor(QColor(128, 128, 128), None, "选择颜色")
if color.isValid():
r, g, b = color.red() / 255.0, color.green() / 255.0, color.blue() / 255.0
self.editGUIElement(gui_element, "color", [r, g, b, 1.0])
def update3DImageTexture(self, model_nodepath, image_path):
from panda3d.core import Texture, TextureStage
try:
# 加载新纹理
new_texture = self.world.loader.loadTexture(image_path)
if new_texture:
# 确保纹理过滤质量
new_texture.setMagfilter(Texture.FT_linear)
new_texture.setMinfilter(Texture.FT_linear_mipmap_linear)
model_nodepath.clearTexture()
# 为3D图像创建独立的纹理阶段
image_stage = TextureStage("3d_image_texture")
image_stage.setSort(0) # 使用第一个纹理槽
image_stage.setMode(TextureStage.MModulate) # 使用调制模式
# 应用纹理到模型,使用独立的纹理阶段
model_nodepath.setTexture(image_stage, new_texture)
# 更新标签
model_nodepath.setTag("gui_image_path", image_path)
# 确保材质设置正确
if not model_nodepath.hasMaterial():
from panda3d.core import Material, LColor
mat = Material()
mat.setName(f"image-material-{id(model_nodepath)}")
mat.setBaseColor(LColor(1, 1, 1, 1))
mat.setDiffuse(LColor(1, 1, 1, 1))
mat.setAmbient(LColor(0.5, 0.5, 0.5, 1))
mat.setSpecular(LColor(0.1, 0.1, 0.1, 1.0))
mat.setShininess(10.0)
model_nodepath.setMaterial(mat, 1)
# 保护子节点的纹理特别是3D文本
self._preserveChildNodeTextures(model_nodepath)
print(f"✅ 3D图像纹理已更新为: {image_path}")
return True
else:
print(f"❌ 无法加载纹理: {image_path}")
return False
except Exception as e:
print(f"❌ 更新纹理时出错: {e}")
return False
def _preserveChildNodeTextures(self, parent_node):
"""保护子节点的纹理不被父节点纹理影响"""
try:
# 遍历所有直接子节点
for i in range(parent_node.getNumChildren()):
child = parent_node.getChild(i)
# 检查子节点是否为3D文本或其他需要特殊处理的节点
if self._is3DTextElement(child):
# 为3D文本创建独立的纹理阶段
self._restore3DTextTexture(child)
elif self._isOtherSpecialElement(child):
# 为其他特殊元素恢复纹理
self._restoreSpecialElementTexture(child)
except Exception as e:
print(f"保护子节点纹理时出错: {e}")
def _is3DTextElement(self, node):
"""检查节点是否为3D文本元素"""
try:
return (hasattr(node, 'getTag') and
node.getTag("gui_type") == "3d_text")
except:
return False
def _isOtherSpecialElement(self, node):
"""检查节点是否为其他需要特殊处理的元素"""
try:
return (hasattr(node, 'getTag') and
node.getTag("gui_type") in ["3d_image", "button", "label"])
except:
return False
def _restore3DTextTexture(self, text_node):
"""恢复3D文本的纹理设置"""
try:
from panda3d.core import TextureStage
# 如果3D文本已经有字体纹理确保它使用正确的纹理阶段
if hasattr(text_node, 'getTexture') and text_node.getTexture():
# 创建专门用于文本的纹理阶段
text_stage = TextureStage("text_texture")
text_stage.setSort(10) # 使用较高的纹理槽索引
text_stage.setMode(TextureStage.MModulate)
# 重新应用文本纹理
current_texture = text_node.getTexture()
text_node.setTexture(text_stage, current_texture)
print(f"已恢复3D文本纹理: {text_node.getName()}")
except Exception as e:
print(f"恢复3D文本纹理失败: {e}")
def update2DImageTexture(self, gui_element, image_path):
"""更新2D图片纹理"""
try:
# 加载新纹理
new_texture = self.world.loader.loadTexture(image_path)
if new_texture:
new_texture.setMagfilter(Texture.FT_linear)
new_texture.setMinfilter(Texture.FT_linear_mipmap_linear)
new_texture.setFormat(Texture.F_rgba)
new_texture.setWrapU(Texture.WM_clamp)
new_texture.setWrapV(Texture.WM_clamp)
# 应用纹理到模型
gui_element.setTexture(new_texture, 1)
# 更新标签
gui_element.setTag("texture_path", image_path)
gui_element.setTag("image_path", image_path)
print(f"✅ 2D图像纹理已更新为: {image_path}")
return True
else:
print(f"❌ 无法加载2D图片纹理: {image_path}")
return False
except Exception as e:
print(f"❌ 更新2D图片纹理时出错: {e}")
return False
# 在gui_manager.py或其他相关文件中添加以下方法
def editGUI2DPosition(self, gui_element, axis, value):
"""编辑2D GUI元素位置并应用边界约束"""
try:
gui_type = gui_element.getTag("gui_type")
if gui_type in ["button", "label", "entry", "2d_image","2d_video_screen"]:
# 2D元素使用屏幕坐标需要转换
current_pos = gui_element.getPos()
if axis == "x":
# 转换逻辑坐标到屏幕坐标
screen_x = value * 0.1
new_pos = (screen_x,current_pos[1],current_pos[2])
elif axis == "z":
screen_z = value * 0.1
new_pos = (current_pos[0],current_pos[1],screen_z)
else:
return False
gui_element.setPos(*new_pos)
return True
else:
print(f"✗ 不支持的GUI类型进行2D位置编辑: {gui_type}")
return False
except Exception as e:
print(f"✗ 更新2D GUI元素位置失败: {e}")
import traceback
traceback.print_exc()
return False
def editGUI3DPosition(self, gui_element, axis, value):
"""编辑3D GUI元素位置"""
try:
gui_type = gui_element.getTag("gui_type")
if gui_type in ["3d_text", "3d_image", "video_screen","info_panel","info_panel_3d"]:
current_pos = gui_element.getPos()
if axis == "x":
new_pos = (value, current_pos.getY(), current_pos.getZ())
elif axis == "y":
new_pos = (current_pos.getX(), value, current_pos.getZ())
elif axis == "z":
new_pos = (current_pos.getX(), current_pos.getY(), value)
else:
return False
gui_element.setPos(*new_pos)
#print(f"✓ 更新3D GUI元素位置: {axis}={value}")
return True
else:
print(f"✗ 不支持的GUI类型进行3D位置编辑: {gui_type}")
return False
except Exception as e:
print(f"✗ 更新3D GUI元素位置失败: {e}")
import traceback
traceback.print_exc()
return False
def editGUIScale(self, gui_element, axis, value):
"""编辑GUI元素缩放"""
try:
gui_type = gui_element.getTag("gui_type")
current_scale = gui_element.getScale()
# 确保缩放值不为0
if value == 0:
value = 0.01
if gui_type in ["3d_text", "3d_image","video_screen","virtual_screen","info_panel","info_panel_3d"]:
# 3D元素处理
if axis == "x":
new_scale = (value, current_scale.getY(), current_scale.getZ())
elif axis == "y":
new_scale = (current_scale.getX(), value, current_scale.getZ())
elif axis == "z":
new_scale = (current_scale.getX(), current_scale.getY(), value)
else:
return False
gui_element.setScale(*new_scale)
elif gui_type == "2d_image":
# 2D图像特殊处理 - 分别控制宽度和高度
if axis == "x":
# X轴控制宽度
gui_element.setScale(value, current_scale.getZ() if hasattr(current_scale, 'getZ')
else current_scale[1] if isinstance(current_scale, (list, tuple))
else current_scale)
elif axis == "z":
# Z轴控制高度
gui_element.setScale(current_scale.getX() if hasattr(current_scale, 'getX')
else current_scale[0] if isinstance(current_scale, (list, tuple))
else current_scale, value)
else:
# 其他情况使用统一缩放
gui_element.setScale(value)
gui_element.setTransparency(TransparencyAttrib.MAlpha)
else:
# 其他2D元素处理
if axis in ["x", "z"]: # 对于2D图像x和z分别代表宽度和高度
# 保持原有缩放比例,仅调整指定轴
if axis == "x":
gui_element.setScale(value,
current_scale.getZ() if hasattr(current_scale, 'getZ')
else current_scale[1] if isinstance(current_scale, (list, tuple))
else current_scale)
elif axis == "z":
gui_element.setScale(
current_scale.getX() if hasattr(current_scale, 'getX')
else current_scale[0] if isinstance(current_scale, (list, tuple))
else current_scale, value)
else:
# 对于其他2D元素使用统一缩放
gui_element.setScale(value)
return True
except Exception as e:
print(f"✗ 更新GUI元素缩放失败: {e}")
import traceback
traceback.print_exc()
return False
def createCesiumView(self, main_window=None):
"""创建 Cesium 视图窗口(离线版本)"""
if not WEB_ENGINE_AVAILABLE:
print("❌ 无法创建Cesium视图: Web引擎不可用")
return None
try:
from PyQt5.QtWebEngineWidgets import QWebEngineView
from PyQt5.QtWidgets import QDockWidget
from PyQt5.QtCore import QUrl
import os
# 尝试获取主窗口引用
if main_window is None:
print("🔍 尝试获取主窗口引用...")
# 检查各种可能的主窗口引用
if hasattr(self.world, 'interface_manager'):
print(f" - interface_manager 存在: {self.world.interface_manager}")
if hasattr(self.world.interface_manager, 'main_window'):
main_window = self.world.interface_manager.main_window
print(f" - interface_manager.main_window: {main_window}")
if main_window is None and hasattr(self.world, 'main_window'):
main_window = self.world.main_window
print(f" - world.main_window: {main_window}")
# 如果仍然没有主窗口,尝试从树形控件获取
if main_window is None and self.world.treeWidget:
try:
main_window = self.world.treeWidget.window()
print(f" - 从 treeWidget 获取窗口: {main_window}")
except:
pass
if main_window is None:
print("✗ 无法获取主窗口引用")
return None
else:
print(f"✅ 使用传入的主窗口引用: {main_window}")
# 检查主窗口是否有效
if not hasattr(main_window, 'addDockWidget'):
print(f"✗ 主窗口引用无效,缺少 addDockWidget 方法")
return None
# 检查是否已经存在 Cesium 视图
for element in self.gui_elements:
if hasattr(element, 'objectName') and element.objectName() == "CesiumView":
print("⚠ Cesium 视图已经存在")
# 将其前置显示
element.show()
element.raise_()
return element
# 创建停靠窗口
print(f"🔧 创建 Cesium 停靠窗口,父窗口: {main_window}")
cesium_dock = QDockWidget("Cesium 地图视图(离线)", main_window)
cesium_dock.setObjectName("CesiumView")
# 创建 Web 视图
self.cesium_view = QWebEngineView()
# 使用本地 HTML 文件(离线模式)
local_html_path = os.path.abspath("./cesium_offline.html")
if os.path.exists(local_html_path):
print(f"🌐 加载离线 Cesium: file://{local_html_path}")
self.cesium_view.load(QUrl(f"file://{local_html_path}"))
else:
print("⚠️ 离线文件不存在,使用在线版本")
self.cesium_view.load(QUrl("http://localhost:8080/Apps/HelloWorld.html"))
# 设置内容
cesium_dock.setWidget(self.cesium_view)
# 添加到主窗口
print("📍 将 Cesium 视图添加到主窗口")
main_window.addDockWidget(Qt.RightDockWidgetArea, cesium_dock)
# 添加到GUI元素列表以便管理
self.gui_elements.append(cesium_dock)
print("✓ Cesium 离线视图已创建并集成到项目中")
return cesium_dock
except Exception as e:
print(f"✗ 创建 Cesium 视图失败: {str(e)}")
import traceback
traceback.print_exc()
return None
def toggleCesiumView(self):
"""切换 Cesium 视图显示状态"""
if not WEB_ENGINE_AVAILABLE:
print("✗ QtWebEngineWidgets 不可用,无法切换 Cesium 视图")
return None
try:
# 查找现有的 Cesium 视图
cesium_dock = None
cesium_index = -1
for i, element in enumerate(self.gui_elements):
if hasattr(element, 'objectName') and element.objectName() == "CesiumView":
cesium_dock = element
cesium_index = i
break
# 如果存在则移除,否则创建
if cesium_dock:
# 获取主窗口引用以正确移除停靠窗口
main_window = None
if (hasattr(self.world, 'interface_manager') and
hasattr(self.world.interface_manager, 'main_window') and
self.world.interface_manager.main_window):
main_window = self.world.interface_manager.main_window
elif hasattr(self.world, 'main_window') and self.world.main_window:
main_window = self.world.main_window
if main_window and hasattr(main_window, 'removeDockWidget'):
main_window.removeDockWidget(cesium_dock)
# 从列表中移除
if cesium_index >= 0:
self.gui_elements.pop(cesium_index)
print("✓ Cesium 视图已隐藏")
return None
else:
return self.createCesiumView()
except Exception as e:
print(f"✗ 切换 Cesium 视图失败: {str(e)}")
import traceback
traceback.print_exc()
return None
def refreshCesiumView(self):
"""刷新 Cesium 视图"""
if not WEB_ENGINE_AVAILABLE:
print("✗ QtWebEngineWidgets 不可用,无法刷新 Cesium 视图")
return False
try:
for element in self.gui_elements:
if hasattr(element, 'objectName') and element.objectName() == "CesiumView":
web_view = element.widget()
if isinstance(web_view, QWebEngineView):
web_view.reload()
print("✓ Cesium 视图已刷新")
return True
print("⚠ 未找到 Cesium 视图")
return False
except Exception as e:
print(f"✗ 刷新 Cesium 视图失败: {str(e)}")
return False
def updateCesiumURL(self, url):
"""更新 Cesium 视图的 URL"""
if not WEB_ENGINE_AVAILABLE:
print("✗ QtWebEngineWidgets 不可用,无法更新 Cesium URL")
return False
try:
for element in self.gui_elements:
if hasattr(element, 'objectName') and element.objectName() == "CesiumView":
web_view = element.widget()
if isinstance(web_view, QWebEngineView):
from PyQt5.QtCore import QUrl
web_view.load(QUrl(url))
print(f"✓ Cesium URL 已更新为: {url}")
return True
print("⚠ 未找到 Cesium 视图")
return False
except Exception as e:
print(f"✗ 更新 Cesium URL 失败: {str(e)}")
return False
# 在 GUIManager 类中添加以下方法
def addModelToCesium(self, model_id, model_url, longitude, latitude, height=0, scale=1.0):
"""向 Cesium 添加模型"""
if not WEB_ENGINE_AVAILABLE:
print("✗ QtWebEngineWidgets 不可用,无法操作 Cesium")
return False
try:
# 查找 Cesium 视图
cesium_view = None
for element in self.gui_elements:
if (hasattr(element, 'objectName') and
element.objectName() == "CesiumView" and
hasattr(element, 'widget')):
cesium_view = element.widget()
break
if not cesium_view:
print("✗ 未找到 Cesium 视图")
return False
# 转义特殊字符以防止 JavaScript 语法错误
escaped_model_id = str(model_id).replace("'", "\\'")
escaped_model_url = str(model_url).replace("'", "\\'").replace("\\", "/")
# 构造 JavaScript 调用
js_code = f"""
(function() {{
if (window.CesiumAPI && typeof window.CesiumAPI.addModel === 'function') {{
try {{
var result = window.CesiumAPI.addModel(
'{escaped_model_id}',
'{escaped_model_url}',
{{
longitude: {longitude},
latitude: {latitude},
height: {height}
}},
{scale}
);
console.log('Cesium 添加模型结果:', result);
return result || {{success: true, message: 'Model added'}};
}} catch (error) {{
console.error('JavaScript 错误:', error);
return {{success: false, message: 'JavaScript error: ' + error.message}};
}}
}} else {{
console.error('CesiumAPI.addModel 不可用');
return {{success: false, message: 'CesiumAPI.addModel not available'}};
}}
}})();
"""
# 定义回调函数处理结果
def handle_result(result):
try:
if isinstance(result, dict):
if result.get('success', False):
print(f"✓ 成功在 Cesium 中添加模型: {model_id}")
else:
print(f"✗ 在 Cesium 中添加模型失败: {result.get('message', 'Unknown error')}")
else:
print(f"✓ 已发送添加模型请求: {model_id}")
except Exception as callback_error:
print(f"✗ 处理回调结果时出错: {callback_error}")
# 执行 JavaScript 并获取结果
cesium_view.page().runJavaScript(js_code, handle_result)
return True
except Exception as e:
print(f"✗ 添加模型到 Cesium 失败: {e}")
import traceback
traceback.print_exc()
return False
# 添加新的方法来集成 Panda3D 场景中的 Cesium Tiles
def addCesiumTilesetToScene(self, tileset_name, tileset_url, position=(0, 0, 0)):
"""在 Panda3D 场景中添加 Cesium 3D Tiles"""
try:
# 使用场景管理器加载 tileset
tileset_node = self.world.scene_manager.load_cesium_tileset(tileset_url, position)
if tileset_node:
# 添加到 GUI 元素列表以便管理
self.gui_elements.append({
'type': 'cesium_tileset',
'name': tileset_name,
'node': tileset_node,
'url': tileset_url
})
print(f"✓ 在场景中添加 Cesium tileset: {tileset_name}")
return tileset_node
else:
print(f"✗ 在场景中添加 Cesium tileset 失败: {tileset_name}")
return None
except Exception as e:
print(f"✗ 在场景中添加 Cesium tileset 出错: {e}")
return None
def removeModelFromCesium(self, model_id):
"""从 Cesium 移除模型"""
if not WEB_ENGINE_AVAILABLE:
print("✗ QtWebEngineWidgets 不可用")
return False
try:
# 查找 Cesium 视图
cesium_view = None
for element in self.gui_elements:
if (hasattr(element, 'objectName') and
element.objectName() == "CesiumView" and
hasattr(element, 'widget')):
cesium_view = element.widget()
break
if not cesium_view:
print("✗ 未找到 Cesium 视图")
return False
# 构造 JavaScript 调用
js_code = f"""
if (window.CesiumAPI && typeof window.CesiumAPI.removeModel === 'function') {{
var result = window.CesiumAPI.removeModel('{model_id}');
result;
}} else {{
{{success: false, message: 'CesiumAPI.removeModel not available'}};
}}
"""
# 定义回调函数处理结果
def handle_result(result):
if result and isinstance(result, dict):
if result.get('success', False):
print(f"✓ 成功从 Cesium 中移除模型: {model_id}")
else:
print(f"✗ 从 Cesium 中移除模型失败: {result.get('message', 'Unknown error')}")
else:
print(f"✓ 已发送移除模型请求: {model_id} (无法获取详细结果)")
# 执行 JavaScript 并获取结果
cesium_view.page().runJavaScript(js_code, handle_result)
return True
except Exception as e:
print(f"✗ 从 Cesium 移除模型失败: {e}")
return False
def updateCesiumModelPosition(self, model_id, longitude, latitude, height=0):
"""更新 Cesium 中模型的位置"""
if not WEB_ENGINE_AVAILABLE:
print("✗ QtWebEngineWidgets 不可用")
return False
try:
# 查找 Cesium 视图
cesium_view = None
for element in self.gui_elements:
if (hasattr(element, 'objectName') and
element.objectName() == "CesiumView" and
hasattr(element, 'widget')):
cesium_view = element.widget()
break
if not cesium_view:
print("✗ 未找到 Cesium 视图")
return False
# 使用更安全的 JavaScript 字符串构造方式
escaped_model_id = model_id.replace("'", "\\'")
# 构造 JavaScript 调用
js_code = f"""
(function() {{
if (window.CesiumAPI && typeof window.CesiumAPI.updateModelPosition === 'function') {{
try {{
var result = window.CesiumAPI.updateModelPosition(
'{escaped_model_id}',
{{
longitude: {longitude},
latitude: {latitude},
height: {height}
}}
);
return result || {{success: true, message: 'Position updated'}};
}} catch (error) {{
return {{success: false, message: 'JavaScript error: ' + error.message}};
}}
}} else {{
return {{success: false, message: 'CesiumAPI.updateModelPosition not available'}};
}}
}})();
"""
# 定义回调函数处理结果
def handle_result(result):
try:
if isinstance(result, dict):
if result.get('success', False):
print(f"✓ 成功更新 Cesium 中模型位置: {model_id}")
else:
print(f"✗ 更新 Cesium 中模型位置失败: {result.get('message', 'Unknown error')}")
else:
print(f"✓ 已发送更新模型位置请求: {model_id}")
except Exception as callback_error:
print(f"✗ 处理回调结果时出错: {callback_error}")
# 执行 JavaScript 并获取结果
cesium_view.page().runJavaScript(js_code, handle_result)
return True
except Exception as e:
print(f"✗ 更新 Cesium 中模型位置失败: {e}")
return False
def getAllCesiumModels(self):
"""获取 Cesium 中所有模型的列表"""
if not WEB_ENGINE_AVAILABLE:
print("✗ QtWebEngineWidgets 不可用")
return None
try:
# 查找 Cesium 视图
cesium_view = None
for element in self.gui_elements:
if (hasattr(element, 'objectName') and
element.objectName() == "CesiumView" and
hasattr(element, 'widget')):
cesium_view = element.widget()
break
if not cesium_view:
print("✗ 未找到 Cesium 视图")
return None
# 构造 JavaScript 调用
js_code = """
if (window.CesiumAPI && typeof window.CesiumAPI.getAllModels === 'function') {
var result = window.CesiumAPI.getAllModels();
JSON.stringify(result);
} else {
JSON.stringify({success: false, message: 'CesiumAPI.getAllModels not available'});
}
"""
# 定义回调函数处理结果
def handle_result(result):
try:
if isinstance(result, str):
import json
result = json.loads(result)
if result and result.get('success', False):
models = result.get('models', [])
print(f"✓ Cesium 中的模型列表: {models}")
return models
else:
print(f"✗ 获取 Cesium 模型列表失败: {result.get('message', 'Unknown error')}")
return []
except Exception as e:
print(f"✗ 解析 Cesium 模型列表结果失败: {e}")
return []
# 执行 JavaScript 并获取结果
cesium_view.page().runJavaScript(js_code)
return None # 异步操作,实际结果通过回调处理
except Exception as e:
print(f"✗ 获取 Cesium 模型列表失败: {e}")
return None
# 添加一个便捷方法来加载本地模型文件
def addLocalModelToCesium(self, model_id, local_model_path, longitude, latitude, height=0, scale=1.0):
"""向 Cesium 添加本地模型文件"""
try:
# 将本地路径转换为相对路径或 URL
import os
if os.path.exists(local_model_path):
# 如果 Cesium 服务器可以访问该路径,可以直接使用
# 否则需要将模型文件放在 Cesium 的静态资源目录中
model_url = local_model_path.replace('\\', '/') # 确保使用正斜杠
return self.addModelToCesium(model_id, model_url, longitude, latitude, height, scale)
else:
print(f"✗ 模型文件不存在: {local_model_path}")
return False
except Exception as e:
print(f"✗ 添加本地模型失败: {e}")
return False