503 lines
17 KiB
C#
503 lines
17 KiB
C#
#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
|
||
}
|