#nullable enable using System; using System.Collections.Generic; using EPOOutline; using RTGLite; using UnityEngine; using UVC.Core; using UVC.Studio.Command; using UVC.Studio.Config; using UVC.Studio.Window; using UVC.UIToolkit; namespace UVC.Studio.Manager { /// /// 변환 도구 타입 /// public enum TransformToolType { /// 선택 도구 (기즈모 없음) Select, /// 이동 도구 Move, /// 회전 도구 Rotate, /// 크기 조절 도구 Scale } /// /// 스테이지 객체의 선택 상태를 관리하는 매니저 클래스 /// public class SelectionManager { #region Private Fields - Selection /// /// 현재 선택된 StageObject 목록 /// private readonly HashSet _selectedObjects = new(); /// /// StageObjectManager 참조 /// private readonly StageObjectManager _stageObjectManager; /// /// PropertyWindow 참조 /// private readonly UTKPropertyTabListWindow? _propertyWindow; /// /// 현재 PropertyWindow에 표시 중인 StageObject /// private StageObjectManager.StageObject? _currentDisplayedObject; /// /// 현재 PropertyWindow에 표시 중인 자식 Transform (모델의 자식 선택 시) /// private Transform? _currentDisplayedChildTransform; /// /// 미리보기 색상 적용 전 원본 Material 색상을 저장하는 딕셔너리 /// Key: Renderer, Value: Material 인덱스별 (프로퍼티 이름, 원본 색상) 리스트 /// private readonly Dictionary> _originalColors = new(); /// /// 캐싱된 Outliner 참조 (매번 FindFirstObjectByType 호출 방지) /// private Outliner? _cachedOutliner; /// /// Outliner 캐시 초기화 여부 /// private bool _outlinerCacheInitialized = false; #endregion #region Private Fields - Gizmos /// /// 이동 기즈모 /// private MoveGizmo? _moveGizmo; /// /// 회전 기즈모 /// private RotateGizmo? _rotateGizmo; /// /// 크기 조절 기즈모 /// private ScaleGizmo? _scaleGizmo; /// /// 현재 활성화된 변환 도구 타입 /// private TransformToolType _currentToolType = TransformToolType.Select; /// /// 기즈모 초기화 여부 /// private bool _gizmosInitialized = false; /// /// 기즈모 타겟 객체 리스트 (캐시) /// private readonly List _gizmoTargetObjects = new(); #endregion #region Events /// /// 선택 변경 시 발생하는 이벤트 /// (선택된 객체, 선택 여부) /// public event Action? OnSelectionChanged; /// /// 객체 이름이 변경되었을 때 발생하는 이벤트 /// (StageObject ID, 새 이름) /// public event Action? OnObjectNameChanged; /// /// 변환 도구가 변경되었을 때 발생하는 이벤트 /// public event Action? OnToolChanged; #endregion #region Public Properties /// /// 현재 선택된 객체 목록 (읽기 전용) /// public IReadOnlyCollection SelectedObjects => _selectedObjects; /// /// 선택된 객체 수 /// public int SelectedCount => _selectedObjects.Count; /// /// 현재 활성화된 변환 도구 타입 /// public TransformToolType CurrentToolType => _currentToolType; #endregion /// /// 생성자 /// /// StageObjectManager 참조 /// PropertyWindow 참조 (선택) public SelectionManager(StageObjectManager stageObjectManager, UTKPropertyTabListWindow? propertyWindow = null) { _stageObjectManager = stageObjectManager; _propertyWindow = propertyWindow; // PropertyWindow 값 변경 이벤트 구독 if (_propertyWindow != null) { _propertyWindow.OnPropertyValueChanged += OnPropertyValueChanged; } } /// /// 객체를 선택합니다 /// /// 선택할 객체 /// 기존 선택에 추가할지 여부 (false면 기존 선택 해제) /// 기존 선택 해제 시 HierarchyWindow 선택도 해제할지 여부 public void Select(StageObjectManager.StageObject stageObject, bool addToSelection = false, bool clearHierarchySelection = true) { if (stageObject == null) return; // 다중 선택이 아니면 기존 선택 해제 if (!addToSelection) { DeselectAll(clearHierarchySelection); } // 이미 선택되어 있으면 무시 if (_selectedObjects.Contains(stageObject)) return; // 선택 추가 _selectedObjects.Add(stageObject); // Outlinable 활성화 SetOutlinableEnabled(stageObject, true); Debug.Log($"[SelectionManager] Selected: {stageObject.GameObject?.name}"); OnSelectionChanged?.Invoke(stageObject, true); // PropertyWindow에 선택된 객체의 속성 표시 DisplayEquipmentProperties(stageObject); // 기즈모 타겟 업데이트 UpdateGizmoTargets(); } /// /// StageObject ID로 객체를 선택합니다 /// /// 선택할 객체의 ID /// 기존 선택에 추가할지 여부 /// 기존 선택 해제 시 HierarchyWindow 선택도 해제할지 여부 public void SelectById(string stageObjectId, bool addToSelection = false, bool clearHierarchySelection = true) { var stageObject = _stageObjectManager.GetById(stageObjectId); if (stageObject != null) { Select(stageObject, addToSelection, clearHierarchySelection); } } /// /// 객체 선택을 해제합니다 /// /// 선택 해제할 객체 public void Deselect(StageObjectManager.StageObject stageObject) { if (stageObject == null) return; if (_selectedObjects.Remove(stageObject)) { // Outlinable 비활성화 SetOutlinableEnabled(stageObject, false); Debug.Log($"[SelectionManager] Deselected: {stageObject.GameObject?.name}"); OnSelectionChanged?.Invoke(stageObject, false); // 모든 선택이 해제되면 정리 작업 수행 if (_selectedObjects.Count == 0) { // 미리보기 색상 원복 및 클리어 ClearPreviewColor(); // PropertyWindow 항목 모두 제거 if (_propertyWindow != null) { _propertyWindow.Clear(); } _currentDisplayedObject = null; _gizmoTargetObjects.Clear(); } // 기즈모 타겟 업데이트 UpdateGizmoTargets(); } } /// /// StageObject ID로 객체 선택을 해제합니다 /// /// 선택 해제할 객체의 ID public void DeselectById(string stageObjectId) { var stageObject = _stageObjectManager.GetById(stageObjectId); if (stageObject != null) { Deselect(stageObject); } } /// /// 모든 선택을 해제합니다 /// /// HierarchyWindow 선택도 해제할지 여부 (기본값: true) public void DeselectAll(bool clearHierarchySelection = true) { var objectsToDeselect = new List(_selectedObjects); foreach (var obj in objectsToDeselect) { Deselect(obj); } // UTKComponentListWindow(TreeList) 선택도 해제 // UTKComponentListWindow에서 선택 처리 중일 때는 TreeList가 자체적으로 선택을 관리하므로 생략 if (clearHierarchySelection) { var hierarchy = InjectorAppContext.Instance.Get(); if (hierarchy != null) { hierarchy.ClearSelection(); } } // 자식 Transform 표시도 정리 ClearChildTransformDisplay(); } /// /// 객체가 선택되어 있는지 확인합니다 /// /// 확인할 객체 /// 선택 여부 public bool IsSelected(StageObjectManager.StageObject stageObject) { return stageObject != null && _selectedObjects.Contains(stageObject); } /// /// 선택 상태를 토글합니다 /// /// 토글할 객체 /// 다중 선택 모드 public void ToggleSelection(StageObjectManager.StageObject stageObject, bool addToSelection = false) { if (IsSelected(stageObject)) { Deselect(stageObject); } else { Select(stageObject, addToSelection); } } /// /// Outlinable 컴포넌트의 활성화 상태를 설정합니다 /// private void SetOutlinableEnabled(StageObjectManager.StageObject stageObject, bool enabled) { if (stageObject.GameObject == null) { Debug.LogWarning("[SelectionManager] SetOutlinableEnabled: GameObject is null"); return; } var outlinable = stageObject.GameObject.GetComponent(); if (outlinable == null) { // 자식에서도 검색 outlinable = stageObject.GameObject.GetComponentInChildren(); } if (outlinable != null) { outlinable.enabled = enabled; // Outliner 존재 여부 확인 (캐싱) if (!_outlinerCacheInitialized) { _cachedOutliner = UnityEngine.Object.FindFirstObjectByType(); _outlinerCacheInitialized = true; if (_cachedOutliner == null) { Debug.LogWarning("[SelectionManager] Outliner component not found in scene! EPOOutline requires Outliner on camera."); } } } } /// /// 객체의 가시성을 설정합니다 /// /// 대상 객체 /// 가시성 여부 public void SetVisibility(StageObjectManager.StageObject stageObject, bool visible) { if (stageObject?.GameObject != null) { stageObject.GameObject.SetActive(visible); Debug.Log($"[SelectionManager] SetVisibility: {stageObject.GameObject.name} = {visible}"); } } /// /// StageObject ID로 객체의 가시성을 설정합니다 /// /// 대상 객체의 ID /// 가시성 여부 public void SetVisibilityById(string stageObjectId, bool visible) { var stageObject = _stageObjectManager.GetById(stageObjectId); if (stageObject != null) { SetVisibility(stageObject, visible); } } #region Preview Color /// /// 선택된 객체들의 Material 색상을 미리보기 색상으로 변경합니다. /// 원본 색상은 저장되어 ClearPreviewColor 호출 시 복원됩니다. /// /// 미리보기 색상 /// /// Material에서 색상 프로퍼티 이름을 찾습니다. /// 다양한 셰이더 (Standard, URP, glTF 등)를 지원합니다. /// private static string? GetColorPropertyName(Material material) { // 일반적인 색상 프로퍼티 이름들 (우선순위 순) string[] colorPropertyNames = { "_Color", // Standard Shader "_BaseColor", // URP Lit Shader "baseColorFactor", // glTFast Shader "_BaseColorFactor", // glTF Shader (대체) "Base_Color", // Shader Graph 기본 이름 "_MainColor" // 커스텀 셰이더 }; foreach (var propName in colorPropertyNames) { if (material.HasProperty(propName)) { return propName; } } // 찾지 못한 경우 디버그용: Material의 모든 프로퍼티 출력 // Debug.LogWarning($"[SelectionManager] Shader: {material.shader.name}"); // var propertyCount = material.shader.GetPropertyCount(); // for (int i = 0; i < propertyCount; i++) // { // var propName = material.shader.GetPropertyName(i); // var propType = material.shader.GetPropertyType(i); // if (propType == UnityEngine.Rendering.ShaderPropertyType.Color) // { // Debug.LogWarning($"[SelectionManager] Color Property: {propName}"); // } // } return null; } public void PreviewColor(Color previewColor) { if (_selectedObjects.Count == 0) return; foreach (var stageObject in _selectedObjects) { if (stageObject.GameObject == null) continue; var renderers = stageObject.GameObject.GetComponentsInChildren(); foreach (var renderer in renderers) { if (renderer == null) continue; var materials = renderer.materials; // 원본 색상 저장 (아직 저장되지 않은 경우에만) if (!_originalColors.ContainsKey(renderer)) { var originalColorList = new List<(int materialIndex, string propertyName, Color color)>(); for (int i = 0; i < materials.Length; i++) { var material = materials[i]; if (material == null) continue; var colorPropName = GetColorPropertyName(material); if (colorPropName == null) continue; originalColorList.Add((i, colorPropName, material.GetColor(colorPropName))); } if (originalColorList.Count > 0) { _originalColors[renderer] = originalColorList; } } // 미리보기 색상 적용 (모든 Material에) for (int i = 0; i < materials.Length; i++) { var material = materials[i]; if (material == null) continue; var colorPropName = GetColorPropertyName(material); if (colorPropName == null) continue; material.SetColor(colorPropName, previewColor); } } } } /// /// 미리보기 색상을 해제하고 원본 Material 색상으로 복원합니다. /// public void ClearPreviewColor() { foreach (var kvp in _originalColors) { var renderer = kvp.Key; var originalColorList = kvp.Value; if (renderer == null) continue; var materials = renderer.materials; // 저장된 각 Material의 원본 색상 복원 foreach (var (materialIndex, propertyName, originalColor) in originalColorList) { if (materialIndex < materials.Length && materials[materialIndex] != null) { materials[materialIndex].SetColor(propertyName, originalColor); } } } _originalColors.Clear(); } #endregion #region PropertyWindow Integration /// /// 자식 Transform의 속성을 PropertyWindow에 표시합니다. /// /// 표시할 자식 Transform /// 자식 이름 public void DisplayChildTransformProperties(Transform childTransform, string childName) { if (_propertyWindow == null || childTransform == null) return; // 기존 StageObject 표시 상태 초기화 _currentDisplayedObject = null; _currentDisplayedChildTransform = childTransform; var entries = new List(); // 1. 자식 이름 표시 (읽기 전용) var nameProperty = new UTKStringPropertyItem("child_name", "Name", childName) { IsReadOnly = true, }; entries.Add(nameProperty); // 2. Transform 그룹 추가 var transformGroup = new UTKPropertyGroup("transform", "Transform"); transformGroup.AddItem(new UTKVector3PropertyItem("transform_position", "Position", childTransform.localPosition)); transformGroup.AddItem(new UTKVector3PropertyItem("transform_rotation", "Rotation", childTransform.localEulerAngles)); transformGroup.AddItem(new UTKVector3PropertyItem("transform_scale", "Scale", childTransform.localScale)); entries.Add(transformGroup); _propertyWindow.LoadMixedProperties(entries); Debug.Log($"[SelectionManager] DisplayChildTransformProperties: {childName}"); } /// /// 자식 Transform 표시 상태를 초기화합니다. /// public void ClearChildTransformDisplay() { _currentDisplayedChildTransform = null; } /// /// StageObject의 Equipment PropertiesInfo를 PropertyWindow에 표시합니다. /// /// 표시할 StageObject private void DisplayEquipmentProperties(StageObjectManager.StageObject stageObject) { if (_propertyWindow == null) return; // 자식 Transform 표시 상태 초기화 _currentDisplayedChildTransform = null; _currentDisplayedObject = stageObject; var equipment = stageObject.Equipment; var entries = new List(); // 1. object_name 속성 추가 (수정 가능, 그룹 없이 개별) var nameProperty = new UTKStringPropertyItem("object_name", "Name", stageObject.GameObject != null ? stageObject.GameObject.name : "Unknown") { IsReadOnly = false, }; entries.Add(nameProperty); // 2. Transform 그룹 추가 if (stageObject.GameObject != null) { var transform = stageObject.GameObject.transform; var transformGroup = new UTKPropertyGroup("transform", "Transform"); transformGroup.AddItem(new UTKVector3PropertyItem("transform_position", "Position", transform.localPosition)); transformGroup.AddItem(new UTKVector3PropertyItem("transform_rotation", "Rotation", transform.localEulerAngles)); transformGroup.AddItem(new UTKVector3PropertyItem("transform_scale", "Scale", transform.localScale)); entries.Add(transformGroup); } // 3. Equipment의 PropertiesInfo를 PropertyGroup으로 변환 if (equipment?.propertiesInfo != null) { foreach (var propInfo in equipment.propertiesInfo) { // section이 "root"이면 그룹 없이 개별 등록 if (string.Equals(propInfo.section, "root", StringComparison.OrdinalIgnoreCase)) { foreach (var prop in propInfo.properties) { var propertyItem = CreatePropertyItem(prop); if (propertyItem != null) { entries.Add(propertyItem); } } } else { // 일반 섹션은 그룹으로 묶음 var group = new UTKPropertyGroup( $"section_{propInfo.section}", propInfo.section ?? "Properties" ); foreach (var prop in propInfo.properties) { var propertyItem = CreatePropertyItem(prop); if (propertyItem != null) { group.AddItem(propertyItem); } } if (group.ItemCount > 0) { entries.Add(group); } } } } // 4. StatusInfo 추가 if (equipment?.statusInfo != null) { // Network 상태 섹션 if (equipment.statusInfo.network != null) { var networkGroup = CreateStatusSectionGroup(equipment.statusInfo.network, "status_network"); if (networkGroup != null) { entries.Add(networkGroup); } } // Equipment 상태 섹션 if (equipment.statusInfo.equipment != null) { var equipmentGroup = CreateStatusSectionGroup(equipment.statusInfo.equipment, "status_equipment"); if (equipmentGroup != null) { entries.Add(equipmentGroup); } } } _propertyWindow.LoadMixedProperties(entries); } /// /// PropertyWindow에서 속성 값이 변경되었을 때 호출되는 핸들러 /// private void OnPropertyValueChanged(UTKPropertyValueChangedEventArgs e) { // 자식 Transform이 표시 중인 경우 if (_currentDisplayedChildTransform != null) { HandleChildTransformPropertyChange(e); return; } if (_currentDisplayedObject == null) return; HandleStageObjectPropertyChange(e); } /// /// 자식 Transform 속성 변경 처리 /// private void HandleChildTransformPropertyChange(UTKPropertyValueChangedEventArgs e) { if (_currentDisplayedChildTransform == null) return; bool transformChanged = false; switch (e.PropertyId) { case "transform_position": if (e.NewValue is Vector3 childPos && e.OldValue is Vector3 oldChildPos) { _currentDisplayedChildTransform.localPosition = childPos; RecordTransformPropertyChange(_currentDisplayedChildTransform, "position", oldChildPos, childPos); transformChanged = true; } break; case "transform_rotation": if (e.NewValue is Vector3 childRot && e.OldValue is Vector3 oldChildRot) { _currentDisplayedChildTransform.localEulerAngles = childRot; RecordTransformPropertyChange(_currentDisplayedChildTransform, "rotation", oldChildRot, childRot); transformChanged = true; } break; case "transform_scale": if (e.NewValue is Vector3 childScale && e.OldValue is Vector3 oldChildScale) { _currentDisplayedChildTransform.localScale = childScale; RecordTransformPropertyChange(_currentDisplayedChildTransform, "scale", oldChildScale, childScale); transformChanged = true; } break; } // Transform이 변경되었으면 Gizmo 새로고침 if (transformChanged) { GizmoUndoBridge.Instance?.RefreshGizmos(); } } /// /// StageObject 속성 변경 처리 /// private void HandleStageObjectPropertyChange(UTKPropertyValueChangedEventArgs e) { if (_currentDisplayedObject?.GameObject == null) return; bool transformChanged = false; switch (e.PropertyId) { case "object_name": HandleObjectNameChanged(e.NewValue?.ToString() ?? ""); break; case "transform_position": if (e.NewValue is Vector3 pos && e.OldValue is Vector3 oldPos) { _currentDisplayedObject.GameObject.transform.localPosition = pos; RecordTransformPropertyChange(_currentDisplayedObject.GameObject.transform, "position", oldPos, pos); transformChanged = true; } break; case "transform_rotation": if (e.NewValue is Vector3 rot && e.OldValue is Vector3 oldRot) { _currentDisplayedObject.GameObject.transform.localEulerAngles = rot; RecordTransformPropertyChange(_currentDisplayedObject.GameObject.transform, "rotation", oldRot, rot); transformChanged = true; } break; case "transform_scale": if (e.NewValue is Vector3 scale && e.OldValue is Vector3 oldScale) { _currentDisplayedObject.GameObject.transform.localScale = scale; RecordTransformPropertyChange(_currentDisplayedObject.GameObject.transform, "scale", oldScale, scale); transformChanged = true; } break; default: // 일반 속성 변경은 PropertyChangeCommand로 기록 RecordPropertyChange(e); break; } // Transform이 변경되었으면 Gizmo 새로고침 if (transformChanged) { GizmoUndoBridge.Instance?.RefreshGizmos(); } } /// /// Transform 속성 변경을 UndoRedoManager에 기록합니다. /// private void RecordTransformPropertyChange(Transform transform, string propertyType, Vector3 oldValue, Vector3 newValue) { if (oldValue == newValue) return; var command = new TransformPropertyChangeCommand(transform, propertyType, oldValue, newValue); var undoRedoManager = UndoRedoManager.Instance; if (undoRedoManager != null) { undoRedoManager.RecordCommand(command); Debug.Log($"[SelectionManager] Recorded transform property change: {propertyType}"); } } /// /// 일반 속성 변경을 UndoRedoManager에 기록합니다. /// private void RecordPropertyChange(UTKPropertyValueChangedEventArgs e) { if (e.OldValue == e.NewValue) return; // PropertyChangeCommand에 값 적용 함수 전달 var command = new PropertyChangeCommand( e.PropertyId, e.PropertyId, // 속성 이름으로 ID 사용 e.OldValue, e.NewValue, ApplyPropertyValue ); var undoRedoManager = UndoRedoManager.Instance; if (undoRedoManager != null) { undoRedoManager.RecordCommand(command); Debug.Log($"[SelectionManager] Recorded property change: {e.PropertyId}"); } } /// /// 속성 값을 적용합니다. (Undo/Redo 시 호출) /// private void ApplyPropertyValue(string propertyId, object? value) { if (_currentDisplayedObject == null) return; // PropertyWindow에 값 반영 if (_propertyWindow != null) { _propertyWindow.SetPropertyValue(propertyId, value); } Debug.Log($"[SelectionManager] Applied property value: {propertyId} = {value}"); } /// /// 객체 이름 변경 처리 /// /// 새 이름 private void HandleObjectNameChanged(string newName) { if (_currentDisplayedObject == null || string.IsNullOrEmpty(newName)) return; // 1. GameObject.name 변경 if (_currentDisplayedObject.GameObject != null) { _currentDisplayedObject.GameObject.name = newName; } // 2. HierarchyWindow 반영 if (InjectorAppContext.Instance != null) { var hierarchy = InjectorAppContext.Instance.Get(); if (hierarchy != null) { hierarchy.UpdateItemName(_currentDisplayedObject.Id, newName); } } // 3. 이벤트 발생 OnObjectNameChanged?.Invoke(_currentDisplayedObject.Id, newName); Debug.Log($"[SelectionManager] Object name changed: {_currentDisplayedObject.Id} -> {newName}"); } /// /// StatusSection을 PropertyGroup으로 변환합니다. /// ColorStateProperty를 사용하여 label, stat, color를 하나의 항목으로 표시합니다. /// /// 변환할 StatusSection /// 그룹 ID 접두사 /// 순서 /// 생성된 PropertyGroup, 속성이 없으면 null private static UTKPropertyGroup? CreateStatusSectionGroup(StatusSection section, string groupIdPrefix) { if (section.properties == null || section.properties.Count == 0) return null; var group = new UTKPropertyGroup( groupIdPrefix, section.section ?? "Status" ); for (int i = 0; i < section.properties.Count; i++) { var statusProp = section.properties[i]; string propId = $"{groupIdPrefix}_{i}"; // value가 null이 아닌 경우에만 Color 파싱 Color? color = statusProp.value != null ? ParseHexColor(statusProp.value) : null; // ColorStateProperty 생성: Tuple var colorStateProperty = new UTKColorStatePropertyItem( propId, statusProp.label ?? "", // name으로 label 사용 new UTKColorState(statusProp.stat ?? "", color ?? Color.white) ); group.AddItem(colorStateProperty); } return group.ItemCount > 0 ? group : null; } /// /// HEX 색상 코드를 Color로 변환합니다. /// /// HEX 색상 코드 (예: "#FF0000") /// 변환된 Color, 실패 시 white private static Color ParseHexColor(string hex) { if (string.IsNullOrEmpty(hex)) return Color.white; // # 제거 if (hex.StartsWith("#")) hex = hex[1..]; if (ColorUtility.TryParseHtmlString($"#{hex}", out Color color)) return color; return Color.white; } /// /// Library.PropertyItem을 UTKPropertyList.IUTKPropertyItem으로 변환합니다. /// /// 변환할 PropertyItem /// 변환된 IUTKPropertyItem private static IUTKPropertyItem CreatePropertyItem(PropertyItem prop) { string id = prop.id ?? Guid.NewGuid().ToString("N")[..8]; string label = prop.label ?? id; // 단위가 있으면 레이블에 표시 if (!string.IsNullOrEmpty(prop.unit)) { label = $"{label} ({prop.unit})"; } IUTKPropertyItem item = prop.type?.ToLower() switch { "float" => new UTKFloatPropertyItem(id, label, prop.GetFloatValue(), false), "int" => new UTKIntPropertyItem(id, label, prop.GetIntValue()), _ => new UTKStringPropertyItem(id, label, prop.GetStringValue()) }; return item; } #endregion #region Gizmo Management /// /// 기즈모를 초기화합니다. RTGizmos가 준비된 후 호출해야 합니다. /// public void InitializeGizmos() { if (_gizmosInitialized) return; if (RTGizmos.get == null) { Debug.LogWarning("[SelectionManager] RTGizmos가 아직 초기화되지 않았습니다."); return; } // 기즈모 생성 _moveGizmo = RTGizmos.get.CreateObjectMoveGizmo(); _rotateGizmo = RTGizmos.get.CreateObjectRotateGizmo(); _scaleGizmo = RTGizmos.get.CreateObjectScaleGizmo(); _moveGizmo.objectTransformGizmo.pivot = EGizmoPivot.Pivot; _rotateGizmo.objectTransformGizmo.pivot = EGizmoPivot.Pivot; _scaleGizmo.objectTransformGizmo.pivot = EGizmoPivot.Pivot; // 기본적으로 모든 기즈모 비활성화 (Select 모드) _moveGizmo.enabled = false; _rotateGizmo.enabled = false; _scaleGizmo.enabled = false; _gizmosInitialized = true; _currentToolType = TransformToolType.Select; Debug.Log("[SelectionManager] 기즈모가 초기화되었습니다."); } /// /// 선택된 모든 객체를 기즈모 타겟으로 설정합니다. /// public void UpdateGizmoTargets() { if (!_gizmosInitialized) return; Debug.Log($"[SelectionManager] _currentToolType:{_currentToolType} _selectedObjects.Count:{_selectedObjects.Count}"); // 캐시된 리스트 재사용 _gizmoTargetObjects.Clear(); foreach (var stageObject in _selectedObjects) { if (stageObject.GameObject != null) { _gizmoTargetObjects.Add(stageObject.GameObject); } } // 모든 기즈모에 타겟 설정 if (_moveGizmo != null) { _moveGizmo.objectTransformGizmo.SetTargets(_gizmoTargetObjects); } if (_rotateGizmo != null) { _rotateGizmo.objectTransformGizmo.SetTargets(_gizmoTargetObjects); } if (_scaleGizmo != null) { _scaleGizmo.objectTransformGizmo.SetTargets(_gizmoTargetObjects); } // 현재 활성 기즈모 새로고침 RefreshActiveGizmo(); Debug.Log($"[SelectionManager] 기즈모 타겟 업데이트: {_gizmoTargetObjects.Count}개 객체"); } /// /// 활성 변환 도구를 설정합니다. /// /// 설정할 도구 타입 public void SetActiveTool(TransformToolType toolType) { if (!_gizmosInitialized) { Debug.LogWarning("[SelectionManager] 기즈모가 초기화되지 않았습니다. InitializeGizmos()를 먼저 호출하세요."); return; } Debug.Log($"[SelectionManager] SetActiveTool called: {toolType}"); _currentToolType = toolType; // 모든 기즈모 비활성화 if (_moveGizmo != null) _moveGizmo.enabled = false; if (_rotateGizmo != null) _rotateGizmo.enabled = false; if (_scaleGizmo != null) _scaleGizmo.enabled = false; if(_selectedObjects.Count == 0) { Debug.Log("[SelectionManager] No selected objects. Gizmos will remain disabled."); return; } // 선택된 도구에 해당하는 기즈모 활성화 switch (toolType) { case TransformToolType.Move: if (_moveGizmo != null) { _moveGizmo.enabled = true; _moveGizmo.objectTransformGizmo.Refresh(); } break; case TransformToolType.Rotate: if (_rotateGizmo != null) { _rotateGizmo.enabled = true; _rotateGizmo.objectTransformGizmo.Refresh(); } break; case TransformToolType.Scale: if (_scaleGizmo != null) { _scaleGizmo.enabled = true; _scaleGizmo.objectTransformGizmo.Refresh(); } break; case TransformToolType.Select: default: // 모든 기즈모 비활성화 상태 유지 break; } Debug.Log($"[SelectionManager] 활성 도구 변경: {toolType}"); OnToolChanged?.Invoke(toolType); } /// /// 현재 활성화된 기즈모를 새로고침합니다. /// 선택된 객체가 없으면 기즈모를 비활성화합니다. /// public void RefreshActiveGizmo() { if (!_gizmosInitialized) return; // 선택된 객체가 없으면 모든 기즈모 비활성화 if (_selectedObjects.Count == 0) { if (_moveGizmo != null) _moveGizmo.enabled = false; if (_rotateGizmo != null) _rotateGizmo.enabled = false; if (_scaleGizmo != null) _scaleGizmo.enabled = false; return; } switch (_currentToolType) { case TransformToolType.Move: if (_moveGizmo != null) { _moveGizmo.enabled = true; _moveGizmo.objectTransformGizmo.Refresh(); } break; case TransformToolType.Rotate: if (_rotateGizmo != null) { _rotateGizmo.enabled = true; _rotateGizmo.objectTransformGizmo.Refresh(); } break; case TransformToolType.Scale: if (_scaleGizmo != null) { _scaleGizmo.enabled = true; _scaleGizmo.objectTransformGizmo.Refresh(); } break; } } /// /// 기즈모가 초기화되었는지 여부를 반환합니다. /// public bool IsGizmosInitialized => _gizmosInitialized; #endregion #region Memory Management /// /// 리소스를 정리합니다. 씬 전환 시 호출해야 합니다. /// public void Dispose() { // 이벤트 구독 해제 if (_propertyWindow != null) { _propertyWindow.OnPropertyValueChanged -= OnPropertyValueChanged; } // 선택 해제 DeselectAll(); // 원본 색상 딕셔너리 정리 _originalColors.Clear(); // 캐시 초기화 _cachedOutliner = null; _outlinerCacheInitialized = false; // 기즈모 참조 해제 (RTGizmos가 실제 파괴 담당) _moveGizmo = null; _rotateGizmo = null; _scaleGizmo = null; _gizmosInitialized = false; Debug.Log("[SelectionManager] Disposed"); } /// /// 파괴된 Renderer 참조를 정리합니다. /// 주기적으로 또는 객체 삭제 시 호출하면 좋습니다. /// public void CleanupDestroyedRenderers() { // Unity에서 파괴된 객체는 == null로 체크 가능하지만 실제로는 null이 아님 // 따라서 null 체크된 키들을 별도 리스트에 저장 후 삭제 var keysToRemove = new List(); foreach (var kvp in _originalColors) { // Unity 객체의 파괴 여부 확인 if (kvp.Key == null || !kvp.Key) { keysToRemove.Add(kvp.Key!); } } int removedCount = 0; foreach (var key in keysToRemove) { if (_originalColors.Remove(key)) { removedCount++; } } if (removedCount > 0) { Debug.Log($"[SelectionManager] Cleaned up {removedCount} destroyed renderer references"); } } #endregion } }