""" 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)}") 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): """创建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 # 设置GUI父节点 ) # 设置节点标签 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("created_by_user", "1") button.setTag("gui_parent_type", "gui" if parent_gui_node else "3d") 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) 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("is_scene_element", "1") label.setTag("created_by_user", "1") label.setTag("gui_parent_type", "gui" if parent_gui_node else "3d") 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)}") entry = DirectEntry( text="", pos=gui_pos, scale=size, command=self.onGUIEntrySubmit, extraArgs=[f"entry_{len(self.gui_elements)}"], initialText=placeholder, numLines=1, width=12, focus=0, parent=parent_gui_node # 设置GUI父节点 ) # 设置节点标签 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("is_scene_element", "1") entry.setTag("created_by_user", "1") entry.setTag("gui_parent_type", "gui" if parent_gui_node else "3d") 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=0.2): """创建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)}") # 使用CardMaker创建一个更可靠的图片框架 cm = CardMaker('gui-2d-image') cm.setFrame(-size, size, -size, size) # 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: 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("created_by_user", "1") image_node.setTag("gui_parent_type", "gui" if parent_gui_node else "3d") 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("created_by_user", "1") # 添加到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): """创建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[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 = 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("gui_image_path", image_path) image_node.setTag("is_gui_element", "1") image_node.setTag("is_scene_element", "1") image_node.setTag("created_by_user", "1") # 添加到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=0.2, 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 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("created_by_user", "1") # 设置视频路径标签 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 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: 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: print(f"❌ 视频屏幕没有关联的视频纹理: {video_screen.getName()}") self._debugVideoScreenTextures(video_screen) return False 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: from panda3d.core import Texture, TextureStage import os if not os.path.exists(video_path): print(f"❌ 视频文件不存在: {video_path}") return False # 加载新的视频纹理 movie_texture = self._loadMovieTexture(video_path) 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", video_path) # 确保视频屏幕有正确的材质 self._ensureVideoScreenMaterial(video_screen) print(f"✅ 成功加载新视频: {video_path}") return True else: print(f"❌ 无法加载视频文件: {video_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 import os # 检查文件是否存在 if not os.path.exists(video_path): print(f"❌ 视频文件不存在: {video_path}") return None print(f"🔍 尝试加载视频文件: {video_path}") # 方法1: 尝试使用 MovieTexture(专门用于视频) try: movie_texture = MovieTexture(video_path) if movie_texture.read(video_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(video_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(video_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 print(f"📺 开始创建2D视频屏幕,位置: {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_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("created_by_user", "1") # 设置视频路径标签 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_{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 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 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 if 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) video_screen.setTag("video_path", video_path) 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("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("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": if gui_type in ["button", "label"]: gui_element['text'] = value print(f"成功更新2D GUI文本: {value}") 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 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"]: 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"]: # 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