선택한 모델 PropertyWindow 연결 완료

This commit is contained in:
logonkhi
2025-12-19 15:27:35 +09:00
parent deeaa9a7ad
commit 158a42ab9b
24 changed files with 2278 additions and 1185 deletions

View File

@@ -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

View File

@@ -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
}
}

View File

@@ -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);

View File

@@ -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);
}
}

View File

@@ -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
}
}
}

View File

@@ -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)
{
// 최종 위치 계산

View File

@@ -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>