forked from Rowland/EG
649 lines
22 KiB
Python
649 lines
22 KiB
Python
from abc import ABC, abstractmethod
|
||
from collections import deque
|
||
from typing import List
|
||
from panda3d.core import NodePath, Point3
|
||
|
||
|
||
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):
|
||
"""
|
||
重新设置节点父子关系命令 - 增强版(同时处理Panda3D和Qt树)
|
||
"""
|
||
|
||
def __init__(self, node: NodePath, old_parent: NodePath, new_parent: NodePath,
|
||
old_parent_item=None, new_parent_item=None, is_2d_gui=False, world=None):
|
||
self.node = node
|
||
self.old_parent = old_parent
|
||
self.new_parent = new_parent
|
||
self.old_parent_item = old_parent_item # Qt树中的旧父节点项
|
||
self.new_parent_item = new_parent_item # Qt树中的新父节点项
|
||
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 _updateQtTree(self, node_item, new_parent_item):
|
||
"""更新Qt树控件中的节点位置"""
|
||
if not node_item or not new_parent_item:
|
||
return
|
||
|
||
# 从当前父节点中移除
|
||
current_parent = node_item.parent()
|
||
if current_parent:
|
||
current_parent.removeChild(node_item)
|
||
else:
|
||
# 如果是顶级项目
|
||
tree_widget = node_item.treeWidget()
|
||
if tree_widget:
|
||
index = tree_widget.indexOfTopLevelItem(node_item)
|
||
if index >= 0:
|
||
tree_widget.takeTopLevelItem(index)
|
||
|
||
# 添加到新父节点
|
||
new_parent_item.addChild(node_item)
|
||
|
||
def execute(self):
|
||
"""
|
||
执行重新父化操作
|
||
"""
|
||
# 更新Panda3D节点父子关系
|
||
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()
|
||
|
||
# 恢复Panda3D节点父子关系
|
||
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()
|
||
|
||
# 重新执行Panda3D节点父子关系更新
|
||
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)
|
||
|
||
|
||
class CompositeCommand(Command):
|
||
"""
|
||
组合命令类,用于同时执行多个命令
|
||
"""
|
||
def __init__(self,commands:List[Command]):
|
||
self.commands = commands
|
||
|
||
def execute(self):
|
||
"""
|
||
执行所有命令
|
||
"""
|
||
for command in self.commands:
|
||
command.execute()
|
||
|
||
def undo(self):
|
||
"""
|
||
撤销所有命令(逆序执行)
|
||
"""
|
||
for command in reversed(self.commands):
|
||
command.undo()
|
||
|
||
def redo(self):
|
||
"""
|
||
重做所有命令
|
||
"""
|
||
for command in self.commands:
|
||
command.redo()
|
||
|
||
class MoveLightCommand(Command):
|
||
def __init__(self, node, old_pos, new_pos, light_object=None):
|
||
self.node = node
|
||
self.old_pos = Point3(old_pos)
|
||
self.new_pos = Point3(new_pos)
|
||
self.light_object = light_object
|
||
|
||
def execute(self): # 将原来的 do() 改为 execute()
|
||
if self.light_object:
|
||
self.light_object.pos = self.new_pos
|
||
if self.node:
|
||
self.node.setPos(self.new_pos)
|
||
|
||
def undo(self):
|
||
if self.light_object:
|
||
self.light_object.pos = self.old_pos
|
||
if self.node:
|
||
self.node.setPos(self.old_pos)
|
||
|
||
def redo(self):
|
||
self.execute() # 调用 execute() 而不是 do()
|
||
|
||
|
||
|