373 lines
13 KiB
C#
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();
|
|
}
|
|
}
|
|
}
|