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 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()