From ddeb40ea54a7b4aea4a7f07d6b428db3de8a625c Mon Sep 17 00:00:00 2001 From: Hector <2055590199@qq.com> Date: Tue, 23 Sep 2025 16:39:55 +0800 Subject: [PATCH] =?UTF-8?q?=E6=92=A4=E9=94=80=E9=87=8D=E5=81=9A=EF=BC=88?= =?UTF-8?q?=E4=BB=85=E4=BD=BF=E7=94=A8=E6=8B=96=E6=8B=BD=E6=97=B6=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/Command_System.py | 570 +++++++++++++++++++++++++++++++++++++++++ core/selection.py | 33 ++- gui/gui_manager.py | 74 +++++- main.py | 5 +- scene/scene_manager.py | 188 +------------- ui/main_window.py | 31 ++- ui/widgets.py | 9 +- 7 files changed, 727 insertions(+), 183 deletions(-) create mode 100644 core/Command_System.py diff --git a/core/Command_System.py b/core/Command_System.py new file mode 100644 index 00000000..49eb39e9 --- /dev/null +++ b/core/Command_System.py @@ -0,0 +1,570 @@ +from abc import ABC, abstractmethod +from collections import deque +from typing import List +from panda3d.core import NodePath + + +class Command(ABC): + """ + 抽象命令类,所有具体命令都需要继承此类 + """ + + @abstractmethod + def execute(self): + """ + 执行命令 + """ + pass + + @abstractmethod + def undo(self): + """ + 撤销命令 + """ + pass + + @abstractmethod + def redo(self): + """ + 重做命令 + """ + pass + + +class CommandManager: + """ + 命令管理器,负责管理命令的执行、撤销和重做 + """ + + def __init__(self, max_history: int = 100): + # 用于存储已执行的命令的历史记录 + self._undo_stack: deque = deque(maxlen=max_history) + # 用于存储已撤销的命令,支持重做 + self._redo_stack: deque = deque(maxlen=max_history) + # 最大历史记录数 + self._max_history = max_history + + def execute_command(self, command: Command): + """ + 执行命令,并将其添加到撤销栈中 + """ + try: + command.execute() + self._undo_stack.append(command) + # 清空重做栈,因为执行新命令后就无法重做之前的命令了 + self._redo_stack.clear() + except Exception as e: + print(f"执行命令时出错: {e}") + raise + + def undo(self) -> bool: + """ + 撤销上一个命令 + 返回是否成功撤销 + """ + if not self._undo_stack: + return False + + try: + command = self._undo_stack.pop() + command.undo() + self._redo_stack.append(command) + return True + except Exception as e: + print(f"撤销命令时出错: {e}") + # 如果撤销失败,将命令放回撤销栈 + self._undo_stack.append(command) + return False + + def redo(self) -> bool: + """ + 重做上一个被撤销的命令 + 返回是否成功重做 + """ + if not self._redo_stack: + return False + + try: + command = self._redo_stack.pop() + command.redo() + self._undo_stack.append(command) + return True + except Exception as e: + print(f"重做命令时出错: {e}") + # 如果重做失败,将命令放回重做栈 + self._redo_stack.append(command) + return False + + def can_undo(self) -> bool: + """ + 检查是否可以撤销 + """ + return len(self._undo_stack) > 0 + + def can_redo(self) -> bool: + """ + 检查是否可以重做 + """ + return len(self._redo_stack) > 0 + + def clear_history(self): + """ + 清空所有历史记录 + """ + self._undo_stack.clear() + self._redo_stack.clear() + + def get_undo_count(self) -> int: + """ + 获取可撤销的命令数量 + """ + return len(self._undo_stack) + + def get_redo_count(self) -> int: + """ + 获取可重做的命令数量 + """ + return len(self._redo_stack) + + +# 示例命令实现 +class MoveNodeCommand(Command): + """ + 移动节点命令示例 + """ + + def __init__(self, node: NodePath, old_pos, new_pos): + self.node = node + self.old_pos = old_pos + self.new_pos = new_pos + + def execute(self): + """ + 执行移动操作 + """ + self.node.setPos(self.new_pos) + + def undo(self): + """ + 撤销移动操作 + """ + self.node.setPos(self.old_pos) + + def redo(self): + """ + 重做移动操作 + """ + self.node.setPos(self.new_pos) + + +class DeleteNodeCommand(Command): + """ + 删除节点命令示例 + """ + + def __init__(self, node: NodePath, parent_node: NodePath,world=None): + self.node = node + self.parent_node = parent_node + self.world = world + + self.node_name = node.getName() + self.node_pos = node.getPos() + self.node_hpr = node.getHpr() + self.node_scale = node.getScale() + + # 保存节点类型信息 + self.node_type = "NODE" + if node.hasTag("tree_item_type"): + self.node_type = node.getTag("tree_item_type") + elif node.hasTag("gui_type"): + gui_type = node.getTag("gui_type") + if gui_type == "button": + self.node_type = "GUI_BUTTON" + elif gui_type == "label": + self.node_type = "GUI_LABEL" + elif gui_type == "entry": + self.node_type = "GUI_ENTRY" + elif gui_type == "2d_image": + self.node_type = "GUI_IMAGE" + elif gui_type == "3d_text": + self.node_type = "GUI_3DTEXT" + elif gui_type == "3d_image": + self.node_type = "GUI_3DIMAGE" + elif gui_type == "video_screen": + self.node_type = "GUI_VIDEO_SCREEN" + elif gui_type == "2d_video_screen": + self.node_type = "GUI_2D_VIDEO_SCREEN" + elif node.hasTag("light_type"): + self.node_type = "LIGHT_NODE" + elif node.hasTag("element_type") and node.getTag("element_type") == "cesium_tileset": + self.node_type = "CESIUM_TILESET_NODE" + elif node.hasTag("is_scene_element"): + self.node_type = "SCENE_NODE" + + self.node_tags = {} + if hasattr(node,'hasTag'): + for tag_key in node.getNetTag('tags').split(',') if node.hasTag('tags') else []: + if node.hasTag(tag_key): + self.node_tags[tag_key] = node.getTag(tag_key) + else: + try: + if hasattr(node,'getTag'): + common_tags = ['is_scene_element','tree_item_type','gui_type','light_type', + 'element_type','file','model_path','video_path','image_path', + 'gui_text','name','created_by_user'] + for tag in common_tags: + if node.hasTag(tag): + self.node_tags[tag] = node.getTag(tag) + except: + pass + + self.node_python_tags = {} + if hasattr(node,'getPythonTagKeys'): + try: + for tag_key in node.getPythonTagKeys(): + self.node_python_tags[tag_key] = node.getPythonTag(tag_key) + except Exception as e: + pass + + # 对于特定类型的节点,保存额外的数据 + self.extra_data = {} + if self.node_type in ["GUI_BUTTON", "GUI_LABEL", "GUI_ENTRY", "GUI_IMAGE", + "GUI_3DTEXT", "GUI_3DIMAGE", "GUI_VIDEO_SCREEN", "GUI_2D_VIDEO_SCREEN"]: + if node.hasTag("gui_text"): + self.extra_data["gui_text"] = node.getTag("gui_text") + if node.hasTag("video_path"): + self.extra_data["video_path"] = node.getTag("video_path") + if node.hasTag("image_path"): + self.extra_data["image_path"] = node.getTag("image_path") + elif self.node_type == "LIGHT_NODE": + if node.hasTag("light_type"): + self.extra_data["light_type"] = node.getTag("light_type") + rp_light = node.getPythonTag("rp_light_object") + if rp_light: + self.extra_data["light_data"] = { + 'energy': getattr(rp_light, 'energy', 5000), + 'radius': getattr(rp_light, 'radius', 1000), + 'fov': getattr(rp_light, 'fov', 70) if hasattr(rp_light, 'fov') else 70, + 'inner_radius': getattr(rp_light, 'inner_radius', 0.4) if hasattr(rp_light, + 'inner_radius') else 0.4, + 'casts_shadows': getattr(rp_light, 'casts_shadows', True), + 'shadow_map_resolution': getattr(rp_light, 'shadow_map_resolution', 256) + } + elif self.node_type == "CESIUM_TILESET_NODE": + if node.hasTag("tileset_url"): + self.extra_data["tileset_url"] = node.getTag("tileset_url") + + def execute(self): + """ + 执行删除操作 + """ + # 从world的相应列表中移除节点引用 + if self.world and hasattr(self.world, 'scene_manager'): + scene_manager = self.world.scene_manager + if self.node_type == "LIGHT_NODE": + if self.node.hasTag("light_type"): + light_type = self.node.getTag("light_type") + if light_type == "spot_light" and hasattr(scene_manager, + 'Spotlight') and self.node in scene_manager.Spotlight: + scene_manager.Spotlight.remove(self.node) + elif light_type == "point_light" and hasattr(scene_manager, + 'Pointlight') and self.node in scene_manager.Pointlight: + scene_manager.Pointlight.remove(self.node) + elif self.node_type == "IMPORTED_MODEL_NODE" and hasattr(scene_manager, + 'models') and self.node in scene_manager.models: + scene_manager.models.remove(self.node) + elif self.node_type.startswith("GUI_") and hasattr(self.world, + 'gui_elements') and self.node in self.world.gui_elements: + self.world.gui_elements.remove(self.node) + elif self.node_type == "CESIUM_TILESET_NODE": + # 从tilesets列表中移除 + if hasattr(scene_manager, 'tilesets'): + tilesets_to_remove = [] + for i, tileset_info in enumerate(scene_manager.tilesets): + if tileset_info.get('node') == self.node: + tilesets_to_remove.append(i) + for i in reversed(tilesets_to_remove): + del scene_manager.tilesets[i] + + # 从场景图中移除节点 + if self.node and not self.node.isEmpty(): + self.node.removeNode() + + def undo(self): + """ + 撤销删除操作(重新创建节点) + """ + try: + # 使用场景管理器重建节点 + if self.world and hasattr(self.world, 'scene_manager'): + scene_manager = self.world.scene_manager + + # 创建节点数据字典 + node_data = { + 'name': self.node_name, + 'node_type': self.node_type, + 'pos': (self.node_pos.x, self.node_pos.y, self.node_pos.z), + 'hpr': (self.node_hpr.x, self.node_hpr.y, self.node_hpr.z), + 'scale': (self.node_scale.x, self.node_scale.y, self.node_scale.z), + 'tags': self.node_tags + } + + # 添加额外数据 + if self.extra_data: + if self.node_type.startswith("GUI_"): + node_data['gui_data'] = self.extra_data + elif self.node_type == "LIGHT_NODE": + node_data['light_data'] = self.extra_data.get('light_data', {}) + elif self.node_type == "CESIUM_TILESET_NODE": + node_data['tileset_url'] = self.extra_data.get('tileset_url', '') + + # 重建节点 + new_node = scene_manager.recreateNodeFromData(node_data, self.parent_node) + + if new_node: + print(f"✅ 成功撤销删除操作,节点 {self.node_name} 已恢复") + # 更新节点引用 + self.node = new_node + else: + print(f"❌ 撤销删除操作失败,无法重建节点 {self.node_name}") + else: + print("❌ 无法撤销删除操作,缺少场景管理器引用") + + except Exception as e: + print(f"❌ 撤销删除操作时出错: {e}") + import traceback + traceback.print_exc() + + def redo(self): + """ + 重做删除操作 + """ + self.execute() + + +class RotateNodeCommand(Command): + """ + 旋转节点命令 + """ + + def __init__(self, node: NodePath, old_hpr, new_hpr): + self.node = node + self.old_hpr = old_hpr + self.new_hpr = new_hpr + + def execute(self): + """ + 执行旋转操作 + """ + self.node.setHpr(self.new_hpr) + + def undo(self): + """ + 撤销旋转操作 + """ + self.node.setHpr(self.old_hpr) + + def redo(self): + """ + 重做旋转操作 + """ + self.node.setHpr(self.new_hpr) + + +class ScaleNodeCommand(Command): + """ + 缩放节点命令 + """ + + def __init__(self, node: NodePath, old_scale, new_scale): + self.node = node + self.old_scale = old_scale + self.new_scale = new_scale + + def execute(self): + """ + 执行缩放操作 + """ + self.node.setScale(self.new_scale) + + def undo(self): + """ + 撤销缩放操作 + """ + self.node.setScale(self.old_scale) + + def redo(self): + """ + 重做缩放操作 + """ + self.node.setScale(self.new_scale) + + +class CreateNodeCommand(Command): + """ + 创建节点命令 + """ + + def __init__(self, node_creator_func,parent_node, *args, **kwargs): + self.node_creator_func = node_creator_func + self.parent_node = parent_node + self.args = args + self.kwargs = kwargs + self.created_node = None + + def execute(self): + """ + 执行创建节点操作 + """ + self.created_node = self.node_creator_func(self.parent_node,*self.args, **self.kwargs) + return self.created_node + + def undo(self): + """ + 撤销创建节点操作 + """ + if self.created_node: + self.created_node.removeNode() + + def redo(self): + """ + 重做创建节点操作 + """ + self.execute() + + +class ReparentNodeCommand(Command): + """ + 重新设置节点父子关系命令 + """ + + def __init__(self, node: NodePath, old_parent: NodePath, new_parent: NodePath, is_2d_gui=False, world=None): + self.node = node + self.old_parent = old_parent + self.new_parent = new_parent + self.is_2d_gui = is_2d_gui + self.world = world + # 保存节点在操作前的世界坐标和局部坐标,以便在撤销/重做时保持位置不变 + self.world_pos = node.getPos(self.world.render if self.world else node.getParent()) + self.world_hpr = node.getHpr(self.world.render if self.world else node.getParent()) + self.world_scale = node.getScale(self.world.render if self.world else node.getParent()) + # 同时保存局部坐标,因为在父节点改变后可能需要恢复 + self.local_pos = node.getPos() + self.local_hpr = node.getHpr() + self.local_scale = node.getScale() + + def execute(self): + """ + 执行重新父化操作 + """ + if self.is_2d_gui and self.world: + # 2D GUI元素需要特殊处理 + if self.new_parent and not self.new_parent.isEmpty(): + if hasattr(self.new_parent, 'getTag') and self.new_parent.getTag("is_gui_element") == "1": + # 目标是GUI元素,直接重新父化 + self.node.wrtReparentTo(self.new_parent) + else: + # 目标是3D节点,保持GUI特性,重新父化到aspect2d + self.node.wrtReparentTo(self.world.aspect2d) + print(f"2D GUI元素保持在aspect2d下") + else: + # 如果新父节点为None,重新父化到aspect2d + self.node.wrtReparentTo(self.world.aspect2d) + print(f"2D GUI元素重新父化到aspect2d") + else: + # 普通3D节点的处理 + if self.new_parent and not self.new_parent.isEmpty(): + self.node.wrtReparentTo(self.new_parent) + else: + # 如果新父节点为空,将其父化到render节点 + if self.world: + self.node.wrtReparentTo(self.world.render) + else: + # 备用方案 + from panda3d.core import NodePath + self.node.wrtReparentTo(NodePath("render")) + + def undo(self): + """ + 撤销重新父化操作 + """ + # 在改变父节点前保存当前的缩放值 + current_scale = self.node.getScale() + + if self.is_2d_gui and self.world: + # 2D GUI元素需要特殊处理 + if self.old_parent and not self.old_parent.isEmpty(): + if hasattr(self.old_parent, 'getTag') and self.old_parent.getTag("is_gui_element") == "1": + # 原父节点是GUI元素,直接重新父化 + self.node.wrtReparentTo(self.old_parent) + else: + # 原父节点是3D节点,保持GUI特性,重新父化到aspect2d + self.node.wrtReparentTo(self.world.aspect2d) + print(f"2D GUI元素恢复到aspect2d下") + else: + # 如果原父节点为空,重新父化到aspect2d + self.node.wrtReparentTo(self.world.aspect2d) + print(f"2D GUI元素恢复到aspect2d") + else: + # 普通3D节点的处理 + if self.old_parent and not self.old_parent.isEmpty(): + self.node.wrtReparentTo(self.old_parent) + else: + # 如果原父节点为空,将其父化到render节点 + if self.world: + self.node.wrtReparentTo(self.world.render) + else: + # 备用方案 + from panda3d.core import NodePath + self.node.wrtReparentTo(NodePath("render")) + + # 恢复局部坐标(不是世界坐标),因为父节点已经改变 + self.node.setPos(self.local_pos) + self.node.setHpr(self.local_hpr) + # 特别处理缩放,确保GUI元素的缩放不会异常变化 + if not self.is_2d_gui or abs(current_scale.length() - self.local_scale.length()) > 0.001: + self.node.setScale(self.local_scale) + + def redo(self): + """ + 重做重新父化操作 + """ + # 在改变父节点前保存当前的缩放值 + current_scale = self.node.getScale() + + if self.is_2d_gui and self.world: + # 2D GUI元素需要特殊处理 + if self.new_parent and not self.new_parent.isEmpty(): + if hasattr(self.new_parent, 'getTag') and self.new_parent.getTag("is_gui_element") == "1": + # 目标是GUI元素,直接重新父化 + self.node.wrtReparentTo(self.new_parent) + else: + # 目标是3D节点,保持GUI特性,重新父化到aspect2d + self.node.wrtReparentTo(self.world.aspect2d) + print(f"2D GUI元素保持在aspect2d下") + else: + # 如果新父节点为None,重新父化到aspect2d + self.node.wrtReparentTo(self.world.aspect2d) + print(f"2D GUI元素重新父化到aspect2d") + else: + # 普通3D节点的处理 + if self.new_parent and not self.new_parent.isEmpty(): + self.node.wrtReparentTo(self.new_parent) + else: + # 如果新父节点为空,将其父化到render节点 + if self.world: + self.node.wrtReparentTo(self.world.render) + else: + # 备用方案 + from panda3d.core import NodePath + self.node.wrtReparentTo(NodePath("render")) + + # 恢复局部坐标(不是世界坐标),因为父节点已经改变 + self.node.setPos(self.local_pos) + self.node.setHpr(self.local_hpr) + # 特别处理缩放,确保GUI元素的缩放不会异常变化 + if not self.is_2d_gui or abs(current_scale.length() - self.local_scale.length()) > 0.001: + self.node.setScale(self.local_scale) + + + diff --git a/core/selection.py b/core/selection.py index 8a3302aa..e2a3c639 100644 --- a/core/selection.py +++ b/core/selection.py @@ -148,7 +148,7 @@ class SelectionSystem: # 初始更新选择框 #print(" 开始初始化选择框几何体...") - #self.updateSelectionBoxGeometry() + self.updateSelectionBoxGeometry() #print(f" ✓ 为节点 {nodePath.getName()} 创建了选择框") @@ -1900,6 +1900,37 @@ class SelectionSystem: """停止坐标轴拖拽""" print(f"停止坐标轴拖拽 - 轴: {self.dragGizmoAxis}") + if hasattr(self.world,'command_manager') and self.world.command_manager and self.gizmoTarget: + current_pos = self.gizmoTarget.getPos() + + if (hasattr(self,'gizmoTargetStartPos') and self.gizmoTargetStartPos and + (abs(current_pos.x-self.gizmoTargetStartPos.x)>0.001 or + abs(current_pos.y-self.gizmoTargetStartPos.y)>0.001 or + abs(current_pos.z-self.gizmoTargetStartPos.z)>0.001)): + from core.Command_System import MoveNodeCommand + command = MoveNodeCommand(self.gizmoTarget,self.gizmoTargetStartPos,current_pos) + self.world.command_manager.execute_command(command) + # 如果是缩放操作且缩放发生了变化,则创建缩放命令 + elif (hasattr(self, 'gizmoTargetStartScale') and hasattr(self, 'gizmoTargetStartScale') and + self.gizmoTargetStartScale): + current_scale = self.gizmoTarget.getScale() + if (abs(current_scale.x - self.gizmoTargetStartScale.x) > 0.001 or + abs(current_scale.y - self.gizmoTargetStartScale.y) > 0.001 or + abs(current_scale.z - self.gizmoTargetStartScale.z) > 0.001): + from core.Command_System import ScaleNodeCommand + command = ScaleNodeCommand(self.gizmoTarget, self.gizmoTargetStartScale, current_scale) + self.world.command_manager.execute_command(command) + # 如果是旋转操作且旋转发生了变化,则创建旋转命令 + elif (hasattr(self, 'gizmoTargetStartHpr') and hasattr(self, 'gizmoTargetStartHpr') and + self.gizmoTargetStartHpr): + current_hpr = self.gizmoTarget.getHpr() + if (abs(current_hpr.x - self.gizmoTargetStartHpr.x) > 0.001 or + abs(current_hpr.y - self.gizmoTargetStartHpr.y) > 0.001 or + abs(current_hpr.z - self.gizmoTargetStartHpr.z) > 0.001): + from core.Command_System import RotateNodeCommand + command = RotateNodeCommand(self.gizmoTarget, self.gizmoTargetStartHpr, current_hpr) + self.world.command_manager.execute_command(command) + # 恢复所有轴的颜色 for axis_name in ["x", "y", "z"]: self.setGizmoAxisColor(axis_name, self.gizmo_colors[axis_name]) diff --git a/gui/gui_manager.py b/gui/gui_manager.py index cfd10728..22bb47ef 100644 --- a/gui/gui_manager.py +++ b/gui/gui_manager.py @@ -1509,9 +1509,11 @@ class GUIManager: video_screen.setTag("tree_item_type", "GUI_2D_VIDEO_SCREEN") video_screen.setTag("created_by_user", "1") video_screen.setTag("name",screen_name) - - video_screen.setTag("video_path", video_path if video_path else "") - print(f"🔧 设置2D视频屏幕标签 - video_path: {video_path if video_path else '空'}") + # 修复后 + if video_path is not None: + video_screen.setTag("video_path", video_path) + else: + video_screen.setTag("video_path", "") # 关键修改:预先创建一个占位符纹理,为后续视频播放做准备 placeholder_texture = Texture(f"placeholder_video_texture_{len(self.gui_elements)}") @@ -2186,9 +2188,17 @@ class GUIManager: print(f"开始编辑GUI元素: 类型={gui_type}, 属性={property_name}, 值={value}") if property_name == "text": + original_frame_size = None + if hasattr(gui_element,'getFrameSize'): + try: + original_frame_size = gui_element.getFrameSize() + except: + pass if gui_type in ["button", "label"]: gui_element['text'] = value print(f"成功更新2D GUI文本: {value}") + # if gui_type == "button": + # self._resizeButtonToText(gui_element,value,original_frame_size) elif gui_type == "entry": gui_element.set(value) print(f"成功更新输入框文本: {value}") @@ -2267,6 +2277,64 @@ class GUIManager: traceback.print_exc() return False + def _resizeButtonToText(self, button, text, original_frame_size=None): + """ + 根据文本内容调整按钮大小 + + Args: + button: DirectButton 对象 + text: 新的文本内容 + original_frame_size: 原始frameSize,用于计算合适的padding + """ + try: + # 获取按钮当前的文本缩放 + text_scale = 0.03 # 默认文本缩放 + padding = 0.2 # 默认边距 + + # 尝试从按钮获取实际的文本缩放值 + if hasattr(button, 'getTextScale'): + text_scale = button.getTextScale() + elif hasattr(button, 'textScale'): + text_scale = button.textScale + + # 根据原始frameSize计算合适的padding + if original_frame_size and len(original_frame_size) >= 4: + # 基于原始按钮宽度计算合适的padding + padding = max((original_frame_size[1] - original_frame_size[0]) * 0.15, 0.1) + + # 更精确的文本尺寸估算 + # 考虑中文字符和英文字符的不同宽度 + chinese_chars = len([c for c in text if ord(c) > 127]) + english_chars = len(text) - chinese_chars + + # 中文字符通常比英文字符宽 + char_width = text_scale * 0.6 + text_width = (chinese_chars * char_width * 1.5) + (english_chars * char_width) + text_height = text_scale * 1.2 # 文本高度 + + # 计算新的frameSize,确保有足够的边距 + half_width = max(text_width * 0.5 + padding, 0.3) # 最小宽度0.3 + half_height = max(text_height * 0.5 + padding * 0.5, 0.15) # 最小高度0.15 + + # 正确设置frameSize - 使用字典方式设置 + new_frame = (-half_width, half_width, -half_height, half_height) + button['frameSize'] = new_frame # 使用字典方式设置 + + print(f"按钮大小已调整: {new_frame} (文本: {text})") + + except Exception as e: + print(f"调整按钮大小失败: {e}") + # 如果自动调整失败,保持原有大小或设置一个合理的默认大小 + try: + if original_frame_size: + # 保持原有大小 + button['frameSize'] = original_frame_size + else: + # 设置一个合理的默认大小 + button['frameSize'] = (-0.5, 0.5, -0.15, 0.15) + except: + pass + def duplicateGUIElement(self, gui_element): """复制GUI元素""" try: diff --git a/main.py b/main.py index bd204899..e8c9c590 100644 --- a/main.py +++ b/main.py @@ -23,6 +23,7 @@ from core.vr_manager import VRManager from core.vr_input_handler import VRInputHandler from core.alvr_streamer import ALVRStreamer from core.patrol_system import PatrolSystem +from core.Command_System import CommandManager from gui.gui_manager import GUIManager from core.terrain_manager import TerrainManager from scene.scene_manager import SceneManager @@ -110,6 +111,8 @@ class MyWorld(CoreWorld): self.info_panel_manager = InfoPanelManager(self) + self.command_manager = CommandManager() + # 初始化碰撞管理器 from core.collision_manager import CollisionManager self.collision_manager = CollisionManager(self) @@ -239,7 +242,7 @@ class MyWorld(CoreWorld): """创建3D空间文本""" return self.gui_manager.createGUI3DText(pos, text, size) - def createGUI3DImage(self,pos=(0,0,0),text="3D图片",size=(2,2)): + def createGUI3DImage(self,pos=(0,0,0),text="3D图片",size=(1,1)): """创建3D图片""" return self.gui_manager.createGUI3DImage(pos,text,size) diff --git a/scene/scene_manager.py b/scene/scene_manager.py index 1ad2d9ab..f11c0832 100644 --- a/scene/scene_manager.py +++ b/scene/scene_manager.py @@ -2506,6 +2506,7 @@ class SceneManager: light.radius = 1000 light.casts_shadows = True light.shadow_map_resolution = 256 + light.setPos(pos) # 添加到渲染管线 render_pipeline.add_light(light) @@ -2682,177 +2683,6 @@ class SceneManager: pass return None - # def createSpotLight(self, pos=(0, 0, 0)): - # """创建聚光灯 - 使用统一的create_item方法""" - # try: - # # 调用CustomTreeWidget的create_item方法创建聚光灯节点 - # if hasattr(self.world, 'interface_manager') and hasattr(self.world.interface_manager, 'treeWidget'): - # tree_widget = self.world.interface_manager.treeWidget - # if tree_widget and hasattr(tree_widget, 'create_item'): - # # 创建聚光灯节点 - # created_nodes = tree_widget.create_item("spot_light") - # - # if created_nodes: - # # 获取创建的节点 - # light_np, qt_item = created_nodes[0] - # - # # 设置位置(如果指定了非默认位置) - # if pos != (0, 0, 0): - # light_np.setPos(*pos) - # # 同时更新光源对象的位置 - # light_obj = light_np.getPythonTag("rp_light_object") - # if light_obj: - # light_obj.setPos(*pos) - # - # print(f"✅ 通过create_item创建聚光灯成功: {light_np.getName()}") - # return light_np - # else: - # print("❌ create_item创建聚光灯失败") - # return None - # else: - # print("❌ 无法访问树形控件的create_item方法") - # return None - # else: - # print("❌ 无法访问界面管理器或树形控件") - # return None - # - # except Exception as e: - # print(f"❌ 创建聚光灯时发生错误: {str(e)}") - # import traceback - # traceback.print_exc() - # return None - - # def createSpotLight(self, pos=(0, 0, 0)): - # from RenderPipelineFile.rpcore import SpotLight, RenderPipeline - # from panda3d.core import Vec3,NodePath - # - # render_pipeline = get_render_pipeline() - # - # # 创建一个挂载节点(你控制的) - # light_np = NodePath("SpotlightAttachNode") - # light_np.reparentTo(self.world.render) - # #light_np.setPos(*pos) - # - # self.half_energy = 5000 - # self.lamp_fov = 70 - # self.lamp_radius = 1000 - # - # light = SpotLight() - # light.direction = Vec3(0, 0, -1) # 光照方向 - # light.fov = self.lamp_fov # 光源角度(类似手电筒) - # light.set_color_from_temperature(5 * 1000.0) # 色温(K) - # light.energy = self.half_energy # 光照强度 - # light.radius = self.lamp_radius # 影响范围 - # light.casts_shadows = True # 是否投射阴影 - # light.shadow_map_resolution = 256 # 阴影分辨率 - # light.setPos(*pos) - # #light_np.setPos(*pos) - # - # #light_np = render_pipeline.add_light(light, parent=self.world.render) - # render_pipeline.add_light(light) # 添加到渲染管线 - # - # light_name = f"Spotlight_{len(self.Spotlight)}" - # - # light_np.setName(light_name) # 设置唯一名称 - # #light_np.reparentTo(self.world.render) # 挂载到场景根节点 - # - # light_np.setTag("light_type", "spot_light") - # light_np.setTag("is_scene_element", "1") - # light_np.setTag("light_energy", str(light.energy)) - # - # light_np.setPythonTag("rp_light_object", light) - # - # self.Spotlight.append(light_np) - # - # if hasattr(self.world, 'updateSceneTree'): - # self.world.updateSceneTree() - # - # #print("nikan"+light_np.getHpr()) - - # def createPointLight(self, pos=(0, 0, 0)): - # """创建点光源 - 使用统一的create_item方法""" - # try: - # # 调用CustomTreeWidget的create_item方法创建点光源节点 - # if hasattr(self.world, 'interface_manager') and hasattr(self.world.interface_manager, 'treeWidget'): - # tree_widget = self.world.interface_manager.treeWidget - # if tree_widget and hasattr(tree_widget, 'create_item'): - # # 创建点光源节点 - # created_nodes = tree_widget.create_item("point_light") - # - # if created_nodes: - # # 获取创建的节点 - # light_np, qt_item = created_nodes[0] - # - # # 设置位置(如果指定了非默认位置) - # if pos != (0, 0, 0): - # light_np.setPos(*pos) - # # 同时更新光源对象的位置 - # light_obj = light_np.getPythonTag("rp_light_object") - # if light_obj: - # light_obj.setPos(*pos) - # - # print(f"✅ 通过create_item创建点光源成功: {light_np.getName()}") - # return light_np - # else: - # print("❌ create_item创建点光源失败") - # return None - # else: - # print("❌ 无法访问树形控件的create_item方法") - # return None - # else: - # print("❌ 无法访问界面管理器或树形控件") - # return None - # - # except Exception as e: - # print(f"❌ 创建点光源时发生错误: {str(e)}") - # import traceback - # traceback.print_exc() - # return None - - # def createPointLight(self, pos=(0, 0, 0)): - # from RenderPipelineFile.rpcore import PointLight, RenderPipeline - # from panda3d.core import Vec3, NodePath - # - # render_pipeline = get_render_pipeline() - # - # # 创建一个挂载节点(你控制的) - # light_np = NodePath("PointlightAttachNode") - # light_np.reparentTo(self.world.render) - # - # - # light = PointLight() - # light.setPos(*pos) - # light_np.setPos(*pos) - # light.energy = 5000 - # light.radius = 1000 - # light.inner_radius = 0.4 - # light.set_color_from_temperature(5 * 1000.0) # 色温(K) - # light.casts_shadows = True # 是否投射阴影 - # light.shadow_map_resolution = 256 # 阴影分辨率 - # - # render_pipeline.add_light(light) # 添加到渲染管线 - # - # light_name = f"Pointlight{len(self.Pointlight)}" - # - # light_np.setName(light_name) # 设置唯一名称 - # - # #light_np = NodePath(f"PointLight_{len(self.Pointlight)}") - # #light_np.reparentTo(self.world.render) - # #light_np.setPos(*pos) - # - # light_np.setTag("light_type", "point_light") - # light_np.setTag("is_scene_element", "1") - # light_np.setTag("light_energy", str(light.energy)) - # - # # 保存光源对象引用(重要!用于属性面板) - # light_np.setPythonTag("rp_light_object", light) - # - # self.Pointlight.append(light_np) - # - # if hasattr(self.world, 'updateSceneTree'): - # self.world.updateSceneTree() - # - # return light,light_np # ==================== GLB 转换方法 ==================== @@ -3762,7 +3592,7 @@ except Exception as e: elif node_type == "CESIUM_TILESET_NODE": new_node = self._recreateTilesetFromData(node_data, parent_node, unique_name) elif node_type in ["GUI_BUTTON", "GUI_LABEL", "GUI_ENTRY", "GUI_IMAGE", - "GUI_3DTEXT", "GUI_3DIMAGE", "GUI_VIRTUAL_SCREEN"]: + "GUI_3DTEXT", "GUI_3DIMAGE", "GUI_VIDEO_SCREEN","GUI_2D_VIDEO_SCREEN"]: new_node = self._recreateGUIFromData(node_data, parent_node, unique_name) elif node_type == "IMPORTED_MODEL_NODE": new_node = self._recreateModelFromData(node_data, parent_node, unique_name) @@ -3947,7 +3777,7 @@ except Exception as e: new_gui_element = self.world.createGUI3DText(pos, text, scale) elif gui_type == "3d_image" and hasattr(self.world, 'createGUI3DImage'): pos = node_data.get('pos', (0, 0, 0)) - image_path = node_data.get('tags').get('image_path', '') + image_path = node_data.get('tags').get('gui_image_path', '') scale = node_data.get('scale', (1, 1)) if isinstance(scale, (int, float)): scale = (scale, scale) @@ -3956,7 +3786,17 @@ except Exception as e: else: scale = (1, 1) print(f"正在创建3D图片: 位置={pos}, 路径={image_path}, 大小={scale}") - new_gui_element = self.world.createGUI3DImage(pos, image_path, scale) + new_gui_element = self.world.gui_manager.createGUI3DImage(pos, image_path, scale) + elif gui_type == "video_screen" and hasattr(self.world.gui_manager, 'createVideoScreen'): + pos = node_data.get('pos', (0, 0, 0)) + video_path = node_data.get('tags').get('video_path', '') + scale = node_data.get('scale', (1, 1,1)) + new_gui_element = self.world.gui_manager.createVideoScreen(pos,scale,video_path) + elif gui_type == "2d_video_screen" and hasattr(self.world.gui_manager, 'createGUI2DVideoScreen'): + pos = node_data.get('pos', (0, 0, 0)) + video_path = node_data.get('tags').get('video_path', '') + scale = node_data.get('scale', (1, 1, 1)) + new_gui_element = self.world.gui_manager.createGUI2DVideoScreen(pos,scale,video_path) if new_gui_element: # 设置名称和变换 diff --git a/ui/main_window.py b/ui/main_window.py index 4fb010b5..ecb46dfd 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -2134,12 +2134,37 @@ class MainWindow(QMainWindow): # 添加撤销/重做功能的基础实现 def onUndo(self): """撤销操作""" - QMessageBox.information(self, "提示", "撤销功能将在后续版本中实现") + if hasattr(self.world,'command_manager'): + if self.world.command_manager.can_undo(): + success = self.world.command_manager.undo() + if success: + print("成功操作") + else: + print("撤销失败") + QMessageBox.information(self,"提示","撤销操作失败") + else: + print("没有可撤销的操作") + QMessageBox.information(self,"提示","没有可撤销的操作") + else: + print("命令管理器未初始化") + QMessageBox.information(self,"提示","命令系统未初始化") def onRedo(self): """重做操作""" - QMessageBox.information(self, "提示", "重做功能将在后续版本中实现") - + if hasattr(self.world,'command_manager'): + if self.world.command_manager.can_redo(): + success = self.world.command_manager.redo() + if success: + print("成功重做") + else: + print("重做失败") + QMessageBox.information(self,"提示","重做操作失败") + else: + print("没有可重做的操作") + QMessageBox.information(self,"提示","没有可重做的操作") + else: + print("命令管理器未初始化") + QMessageBox.information(self,"提示","命令系统未初始化") def onCreateCesiumView(self): if hasattr(self.world,'gui_manager') and self.world.gui_manager: diff --git a/ui/widgets.py b/ui/widgets.py index cefd9b3f..d5978703 100644 --- a/ui/widgets.py +++ b/ui/widgets.py @@ -2165,7 +2165,6 @@ class CustomTreeWidget(QTreeWidget): if hasattr(panda_node, 'getPythonTag'): light_object = panda_node.getPythonTag('rp_light_object') if light_object and hasattr(self.world, 'render_pipeline'): - print(f'11111111111111111111111111,{light_object.casts_shadows}') self.world.render_pipeline.remove_light(light_object) # 从world列表中移除 @@ -2231,6 +2230,14 @@ class CustomTreeWidget(QTreeWidget): print("ℹ️ 尝试删除一个空的或无效的节点,操作取消。") return + # #如果有命令管理系统,则使用命令系统 + # if hasattr(self.world,'command_manager') and self.world.command_manager: + # from core.Command_System import DeleteNodeCommand + # parent_node = panda_node.getParent() + # command = DeleteNodeCommand(panda_node,parent_node) + # self.world.command_manager.execute_command(command) + # return + # --- 关键修复:在操作前,安全地获取节点名字 --- node_name_for_logging = panda_node.getName()