Files
EnglewoodLAB/Assets/Scripts/Studio/Manager/UndoRedoManager.cs

373 lines
13 KiB
C#

#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
{
/// <summary>
/// Undo/Redo 히스토리를 관리하는 매니저 클래스
/// IUndoableCommand 기반으로 작업 이력을 추적
/// SingletonScene 기반으로 씬 전환 시 자동 정리
/// </summary>
public class UndoRedoManager : SingletonScene<UndoRedoManager>
{
[SerializeField]
[Tooltip("Undo 스택 최대 크기")]
private int _maxStackSize = 50;
private readonly Stack<IUndoableCommand> _undoStack = new();
private readonly Stack<IUndoableCommand> _redoStack = new();
/// <summary>히스토리 변경 시 발생하는 이벤트</summary>
public event Action? OnHistoryChanged;
/// <summary>Undo 실행 시 발생하는 이벤트</summary>
public event Action<IUndoableCommand>? OnUndo;
/// <summary>Redo 실행 시 발생하는 이벤트</summary>
public event Action<IUndoableCommand>? OnRedo;
/// <summary>Undo 가능 여부</summary>
public bool CanUndo => _undoStack.Count > 0;
/// <summary>Redo 가능 여부</summary>
public bool CanRedo => _redoStack.Count > 0;
/// <summary>Undo 스택 개수</summary>
public int UndoCount => _undoStack.Count;
/// <summary>Redo 스택 개수</summary>
public int RedoCount => _redoStack.Count;
/// <summary>최근 Undo 가능한 작업 설명</summary>
public string? UndoDescription => _undoStack.Count > 0 ? _undoStack.Peek().Description : null;
/// <summary>최근 Redo 가능한 작업 설명</summary>
public string? RedoDescription => _redoStack.Count > 0 ? _redoStack.Peek().Description : null;
/// <summary>
/// 초기화
/// </summary>
protected override void Init()
{
Debug.Log($"[UndoRedoManager] Initialized with maxStackSize: {_maxStackSize}");
}
/// <summary>
/// Command를 실행하고 히스토리에 기록
/// </summary>
/// <param name="command">실행할 IUndoableCommand</param>
/// <param name="parameter">실행 파라미터</param>
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;
}
}
/// <summary>
/// 이미 실행된 Command를 히스토리에만 기록 (Execute 없이)
/// RTGizmo 등 외부에서 이미 실행된 작업을 기록할 때 사용
/// </summary>
/// <param name="command">기록할 IUndoableCommand</param>
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();
}
/// <summary>
/// 마지막 작업 취소 (Undo)
/// </summary>
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();
}
}
/// <summary>
/// 마지막 취소 작업 다시 실행 (Redo)
/// </summary>
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();
}
}
/// <summary>
/// 모든 히스토리 지우기
/// </summary>
public void Clear()
{
_undoStack.Clear();
_redoStack.Clear();
Debug.Log("[UndoRedoManager] History cleared");
OnHistoryChanged?.Invoke();
}
/// <summary>
/// Redo 스택에 커맨드를 직접 추가합니다.
/// 복제 Redo 시 Transform 이력 커맨드를 추가할 때 사용합니다.
/// </summary>
/// <param name="command">추가할 커맨드</param>
public void PushToRedoStack(IUndoableCommand command)
{
_redoStack.Push(command);
}
/// <summary>
/// Redo 스택에서 유효하지 않은 TransformChangeCommand를 제거합니다.
/// 객체 삭제 후 호출하여 파괴된 객체를 참조하는 커맨드를 정리합니다.
/// </summary>
public void CleanupInvalidRedoCommands()
{
if (_redoStack.Count == 0) return;
var validCommands = new Stack<IUndoableCommand>();
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();
}
}
/// <summary>
/// 지정된 Transform을 참조하는 TransformChangeCommand를 Redo 스택에서 제거합니다.
/// </summary>
/// <param name="transforms">제거할 대상 Transform 목록</param>
public void RemoveCommandsForTransforms(IEnumerable<Transform> transforms)
{
if (_redoStack.Count == 0) return;
var targetTransforms = new HashSet<Transform>(transforms);
var validCommands = new Stack<IUndoableCommand>();
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();
}
}
/// <summary>
/// Undo 스택에 Command 추가 (스택 크기 제한 적용)
/// </summary>
private void PushUndo(IUndoableCommand command)
{
if (_undoStack.Count >= _maxStackSize)
{
// 스택이 가득 찼으면 가장 오래된 것 제거
TrimStack();
}
_undoStack.Push(command);
}
/// <summary>
/// 스택 크기 초과 시 가장 오래된 항목 제거
/// </summary>
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]);
}
}
/// <summary>
/// Undo 히스토리 목록 가져오기 (UI 표시용)
/// </summary>
/// <param name="maxCount">최대 개수</param>
/// <returns>작업 설명 목록 (최신순)</returns>
public IEnumerable<string> GetUndoHistory(int maxCount = 10)
{
int count = 0;
foreach (var cmd in _undoStack)
{
if (count++ >= maxCount) break;
yield return cmd.Description;
}
}
/// <summary>
/// Redo 히스토리 목록 가져오기 (UI 표시용)
/// </summary>
/// <param name="maxCount">최대 개수</param>
/// <returns>작업 설명 목록 (최신순)</returns>
public IEnumerable<string> GetRedoHistory(int maxCount = 10)
{
int count = 0;
foreach (var cmd in _redoStack)
{
if (count++ >= maxCount) break;
yield return cmd.Description;
}
}
/// <summary>
/// 씬 전환 시 정리
/// </summary>
protected override void OnDestroy()
{
Clear();
base.OnDestroy();
}
}
}