#nullable enable using System.Collections.Generic; using UnityEngine; using UnityEngine.UIElements; using UVC.UIToolkit; /// /// UTKImageListWindow의 기능을 테스트하기 위한 샘플 MonoBehaviour입니다. /// 이미지 리스트 데이터를 생성하고 다양한 이벤트 핸들러를 등록하여 동작을 확인합니다. /// /// 테스트 기능: /// /// 이미지+텍스트 아이템 표시 /// 검색 필터링 (3글자 이상) /// 아이템 클릭/선택 이벤트 /// 드래그 앤 드롭 이벤트 /// /// public class UTKImageListWindowSample : MonoBehaviour { #region 필드 (Fields) [SerializeField] [Tooltip("UI를 표시할 UIDocument 컴포넌트")] public UIDocument? uiDocument; [SerializeField] [Tooltip("드래그 시 이미지가 커서를 따라다니도록 설정")] private bool dragImageFollowCursor = true; /// UTKImageListWindow 인스턴스 private UTKImageListWindow? _imageListWindow; /// 드롭 위치 (스크린 좌표) private Vector2 _lastDropScreenPosition; /// 메인 카메라 참조 private Camera? _mainCamera; /// 드래그 중인 3D 프리뷰 오브젝트 private GameObject? _dragPreview3D; /// 드래그가 리스트 영역 밖에 있는지 여부 private bool _isOutsideListArea; #endregion #region Unity 라이프사이클 private void Start() { // UIDocument 참조 확인 uiDocument ??= GetComponent(); if (uiDocument == null) { Debug.LogError("[UTKImageListWindowSample] UIDocument가 할당되지 않았습니다."); return; } // UTKImageListWindow 인스턴스 생성 및 추가 _imageListWindow = new UTKImageListWindow(); _imageListWindow.DragImageFollowCursor = dragImageFollowCursor; _imageListWindow.Title = "LIBRARY"; _imageListWindow.ShowCloseButton = true; uiDocument.rootVisualElement.Add(_imageListWindow); // 테스트 데이터 생성 CreateTestData(); // 이벤트 핸들러 등록 RegisterEventHandlers(); // 윈도우 표시 _imageListWindow.Show(); Debug.Log("[UTKImageListWindowSample] 초기화 완료"); } private void OnDestroy() { // 리소스 정리 _imageListWindow?.Dispose(); _imageListWindow = null; } #endregion #region 테스트 데이터 생성 /// /// 테스트용 이미지 리스트 데이터를 생성합니다. /// imagePath와 prefabPath 리스트를 사용하여 데이터를 생성합니다. /// private void CreateTestData() { if (_imageListWindow == null) return; // 이미지 경로 리스트 List imagePaths = new() { "Simulator/Images/lib_forklift_400x300", "Simulator/Images/lib_pallet_400x300", "Simulator/Images/lib_worker_400x300", }; // 프리팹 경로 리스트 List 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(); // 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 이벤트 핸들러 등록 /// /// UTKImageListWindow의 이벤트 핸들러들을 등록합니다. /// 클릭, 드래그 앤 드롭, 윈도우 닫기 이벤트를 처리합니다. /// 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(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) /// /// UI Toolkit 좌표를 Unity Screen 좌표로 변환합니다. /// UI Toolkit: 좌상단 원점, Y축 아래로 증가 /// Unity Screen: 좌하단 원점, Y축 위로 증가 /// /// UI Toolkit 좌표 /// Unity Screen 좌표 private Vector2 UIToolkitToScreenPosition(Vector2 uiToolkitPosition) { return new Vector2(uiToolkitPosition.x, Screen.height - uiToolkitPosition.y); } /// /// 스크린 좌표를 월드 좌표로 변환합니다. /// 바닥면(Y=0)에 레이캐스트하여 위치를 계산합니다. /// /// UI Toolkit 좌표 (좌상단 원점) /// 월드 좌표 (레이캐스트 실패 시 카메라 전방 10m 위치) 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) /// /// 드래그 중 3D 프리뷰 오브젝트를 생성합니다. /// /// 아이템 데이터 /// 화면 좌표 private void Create3DPreview(UTKImageListItemData item, Vector2 screenPosition) { // 기존 프리뷰가 있으면 제거 Destroy3DPreview(); // 프리팹 로드 var prefab = Resources.Load(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}"); } /// /// 3D 프리뷰 오브젝트를 제거합니다. /// private void Destroy3DPreview() { if (_dragPreview3D != null) { Destroy(_dragPreview3D); _dragPreview3D = null; } } /// /// 3D 프리뷰 오브젝트의 위치를 업데이트합니다. /// /// 화면 좌표 private void Update3DPreviewPosition(Vector2 screenPosition) { if (_dragPreview3D == null) return; Vector3 worldPosition = ScreenToWorldPosition(screenPosition); _dragPreview3D.transform.position = worldPosition; } /// /// 오브젝트의 모든 렌더러를 반투명하게 설정합니다. /// /// 대상 게임오브젝트 /// 투명도 (0-1) private void SetPreviewTransparency(GameObject obj, float alpha) { var renderers = obj.GetComponentsInChildren(); 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; } } } } /// /// 오브젝트의 모든 콜라이더를 비활성화합니다. /// /// 대상 게임오브젝트 private void DisableColliders(GameObject obj) { var colliders = obj.GetComponentsInChildren(); foreach (var collider in colliders) { collider.enabled = false; } } #endregion #region 에디터 테스트용 메서드 /// /// 런타임에 아이템을 추가하는 테스트 메서드입니다. /// Inspector에서 컨텍스트 메뉴로 호출할 수 있습니다. /// [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}"); } /// /// 모든 아이템을 제거하는 테스트 메서드입니다. /// [ContextMenu("Clear All Items")] public void ClearAllItems() { _imageListWindow?.Clear(); Debug.Log("[UTKImageListWindowSample] 모든 아이템 제거됨"); } /// /// 검색을 테스트하는 메서드입니다. /// [ContextMenu("Test Search (전기)")] public void TestSearch() { _imageListWindow?.ApplySearch("전기"); Debug.Log("[UTKImageListWindowSample] 검색 테스트: '전기'"); } /// /// 윈도우를 다시 표시하는 메서드입니다. /// [ContextMenu("Show Window")] public void ShowWindow() { _imageListWindow?.Show(); } #endregion }