1
0
forked from Rowland/EG
EG/core/Command_System.py

571 lines
20 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

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

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)