EG/core/Command_System.py
2026-03-27 11:15:16 +08:00

1014 lines
36 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, Point3
def _is_valid_node(node) -> bool:
return bool(node) and hasattr(node, "isEmpty") and (not node.isEmpty())
def _is_light_node(node: NodePath) -> bool:
return bool(node) and hasattr(node, "hasTag") and node.hasTag("light_type")
def _is_terrain_node(node: NodePath) -> bool:
return bool(node) and hasattr(node, "hasTag") and node.hasTag("tree_item_type") and node.getTag("tree_item_type") == "TERRAIN_NODE"
def _set_light_registration(world, node: NodePath, registered: bool):
if not world or not _is_valid_node(node) or not _is_light_node(node):
return
scene_manager = getattr(world, "scene_manager", None)
light_type = node.getTag("light_type")
light_lists = []
if scene_manager:
if light_type == "spot_light" and hasattr(scene_manager, "Spotlight"):
light_lists.append(scene_manager.Spotlight)
elif light_type == "point_light" and hasattr(scene_manager, "Pointlight"):
light_lists.append(scene_manager.Pointlight)
rp_light = node.getPythonTag("rp_light_object") if hasattr(node, "hasPythonTag") and node.hasPythonTag("rp_light_object") else None
current_registered = bool(node.getPythonTag("engine_light_registered")) if hasattr(node, "hasPythonTag") and node.hasPythonTag("engine_light_registered") else False
if registered:
for light_list in light_lists:
if node not in light_list:
light_list.append(node)
if not current_registered:
try:
if rp_light is not None and getattr(world, "render_pipeline", None):
world.render_pipeline.add_light(rp_light)
elif hasattr(world, "render") and world.render:
world.render.setLight(node)
except Exception:
pass
try:
node.setPythonTag("engine_light_registered", True)
except Exception:
pass
return
for light_list in light_lists:
try:
while node in light_list:
light_list.remove(node)
except Exception:
pass
if current_registered:
try:
if rp_light is not None and getattr(world, "render_pipeline", None):
world.render_pipeline.remove_light(rp_light)
elif hasattr(world, "render") and world.render:
world.render.clearLight(node)
except Exception:
pass
try:
node.setPythonTag("engine_light_registered", False)
except Exception:
pass
def _set_terrain_registration(world, node: NodePath, registered: bool):
if not world or not _is_valid_node(node) or not _is_terrain_node(node):
return
terrain_manager = getattr(world, "terrain_manager", None)
if not terrain_manager or not hasattr(terrain_manager, "terrains"):
return
terrain_info = None
if hasattr(node, "hasPythonTag") and node.hasPythonTag("terrain_info"):
terrain_info = node.getPythonTag("terrain_info")
else:
for info in getattr(terrain_manager, "terrains", []):
if info.get("node") == node:
terrain_info = info
break
if terrain_info is not None:
try:
node.setPythonTag("terrain_info", terrain_info)
except Exception:
pass
if registered:
if terrain_info is not None:
terrain_info["node"] = node
if all(info.get("node") != node for info in terrain_manager.terrains):
terrain_manager.terrains.append(terrain_info)
return
try:
terrain_manager.terrains = [info for info in terrain_manager.terrains if info.get("node") != node]
except Exception:
pass
def _register_scene_node(world, node: NodePath):
if not world or not _is_valid_node(node):
return
scene_manager = getattr(world, "scene_manager", None)
_set_light_registration(world, node, True)
_set_terrain_registration(world, node, True)
if scene_manager and hasattr(scene_manager, "models") and node not in scene_manager.models:
scene_manager.models.append(node)
try:
if hasattr(world, "updateSceneTree"):
world.updateSceneTree()
except Exception:
pass
def _unregister_scene_node(world, node: NodePath):
if not world or not node:
return
scene_manager = getattr(world, "scene_manager", None)
_set_light_registration(world, node, False)
_set_terrain_registration(world, node, False)
if scene_manager and hasattr(scene_manager, "models"):
try:
while node in scene_manager.models:
scene_manager.models.remove(node)
except Exception:
pass
try:
if hasattr(world, "updateSceneTree"):
world.updateSceneTree()
except Exception:
pass
def _refresh_scene_tree(world):
if not world:
return
try:
if hasattr(world, "updateSceneTree"):
world.updateSceneTree()
except Exception:
pass
def _resolve_world(world=None):
if world:
return world
try:
from direct.showbase.ShowBaseGlobal import base
return base
except Exception:
return None
def _sync_scene_node_side_effects(world, nodes):
world = _resolve_world(world)
if not world:
return
ssbo_editor = getattr(world, "ssbo_editor", None)
if ssbo_editor and hasattr(ssbo_editor, "sync_scene_nodes_to_pick"):
try:
ssbo_editor.sync_scene_nodes_to_pick(nodes or [])
except Exception:
pass
def _sync_transform_side_effects(world, nodes):
_sync_scene_node_side_effects(world, nodes)
def _apply_vec3_method(node, method_name: str, value, reference_node=None):
if not _is_valid_node(node):
return
if reference_node is not None and not _is_valid_node(reference_node):
reference_node = None
# Mark node as dirty for the save system if it's a transform operation
if method_name.startswith("set") and method_name in (
"setPos", "setHpr", "setScale", "setX", "setY", "setZ",
"setH", "setP", "setR", "setSx", "setSy", "setSz", "setMat"
):
try:
node.setTag("scene_transform_dirty", "true")
except Exception:
pass
method = getattr(node, method_name)
if reference_node is not None:
try:
method(reference_node, value)
return
except Exception:
pass
if isinstance(value, (tuple, list)) and len(value) >= 3:
method(reference_node, value[0], value[1], value[2])
return
else:
try:
method(value)
return
except Exception:
pass
if isinstance(value, (tuple, list)) and len(value) >= 3:
method(value[0], value[1], value[2])
return
method(value)
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.record_command(command)
except Exception as e:
print(f"执行命令时出错: {e}")
raise
def record_command(self, command: Command):
"""记录一个已经执行完成的命令。"""
self._undo_stack.append(command)
self._redo_stack.clear()
def pop_last_command(self):
"""弹出最后一个已执行命令,供复合操作合并历史使用。"""
if not self._undo_stack:
return None
return self._undo_stack.pop()
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):
"""
Move node command.
"""
def __init__(self, node: NodePath, old_pos, new_pos, reference_node=None, world=None):
self.node = node
self.old_pos = old_pos
self.new_pos = new_pos
self.reference_node = reference_node
self.world = world
def _apply(self, value):
_apply_vec3_method(self.node, "setPos", value, self.reference_node)
_sync_transform_side_effects(self.world, [self.node])
def execute(self):
"""
Execute move operation.
"""
self._apply(self.new_pos)
def undo(self):
"""
Undo move operation.
"""
self._apply(self.old_pos)
def redo(self):
"""
Redo move operation.
"""
self._apply(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)
if hasattr(scene_manager, 'models') and self.node in scene_manager.models:
scene_manager.models.remove(self.node)
if 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]
_unregister_scene_node(self.world, self.node)
# 从场景图中移除节点,使用 detachNode 而不是 removeNode 以便可以撤销
if _is_valid_node(self.node):
self.node.detachNode()
def undo(self):
"""
撤销删除操作(恢复旧节点)
"""
try:
if _is_valid_node(self.node):
# 直接将节点挂载回原父节点
if _is_valid_node(self.parent_node):
self.node.reparentTo(self.parent_node)
elif self.world and _is_valid_node(getattr(self.world, "render", None)):
self.node.reparentTo(self.world.render)
# 恢复到相应的管理器列表中
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 not in scene_manager.Spotlight:
scene_manager.Spotlight.append(self.node)
elif light_type == "point_light" and hasattr(scene_manager, 'Pointlight') and self.node not in scene_manager.Pointlight:
scene_manager.Pointlight.append(self.node)
if hasattr(scene_manager, 'models') and self.node not in scene_manager.models:
scene_manager.models.append(self.node)
if self.node_type.startswith("GUI_") and hasattr(self.world, 'gui_elements') and self.node not in self.world.gui_elements:
self.world.gui_elements.append(self.node)
elif self.node_type == "CESIUM_TILESET_NODE":
# 简单恢复到 tilesets
if hasattr(scene_manager, 'tilesets'):
scene_manager.tilesets.append({'node': self.node, 'url': self.extra_data.get('tileset_url', '')})
_register_scene_node(self.world, self.node)
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):
"""
Rotate node command.
"""
def __init__(self, node: NodePath, old_hpr, new_hpr, reference_node=None, world=None):
self.node = node
self.old_hpr = old_hpr
self.new_hpr = new_hpr
self.reference_node = reference_node
self.world = world
def _apply(self, value):
_apply_vec3_method(self.node, "setHpr", value, self.reference_node)
_sync_transform_side_effects(self.world, [self.node])
def execute(self):
"""
Execute move operation.
"""
self._apply(self.new_hpr)
def undo(self):
"""
Undo move operation.
"""
self._apply(self.old_hpr)
def redo(self):
"""
Redo move operation.
"""
self._apply(self.new_hpr)
class ScaleNodeCommand(Command):
"""
Scale node command.
"""
def __init__(self, node: NodePath, old_scale, new_scale, world=None):
self.node = node
self.old_scale = old_scale
self.new_scale = new_scale
self.world = world
def _apply(self, value):
_apply_vec3_method(self.node, "setScale", value)
_sync_transform_side_effects(self.world, [self.node])
def execute(self):
"""
Execute rotate operation.
"""
self._apply(self.new_scale)
def undo(self):
"""
Undo rotate operation.
"""
self._apply(self.old_scale)
def redo(self):
"""
Redo rotate operation.
"""
self._apply(self.new_scale)
class RenameNodeCommand(Command):
"""Rename a node and refresh scene tree bindings."""
def __init__(self, node: NodePath, old_name: str, new_name: str, world=None):
self.node = node
self.old_name = old_name
self.new_name = new_name
self.world = world
def execute(self):
if _is_valid_node(self.node):
self.node.setName(self.new_name)
_refresh_scene_tree(self.world)
def undo(self):
if _is_valid_node(self.node):
self.node.setName(self.old_name)
_refresh_scene_tree(self.world)
def redo(self):
self.execute()
class VisibilityNodeCommand(Command):
"""Toggle editor visibility state for a node."""
def __init__(self, node: NodePath, old_visible: bool, new_visible: bool, world=None):
self.node = node
self.old_visible = bool(old_visible)
self.new_visible = bool(new_visible)
self.world = world
def _apply(self, visible: bool):
if not _is_valid_node(self.node):
return
self.node.setPythonTag("user_visible", bool(visible))
if visible:
self.node.show()
else:
self.node.hide()
_sync_scene_node_side_effects(self.world, [self.node])
def execute(self):
self._apply(self.new_visible)
def undo(self):
self._apply(self.old_visible)
def redo(self):
self.execute()
class MaterialStateCommand(Command):
"""Replay a captured material snapshot for undo/redo."""
def __init__(self, apply_state_callback, before_state, after_state):
self.apply_state_callback = apply_state_callback
self.before_state = before_state
self.after_state = after_state
def execute(self):
if self.apply_state_callback:
self.apply_state_callback(self.after_state)
def undo(self):
if self.apply_state_callback:
self.apply_state_callback(self.before_state)
def redo(self):
self.execute()
class SnapshotStateCommand(MaterialStateCommand):
"""Generic callback-based snapshot command."""
class CreateNodeCommand(Command):
"""
创建节点命令
"""
def __init__(self, node_creator_func, parent_node, *args, world=None, **kwargs):
self.node_creator_func = node_creator_func
self.parent_node = parent_node
self.args = args
self.kwargs = kwargs
self.world = world
self.created_node = None
def execute(self):
"""
执行创建节点操作
"""
if _is_valid_node(self.created_node):
target_parent = self.parent_node
if (not _is_valid_node(target_parent)) and self.world:
target_parent = getattr(self.world, "render", None)
if _is_valid_node(target_parent):
self.created_node.wrtReparentTo(target_parent)
_register_scene_node(self.world, self.created_node)
_sync_scene_node_side_effects(self.world, [self.created_node])
return self.created_node
self.created_node = self.node_creator_func(self.parent_node, *self.args, **self.kwargs)
_register_scene_node(self.world, self.created_node)
_sync_scene_node_side_effects(self.world, [self.created_node])
return self.created_node
def undo(self):
"""
撤销创建节点操作
"""
if _is_valid_node(self.created_node):
_unregister_scene_node(self.world, self.created_node)
self.created_node.detachNode()
_sync_scene_node_side_effects(self.world, [self.created_node])
def redo(self):
"""
重做创建节点操作
"""
if _is_valid_node(self.created_node):
target_parent = self.parent_node
if (not _is_valid_node(target_parent)) and self.world:
target_parent = getattr(self.world, "render", None)
if _is_valid_node(target_parent):
self.created_node.wrtReparentTo(target_parent)
_register_scene_node(self.world, self.created_node)
_sync_scene_node_side_effects(self.world, [self.created_node])
return
self.execute()
class AttachNodeCommand(Command):
"""Attach an existing detached node into a parent and make it undoable."""
def __init__(self, node: NodePath, parent_node: NodePath, world=None):
self.node = node
self.parent_node = parent_node
self.world = world
def execute(self):
target_parent = self.parent_node
if (not target_parent or target_parent.isEmpty()) and self.world:
target_parent = getattr(self.world, "render", None)
if _is_valid_node(self.node) and _is_valid_node(target_parent):
self.node.wrtReparentTo(target_parent)
_register_scene_node(self.world, self.node)
_sync_scene_node_side_effects(self.world, [self.node])
return self.node
def undo(self):
if _is_valid_node(self.node):
_unregister_scene_node(self.world, self.node)
self.node.detachNode()
_sync_scene_node_side_effects(self.world, [self.node])
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 not _is_valid_node(self.node):
return
# 更新Panda3D节点父子关系
if self.is_2d_gui and self.world:
# 2D GUI元素需要特殊处理
if _is_valid_node(self.new_parent):
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 _is_valid_node(self.new_parent):
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):
"""
撤销重新父化操作
"""
if not _is_valid_node(self.node):
return
# 在改变父节点前保存当前的缩放值
current_scale = self.node.getScale()
# 恢复Panda3D节点父子关系
if self.is_2d_gui and self.world:
# 2D GUI元素需要特殊处理
if _is_valid_node(self.old_parent):
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 _is_valid_node(self.old_parent):
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):
"""
重做重新父化操作
"""
if not _is_valid_node(self.node):
return
# 在改变父节点前保存当前的缩放值
current_scale = self.node.getScale()
# 重新执行Panda3D节点父子关系更新
if self.is_2d_gui and self.world:
# 2D GUI元素需要特殊处理
if _is_valid_node(self.new_parent):
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 _is_valid_node(self.new_parent):
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 _apply_light_position(self, pos):
if not self.light_object:
return
try:
self.light_object.setPos(pos)
return
except Exception:
pass
try:
self.light_object.setPos(pos.x, pos.y, pos.z)
return
except Exception:
pass
try:
self.light_object.pos = Point3(pos)
except Exception:
pass
def execute(self): # 将原来的 do() 改为 execute()
self._apply_light_position(self.new_pos)
if _is_valid_node(self.node):
self.node.setPos(self.new_pos)
def undo(self):
self._apply_light_position(self.old_pos)
if _is_valid_node(self.node):
self.node.setPos(self.old_pos)
def redo(self):
self.execute() # 调用 execute() 而不是 do()