1207 lines
44 KiB
C#
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
|
|
}
|
|
}
|