diff --git a/demo.py b/demo.py index b3e3b369..ce81e203 100644 --- a/demo.py +++ b/demo.py @@ -3973,6 +3973,9 @@ class MyWorld(CoreWorld): if gui_element in self.gui_manager.gui_elements: self.gui_manager.gui_elements.remove(gui_element) + # 获取节点名称(在删除之前) + node_name = node.getName() if not node.isEmpty() else '未命名节点' + # 删除节点本身 node.removeNode() @@ -3982,7 +3985,7 @@ class MyWorld(CoreWorld): self.selection.clearSelection() # 添加成功消息 - self.add_success_message(f"已删除节点: {node.getName() or '未命名节点'}") + self.add_success_message(f"已删除节点: {node_name}") def _copy_node(self, node): """复制节点""" @@ -4204,6 +4207,11 @@ class MyWorld(CoreWorld): text_node.setAlign(TextNode.ACenter) text_node.setTextColor(1, 1, 1, 1) # 白色 + # 设置中文字体 + chinese_font = self._get_chinese_font() + if chinese_font: + text_node.setFont(chinese_font) + # 创建节点路径并设置位置 text_np = NodePath(text_node) text_np.reparentTo(self.render) diff --git a/gui/gui_manager.py b/gui/gui_manager.py index 111cd8e7..ceaaface 100644 --- a/gui/gui_manager.py +++ b/gui/gui_manager.py @@ -1,3994 +1,4027 @@ -""" -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管理系统初始化完成") - - # ==================== 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) - if self.world.getChineseFont(): - textNode.setFont(self.world.getChineseFont()) - - # 挂载到选中的父节点 - 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 +""" +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