Files
EnglewoodLAB/Assets/Sample/UIToolkit/UTKImageListWindowSample.cs
2026-03-10 11:35:30 +09:00

503 lines
17 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#nullable enable
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;
using UVC.UIToolkit;
/// <summary>
/// UTKImageListWindow의 기능을 테스트하기 위한 샘플 MonoBehaviour입니다.
/// 이미지 리스트 데이터를 생성하고 다양한 이벤트 핸들러를 등록하여 동작을 확인합니다.
///
/// <para><b>테스트 기능:</b></para>
/// <list type="bullet">
/// <item>이미지+텍스트 아이템 표시</item>
/// <item>검색 필터링 (3글자 이상)</item>
/// <item>아이템 클릭/선택 이벤트</item>
/// <item>드래그 앤 드롭 이벤트</item>
/// </list>
/// </summary>
public class UTKImageListWindowSample : MonoBehaviour
{
#region (Fields)
[SerializeField]
[Tooltip("UI를 표시할 UIDocument 컴포넌트")]
public UIDocument? uiDocument;
[SerializeField]
[Tooltip("드래그 시 이미지가 커서를 따라다니도록 설정")]
private bool dragImageFollowCursor = true;
[SerializeField]
[Tooltip("시작 시 적용할 테마")]
private UTKTheme initialTheme = UTKTheme.Dark;
private UTKToggle _themeToggle;
/// <summary>UTKImageListWindow 인스턴스</summary>
private UTKImageListWindow? _imageListWindow;
/// <summary>드롭 위치 (스크린 좌표)</summary>
private Vector2 _lastDropScreenPosition;
/// <summary>메인 카메라 참조</summary>
private Camera? _mainCamera;
/// <summary>드래그 중인 3D 프리뷰 오브젝트</summary>
private GameObject? _dragPreview3D;
/// <summary>드래그가 리스트 영역 밖에 있는지 여부</summary>
private bool _isOutsideListArea;
#endregion
#region Unity
private void Start()
{
// UIDocument 참조 확인
var doc = GetComponent<UIDocument>();
if (doc == null)
{
Debug.LogError("UIDocument가 할당되지 않았습니다.");
return;
}
uiDocument = doc;
var toggle = uiDocument.rootVisualElement.Q<UTKToggle>("toggle");
if (toggle == null)
{
Debug.LogError("UXML에서 UTKToggle을 찾을 수 없습니다.");
return;
}
_themeToggle = toggle;
var window = uiDocument.rootVisualElement.Q<UTKImageListWindow>("window");
if (window == null)
{
Debug.LogError("UXML에서 UTKImageListWindow를 찾을 수 없습니다.");
return;
}
_imageListWindow = window;
// UTKImageListWindow 인스턴스 생성 및 추가
_imageListWindow.DragImageFollowCursor = dragImageFollowCursor;
_imageListWindow.Title = "LIBRARY";
_imageListWindow.ShowCloseButton = true;
UTKThemeManager.Instance.RegisterRoot(uiDocument.rootVisualElement);
UTKThemeManager.Instance.SetTheme(initialTheme);
_themeToggle.OnValueChanged += (isOn) =>
{
UTKThemeManager.Instance.SetTheme(!isOn ? UTKTheme.Dark : UTKTheme.Light);
};
// 테스트 데이터 생성
CreateTestData();
// 이벤트 핸들러 등록
RegisterEventHandlers();
// 윈도우 표시
_imageListWindow.Show();
Debug.Log("[UTKImageListWindowSample] 초기화 완료");
}
private void OnDestroy()
{
// 리소스 정리
_imageListWindow?.Dispose();
_imageListWindow = null;
}
#endregion
#region
/// <summary>
/// 테스트용 이미지 리스트 데이터를 생성합니다.
/// imagePath와 prefabPath 리스트를 사용하여 데이터를 생성합니다.
/// </summary>
private void CreateTestData()
{
if (_imageListWindow == null) return;
// 이미지 경로 리스트
List<string> imagePaths = new()
{
"Simulator/Images/lib_forklift_400x300",
"Simulator/Images/lib_pallet_400x300",
"Simulator/Images/lib_worker_400x300",
};
// 프리팹 경로 리스트
List<string> prefabPaths = new()
{
"Simulator/FreeForkLift/Prefabs/Forklift",
"Simulator/FreeForkLift/Prefabs/PalletEmpty",
"Simulator/CharCrafter Free Preset Characters Pack (Vol. 1)/Prefabs/Male Young Guy",
};
// 아이템 이름 리스트
string[] itemNames = { "지게차", "팔레트", "작업자" };
var data = new List<UTKImageListItemData>();
// imagePath, prefabPath 리스트를 사용하여 데이터 생성
for (int i = 0; i < 19; i++)
{
var itemData = new UTKImageListItemData
{
externalId = $"item-{i:D4}",
itemName = itemNames[i % itemNames.Length],
imagePath = imagePaths[i % imagePaths.Count],
objectPrefabPath = prefabPaths[i % prefabPaths.Count],
tag = "시뮬레이터"
};
data.Add(itemData);
}
// 데이터 설정
_imageListWindow.SetData(data);
Debug.Log($"[UTKImageListWindowSample] 테스트 데이터 생성 완료: {data.Count}개 아이템");
}
#endregion
#region
/// <summary>
/// UTKImageListWindow의 이벤트 핸들러들을 등록합니다.
/// 클릭, 드래그 앤 드롭, 윈도우 닫기 이벤트를 처리합니다.
/// </summary>
private void RegisterEventHandlers()
{
if (_imageListWindow == null) return;
// 아이템 클릭 이벤트
// 사용자가 아이템을 클릭할 때 발생
_imageListWindow.OnItemClick += (UTKImageListItemData item) =>
{
Debug.Log($"[클릭] {item.itemName} (ID: {item.externalId}, Tag: {item.tag})");
};
// 드래그 시작 이벤트
// 사용자가 아이템을 드래그하기 시작할 때 발생
_imageListWindow.OnItemBeginDrag += (UTKImageListItemData item, Vector2 position) =>
{
Debug.Log($"[드래그 시작] {item.itemName} at {position}");
};
// 드래그 중 이벤트
// 아이템을 드래그하는 동안 지속적으로 발생
_imageListWindow.OnItemDrag += (UTKImageListItemData item, Vector2 position) =>
{
// 3D 프리뷰가 있으면 위치 업데이트
if (_dragPreview3D != null)
{
Update3DPreviewPosition(position);
}
};
// 드래그 종료 이벤트
// 드래그가 끝났을 때 발생 (드롭 직전)
_imageListWindow.OnItemEndDrag += (UTKImageListItemData item, Vector2 position) =>
{
Debug.Log($"[드래그 종료] {item.itemName} at {position}");
// 드롭 위치 저장 (스크린 좌표)
_lastDropScreenPosition = position;
// 3D 프리뷰는 OnItemDrop에서 처리
};
// 리스트 영역 이탈 이벤트
// 드래그 중 리스트 영역을 벗어났을 때 3D 프리팹 표시
_imageListWindow.OnDragExitList += (UTKImageListItemData item, Vector2 position) =>
{
Debug.Log($"[리스트 영역 이탈] {item.itemName} - 3D 프리뷰 생성");
_isOutsideListArea = true;
Create3DPreview(item, position);
};
// 리스트 영역 진입 이벤트
// 드래그 중 리스트 영역에 다시 들어왔을 때 3D 프리팹 숨김
_imageListWindow.OnDragEnterList += (UTKImageListItemData item, Vector2 position) =>
{
Debug.Log($"[리스트 영역 진입] {item.itemName} - 3D 프리뷰 제거");
_isOutsideListArea = false;
Destroy3DPreview();
};
// 아이템 드롭 이벤트
// 드래그가 완료되어 아이템이 드롭되었을 때 발생
_imageListWindow.OnItemDrop += (UTKImageListItemData item) =>
{
Debug.Log($"[드롭] {item.itemName} - ObjectPrefabPath: {item.objectPrefabPath}");
// 리스트 영역 내부에서 드롭한 경우 프리팹 생성 안함
if (!_isOutsideListArea)
{
Debug.Log("리스트 영역 내부에서 드롭 - 프리팹 생성 안함");
Destroy3DPreview();
return;
}
// 프리팹 로드
var prefab = Resources.Load<GameObject>(item.objectPrefabPath);
if (prefab == null)
{
Debug.LogWarning($"[UTKImageListWindowSample] 프리팹을 찾을 수 없습니다: {item.objectPrefabPath}");
Destroy3DPreview();
_isOutsideListArea = false;
return;
}
// 월드 좌표 계산
Vector3 worldPosition = ScreenToWorldPosition(_lastDropScreenPosition);
// 프리팹 인스턴스화
var instance = Instantiate(prefab, worldPosition, Quaternion.identity);
Debug.Log($"[UTKImageListWindowSample] 프리팹 생성됨: {instance.name} at {worldPosition}");
// 3D 프리뷰 제거 및 플래그 리셋
Destroy3DPreview();
_isOutsideListArea = false;
};
// 윈도우 닫기 이벤트
// 사용자가 닫기 버튼을 클릭하거나 Close() 호출 시 발생
_imageListWindow.OnClosed += () =>
{
Debug.Log("[윈도우 닫힘]");
};
Debug.Log("[UTKImageListWindowSample] 이벤트 핸들러 등록 완료");
}
#endregion
#region (Coordinate Conversion)
/// <summary>
/// UI Toolkit 좌표를 Unity Screen 좌표로 변환합니다.
/// UI Toolkit: 좌상단 원점, Y축 아래로 증가
/// Unity Screen: 좌하단 원점, Y축 위로 증가
/// </summary>
/// <param name="uiToolkitPosition">UI Toolkit 좌표</param>
/// <returns>Unity Screen 좌표</returns>
private Vector2 UIToolkitToScreenPosition(Vector2 uiToolkitPosition)
{
return new Vector2(uiToolkitPosition.x, Screen.height - uiToolkitPosition.y);
}
/// <summary>
/// 스크린 좌표를 월드 좌표로 변환합니다.
/// 바닥면(Y=0)에 레이캐스트하여 위치를 계산합니다.
/// </summary>
/// <param name="uiToolkitPosition">UI Toolkit 좌표 (좌상단 원점)</param>
/// <returns>월드 좌표 (레이캐스트 실패 시 카메라 전방 10m 위치)</returns>
private Vector3 ScreenToWorldPosition(Vector2 uiToolkitPosition)
{
// 메인 카메라 캐시
if (_mainCamera == null)
{
_mainCamera = Camera.main;
}
if (_mainCamera == null)
{
Debug.LogWarning("[UTKImageListWindowSample] 메인 카메라를 찾을 수 없습니다.");
return Vector3.zero;
}
// UI Toolkit 좌표를 Screen 좌표로 변환
Vector2 screenPosition = UIToolkitToScreenPosition(uiToolkitPosition);
// 스크린 좌표에서 레이 생성
Ray ray = _mainCamera.ScreenPointToRay(screenPosition);
// 바닥면(Y=0 평면)과의 교차점 계산
Plane groundPlane = new Plane(Vector3.up, Vector3.zero);
if (groundPlane.Raycast(ray, out float distance))
{
return ray.GetPoint(distance);
}
// 물리 레이캐스트로 콜라이더와 충돌 확인
if (Physics.Raycast(ray, out RaycastHit hit, 100f))
{
return hit.point;
}
// 실패 시 카메라 전방 10m 위치 반환
return ray.GetPoint(10f);
}
#endregion
#region 3D (3D Preview)
/// <summary>
/// 드래그 중 3D 프리뷰 오브젝트를 생성합니다.
/// </summary>
/// <param name="item">아이템 데이터</param>
/// <param name="screenPosition">화면 좌표</param>
private void Create3DPreview(UTKImageListItemData item, Vector2 screenPosition)
{
// 기존 프리뷰가 있으면 제거
Destroy3DPreview();
// 프리팹 로드
var prefab = Resources.Load<GameObject>(item.objectPrefabPath);
if (prefab == null)
{
Debug.LogWarning($"[UTKImageListWindowSample] 프리팹을 찾을 수 없습니다: {item.objectPrefabPath}");
return;
}
// 월드 좌표 계산
Vector3 worldPosition = ScreenToWorldPosition(screenPosition);
// 프리뷰 인스턴스 생성
_dragPreview3D = Instantiate(prefab, worldPosition, Quaternion.identity);
_dragPreview3D.name = $"DragPreview_{item.itemName}";
// 프리뷰임을 나타내기 위해 반투명 처리 (선택적)
SetPreviewTransparency(_dragPreview3D, 0.6f);
// 콜라이더 비활성화 (드래그 중 충돌 방지)
DisableColliders(_dragPreview3D);
Debug.Log($"[UTKImageListWindowSample] 3D 프리뷰 생성됨: {_dragPreview3D.name} at {worldPosition}");
}
/// <summary>
/// 3D 프리뷰 오브젝트를 제거합니다.
/// </summary>
private void Destroy3DPreview()
{
if (_dragPreview3D != null)
{
Destroy(_dragPreview3D);
_dragPreview3D = null;
}
}
/// <summary>
/// 3D 프리뷰 오브젝트의 위치를 업데이트합니다.
/// </summary>
/// <param name="screenPosition">화면 좌표</param>
private void Update3DPreviewPosition(Vector2 screenPosition)
{
if (_dragPreview3D == null) return;
Vector3 worldPosition = ScreenToWorldPosition(screenPosition);
_dragPreview3D.transform.position = worldPosition;
}
/// <summary>
/// 오브젝트의 모든 렌더러를 반투명하게 설정합니다.
/// </summary>
/// <param name="obj">대상 게임오브젝트</param>
/// <param name="alpha">투명도 (0-1)</param>
private void SetPreviewTransparency(GameObject obj, float alpha)
{
var renderers = obj.GetComponentsInChildren<Renderer>();
foreach (var renderer in renderers)
{
foreach (var material in renderer.materials)
{
// 투명 렌더링 모드로 전환
if (material.HasProperty("_Color"))
{
var color = material.color;
color.a = alpha;
material.color = color;
// Standard Shader 투명 모드 설정
material.SetFloat("_Mode", 3); // Transparent
material.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha);
material.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha);
material.SetInt("_ZWrite", 0);
material.DisableKeyword("_ALPHATEST_ON");
material.EnableKeyword("_ALPHABLEND_ON");
material.DisableKeyword("_ALPHAPREMULTIPLY_ON");
material.renderQueue = 3000;
}
}
}
}
/// <summary>
/// 오브젝트의 모든 콜라이더를 비활성화합니다.
/// </summary>
/// <param name="obj">대상 게임오브젝트</param>
private void DisableColliders(GameObject obj)
{
var colliders = obj.GetComponentsInChildren<Collider>();
foreach (var collider in colliders)
{
collider.enabled = false;
}
}
#endregion
#region
/// <summary>
/// 런타임에 아이템을 추가하는 테스트 메서드입니다.
/// Inspector에서 컨텍스트 메뉴로 호출할 수 있습니다.
/// </summary>
[ContextMenu("Add Test Item")]
public void AddTestItem()
{
if (_imageListWindow == null) return;
var newItem = new UTKImageListItemData
{
externalId = $"runtime-{System.Guid.NewGuid():N}",
itemName = $"런타임 아이템 {_imageListWindow.ItemCount + 1}",
imagePath = "Prefabs/Thumbnails/runtime_item",
objectPrefabPath = "Prefabs/Objects/runtime_item",
tag = "런타임"
};
_imageListWindow.AddItem(newItem);
Debug.Log($"[UTKImageListWindowSample] 아이템 추가됨: {newItem.itemName}");
}
/// <summary>
/// 모든 아이템을 제거하는 테스트 메서드입니다.
/// </summary>
[ContextMenu("Clear All Items")]
public void ClearAllItems()
{
_imageListWindow?.Clear();
Debug.Log("[UTKImageListWindowSample] 모든 아이템 제거됨");
}
/// <summary>
/// 검색을 테스트하는 메서드입니다.
/// </summary>
[ContextMenu("Test Search (전기)")]
public void TestSearch()
{
_imageListWindow?.ApplySearch("전기");
Debug.Log("[UTKImageListWindowSample] 검색 테스트: '전기'");
}
/// <summary>
/// 윈도우를 다시 표시하는 메서드입니다.
/// </summary>
[ContextMenu("Show Window")]
public void ShowWindow()
{
_imageListWindow?.Show();
}
#endregion
}