선택한 모델 PropertyWindow 연결 완료
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
@@ -21,7 +22,7 @@ namespace UVC.Studio.Config
|
||||
/// LibraryXXX.json
|
||||
/// └── [equipmentType]: List<EquipmentItem>
|
||||
/// └── EquipmentItem
|
||||
/// ├── model - 모델 식별자 (예: "SingleFork")
|
||||
/// ├── id - 장비 식별자 (예: "SingleFork")
|
||||
/// ├── label - 표시 이름 (예: "Single Fork")
|
||||
/// ├── gltf - glTF 파일 경로 (예: "staker_crane/SingleFork.glb")
|
||||
/// ├── image - 썸네일 이미지 경로 (선택)
|
||||
@@ -65,7 +66,7 @@ namespace UVC.Studio.Config
|
||||
/// // 데이터 접근
|
||||
/// foreach (var crane in library.StakerCraneData.stakerCrane)
|
||||
/// {
|
||||
/// Debug.Log($"Model: {crane.model}, Label: {crane.label}");
|
||||
/// Debug.Log($"ID: {crane.id}, Label: {crane.label}");
|
||||
/// }
|
||||
/// </code>
|
||||
///
|
||||
@@ -132,7 +133,7 @@ namespace UVC.Studio.Config
|
||||
/// // 스태커 크레인 데이터 접근
|
||||
/// foreach (var crane in _library.StakerCraneData.stakerCrane)
|
||||
/// {
|
||||
/// Debug.Log($"Model: {crane.model}, GLTF: {crane.gltf}");
|
||||
/// Debug.Log($"ID: {crane.id}, GLTF: {crane.gltf}");
|
||||
/// }
|
||||
///
|
||||
/// // AGV 데이터 접근
|
||||
@@ -142,10 +143,10 @@ namespace UVC.Studio.Config
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// public EquipmentItem GetEquipmentByModel(string model)
|
||||
/// public EquipmentItem GetEquipmentById(string id)
|
||||
/// {
|
||||
/// return _library.StakerCraneData.stakerCrane
|
||||
/// .FirstOrDefault(e => e.model == model);
|
||||
/// .FirstOrDefault(e => e.id == id);
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
@@ -491,12 +492,12 @@ namespace UVC.Studio.Config
|
||||
public class EquipmentItem
|
||||
{
|
||||
/// <summary>
|
||||
/// 모델 식별자
|
||||
/// 장비 식별자
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 고유한 모델 ID (예: "SingleFork", "Rack_Single")
|
||||
/// 고유한 장비 ID (예: "SingleFork", "Rack_Single")
|
||||
/// </remarks>
|
||||
public string model;
|
||||
public string id;
|
||||
|
||||
/// <summary>
|
||||
/// 표시 이름
|
||||
@@ -754,7 +755,7 @@ namespace UVC.Studio.Config
|
||||
/// UI 표시용 색상 코드 (예: "#228B22" - 녹색, "#8B0000" - 빨간색)
|
||||
/// 빈 문자열이면 기본 색상 사용
|
||||
/// </remarks>
|
||||
public string value;
|
||||
public string? value;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using EPOOutline;
|
||||
using UnityEngine;
|
||||
using UVC.Core;
|
||||
using UVC.Studio.Config;
|
||||
using UVC.Studio.Tab;
|
||||
using UVC.UI.Window.PropertyWindow;
|
||||
|
||||
namespace UVC.Studio.Manager
|
||||
{
|
||||
@@ -21,12 +26,34 @@ namespace UVC.Studio.Manager
|
||||
/// </summary>
|
||||
private readonly StageObjectManager _stageObjectManager;
|
||||
|
||||
/// <summary>
|
||||
/// PropertyWindow 참조
|
||||
/// </summary>
|
||||
private readonly PropertyWindow? _propertyWindow;
|
||||
|
||||
/// <summary>
|
||||
/// 현재 PropertyWindow에 표시 중인 StageObject
|
||||
/// </summary>
|
||||
private StageObjectManager.StageObject? _currentDisplayedObject;
|
||||
|
||||
/// <summary>
|
||||
/// 미리보기 색상 적용 전 원본 Material 색상을 저장하는 딕셔너리
|
||||
/// Key: Renderer, Value: (프로퍼티 이름, 원본 색상)
|
||||
/// </summary>
|
||||
private readonly Dictionary<Renderer, (string propertyName, Color color)> _originalColors = new();
|
||||
|
||||
/// <summary>
|
||||
/// 선택 변경 시 발생하는 이벤트
|
||||
/// (선택된 객체, 선택 여부)
|
||||
/// </summary>
|
||||
public event Action<StageObjectManager.StageObject, bool>? OnSelectionChanged;
|
||||
|
||||
/// <summary>
|
||||
/// 객체 이름이 변경되었을 때 발생하는 이벤트
|
||||
/// (StageObject ID, 새 이름)
|
||||
/// </summary>
|
||||
public event Action<string, string>? OnObjectNameChanged;
|
||||
|
||||
/// <summary>
|
||||
/// 현재 선택된 객체 목록 (읽기 전용)
|
||||
/// </summary>
|
||||
@@ -41,9 +68,17 @@ namespace UVC.Studio.Manager
|
||||
/// 생성자
|
||||
/// </summary>
|
||||
/// <param name="stageObjectManager">StageObjectManager 참조</param>
|
||||
public SelectionManager(StageObjectManager stageObjectManager)
|
||||
/// <param name="propertyWindow">PropertyWindow 참조 (선택)</param>
|
||||
public SelectionManager(StageObjectManager stageObjectManager, PropertyWindow? propertyWindow = null)
|
||||
{
|
||||
_stageObjectManager = stageObjectManager;
|
||||
_propertyWindow = propertyWindow;
|
||||
|
||||
// PropertyWindow 값 변경 이벤트 구독
|
||||
if (_propertyWindow != null)
|
||||
{
|
||||
_propertyWindow.PropertyValueChanged += OnPropertyValueChanged;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -72,6 +107,9 @@ namespace UVC.Studio.Manager
|
||||
|
||||
Debug.Log($"[SelectionManager] Selected: {stageObject.GameObject?.name}");
|
||||
OnSelectionChanged?.Invoke(stageObject, true);
|
||||
|
||||
// PropertyWindow에 선택된 객체의 속성 표시
|
||||
DisplayEquipmentProperties(stageObject);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -103,6 +141,20 @@ namespace UVC.Studio.Manager
|
||||
|
||||
Debug.Log($"[SelectionManager] Deselected: {stageObject.GameObject?.name}");
|
||||
OnSelectionChanged?.Invoke(stageObject, false);
|
||||
|
||||
// 모든 선택이 해제되면 정리 작업 수행
|
||||
if (_selectedObjects.Count == 0)
|
||||
{
|
||||
// 미리보기 색상 원복 및 클리어
|
||||
ClearPreviewColor();
|
||||
|
||||
// PropertyWindow 항목 모두 제거
|
||||
if (_propertyWindow != null)
|
||||
{
|
||||
_propertyWindow.Clear();
|
||||
}
|
||||
_currentDisplayedObject = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,5 +325,386 @@ namespace UVC.Studio.Manager
|
||||
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 colorPropName = GetColorPropertyName(renderer.material);
|
||||
if (colorPropName == null) continue;
|
||||
|
||||
// 원본 색상 저장 (아직 저장되지 않은 경우에만)
|
||||
if (!_originalColors.ContainsKey(renderer))
|
||||
{
|
||||
_originalColors[renderer] = (colorPropName, renderer.material.GetColor(colorPropName));
|
||||
}
|
||||
|
||||
// 미리보기 색상 적용
|
||||
renderer.material.SetColor(colorPropName, previewColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 미리보기 색상을 해제하고 원본 Material 색상으로 복원합니다.
|
||||
/// </summary>
|
||||
public void ClearPreviewColor()
|
||||
{
|
||||
foreach (var kvp in _originalColors)
|
||||
{
|
||||
var renderer = kvp.Key;
|
||||
var (propertyName, originalColor) = kvp.Value;
|
||||
|
||||
if (renderer == null) continue;
|
||||
|
||||
// 저장된 프로퍼티 이름으로 원본 색상 복원
|
||||
renderer.material.SetColor(propertyName, originalColor);
|
||||
}
|
||||
|
||||
_originalColors.Clear();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region PropertyWindow Integration
|
||||
|
||||
/// <summary>
|
||||
/// StageObject의 Equipment PropertiesInfo를 PropertyWindow에 표시합니다.
|
||||
/// </summary>
|
||||
/// <param name="stageObject">표시할 StageObject</param>
|
||||
private void DisplayEquipmentProperties(StageObjectManager.StageObject stageObject)
|
||||
{
|
||||
if (_propertyWindow == null) return;
|
||||
|
||||
_currentDisplayedObject = stageObject;
|
||||
|
||||
var equipment = stageObject.Equipment;
|
||||
var entries = new List<IPropertyEntry>();
|
||||
int orderIndex = 0;
|
||||
|
||||
// 1. object_name 속성 추가 (수정 가능, 그룹 없이 개별)
|
||||
var nameProperty = new StringProperty("object_name", "Name",
|
||||
stageObject.GameObject != null ? stageObject.GameObject.name : "Unknown")
|
||||
{
|
||||
IsReadOnly = false,
|
||||
Order = orderIndex++
|
||||
};
|
||||
entries.Add(nameProperty);
|
||||
|
||||
// 2. Transform 그룹 추가
|
||||
if (stageObject.GameObject != null)
|
||||
{
|
||||
var transform = stageObject.GameObject.transform;
|
||||
var transformGroup = new PropertyGroup("transform", "Transform", order: orderIndex++);
|
||||
transformGroup.AddItems(new IPropertyItem[]
|
||||
{
|
||||
new Vector3Property("transform_position", "Position", transform.localPosition),
|
||||
new Vector3Property("transform_rotation", "Rotation", transform.localEulerAngles),
|
||||
new Vector3Property("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, orderIndex++);
|
||||
if (propertyItem != null)
|
||||
{
|
||||
entries.Add(propertyItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 일반 섹션은 그룹으로 묶음
|
||||
var group = new PropertyGroup(
|
||||
$"section_{propInfo.section}",
|
||||
propInfo.section ?? "Properties",
|
||||
order: orderIndex++
|
||||
);
|
||||
|
||||
foreach (var prop in propInfo.properties)
|
||||
{
|
||||
var propertyItem = CreatePropertyItem(prop);
|
||||
if (propertyItem != null)
|
||||
{
|
||||
group.AddItem(propertyItem);
|
||||
}
|
||||
}
|
||||
|
||||
if (group.Count > 0)
|
||||
{
|
||||
entries.Add(group);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. StatusInfo 추가
|
||||
if (equipment?.statusInfo != null)
|
||||
{
|
||||
// Network 상태 섹션
|
||||
if (equipment.statusInfo.network != null)
|
||||
{
|
||||
var networkGroup = CreateStatusSectionGroup(
|
||||
equipment.statusInfo.network,
|
||||
"status_network",
|
||||
orderIndex++
|
||||
);
|
||||
if (networkGroup != null)
|
||||
{
|
||||
entries.Add(networkGroup);
|
||||
}
|
||||
}
|
||||
|
||||
// Equipment 상태 섹션
|
||||
if (equipment.statusInfo.equipment != null)
|
||||
{
|
||||
var equipmentGroup = CreateStatusSectionGroup(
|
||||
equipment.statusInfo.equipment,
|
||||
"status_equipment",
|
||||
orderIndex++
|
||||
);
|
||||
if (equipmentGroup != null)
|
||||
{
|
||||
entries.Add(equipmentGroup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_propertyWindow.LoadMixedProperties(entries);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PropertyWindow에서 속성 값이 변경되었을 때 호출되는 핸들러
|
||||
/// </summary>
|
||||
private void OnPropertyValueChanged(object? sender, PropertyValueChangedEventArgs e)
|
||||
{
|
||||
if (_currentDisplayedObject == null) return;
|
||||
|
||||
switch (e.PropertyId)
|
||||
{
|
||||
case "object_name":
|
||||
HandleObjectNameChanged(e.NewValue?.ToString() ?? "");
|
||||
break;
|
||||
case "transform_position":
|
||||
if (_currentDisplayedObject.GameObject != null && e.NewValue is Vector3 pos)
|
||||
{
|
||||
_currentDisplayedObject.GameObject.transform.localPosition = pos;
|
||||
}
|
||||
break;
|
||||
case "transform_rotation":
|
||||
if (_currentDisplayedObject.GameObject != null && e.NewValue is Vector3 rot)
|
||||
{
|
||||
_currentDisplayedObject.GameObject.transform.localEulerAngles = rot;
|
||||
}
|
||||
break;
|
||||
case "transform_scale":
|
||||
if (_currentDisplayedObject.GameObject != null && e.NewValue is Vector3 scale)
|
||||
{
|
||||
_currentDisplayedObject.GameObject.transform.localScale = scale;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <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<StudioSideTabBarHierarchy>();
|
||||
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 PropertyGroup? CreateStatusSectionGroup(StatusSection section, string groupIdPrefix, int order)
|
||||
{
|
||||
if (section.properties == null || section.properties.Count == 0)
|
||||
return null;
|
||||
|
||||
var group = new PropertyGroup(
|
||||
groupIdPrefix,
|
||||
section.section ?? "Status",
|
||||
order: order
|
||||
);
|
||||
|
||||
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 ColorStateProperty(
|
||||
propId,
|
||||
statusProp.label ?? "", // name으로 label 사용
|
||||
new Tuple<string, Color?>(
|
||||
statusProp.stat ?? "",
|
||||
color
|
||||
)
|
||||
);
|
||||
|
||||
group.AddItem(colorStateProperty);
|
||||
}
|
||||
|
||||
return group.Count > 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을 PropertyWindow.IPropertyItem으로 변환합니다.
|
||||
/// </summary>
|
||||
/// <param name="prop">변환할 PropertyItem</param>
|
||||
/// <param name="order">순서 (선택)</param>
|
||||
/// <returns>변환된 IPropertyItem</returns>
|
||||
private static IPropertyItem CreatePropertyItem(PropertyItem prop, int order = 0)
|
||||
{
|
||||
string id = prop.id ?? Guid.NewGuid().ToString("N")[..8];
|
||||
string label = prop.label ?? id;
|
||||
|
||||
// 단위가 있으면 레이블에 표시
|
||||
if (!string.IsNullOrEmpty(prop.unit))
|
||||
{
|
||||
label = $"{label} ({prop.unit})";
|
||||
}
|
||||
|
||||
IPropertyItem item = prop.type?.ToLower() switch
|
||||
{
|
||||
"float" => new FloatProperty(id, label, prop.GetFloatValue()),
|
||||
"int" => new IntProperty(id, label, prop.GetIntValue()),
|
||||
_ => new StringProperty(id, label, prop.GetStringValue())
|
||||
};
|
||||
|
||||
if (item is PropertyItem<string> strItem)
|
||||
{
|
||||
strItem.Order = order;
|
||||
}
|
||||
else if (item is PropertyItem<float> floatItem)
|
||||
{
|
||||
floatItem.Order = order;
|
||||
}
|
||||
else if (item is PropertyItem<int> intItem)
|
||||
{
|
||||
intItem.Order = order;
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ namespace UVC.Studio.Manager
|
||||
};
|
||||
|
||||
_objects[id] = stageObject;
|
||||
gameObject.name = GenerateUniqueName(equipment.model);
|
||||
gameObject.name = GenerateUniqueName(equipment.id);
|
||||
|
||||
Debug.Log($"[StageObjectManager] Registered: {gameObject.name}");
|
||||
OnObjectAdded?.Invoke(stageObject);
|
||||
|
||||
@@ -44,8 +44,8 @@ namespace UVC.Studio
|
||||
var stageObjectManager = new StageObjectManager();
|
||||
Injector.RegisterInstance<StageObjectManager>(stageObjectManager);
|
||||
|
||||
// SelectionManager 등록 (StageObjectManager 의존)
|
||||
var selectionManager = new SelectionManager(stageObjectManager);
|
||||
// SelectionManager 등록 (StageObjectManager, PropertyWindow 의존)
|
||||
var selectionManager = new SelectionManager(stageObjectManager, propertyWindow);
|
||||
Injector.RegisterInstance<SelectionManager>(selectionManager);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,8 +19,6 @@ using UVC.UI.ToolBar;
|
||||
using UVC.UI.Tooltip;
|
||||
using UVC.UI.Window.PropertyWindow;
|
||||
using UVC.Util;
|
||||
using UVC.Studio.Config;
|
||||
using UVC.Studio.Manager;
|
||||
using ActionCommand = UVC.UI.Commands.ActionCommand;
|
||||
|
||||
namespace UVC.Studio
|
||||
@@ -41,9 +39,6 @@ namespace UVC.Studio
|
||||
[Inject]
|
||||
private PropertyWindow propertyWindow;
|
||||
|
||||
[Inject]
|
||||
private SelectionManager selectionManager;
|
||||
|
||||
//test code
|
||||
MoveGizmo mMoveGizmo; // 이동 기즈모
|
||||
RotateGizmo mRotateGizmo; // 회전 기즈모
|
||||
@@ -81,8 +76,6 @@ namespace UVC.Studio
|
||||
|
||||
SetupTopMenu();
|
||||
SetupToolBox();
|
||||
SetupPropertyWindow();
|
||||
SetupSelectionManager();
|
||||
sideTabBar.InitTab();
|
||||
|
||||
Initialized?.Invoke();
|
||||
@@ -400,228 +393,5 @@ namespace UVC.Studio
|
||||
};
|
||||
}
|
||||
|
||||
private void SetupPropertyWindow()
|
||||
{
|
||||
propertyWindow.LoadProperties(new List<IPropertyItem>
|
||||
{
|
||||
new StringProperty("prop1", "String Property", "Initial Value")
|
||||
{
|
||||
Description = "This is a sample string property.",
|
||||
Tooltip = "Enter a string value here.",
|
||||
IsReadOnly = false
|
||||
},
|
||||
new IntProperty("prop2", "Int Property", 42, true)
|
||||
{
|
||||
Description = "This is a sample integer property.",
|
||||
Tooltip = "Enter an integer value here.",
|
||||
IsReadOnly = false
|
||||
},
|
||||
new IntRangeProperty("prop2_range", "Int Range Property", new Tuple<int, int>(0, 100))
|
||||
{
|
||||
Description = "This is a sample integer range property.",
|
||||
Tooltip = "Select an integer value within the range here.",
|
||||
IsReadOnly = false
|
||||
},
|
||||
new FloatRangeProperty("prop3_range", "Float Range Property", new Tuple<float, float>(0.0f, 1.0f))
|
||||
{
|
||||
Description = "This is a sample float range property.",
|
||||
Tooltip = "Select a float value within the range here.",
|
||||
IsReadOnly = false
|
||||
},
|
||||
new FloatProperty("prop3", "Float Property", 0.5f, true)
|
||||
{
|
||||
Description = "This is a sample float property.",
|
||||
Tooltip = "Enter an float value here.",
|
||||
IsReadOnly = false
|
||||
},
|
||||
new BoolProperty("prop4", "Boolean Property", true)
|
||||
{
|
||||
Description = "This is a sample boolean property.",
|
||||
Tooltip = "Toggle the boolean value here.",
|
||||
IsReadOnly = false
|
||||
},
|
||||
new ColorProperty("prop5", "Color Property", Color.red)
|
||||
{
|
||||
Description = "This is a sample color property.",
|
||||
Tooltip = "Select a color here.",
|
||||
IsReadOnly = false
|
||||
},
|
||||
new Vector2Property("prop6_vec2", "Vector2 Property", new Vector2(1, 2))
|
||||
{
|
||||
Description = "This is a sample Vector2 property.",
|
||||
Tooltip = "Enter a Vector2 value here.",
|
||||
IsReadOnly = false
|
||||
},
|
||||
new Vector3Property("prop6_vec3", "Vector3 Property", new Vector3(1, 2, 3))
|
||||
{
|
||||
Description = "This is a sample Vector3 property.",
|
||||
Tooltip = "Enter a Vector3 value here.",
|
||||
IsReadOnly = false
|
||||
},
|
||||
new DateProperty("prop6_date", "Date Property", System.DateTime.Now)
|
||||
{
|
||||
//Description = "This is a sample date property.",
|
||||
Tooltip = "Select a date here.",
|
||||
IsReadOnly = false
|
||||
},
|
||||
new DateTimeProperty("prop6_datetime", "DateTime Property", System.DateTime.Now)
|
||||
{
|
||||
Description = "This is a sample date-time property.",
|
||||
Tooltip = "Select a date and time here.",
|
||||
IsReadOnly = false
|
||||
},
|
||||
new DateRangeProperty("prop6_daterange", "Date Range Property", new Tuple<DateTime, DateTime>(DateTime.Now.AddDays(-7), DateTime.Now))
|
||||
{
|
||||
Description = "This is a sample date range property.",
|
||||
Tooltip = "Select a date range here.",
|
||||
IsReadOnly = false
|
||||
},
|
||||
new DateTimeRangeProperty("prop6_datetimerange", "DateTime Range Property", new Tuple<DateTime, DateTime>(DateTime.Now.AddHours(-1), DateTime.Now))
|
||||
{
|
||||
Description = "This is a sample date-time range property.",
|
||||
Tooltip = "Select a date-time range here.",
|
||||
IsReadOnly = false
|
||||
},
|
||||
new EnumProperty("prop6", "Enum Property", SampleEnum.Option1)
|
||||
{
|
||||
Description = "This is a sample enum property.",
|
||||
Tooltip = "Select an option here.",
|
||||
IsReadOnly = false
|
||||
},
|
||||
new ListProperty("prop7", "List Property", new List<string> { "Item1", "Item2", "Item3" }, "Item1")
|
||||
{
|
||||
Description = "This is a sample list property.",
|
||||
Tooltip = "Manage the list items here.",
|
||||
IsReadOnly = false
|
||||
},
|
||||
new RadioGroupProperty("prop8", "Radio Group Property", new List<string> { "Option1", "Option2", "Option3" }, "Option1")
|
||||
{
|
||||
Description = "This is a sample radio group property.",
|
||||
Tooltip = "Select one option here.",
|
||||
IsReadOnly = false
|
||||
}
|
||||
});
|
||||
|
||||
propertyWindow.PropertyValueChanged += (sender, e) =>
|
||||
{
|
||||
Debug.Log($"Property Id:{e.PropertyId}, type:{e.PropertyType}, newValue: {e.NewValue}");
|
||||
};
|
||||
}
|
||||
|
||||
private void SetupSelectionManager()
|
||||
{
|
||||
if (selectionManager == null)
|
||||
{
|
||||
Debug.LogWarning("SelectionManager is not assigned in SceneMain.");
|
||||
return;
|
||||
}
|
||||
|
||||
selectionManager.OnSelectionChanged += OnSelectionChanged;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SelectionManager에서 선택이 변경되었을 때 호출되는 콜백
|
||||
/// </summary>
|
||||
/// <param name="stageObject">선택/해제된 StageObject</param>
|
||||
/// <param name="isSelected">선택 여부 (true: 선택, false: 해제)</param>
|
||||
private void OnSelectionChanged(StageObjectManager.StageObject stageObject, bool isSelected)
|
||||
{
|
||||
if (isSelected && stageObject?.Equipment != null)
|
||||
{
|
||||
DisplayEquipmentProperties(stageObject);
|
||||
}
|
||||
else if (!isSelected && selectionManager.SelectedCount == 0)
|
||||
{
|
||||
// 모든 선택이 해제되면 PropertyWindow를 비움
|
||||
propertyWindow.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// StageObject의 Equipment PropertiesInfo를 PropertyWindow에 표시합니다.
|
||||
/// </summary>
|
||||
/// <param name="stageObject">표시할 StageObject</param>
|
||||
private void DisplayEquipmentProperties(StageObjectManager.StageObject stageObject)
|
||||
{
|
||||
var equipment = stageObject.Equipment;
|
||||
if (equipment?.propertiesInfo == null || equipment.propertiesInfo.Count == 0)
|
||||
{
|
||||
propertyWindow.Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
var groups = new List<IPropertyGroup>();
|
||||
|
||||
// 기본 정보 그룹 추가 (ID, 이름 등)
|
||||
var basicGroup = new PropertyGroup("basic_info", "Basic Info", order: 0);
|
||||
basicGroup.AddItems(new IPropertyItem[]
|
||||
{
|
||||
new StringProperty("object_id", "ID", stageObject.Id) { IsReadOnly = true },
|
||||
new StringProperty("object_name", "Name", stageObject.GameObject?.name ?? "Unknown") { IsReadOnly = true },
|
||||
new StringProperty("equipment_model", "Model", equipment.model ?? "") { IsReadOnly = true },
|
||||
new StringProperty("equipment_label", "Label", equipment.label ?? "") { IsReadOnly = true }
|
||||
});
|
||||
groups.Add(basicGroup);
|
||||
|
||||
// Equipment의 PropertiesInfo를 PropertyGroup으로 변환
|
||||
int orderIndex = 1;
|
||||
foreach (var propInfo in equipment.propertiesInfo)
|
||||
{
|
||||
var group = new PropertyGroup(
|
||||
$"section_{propInfo.section}",
|
||||
propInfo.section ?? "Properties",
|
||||
order: orderIndex++
|
||||
);
|
||||
|
||||
foreach (var prop in propInfo.properties)
|
||||
{
|
||||
var propertyItem = CreatePropertyItem(prop);
|
||||
if (propertyItem != null)
|
||||
{
|
||||
group.AddItem(propertyItem);
|
||||
}
|
||||
}
|
||||
|
||||
if (group.Count > 0)
|
||||
{
|
||||
groups.Add(group);
|
||||
}
|
||||
}
|
||||
|
||||
propertyWindow.LoadGroupedProperties(groups);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Library.PropertyItem을 PropertyWindow.IPropertyItem으로 변환합니다.
|
||||
/// </summary>
|
||||
/// <param name="prop">변환할 PropertyItem</param>
|
||||
/// <returns>변환된 IPropertyItem</returns>
|
||||
private IPropertyItem CreatePropertyItem(Config.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})";
|
||||
}
|
||||
|
||||
return prop.type?.ToLower() switch
|
||||
{
|
||||
"float" => new FloatProperty(id, label, prop.GetFloatValue()),
|
||||
"int" => new IntProperty(id, label, prop.GetIntValue()),
|
||||
_ => new StringProperty(id, label, prop.GetStringValue())
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
enum SampleEnum
|
||||
{
|
||||
Option1,
|
||||
Option2,
|
||||
Option3
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ using UVC.Studio.Manager;
|
||||
using UVC.UI.List.Accordion;
|
||||
using UVC.UI.Tab;
|
||||
using UVC.UI.Window;
|
||||
using UnityEngine.EventSystems;
|
||||
|
||||
namespace UVC.Studio.Tab
|
||||
{
|
||||
@@ -137,6 +138,13 @@ namespace UVC.Studio.Tab
|
||||
/// </summary>
|
||||
private void OnGridItemEndDragHandler(AccordionGridItemData itemData, Vector2 screenPosition)
|
||||
{
|
||||
// UI 위에서 드래그가 끝났으면 프리뷰 제거
|
||||
if (EventSystem.current != null && EventSystem.current.IsPointerOverGameObject())
|
||||
{
|
||||
CancelDragPreview();
|
||||
return;
|
||||
}
|
||||
|
||||
if (dragPreview != null && draggingEquipment != null && stageObjectManager != null)
|
||||
{
|
||||
// 최종 위치 계산
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
using UVC.Core;
|
||||
@@ -54,6 +56,7 @@ namespace UVC.Studio.Tab
|
||||
hierarchyWindow.OnItemDeselected += OnItemDeselectedHandler;
|
||||
hierarchyWindow.OnItemVisibilityChanged += OnItemVisibilityChangedHandler;
|
||||
|
||||
//다른 클래스에서 이 컴포넌트를 주입 받을 수 있도록 등록
|
||||
InjectorAppContext.Instance.Injector.RegisterInstance<StudioSideTabBarHierarchy>(this, ServiceLifetime.Scene);
|
||||
}
|
||||
|
||||
@@ -142,7 +145,7 @@ namespace UVC.Studio.Tab
|
||||
}
|
||||
|
||||
// SelectionManager 가져오기
|
||||
_selectionManager = InjectorAppContext.Instance.Get<SelectionManager>();
|
||||
if (_selectionManager == null) _selectionManager = InjectorAppContext.Instance.Get<SelectionManager>();
|
||||
if (_selectionManager != null)
|
||||
{
|
||||
// 선택 변경 이벤트 구독 (화면 클릭으로 선택 시 HierarchyWindow 동기화)
|
||||
@@ -152,6 +155,7 @@ namespace UVC.Studio.Tab
|
||||
{
|
||||
Debug.LogWarning("[StudioSideTabBarHierarchy] SelectionManager not found.");
|
||||
}
|
||||
isInitialized = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -218,11 +222,12 @@ namespace UVC.Studio.Tab
|
||||
return;
|
||||
}
|
||||
|
||||
// TreeListItemData 생성
|
||||
var treeItem = new TreeListItemData(stageObject.GameObject != null ? stageObject.GameObject.name : stageObject.Equipment.model)
|
||||
{
|
||||
ExternalKey = stageObject.Id
|
||||
};
|
||||
// TreeListItemData 생성 (하위 자식 포함)
|
||||
var treeItem = CreateTreeItemRecursive(
|
||||
stageObject.GameObject != null ? stageObject.GameObject.transform : null,
|
||||
stageObject.GameObject != null ? stageObject.GameObject.name : stageObject.Equipment.id,
|
||||
stageObject.Id
|
||||
);
|
||||
|
||||
// 매핑 저장
|
||||
_stageObjectToTreeItem[stageObject.Id] = treeItem;
|
||||
@@ -230,7 +235,36 @@ namespace UVC.Studio.Tab
|
||||
// HierarchyWindow에 추가
|
||||
hierarchyWindow.AddItem(treeItem);
|
||||
|
||||
Debug.Log($"[StudioSideTabBarHierarchy] Added TreeItem: {treeItem.Name}");
|
||||
Debug.Log($"[StudioSideTabBarHierarchy] Added TreeItem: {treeItem.Name} (including all children)");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Transform의 자식들을 재귀적으로 탐색하여 TreeListItemData를 생성
|
||||
/// </summary>
|
||||
/// <param name="transform">탐색할 Transform (null이면 이름만 사용)</param>
|
||||
/// <param name="name">표시 이름</param>
|
||||
/// <param name="externalKey">외부 키 (루트 노드에만 설정)</param>
|
||||
/// <returns>생성된 TreeListItemData</returns>
|
||||
private TreeListItemData CreateTreeItemRecursive(Transform? transform, string name, string? externalKey = null)
|
||||
{
|
||||
var treeItem = new TreeListItemData(name);
|
||||
|
||||
if (!string.IsNullOrEmpty(externalKey))
|
||||
{
|
||||
treeItem.ExternalKey = externalKey;
|
||||
}
|
||||
|
||||
// Transform이 있으면 자식들을 재귀적으로 추가
|
||||
if (transform != null)
|
||||
{
|
||||
foreach (Transform child in transform)
|
||||
{
|
||||
var childItem = CreateTreeItemRecursive(child, child.name);
|
||||
treeItem.AddChild(childItem);
|
||||
}
|
||||
}
|
||||
|
||||
return treeItem;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -269,13 +303,70 @@ namespace UVC.Studio.Tab
|
||||
return _stageObjectToTreeItem.TryGetValue(stageObjectId, out var treeItem) ? treeItem : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// StageObject의 표시 이름을 변경합니다.
|
||||
/// HierarchyWindow의 TreeList에도 반영됩니다.
|
||||
/// </summary>
|
||||
/// <param name="stageObjectId">변경할 StageObject의 ID</param>
|
||||
/// <param name="newName">새 이름</param>
|
||||
public void UpdateItemName(string stageObjectId, string newName)
|
||||
{
|
||||
if (hierarchyWindow == null) return;
|
||||
if (!_stageObjectToTreeItem.TryGetValue(stageObjectId, out var treeItem)) return;
|
||||
|
||||
// HierarchyWindow.SetItemName을 통해 TreeList UI도 함께 갱신
|
||||
hierarchyWindow.SetItemName(treeItem, newName);
|
||||
Debug.Log($"[StudioSideTabBarHierarchy] Updated item name: {stageObjectId} -> {newName}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 탭 콘텐츠에 데이터를 전달합니다.
|
||||
/// 탭이 활성화될 때 호출되며, SelectionManager의 선택 상태를 HierarchyWindow에 동기화합니다.
|
||||
/// </summary>
|
||||
/// <param name="data">전달할 데이터 객체</param>
|
||||
public void SetContentData(object? data)
|
||||
{
|
||||
Debug.Log("StudioSideTabBarHierarchy: SetContentData called");
|
||||
|
||||
// SelectionManager에 선택된 항목이 있으면 HierarchyWindow에 반영
|
||||
SyncSelectionFromSelectionManager();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SelectionManager의 현재 선택 상태를 HierarchyWindow에 동기화합니다.
|
||||
/// </summary>
|
||||
private async void SyncSelectionFromSelectionManager()
|
||||
{
|
||||
if (!isInitialized)
|
||||
{
|
||||
await UniTask.WaitUntil(() => isInitialized).TimeoutWithoutException(new TimeSpan(0, 0, 1));
|
||||
}
|
||||
Debug.Log($"StudioSideTabBarHierarchy: SyncSelectionFromSelectionManager called. _selectionManager == null:{_selectionManager == null}, hierarchyWindow == null:{hierarchyWindow == null}");
|
||||
|
||||
if (_selectionManager == null || hierarchyWindow == null) return;
|
||||
|
||||
var selectedObjects = _selectionManager.SelectedObjects;
|
||||
if (selectedObjects.Count == 0) return;
|
||||
|
||||
_isProcessingSelection = true;
|
||||
try
|
||||
{
|
||||
foreach (var stageObject in selectedObjects)
|
||||
{
|
||||
if (string.IsNullOrEmpty(stageObject.Id)) continue;
|
||||
|
||||
// 매핑에서 TreeListItemData 찾기
|
||||
if (_stageObjectToTreeItem.TryGetValue(stageObject.Id, out var treeItem))
|
||||
{
|
||||
hierarchyWindow.SelectItem(treeItem.Name);
|
||||
Debug.Log($"[StudioSideTabBarHierarchy] Synced existing selection: {treeItem.Name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isProcessingSelection = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -59,6 +59,12 @@ namespace UVC.GLTF
|
||||
return instantiator.SceneTransform != null ? instantiator.SceneTransform.gameObject : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 여러 LOD 레벨의 glTF/glb 파일을 로드하고 LODGroup으로 설정합니다. 사용않함
|
||||
/// </summary>
|
||||
/// <param name="paths"></param>
|
||||
/// <param name="parentTransform"></param>
|
||||
/// <returns></returns>
|
||||
public static async UniTask<GameObject?> ImportWithLOD(List<string> paths, Transform parentTransform)
|
||||
{
|
||||
if (paths == null || paths.Count == 0) return null;
|
||||
|
||||
@@ -189,6 +189,16 @@ namespace UVC.UI.Window
|
||||
treeList.DeleteItem(data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 아이템의 이름을 변경합니다.
|
||||
/// </summary>
|
||||
/// <param name="data">변경할 아이템 데이터</param>
|
||||
/// <param name="newName">새 이름</param>
|
||||
public void SetItemName(TreeListItemData data, string newName)
|
||||
{
|
||||
treeList.SetItemName(data, newName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 이름으로 아이템 선택
|
||||
/// </summary>
|
||||
|
||||
@@ -27,6 +27,7 @@ namespace UVC.UI.Window.PropertyWindow
|
||||
FloatRange,
|
||||
DateRange,
|
||||
DateTimeRange,
|
||||
ColorState,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -331,5 +332,13 @@ namespace UVC.UI.Window.PropertyWindow
|
||||
}
|
||||
}
|
||||
|
||||
// --- 복합 타입 속성 ---
|
||||
public class ColorStateProperty : PropertyItem<Tuple<string, Color?>>
|
||||
{
|
||||
public override PropertyType PropertyType => PropertyType.ColorState;
|
||||
public ColorStateProperty(string id, string name, Tuple<string, Color?> initialValue) : base(id, name, initialValue) { }
|
||||
}
|
||||
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ namespace UVC.UI.Window.PropertyWindow
|
||||
[SerializeField] private GameObject _numberRangePropertyPrefab;
|
||||
[SerializeField] private GameObject _dateRangePropertyPrefab;
|
||||
[SerializeField] private GameObject _dateTimeRangePropertyPrefab;
|
||||
[SerializeField] private GameObject _colorStatePropertyPrefab;
|
||||
|
||||
/// <summary>
|
||||
/// View가 상호작용할 Controller 인스턴스입니다.
|
||||
@@ -291,6 +292,8 @@ namespace UVC.UI.Window.PropertyWindow
|
||||
return _dateRangePropertyPrefab;
|
||||
case PropertyType.DateTimeRange:
|
||||
return _dateTimeRangePropertyPrefab;
|
||||
case PropertyType.ColorState:
|
||||
return _colorStatePropertyPrefab;
|
||||
default:
|
||||
Debug.LogWarning($"'{type}' 타입에 대한 프리팹이 정의되지 않았습니다.");
|
||||
return null;
|
||||
|
||||
@@ -402,6 +402,9 @@ namespace UVC.UI.Window.PropertyWindow
|
||||
_groupIndex.Clear();
|
||||
_itemIndex.Clear();
|
||||
EntriesCleared?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
// View 갱신하여 UI에서도 항목 제거
|
||||
Refresh();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
using System;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.UI;
|
||||
using UVC.Core;
|
||||
using UVC.Extention;
|
||||
using UVC.Studio.Manager;
|
||||
using UVC.UI.Modal.ColorPicker;
|
||||
using UVC.UI.Tooltip;
|
||||
using UVC.Util;
|
||||
|
||||
namespace UVC.UI.Window.PropertyWindow.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// ColorProperty를 위한 UI를 제어하는 스크립트입니다.
|
||||
/// Image 컴포넌트로 색상을 표시하고, Button으로 색상 선택기를 엽니다.
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(LayoutElement))]
|
||||
public class ColorStatePropertyUI : MonoBehaviour, IPropertyUI
|
||||
{
|
||||
[Header("UI Components")]
|
||||
[SerializeField]
|
||||
private TextMeshProUGUI _nameLabel; // 속성 이름을 표시할 Text
|
||||
|
||||
[SerializeField]
|
||||
private TextMeshProUGUI _descriptionLabel;
|
||||
|
||||
[SerializeField]
|
||||
private TMP_InputField _stateLabel;
|
||||
|
||||
[SerializeField]
|
||||
private LayoutGroup _colorLayoutGroup;
|
||||
|
||||
[SerializeField]
|
||||
private Image _colorPreviewImage; // 현재 색상을 표시할 Image
|
||||
|
||||
[SerializeField]
|
||||
private TMP_InputField _colorLabel;
|
||||
|
||||
[SerializeField]
|
||||
private Button _colorPickerButton; // 색상 선택기를 열기 위한 Button
|
||||
|
||||
[SerializeField]
|
||||
private Button _previewButton; // 컬러를 미리보기 위한 Button
|
||||
|
||||
private ColorStateProperty _propertyItem;
|
||||
private PropertyWindow _controller;
|
||||
|
||||
private bool openningColorPickered = false;
|
||||
|
||||
/// <summary>
|
||||
/// PropertyView에 의해 호출되어 UI를 초기화하고 데이터를 설정합니다.
|
||||
/// </summary>
|
||||
/// <param name="item">UI에 표시할 속성 데이터(IPropertyItem)</param>
|
||||
/// <param name="controller">상호작용할 PropertyWindow</param>
|
||||
public void Setup(IPropertyItem item, PropertyWindow controller)
|
||||
{
|
||||
|
||||
if (!(item is ColorStateProperty typedItem))
|
||||
{
|
||||
Debug.LogError($"ColorStatePropertyUI에 잘못된 타입의 PropertyItem이 전달되었습니다. {item.GetType()}");
|
||||
return;
|
||||
}
|
||||
|
||||
_propertyItem = typedItem;
|
||||
_controller = controller;
|
||||
|
||||
// --- 데이터 바인딩 ---
|
||||
// 1. 속성 이름 설정
|
||||
_nameLabel.text = _propertyItem.Name;
|
||||
|
||||
// 툴팁 설정 (TooltipHandler 컴포넌트가 있다면 연동)
|
||||
TooltipHandler tooltipHandler = _nameLabel.GetComponent<TooltipHandler>();
|
||||
if (tooltipHandler != null && !_propertyItem.Tooltip.IsNullOrEmpty())
|
||||
{
|
||||
tooltipHandler.Tooltip = _propertyItem.Tooltip;
|
||||
}
|
||||
|
||||
if (_propertyItem.Description.IsNullOrEmpty())
|
||||
{
|
||||
_descriptionLabel.gameObject.SetActive(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
_descriptionLabel.gameObject.SetActive(true);
|
||||
_descriptionLabel.text = _propertyItem.Description;
|
||||
}
|
||||
|
||||
_stateLabel.text = _propertyItem.Value.Item1;
|
||||
|
||||
// 2. 색상 미리보기 Image의 색상 설정
|
||||
if (_propertyItem.Value.Item2 == null)
|
||||
{
|
||||
_colorLayoutGroup.gameObject.SetActive(false);
|
||||
_previewButton.gameObject.SetActive(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
_colorLayoutGroup.gameObject.SetActive(true);
|
||||
_colorPreviewImage.color = _propertyItem.Value.Item2.Value;
|
||||
|
||||
_colorLabel.text = ColorUtil.ToHex(_colorPreviewImage.color, true, false);
|
||||
_colorLabel.interactable = !_propertyItem.IsReadOnly;
|
||||
// 3. 읽기 전용 상태에 따라 버튼 상호작용 여부 결정
|
||||
_colorPickerButton.gameObject.SetActive(!_propertyItem.IsReadOnly);
|
||||
_colorPickerButton.onClick.RemoveAllListeners();
|
||||
if (!_propertyItem.IsReadOnly)
|
||||
{
|
||||
// --- 이벤트 리스너 등록 ---
|
||||
_colorPickerButton.onClick.AddListener(OpenColorPicker);
|
||||
}
|
||||
|
||||
_previewButton.gameObject.SetActive(true);
|
||||
|
||||
// PointerDown/PointerUp 핸들러 설정
|
||||
SetupPreviewButtonEvents();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PreviewButton에 PointerDown/PointerUp 이벤트를 설정합니다.
|
||||
/// </summary>
|
||||
private void SetupPreviewButtonEvents()
|
||||
{
|
||||
// 기존 PreviewButtonHandler가 있으면 제거
|
||||
var existingHandler = _previewButton.GetComponent<PreviewButtonHandler>();
|
||||
if (existingHandler != null)
|
||||
{
|
||||
Destroy(existingHandler);
|
||||
}
|
||||
|
||||
// 새 핸들러 추가
|
||||
var handler = _previewButton.gameObject.AddComponent<PreviewButtonHandler>();
|
||||
handler.Initialize(OnPreviewMouseDown, OnPreViewMouseUp);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 색상 선택기 버튼을 클릭했을 때 호출됩니다.
|
||||
/// </summary>
|
||||
private async void OpenColorPicker()
|
||||
{
|
||||
if (openningColorPickered == true) return;
|
||||
openningColorPickered = true;
|
||||
CursorManager.Instance.SetCursor(CursorType.Wait);
|
||||
await ColorPicker.Create(_colorPreviewImage.color, "Color Picker", null, OnColorSelected, OnCloseColorPicker, true);
|
||||
CursorManager.Instance.SetDefaultCursor();
|
||||
Debug.LogWarning($"'{_propertyItem.Name}'의 색상 선택기 로직이 구현되지 않았습니다. 클릭 이벤트만 발생합니다.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 색상 선택기에서 새로운 색상이 선택되었을 때 호출되는 메서드입니다.
|
||||
/// </summary>
|
||||
/// <param name="newColor">선택된 새로운 색상</param>
|
||||
public void OnColorSelected(Color newColor)
|
||||
{
|
||||
|
||||
if (newColor == _propertyItem.Value.Item2.Value)
|
||||
{
|
||||
return; // 변경 사항이 없으므로 아무 작업도 하지 않음
|
||||
}
|
||||
|
||||
// 1. UI의 색상 미리보기를 업데이트합니다.
|
||||
_colorPreviewImage.color = newColor;
|
||||
|
||||
_colorLabel.text = ColorUtil.ToHex(_colorPreviewImage.color, true, false);
|
||||
|
||||
// 2. PropertyController를 통해 데이터 모델의 값을 업데이트합니다.
|
||||
_controller.UpdatePropertyValue(_propertyItem.Id, _propertyItem.PropertyType, new Tuple<string, string, Color?>(_propertyItem.Name, _stateLabel.text, newColor));
|
||||
|
||||
}
|
||||
|
||||
private void OnPreviewMouseDown()
|
||||
{
|
||||
// 미리보기 버튼 클릭 시 동작 구현 (필요시)
|
||||
SelectionManager selectionManager = InjectorAppContext.Instance.Get<SelectionManager>();
|
||||
if (selectionManager != null) selectionManager.PreviewColor(_propertyItem.Value.Item2!.Value);
|
||||
}
|
||||
|
||||
private void OnPreViewMouseUp()
|
||||
{
|
||||
// 미리보기 버튼 클릭 해제 시 동작 구현 (필요시)
|
||||
SelectionManager selectionManager = InjectorAppContext.Instance.Get<SelectionManager>();
|
||||
if (selectionManager != null) selectionManager.ClearPreviewColor();
|
||||
}
|
||||
|
||||
private void OnCloseColorPicker()
|
||||
{
|
||||
openningColorPickered = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// UI의 읽기 전용 상태를 설정합니다.
|
||||
/// </summary>
|
||||
/// <param name="isReadOnly">읽기 전용 여부 (true: 비활성화, false: 활성화)</param>
|
||||
public void SetReadOnly(bool isReadOnly)
|
||||
{
|
||||
if (_propertyItem != null)
|
||||
{
|
||||
_propertyItem.IsReadOnly = isReadOnly;
|
||||
}
|
||||
if (_colorLabel != null) _colorLabel.interactable = !isReadOnly;
|
||||
if (_colorPickerButton != null) _colorPickerButton.gameObject.SetActive(!isReadOnly);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 이 UI 오브젝트가 파괴될 때 Unity에 의해 호출됩니다.
|
||||
/// </summary>
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (_colorPickerButton != null && _colorPickerButton.onClick != null)
|
||||
{
|
||||
_colorPickerButton.onClick.RemoveAllListeners();
|
||||
}
|
||||
if (_previewButton != null)
|
||||
{
|
||||
var existingHandler = _previewButton.GetComponent<PreviewButtonHandler>();
|
||||
if (existingHandler != null)
|
||||
{
|
||||
Destroy(existingHandler);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PreviewButton의 PointerDown/PointerUp 이벤트를 처리하는 핸들러
|
||||
/// </summary>
|
||||
public class PreviewButtonHandler : MonoBehaviour, IPointerDownHandler, IPointerUpHandler
|
||||
{
|
||||
private Action _onPointerDown;
|
||||
private Action _onPointerUp;
|
||||
|
||||
public void Initialize(Action onPointerDown, Action onPointerUp)
|
||||
{
|
||||
_onPointerDown = onPointerDown;
|
||||
_onPointerUp = onPointerUp;
|
||||
}
|
||||
|
||||
public void OnPointerDown(PointerEventData eventData)
|
||||
{
|
||||
_onPointerDown?.Invoke();
|
||||
}
|
||||
|
||||
public void OnPointerUp(PointerEventData eventData)
|
||||
{
|
||||
_onPointerUp?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 508447e589a88f149934d68ac57195ff
|
||||
Reference in New Issue
Block a user