#nullable enable using System; using System.Collections.Generic; using UnityEngine; using UVC.Core; using UVC.Studio.Command; using UVC.UI.Commands; namespace UVC.Studio.Manager { /// /// Undo/Redo 히스토리를 관리하는 매니저 클래스 /// IUndoableCommand 기반으로 작업 이력을 추적 /// SingletonScene 기반으로 씬 전환 시 자동 정리 /// public class UndoRedoManager : SingletonScene { [SerializeField] [Tooltip("Undo 스택 최대 크기")] private int _maxStackSize = 50; private readonly Stack _undoStack = new(); private readonly Stack _redoStack = new(); /// 히스토리 변경 시 발생하는 이벤트 public event Action? OnHistoryChanged; /// Undo 실행 시 발생하는 이벤트 public event Action? OnUndo; /// Redo 실행 시 발생하는 이벤트 public event Action? OnRedo; /// Undo 가능 여부 public bool CanUndo => _undoStack.Count > 0; /// Redo 가능 여부 public bool CanRedo => _redoStack.Count > 0; /// Undo 스택 개수 public int UndoCount => _undoStack.Count; /// Redo 스택 개수 public int RedoCount => _redoStack.Count; /// 최근 Undo 가능한 작업 설명 public string? UndoDescription => _undoStack.Count > 0 ? _undoStack.Peek().Description : null; /// 최근 Redo 가능한 작업 설명 public string? RedoDescription => _redoStack.Count > 0 ? _redoStack.Peek().Description : null; /// /// 초기화 /// protected override void Init() { Debug.Log($"[UndoRedoManager] Initialized with maxStackSize: {_maxStackSize}"); } /// /// Command를 실행하고 히스토리에 기록 /// /// 실행할 IUndoableCommand /// 실행 파라미터 public void ExecuteCommand(IUndoableCommand command, object? parameter = null) { try { command.Execute(parameter); // 병합 가능하면 병합 if (_undoStack.Count > 0 && _undoStack.Peek().CanMerge(command)) { _undoStack.Peek().Merge(command); Debug.Log($"[UndoRedoManager] Merged: {command.Description}"); } else { PushUndo(command); Debug.Log($"[UndoRedoManager] Recorded: {command.Description}"); } // 새 액션 추가 시 Redo 스택 클리어 _redoStack.Clear(); OnHistoryChanged?.Invoke(); } catch (Exception ex) { Debug.LogError($"[UndoRedoManager] Execute failed: {ex.Message}"); throw; } } /// /// 이미 실행된 Command를 히스토리에만 기록 (Execute 없이) /// RTGizmo 등 외부에서 이미 실행된 작업을 기록할 때 사용 /// /// 기록할 IUndoableCommand public void RecordCommand(IUndoableCommand command) { // 병합 가능하면 병합 if (_undoStack.Count > 0 && _undoStack.Peek().CanMerge(command)) { _undoStack.Peek().Merge(command); Debug.Log($"[UndoRedoManager] Merged (record only): {command.Description}"); } else { PushUndo(command); Debug.Log($"[UndoRedoManager] Recorded (no execute): {command.Description}"); } _redoStack.Clear(); OnHistoryChanged?.Invoke(); } /// /// 마지막 작업 취소 (Undo) /// public void Undo() { if (!CanUndo) { Debug.Log("[UndoRedoManager] Nothing to undo"); return; } var command = _undoStack.Pop(); try { command.Undo(); _redoStack.Push(command); Debug.Log($"[UndoRedoManager] Undo: {command.Description}"); OnUndo?.Invoke(command); OnHistoryChanged?.Invoke(); } catch (Exception ex) { Debug.LogError($"[UndoRedoManager] Undo failed: {ex.Message}"); // 실패해도 Redo 스택에 넣어서 사용자가 다시 시도 가능하게 _redoStack.Push(command); OnHistoryChanged?.Invoke(); } } /// /// 마지막 취소 작업 다시 실행 (Redo) /// public void Redo() { if (!CanRedo) { Debug.Log("[UndoRedoManager] Nothing to redo"); return; } var command = _redoStack.Pop(); // TransformChangeCommand의 경우 Transform 복구 시도 후 유효성 검사 if (command is TransformChangeCommand transformCmd) { // StageObject ID 기반 Transform 복구 시도 transformCmd.TryResolveAllTransforms(); if (!transformCmd.IsValid()) { Debug.LogWarning($"[UndoRedoManager] Redo skipped (invalid): {command.Description} - 대상 객체가 파괴됨 (ID 복구 실패)"); // 유효하지 않은 커맨드는 버리고 다음 Redo 시도 OnHistoryChanged?.Invoke(); return; } } try { command.Redo(); _undoStack.Push(command); Debug.Log($"[UndoRedoManager] Redo: {command.Description}"); OnRedo?.Invoke(command); OnHistoryChanged?.Invoke(); } catch (Exception ex) { Debug.LogError($"[UndoRedoManager] Redo failed: {ex.Message}"); // TransformChangeCommand 실패 시 스택에서 제거 (재시도해도 실패할 것이므로) if (command is TransformChangeCommand) { Debug.LogWarning($"[UndoRedoManager] 유효하지 않은 TransformChangeCommand 제거됨"); OnHistoryChanged?.Invoke(); return; } // 다른 커맨드는 실패해도 Undo 스택에 넣어서 사용자가 다시 시도 가능하게 _undoStack.Push(command); OnHistoryChanged?.Invoke(); } } /// /// 모든 히스토리 지우기 /// public void Clear() { _undoStack.Clear(); _redoStack.Clear(); Debug.Log("[UndoRedoManager] History cleared"); OnHistoryChanged?.Invoke(); } /// /// Redo 스택에 커맨드를 직접 추가합니다. /// 복제 Redo 시 Transform 이력 커맨드를 추가할 때 사용합니다. /// /// 추가할 커맨드 public void PushToRedoStack(IUndoableCommand command) { _redoStack.Push(command); } /// /// Redo 스택에서 유효하지 않은 TransformChangeCommand를 제거합니다. /// 객체 삭제 후 호출하여 파괴된 객체를 참조하는 커맨드를 정리합니다. /// public void CleanupInvalidRedoCommands() { if (_redoStack.Count == 0) return; var validCommands = new Stack(); var commands = _redoStack.ToArray(); int removedCount = 0; // 스택을 역순으로 순회하여 유효한 커맨드만 유지 for (int i = commands.Length - 1; i >= 0; i--) { var cmd = commands[i]; if (cmd is TransformChangeCommand transformCmd && !transformCmd.IsValid()) { removedCount++; Debug.Log($"[UndoRedoManager] 유효하지 않은 커맨드 제거: {cmd.Description}"); } else { validCommands.Push(cmd); } } if (removedCount > 0) { _redoStack.Clear(); // 다시 올바른 순서로 추가 var validArray = validCommands.ToArray(); for (int i = validArray.Length - 1; i >= 0; i--) { _redoStack.Push(validArray[i]); } Debug.Log($"[UndoRedoManager] Redo 스택 정리: {removedCount}개 커맨드 제거됨"); OnHistoryChanged?.Invoke(); } } /// /// 지정된 Transform을 참조하는 TransformChangeCommand를 Redo 스택에서 제거합니다. /// /// 제거할 대상 Transform 목록 public void RemoveCommandsForTransforms(IEnumerable transforms) { if (_redoStack.Count == 0) return; var targetTransforms = new HashSet(transforms); var validCommands = new Stack(); var commands = _redoStack.ToArray(); int removedCount = 0; // 스택을 역순으로 순회 for (int i = commands.Length - 1; i >= 0; i--) { var cmd = commands[i]; if (cmd is TransformChangeCommand transformCmd && transformCmd.ContainsAnyTransform(targetTransforms)) { removedCount++; Debug.Log($"[UndoRedoManager] 대상 Transform 커맨드 제거: {cmd.Description}"); } else { validCommands.Push(cmd); } } if (removedCount > 0) { _redoStack.Clear(); var validArray = validCommands.ToArray(); for (int i = validArray.Length - 1; i >= 0; i--) { _redoStack.Push(validArray[i]); } Debug.Log($"[UndoRedoManager] Redo 스택에서 {removedCount}개 Transform 커맨드 제거됨"); OnHistoryChanged?.Invoke(); } } /// /// Undo 스택에 Command 추가 (스택 크기 제한 적용) /// private void PushUndo(IUndoableCommand command) { if (_undoStack.Count >= _maxStackSize) { // 스택이 가득 찼으면 가장 오래된 것 제거 TrimStack(); } _undoStack.Push(command); } /// /// 스택 크기 초과 시 가장 오래된 항목 제거 /// private void TrimStack() { if (_undoStack.Count == 0) return; // Stack을 배열로 변환 후 가장 오래된 것 제외하고 다시 Stack으로 var items = _undoStack.ToArray(); _undoStack.Clear(); // items[0]이 가장 최근, items[^1]이 가장 오래된 것 // 가장 오래된 것(마지막)을 제외하고 역순으로 다시 추가 for (int i = items.Length - 2; i >= 0; i--) { _undoStack.Push(items[i]); } } /// /// Undo 히스토리 목록 가져오기 (UI 표시용) /// /// 최대 개수 /// 작업 설명 목록 (최신순) public IEnumerable GetUndoHistory(int maxCount = 10) { int count = 0; foreach (var cmd in _undoStack) { if (count++ >= maxCount) break; yield return cmd.Description; } } /// /// Redo 히스토리 목록 가져오기 (UI 표시용) /// /// 최대 개수 /// 작업 설명 목록 (최신순) public IEnumerable GetRedoHistory(int maxCount = 10) { int count = 0; foreach (var cmd in _redoStack) { if (count++ >= maxCount) break; yield return cmd.Description; } } /// /// 씬 전환 시 정리 /// protected override void OnDestroy() { Clear(); base.OnDestroy(); } } }