Files
EnglewoodLAB/Assets/Scripts/Command/TransformChangeCommand.cs
2026-03-09 17:00:30 +09:00

332 lines
11 KiB
C#

#nullable enable
using System.Collections.Generic;
using UnityEngine;
using UVC.UI.Commands;
using UVC.Core;
using UVC.Object3d.Manager;
namespace EnglewoodLAB.Command
{
/// <summary>
/// Transform 변경 커맨드 (Undo/Redo 지원)
/// RTGizmo 드래그 완료 시 사용
/// </summary>
public class TransformChangeCommand : IUndoableCommand
{
public string Description { get; }
/// <summary>
/// 단일 객체의 Transform 상태
/// </summary>
private class TransformState
{
public Transform Transform { get; set; } = null!;
public string? StageObjectId { get; set; }
public Vector3 LocalPosition { get; set; }
public Quaternion LocalRotation { get; set; }
public Vector3 LocalScale { get; set; }
public TransformState() { }
public TransformState(Transform transform)
{
Transform = transform;
CaptureStageObjectId();
Capture();
}
/// <summary>
/// StageObjectManager에서 StageObject ID를 캡처합니다.
/// </summary>
private void CaptureStageObjectId()
{
if (Transform == null) return;
var stageObjectManager = InjectorAppContext.Instance?.Get<StageObjectManager>();
if (stageObjectManager != null)
{
var stageObject = stageObjectManager.GetByGameObject(Transform.gameObject);
if (stageObject != null)
{
StageObjectId = stageObject.Id;
}
}
}
/// <summary>
/// Transform이 유효한지 확인 (파괴되지 않았는지)
/// </summary>
public bool IsValid()
{
// Unity에서 파괴된 객체는 == null로 체크
// ReferenceEquals는 실제 null만 체크하므로 Unity 객체에는 부적합
return Transform != null;
}
/// <summary>
/// Transform이 null이면 StageObject ID로 복구를 시도합니다.
/// </summary>
/// <returns>복구 성공 여부</returns>
public bool TryResolveTransform()
{
if (IsValid()) return true;
if (string.IsNullOrEmpty(StageObjectId)) return false;
var stageObjectManager = InjectorAppContext.Instance?.Get<StageObjectManager>();
if (stageObjectManager == null) return false;
var stageObject = stageObjectManager.GetById(StageObjectId);
if (stageObject?.gameObject != null)
{
Transform = stageObject.gameObject.transform;
Debug.Log($"[TransformState] Transform resolved by StageObject ID: {StageObjectId}");
return true;
}
return false;
}
public void Capture()
{
if (!IsValid()) return;
LocalPosition = Transform.localPosition;
LocalRotation = Transform.localRotation;
LocalScale = Transform.localScale;
}
public void Apply()
{
// Transform이 null이면 StageObject ID로 복구 시도
if (!IsValid())
{
TryResolveTransform();
}
if (!IsValid()) return;
Transform.localPosition = LocalPosition;
Transform.localRotation = LocalRotation;
Transform.localScale = LocalScale;
}
public TransformState Clone()
{
return new TransformState
{
Transform = Transform,
StageObjectId = StageObjectId,
LocalPosition = LocalPosition,
LocalRotation = LocalRotation,
LocalScale = LocalScale
};
}
public bool HasChanged(TransformState other)
{
return LocalPosition != other.LocalPosition ||
LocalRotation != other.LocalRotation ||
LocalScale != other.LocalScale;
}
}
/// <summary>
/// 이 커맨드가 유효한지 확인 (대상 Transform이 파괴되지 않았는지)
/// StageObject ID가 있으면 복구를 시도합니다.
/// </summary>
public bool IsValid()
{
// 모든 Transform이 유효해야 함 (복구 시도 포함)
foreach (var state in _beforeStates)
{
if (!state.IsValid() && !state.TryResolveTransform())
{
return false;
}
}
return _beforeStates.Count > 0;
}
/// <summary>
/// Transform 복구를 시도하고 결과를 반환합니다.
/// </summary>
/// <returns>모든 Transform이 복구되었으면 true</returns>
public bool TryResolveAllTransforms()
{
bool allResolved = true;
foreach (var state in _beforeStates)
{
if (!state.IsValid() && !state.TryResolveTransform())
{
allResolved = false;
}
}
foreach (var state in _afterStates)
{
if (!state.IsValid() && !state.TryResolveTransform())
{
allResolved = false;
}
}
return allResolved;
}
/// <summary>
/// 지정된 Transform 중 하나라도 이 커맨드에 포함되어 있는지 확인합니다.
/// </summary>
/// <param name="transforms">확인할 Transform 집합</param>
/// <returns>포함 여부</returns>
public bool ContainsAnyTransform(HashSet<Transform> transforms)
{
foreach (var state in _beforeStates)
{
if (transforms.Contains(state.Transform))
{
return true;
}
}
return false;
}
// 변경 전/후 상태 저장 (다중 객체 지원)
private readonly List<TransformState> _beforeStates = new();
private readonly List<TransformState> _afterStates = new();
/// <summary>
/// 단일 Transform에 대한 생성자
/// </summary>
/// <param name="transform">대상 Transform</param>
/// <param name="description">작업 설명</param>
public TransformChangeCommand(Transform transform, string description = "객체 이동")
{
Description = description;
_beforeStates.Add(new TransformState(transform));
}
/// <summary>
/// 다중 Transform에 대한 생성자
/// </summary>
/// <param name="transforms">대상 Transform 목록</param>
/// <param name="description">작업 설명</param>
public TransformChangeCommand(IEnumerable<Transform> transforms, string description = "객체 이동")
{
Description = description;
foreach (var transform in transforms)
{
_beforeStates.Add(new TransformState(transform));
}
}
/// <summary>
/// GameObject 목록에 대한 생성자
/// </summary>
/// <param name="gameObjects">대상 GameObject 목록</param>
/// <param name="description">작업 설명</param>
public TransformChangeCommand(IEnumerable<GameObject> gameObjects, string description = "객체 이동")
{
Description = description;
foreach (var go in gameObjects)
{
if (go != null)
{
_beforeStates.Add(new TransformState(go.transform));
}
}
}
/// <summary>
/// 드래그 완료 후 현재 상태를 After로 캡처
/// </summary>
public void CaptureAfterState()
{
_afterStates.Clear();
foreach (var beforeState in _beforeStates)
{
var afterState = new TransformState
{
Transform = beforeState.Transform,
StageObjectId = beforeState.StageObjectId
};
afterState.Capture();
_afterStates.Add(afterState);
}
}
/// <summary>
/// 변경사항이 있는지 확인
/// </summary>
public bool HasChanges()
{
if (_beforeStates.Count != _afterStates.Count) return false;
for (int i = 0; i < _beforeStates.Count; i++)
{
if (_beforeStates[i].HasChanged(_afterStates[i]))
{
return true;
}
}
return false;
}
public void Execute(object? parameter = null)
{
// RTGizmo가 이미 Transform을 변경했으므로
// After 상태만 캡처
CaptureAfterState();
}
public void Undo(object? parameter = null)
{
int appliedCount = 0;
foreach (var state in _beforeStates)
{
if (state.IsValid())
{
state.Apply();
appliedCount++;
}
}
Debug.Log($"[TransformChangeCommand] Undo: {appliedCount}/{_beforeStates.Count}개 객체 Transform 복원");
}
public void Redo()
{
// Transform 복구 시도 (StageObject ID 기반)
TryResolveAllTransforms();
// 복구 후에도 유효하지 않으면 스킵
if (!IsValid())
{
Debug.LogWarning($"[TransformChangeCommand] Redo skipped: 대상 객체가 파괴됨");
return;
}
int appliedCount = 0;
foreach (var state in _afterStates)
{
state.Apply(); // Apply 내부에서 TryResolveTransform 호출
if (state.IsValid())
{
appliedCount++;
}
}
Debug.Log($"[TransformChangeCommand] Redo: {appliedCount}/{_afterStates.Count}개 객체 Transform 재적용");
}
public bool CanMerge(IUndoableCommand other)
{
// 기즈모 조절 시 각 드래그 작업이 별도의 Undo 단계가 되도록 병합하지 않음
return false;
}
public void Merge(IUndoableCommand other)
{
if (other is TransformChangeCommand otherCmd)
{
// After 상태만 최신 값으로 업데이트
_afterStates.Clear();
_afterStates.AddRange(otherCmd._afterStates);
}
}
}
}