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

1207 lines
44 KiB
C#

#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
{
/// <summary>
/// 변환 도구 타입
/// </summary>
public enum TransformToolType
{
/// <summary>선택 도구 (기즈모 없음)</summary>
Select,
/// <summary>이동 도구</summary>
Move,
/// <summary>회전 도구</summary>
Rotate,
/// <summary>크기 조절 도구</summary>
Scale
}
/// <summary>
/// 스테이지 객체의 선택 상태를 관리하는 매니저 클래스
/// </summary>
public class SelectionManager
{
#region Private Fields - Selection
/// <summary>
/// 현재 선택된 StageObject 목록
/// </summary>
private readonly HashSet<StageObjectManager.StageObject> _selectedObjects = new();
/// <summary>
/// StageObjectManager 참조
/// </summary>
private readonly StageObjectManager _stageObjectManager;
/// <summary>
/// PropertyWindow 참조
/// </summary>
private readonly UTKPropertyTabListWindow? _propertyWindow;
/// <summary>
/// 현재 PropertyWindow에 표시 중인 StageObject
/// </summary>
private StageObjectManager.StageObject? _currentDisplayedObject;
/// <summary>
/// 현재 PropertyWindow에 표시 중인 자식 Transform (모델의 자식 선택 시)
/// </summary>
private Transform? _currentDisplayedChildTransform;
/// <summary>
/// 미리보기 색상 적용 전 원본 Material 색상을 저장하는 딕셔너리
/// Key: Renderer, Value: Material 인덱스별 (프로퍼티 이름, 원본 색상) 리스트
/// </summary>
private readonly Dictionary<Renderer, List<(int materialIndex, string propertyName, Color color)>> _originalColors = new();
/// <summary>
/// 캐싱된 Outliner 참조 (매번 FindFirstObjectByType 호출 방지)
/// </summary>
private Outliner? _cachedOutliner;
/// <summary>
/// Outliner 캐시 초기화 여부
/// </summary>
private bool _outlinerCacheInitialized = false;
#endregion
#region Private Fields - Gizmos
/// <summary>
/// 이동 기즈모
/// </summary>
private MoveGizmo? _moveGizmo;
/// <summary>
/// 회전 기즈모
/// </summary>
private RotateGizmo? _rotateGizmo;
/// <summary>
/// 크기 조절 기즈모
/// </summary>
private ScaleGizmo? _scaleGizmo;
/// <summary>
/// 현재 활성화된 변환 도구 타입
/// </summary>
private TransformToolType _currentToolType = TransformToolType.Select;
/// <summary>
/// 기즈모 초기화 여부
/// </summary>
private bool _gizmosInitialized = false;
/// <summary>
/// 기즈모 타겟 객체 리스트 (캐시)
/// </summary>
private readonly List<GameObject> _gizmoTargetObjects = new();
#endregion
#region Events
/// <summary>
/// 선택 변경 시 발생하는 이벤트
/// (선택된 객체, 선택 여부)
/// </summary>
public event Action<StageObjectManager.StageObject, bool>? OnSelectionChanged;
/// <summary>
/// 객체 이름이 변경되었을 때 발생하는 이벤트
/// (StageObject ID, 새 이름)
/// </summary>
public event Action<string, string>? OnObjectNameChanged;
/// <summary>
/// 변환 도구가 변경되었을 때 발생하는 이벤트
/// </summary>
public event Action<TransformToolType>? OnToolChanged;
#endregion
#region Public Properties
/// <summary>
/// 현재 선택된 객체 목록 (읽기 전용)
/// </summary>
public IReadOnlyCollection<StageObjectManager.StageObject> SelectedObjects => _selectedObjects;
/// <summary>
/// 선택된 객체 수
/// </summary>
public int SelectedCount => _selectedObjects.Count;
/// <summary>
/// 현재 활성화된 변환 도구 타입
/// </summary>
public TransformToolType CurrentToolType => _currentToolType;
#endregion
/// <summary>
/// 생성자
/// </summary>
/// <param name="stageObjectManager">StageObjectManager 참조</param>
/// <param name="propertyWindow">PropertyWindow 참조 (선택)</param>
public SelectionManager(StageObjectManager stageObjectManager, UTKPropertyTabListWindow? propertyWindow = null)
{
_stageObjectManager = stageObjectManager;
_propertyWindow = propertyWindow;
// PropertyWindow 값 변경 이벤트 구독
if (_propertyWindow != null)
{
_propertyWindow.OnPropertyValueChanged += OnPropertyValueChanged;
}
}
/// <summary>
/// 객체를 선택합니다
/// </summary>
/// <param name="stageObject">선택할 객체</param>
/// <param name="addToSelection">기존 선택에 추가할지 여부 (false면 기존 선택 해제)</param>
/// <param name="clearHierarchySelection">기존 선택 해제 시 HierarchyWindow 선택도 해제할지 여부</param>
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();
}
/// <summary>
/// StageObject ID로 객체를 선택합니다
/// </summary>
/// <param name="stageObjectId">선택할 객체의 ID</param>
/// <param name="addToSelection">기존 선택에 추가할지 여부</param>
/// <param name="clearHierarchySelection">기존 선택 해제 시 HierarchyWindow 선택도 해제할지 여부</param>
public void SelectById(string stageObjectId, bool addToSelection = false, bool clearHierarchySelection = true)
{
var stageObject = _stageObjectManager.GetById(stageObjectId);
if (stageObject != null)
{
Select(stageObject, addToSelection, clearHierarchySelection);
}
}
/// <summary>
/// 객체 선택을 해제합니다
/// </summary>
/// <param name="stageObject">선택 해제할 객체</param>
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();
}
}
/// <summary>
/// StageObject ID로 객체 선택을 해제합니다
/// </summary>
/// <param name="stageObjectId">선택 해제할 객체의 ID</param>
public void DeselectById(string stageObjectId)
{
var stageObject = _stageObjectManager.GetById(stageObjectId);
if (stageObject != null)
{
Deselect(stageObject);
}
}
/// <summary>
/// 모든 선택을 해제합니다
/// </summary>
/// <param name="clearHierarchySelection">HierarchyWindow 선택도 해제할지 여부 (기본값: true)</param>
public void DeselectAll(bool clearHierarchySelection = true)
{
var objectsToDeselect = new List<StageObjectManager.StageObject>(_selectedObjects);
foreach (var obj in objectsToDeselect)
{
Deselect(obj);
}
// UTKComponentListWindow(TreeList) 선택도 해제
// UTKComponentListWindow에서 선택 처리 중일 때는 TreeList가 자체적으로 선택을 관리하므로 생략
if (clearHierarchySelection)
{
var hierarchy = InjectorAppContext.Instance.Get<UTKTreeListWindowController>();
if (hierarchy != null)
{
hierarchy.ClearSelection();
}
}
// 자식 Transform 표시도 정리
ClearChildTransformDisplay();
}
/// <summary>
/// 객체가 선택되어 있는지 확인합니다
/// </summary>
/// <param name="stageObject">확인할 객체</param>
/// <returns>선택 여부</returns>
public bool IsSelected(StageObjectManager.StageObject stageObject)
{
return stageObject != null && _selectedObjects.Contains(stageObject);
}
/// <summary>
/// 선택 상태를 토글합니다
/// </summary>
/// <param name="stageObject">토글할 객체</param>
/// <param name="addToSelection">다중 선택 모드</param>
public void ToggleSelection(StageObjectManager.StageObject stageObject, bool addToSelection = false)
{
if (IsSelected(stageObject))
{
Deselect(stageObject);
}
else
{
Select(stageObject, addToSelection);
}
}
/// <summary>
/// Outlinable 컴포넌트의 활성화 상태를 설정합니다
/// </summary>
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<Outlinable>();
if (outlinable == null)
{
// 자식에서도 검색
outlinable = stageObject.GameObject.GetComponentInChildren<Outlinable>();
}
if (outlinable != null)
{
outlinable.enabled = enabled;
// Outliner 존재 여부 확인 (캐싱)
if (!_outlinerCacheInitialized)
{
_cachedOutliner = UnityEngine.Object.FindFirstObjectByType<Outliner>();
_outlinerCacheInitialized = true;
if (_cachedOutliner == null)
{
Debug.LogWarning("[SelectionManager] Outliner component not found in scene! EPOOutline requires Outliner on camera.");
}
}
}
}
/// <summary>
/// 객체의 가시성을 설정합니다
/// </summary>
/// <param name="stageObject">대상 객체</param>
/// <param name="visible">가시성 여부</param>
public void SetVisibility(StageObjectManager.StageObject stageObject, bool visible)
{
if (stageObject?.GameObject != null)
{
stageObject.GameObject.SetActive(visible);
Debug.Log($"[SelectionManager] SetVisibility: {stageObject.GameObject.name} = {visible}");
}
}
/// <summary>
/// StageObject ID로 객체의 가시성을 설정합니다
/// </summary>
/// <param name="stageObjectId">대상 객체의 ID</param>
/// <param name="visible">가시성 여부</param>
public void SetVisibilityById(string stageObjectId, bool visible)
{
var stageObject = _stageObjectManager.GetById(stageObjectId);
if (stageObject != null)
{
SetVisibility(stageObject, visible);
}
}
#region Preview Color
/// <summary>
/// 선택된 객체들의 Material 색상을 미리보기 색상으로 변경합니다.
/// 원본 색상은 저장되어 ClearPreviewColor 호출 시 복원됩니다.
/// </summary>
/// <param name="previewColor">미리보기 색상</param>
/// <summary>
/// Material에서 색상 프로퍼티 이름을 찾습니다.
/// 다양한 셰이더 (Standard, URP, glTF 등)를 지원합니다.
/// </summary>
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<Renderer>();
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);
}
}
}
}
/// <summary>
/// 미리보기 색상을 해제하고 원본 Material 색상으로 복원합니다.
/// </summary>
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
/// <summary>
/// 자식 Transform의 속성을 PropertyWindow에 표시합니다.
/// </summary>
/// <param name="childTransform">표시할 자식 Transform</param>
/// <param name="childName">자식 이름</param>
public void DisplayChildTransformProperties(Transform childTransform, string childName)
{
if (_propertyWindow == null || childTransform == null) return;
// 기존 StageObject 표시 상태 초기화
_currentDisplayedObject = null;
_currentDisplayedChildTransform = childTransform;
var entries = new List<IUTKPropertyEntry>();
// 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}");
}
/// <summary>
/// 자식 Transform 표시 상태를 초기화합니다.
/// </summary>
public void ClearChildTransformDisplay()
{
_currentDisplayedChildTransform = null;
}
/// <summary>
/// StageObject의 Equipment PropertiesInfo를 PropertyWindow에 표시합니다.
/// </summary>
/// <param name="stageObject">표시할 StageObject</param>
private void DisplayEquipmentProperties(StageObjectManager.StageObject stageObject)
{
if (_propertyWindow == null) return;
// 자식 Transform 표시 상태 초기화
_currentDisplayedChildTransform = null;
_currentDisplayedObject = stageObject;
var equipment = stageObject.Equipment;
var entries = new List<IUTKPropertyEntry>();
// 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);
}
/// <summary>
/// PropertyWindow에서 속성 값이 변경되었을 때 호출되는 핸들러
/// </summary>
private void OnPropertyValueChanged(UTKPropertyValueChangedEventArgs e)
{
// 자식 Transform이 표시 중인 경우
if (_currentDisplayedChildTransform != null)
{
HandleChildTransformPropertyChange(e);
return;
}
if (_currentDisplayedObject == null) return;
HandleStageObjectPropertyChange(e);
}
/// <summary>
/// 자식 Transform 속성 변경 처리
/// </summary>
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();
}
}
/// <summary>
/// StageObject 속성 변경 처리
/// </summary>
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();
}
}
/// <summary>
/// Transform 속성 변경을 UndoRedoManager에 기록합니다.
/// </summary>
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}");
}
}
/// <summary>
/// 일반 속성 변경을 UndoRedoManager에 기록합니다.
/// </summary>
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}");
}
}
/// <summary>
/// 속성 값을 적용합니다. (Undo/Redo 시 호출)
/// </summary>
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}");
}
/// <summary>
/// 객체 이름 변경 처리
/// </summary>
/// <param name="newName">새 이름</param>
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<UTKTreeListWindowController>();
if (hierarchy != null)
{
hierarchy.UpdateItemName(_currentDisplayedObject.Id, newName);
}
}
// 3. 이벤트 발생
OnObjectNameChanged?.Invoke(_currentDisplayedObject.Id, newName);
Debug.Log($"[SelectionManager] Object name changed: {_currentDisplayedObject.Id} -> {newName}");
}
/// <summary>
/// StatusSection을 PropertyGroup으로 변환합니다.
/// ColorStateProperty를 사용하여 label, stat, color를 하나의 항목으로 표시합니다.
/// </summary>
/// <param name="section">변환할 StatusSection</param>
/// <param name="groupIdPrefix">그룹 ID 접두사</param>
/// <param name="order">순서</param>
/// <returns>생성된 PropertyGroup, 속성이 없으면 null</returns>
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<label, stat, color?>
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;
}
/// <summary>
/// HEX 색상 코드를 Color로 변환합니다.
/// </summary>
/// <param name="hex">HEX 색상 코드 (예: "#FF0000")</param>
/// <returns>변환된 Color, 실패 시 white</returns>
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;
}
/// <summary>
/// Library.PropertyItem을 UTKPropertyList.IUTKPropertyItem으로 변환합니다.
/// </summary>
/// <param name="prop">변환할 PropertyItem</param>
/// <returns>변환된 IUTKPropertyItem</returns>
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
/// <summary>
/// 기즈모를 초기화합니다. RTGizmos가 준비된 후 호출해야 합니다.
/// </summary>
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] 기즈모가 초기화되었습니다.");
}
/// <summary>
/// 선택된 모든 객체를 기즈모 타겟으로 설정합니다.
/// </summary>
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}개 객체");
}
/// <summary>
/// 활성 변환 도구를 설정합니다.
/// </summary>
/// <param name="toolType">설정할 도구 타입</param>
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);
}
/// <summary>
/// 현재 활성화된 기즈모를 새로고침합니다.
/// 선택된 객체가 없으면 기즈모를 비활성화합니다.
/// </summary>
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;
}
}
/// <summary>
/// 기즈모가 초기화되었는지 여부를 반환합니다.
/// </summary>
public bool IsGizmosInitialized => _gizmosInitialized;
#endregion
#region Memory Management
/// <summary>
/// 리소스를 정리합니다. 씬 전환 시 호출해야 합니다.
/// </summary>
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");
}
/// <summary>
/// 파괴된 Renderer 참조를 정리합니다.
/// 주기적으로 또는 객체 삭제 시 호출하면 좋습니다.
/// </summary>
public void CleanupDestroyedRenderers()
{
// Unity에서 파괴된 객체는 == null로 체크 가능하지만 실제로는 null이 아님
// 따라서 null 체크된 키들을 별도 리스트에 저장 후 삭제
var keysToRemove = new List<Renderer>();
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
}
}