1014 lines
36 KiB
Python
1014 lines
36 KiB
Python
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()
|
||
|
||
|
||
|