#nullable enable using System.Collections.Generic; using UnityEngine; using UVC.UI.Commands; using UVC.Studio.Manager; using UVC.Core; namespace UVC.Studio.Command { /// /// Transform 변경 커맨드 (Undo/Redo 지원) /// RTGizmo 드래그 완료 시 사용 /// public class TransformChangeCommand : IUndoableCommand { public string Description { get; } /// /// 단일 객체의 Transform 상태 /// 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(); } /// /// StageObjectManager에서 StageObject ID를 캡처합니다. /// private void CaptureStageObjectId() { if (Transform == null) return; var stageObjectManager = InjectorAppContext.Instance?.Get(); if (stageObjectManager != null) { var stageObject = stageObjectManager.GetByGameObject(Transform.gameObject); if (stageObject != null) { StageObjectId = stageObject.Id; } } } /// /// Transform이 유효한지 확인 (파괴되지 않았는지) /// public bool IsValid() { // Unity에서 파괴된 객체는 == null로 체크 // ReferenceEquals는 실제 null만 체크하므로 Unity 객체에는 부적합 return Transform != null; } /// /// Transform이 null이면 StageObject ID로 복구를 시도합니다. /// /// 복구 성공 여부 public bool TryResolveTransform() { if (IsValid()) return true; if (string.IsNullOrEmpty(StageObjectId)) return false; var stageObjectManager = InjectorAppContext.Instance?.Get(); 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; } } /// /// 이 커맨드가 유효한지 확인 (대상 Transform이 파괴되지 않았는지) /// StageObject ID가 있으면 복구를 시도합니다. /// public bool IsValid() { // 모든 Transform이 유효해야 함 (복구 시도 포함) foreach (var state in _beforeStates) { if (!state.IsValid() && !state.TryResolveTransform()) { return false; } } return _beforeStates.Count > 0; } /// /// Transform 복구를 시도하고 결과를 반환합니다. /// /// 모든 Transform이 복구되었으면 true 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; } /// /// 지정된 Transform 중 하나라도 이 커맨드에 포함되어 있는지 확인합니다. /// /// 확인할 Transform 집합 /// 포함 여부 public bool ContainsAnyTransform(HashSet transforms) { foreach (var state in _beforeStates) { if (transforms.Contains(state.Transform)) { return true; } } return false; } // 변경 전/후 상태 저장 (다중 객체 지원) private readonly List _beforeStates = new(); private readonly List _afterStates = new(); /// /// 단일 Transform에 대한 생성자 /// /// 대상 Transform /// 작업 설명 public TransformChangeCommand(Transform transform, string description = "객체 이동") { Description = description; _beforeStates.Add(new TransformState(transform)); } /// /// 다중 Transform에 대한 생성자 /// /// 대상 Transform 목록 /// 작업 설명 public TransformChangeCommand(IEnumerable transforms, string description = "객체 이동") { Description = description; foreach (var transform in transforms) { _beforeStates.Add(new TransformState(transform)); } } /// /// GameObject 목록에 대한 생성자 /// /// 대상 GameObject 목록 /// 작업 설명 public TransformChangeCommand(IEnumerable gameObjects, string description = "객체 이동") { Description = description; foreach (var go in gameObjects) { if (go != null) { _beforeStates.Add(new TransformState(go.transform)); } } } /// /// 드래그 완료 후 현재 상태를 After로 캡처 /// 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); } } /// /// 변경사항이 있는지 확인 /// 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); } } } }