Files
XRLib/Assets/Scripts/Studio/Tab/StudioSideTabBarAccordion.cs
2025-12-19 15:27:35 +09:00

630 lines
23 KiB
C#

#nullable enable
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using EPOOutline;
using UnityEngine;
using UVC.Core;
using UVC.GLTF;
using UVC.Studio.Config;
using UVC.Studio.Manager;
using UVC.UI.List.Accordion;
using UVC.UI.Tab;
using UVC.UI.Window;
using UnityEngine.EventSystems;
namespace UVC.Studio.Tab
{
public class StudioSideTabBarAccordion : MonoBehaviour, ITabContent
{
[SerializeField]
private AccordionWindow? accordionWindow;
[SerializeField]
private Transform? stageRoot;
[SerializeField]
private Camera? mainCamera;
[SerializeField]
private LayerMask groundLayerMask = ~0;
[SerializeField]
private float defaultPlacementHeight = 0f;
private Library? library;
private StageObjectManager? stageObjectManager;
private bool isInitialized = false;
/// <summary>
/// GLTF 모델 캐시 (gltf 경로 -> 로드된 원본 GameObject)
/// </summary>
private readonly Dictionary<string, GameObject> gltfCache = new();
/// <summary>
/// 현재 로딩 중인 GLTF 경로 목록 (중복 로딩 방지)
/// </summary>
private readonly HashSet<string> loadingPaths = new();
/// <summary>
/// 현재 드래그 중인 3D 프리뷰 객체
/// </summary>
private GameObject? dragPreview;
/// <summary>
/// 현재 드래그 중인 장비 정보
/// </summary>
private EquipmentItem? draggingEquipment;
private GameObject? stage;
private void Awake()
{
if (accordionWindow == null)
{
accordionWindow = GetComponentInChildren<AccordionWindow>();
}
if (accordionWindow == null)
{
Debug.LogError("StudioSideTabBarAccordion component is not assigned or found in Children.");
return;
}
if (mainCamera == null)
{
mainCamera = Camera.main;
}
InjectorAppContext.Instance.Injector.RegisterInstance<StudioSideTabBarAccordion>(this, ServiceLifetime.Scene);
}
public async void Start()
{
// 동적으로 로드되는 Prefab이므로 [Inject]가 자동으로 동작하지 않음
// InjectorAppContext에서 직접 Library를 가져옴
if (!InjectorAppContext.Instance.IsInitialized)
{
await InjectorAppContext.Instance.WaitForInitializationAsync();
}
library = InjectorAppContext.Instance.Get<Library>();
stageObjectManager = InjectorAppContext.Instance.Get<StageObjectManager>();
stage = InjectorAppContext.Instance.Get<GameObject>();
stageRoot = stage?.transform;
SetupData();
SetupEvents();
}
/// <summary>
/// 이벤트 핸들러 설정
/// </summary>
private void SetupEvents()
{
if (accordionWindow != null)
{
accordionWindow.AccordionList.OnGridItemBeginDrag += OnGridItemBeginDragHandler;
accordionWindow.AccordionList.OnGridItemDrag += OnGridItemDragHandler;
accordionWindow.AccordionList.OnGridItemEndDrag += OnGridItemEndDragHandler;
}
}
/// <summary>
/// 드래그 시작 핸들러 - 3D 프리뷰 생성
/// </summary>
private void OnGridItemBeginDragHandler(AccordionGridItemData itemData, Vector2 screenPosition)
{
if (itemData.Data is EquipmentItem equipment)
{
draggingEquipment = equipment;
CreateDragPreviewAsync(equipment, screenPosition).Forget();
}
}
/// <summary>
/// 드래그 중 핸들러 - 3D 프리뷰 위치 업데이트
/// </summary>
private void OnGridItemDragHandler(AccordionGridItemData itemData, Vector2 screenPosition)
{
if (dragPreview != null)
{
UpdatePreviewPosition(screenPosition);
}
}
/// <summary>
/// 드래그 종료 핸들러 - 프리뷰를 실제 객체로 전환
/// </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)
{
// 최종 위치 계산
var worldPosition = ScreenToWorldPosition(screenPosition);
// 프리뷰를 실제 객체로 전환
dragPreview.transform.position = worldPosition;
// 프리뷰 머티리얼을 원래대로 복원
RestorePreviewMaterials(dragPreview);
// Collider 추가 (Raycast로 클릭 감지용)
AddCollidersToModel(dragPreview);
// Outlinable 컴포넌트 추가 (선택 시 아웃라인 표시용)
AddOutlinableComponent(dragPreview);
// StageObjectManager에 등록
stageObjectManager.Register(draggingEquipment, dragPreview);
Debug.Log($"[StudioSideTabBarAccordion] Placed object at: {worldPosition}");
// 프리뷰 참조 해제 (파괴하지 않음 - 실제 객체로 사용)
dragPreview = null;
draggingEquipment = null;
}
else
{
// 드래그 취소 또는 실패 시 프리뷰 제거
CancelDragPreview();
}
}
/// <summary>
/// 드래그 프리뷰 생성
/// </summary>
private async UniTaskVoid CreateDragPreviewAsync(EquipmentItem equipment, Vector2 screenPosition)
{
if (library == null) return;
var gltfPath = library.LibraryPath + equipment.gltf;
Debug.Log($"[StudioSideTabBarAccordion] Creating drag preview: {equipment.label}");
// 캐시에서 모델 가져오기 또는 로드
var cachedModel = await GetOrLoadModelAsync(gltfPath);
if (cachedModel == null)
{
Debug.LogError($"Failed to load model for preview: {gltfPath}");
return;
}
// 드래그 중 취소되었는지 확인
if (draggingEquipment != equipment)
{
return;
}
// 프리뷰 인스턴스 생성
var parent = stageRoot != null ? stageRoot : null;
dragPreview = Instantiate(cachedModel, parent);
dragPreview.layer = LayerMask.NameToLayer("Model");
dragPreview.SetActive(true);
dragPreview.name = $"{equipment.label}_Preview";
// 프리뷰 머티리얼 설정 (반투명)
ApplyPreviewMaterials(dragPreview);
// 초기 위치 설정
UpdatePreviewPosition(screenPosition);
}
/// <summary>
/// 프리뷰 위치 업데이트
/// </summary>
private void UpdatePreviewPosition(Vector2 screenPosition)
{
if (dragPreview == null) return;
var worldPosition = ScreenToWorldPosition(screenPosition);
dragPreview.transform.position = worldPosition;
}
/// <summary>
/// 스크린 좌표를 월드 좌표로 변환
/// </summary>
private Vector3 ScreenToWorldPosition(Vector2 screenPosition)
{
if (mainCamera == null)
{
mainCamera = Camera.main;
if (mainCamera == null)
{
return new Vector3(0, defaultPlacementHeight, 0);
}
}
// 레이캐스트로 바닥 위치 찾기
var ray = mainCamera.ScreenPointToRay(screenPosition);
if (Physics.Raycast(ray, out var hit, 1000f, groundLayerMask))
{
return hit.point;
}
// 바닥이 없으면 고정 높이의 평면과 교차점 계산
var plane = new Plane(Vector3.up, new Vector3(0, defaultPlacementHeight, 0));
if (plane.Raycast(ray, out var distance))
{
return ray.GetPoint(distance);
}
// 기본값
return new Vector3(0, defaultPlacementHeight, 0);
}
/// <summary>
/// 프리뷰 머티리얼 적용 (반투명)
/// </summary>
private void ApplyPreviewMaterials(GameObject obj)
{
var renderers = obj.GetComponentsInChildren<Renderer>();
foreach (var renderer in renderers)
{
var materials = renderer.materials;
for (int i = 0; i < materials.Length; i++)
{
var mat = materials[i];
// 원본 색상 저장 (나중에 복원용)
var originalColor = mat.HasProperty("_Color") ? mat.color : Color.white;
// 반투명 설정
mat.SetFloat("_Mode", 3); // Transparent mode
mat.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha);
mat.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha);
mat.SetInt("_ZWrite", 0);
mat.DisableKeyword("_ALPHATEST_ON");
mat.EnableKeyword("_ALPHABLEND_ON");
mat.DisableKeyword("_ALPHAPREMULTIPLY_ON");
mat.renderQueue = 3000;
// 색상을 반투명으로 설정
if (mat.HasProperty("_Color"))
{
mat.color = new Color(originalColor.r, originalColor.g, originalColor.b, 0.5f);
}
}
renderer.materials = materials;
}
}
/// <summary>
/// 프리뷰 머티리얼 복원 (불투명)
/// </summary>
private void RestorePreviewMaterials(GameObject obj)
{
var renderers = obj.GetComponentsInChildren<Renderer>();
foreach (var renderer in renderers)
{
var materials = renderer.materials;
for (int i = 0; i < materials.Length; i++)
{
var mat = materials[i];
// 불투명 설정 복원
mat.SetFloat("_Mode", 0); // Opaque mode
mat.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.One);
mat.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.Zero);
mat.SetInt("_ZWrite", 1);
mat.DisableKeyword("_ALPHATEST_ON");
mat.DisableKeyword("_ALPHABLEND_ON");
mat.DisableKeyword("_ALPHAPREMULTIPLY_ON");
mat.renderQueue = -1;
// 색상을 불투명으로 복원
if (mat.HasProperty("_Color"))
{
var color = mat.color;
mat.color = new Color(color.r, color.g, color.b, 1f);
}
}
renderer.materials = materials;
}
}
/// <summary>
/// 드래그 프리뷰 취소
/// </summary>
private void CancelDragPreview()
{
if (dragPreview != null)
{
Destroy(dragPreview);
dragPreview = null;
}
draggingEquipment = null;
}
/// <summary>
/// 모델에 Collider를 추가하고 레이어를 설정합니다 (Raycast로 클릭 감지용)
/// </summary>
private void AddCollidersToModel(GameObject obj)
{
if (obj == null) return;
int modelLayer = LayerMask.NameToLayer("Model");
int colliderCount = 0;
// 모든 자식 객체의 레이어를 Model로 설정
var allTransforms = obj.GetComponentsInChildren<Transform>(true);
foreach (var t in allTransforms)
{
t.gameObject.layer = modelLayer;
}
// MeshFilter가 있는 모든 자식에 MeshCollider 추가
var meshFilters = obj.GetComponentsInChildren<MeshFilter>();
foreach (var meshFilter in meshFilters)
{
// 이미 Collider가 있으면 스킵
if (meshFilter.GetComponent<Collider>() != null) continue;
// Mesh가 없으면 스킵
if (meshFilter.sharedMesh == null) continue;
// MeshCollider 추가
var meshCollider = meshFilter.gameObject.AddComponent<MeshCollider>();
meshCollider.sharedMesh = meshFilter.sharedMesh;
meshCollider.convex = false; // 정확한 충돌 감지를 위해 convex=false
colliderCount++;
}
Debug.Log($"[StudioSideTabBarAccordion] Added {colliderCount} MeshColliders to: {obj.name} (total meshes: {meshFilters.Length})");
}
/// <summary>
/// Outlinable 컴포넌트를 추가합니다 (선택 시 아웃라인 표시용)
/// </summary>
private void AddOutlinableComponent(GameObject obj)
{
if (obj == null) return;
// 이미 Outlinable이 있는지 확인
var existingOutlinable = obj.GetComponent<Outlinable>();
if (existingOutlinable == null)
{
existingOutlinable = obj.GetComponentInChildren<Outlinable>();
}
if (existingOutlinable != null)
{
// 이미 있으면 비활성화만
existingOutlinable.enabled = false;
return;
}
// Outlinable 추가
var outlinable = obj.AddComponent<Outlinable>();
outlinable.enabled = false; // 기본적으로 비활성화 (선택 시 활성화)
// DrawingMode를 Normal로 설정
outlinable.DrawingMode = OutlinableDrawingMode.Normal;
// RenderStyle을 FrontBack으로 설정 (앞면과 뒷면 모두 아웃라인)
outlinable.RenderStyle = RenderStyle.FrontBack;
// Front 아웃라인 설정 (보이는 부분)
outlinable.FrontParameters.Enabled = true;
outlinable.FrontParameters.Color = new Color(1f, 0.5f, 0f, 1f); // 주황색
outlinable.FrontParameters.DilateShift = 1.0f;
outlinable.FrontParameters.BlurShift = 0.0f;
// Back 아웃라인 설정 (가려진 부분)
outlinable.BackParameters.Enabled = true;
outlinable.BackParameters.Color = new Color(1f, 0.5f, 0f, 0.5f); // 반투명 주황색
outlinable.BackParameters.DilateShift = 1.0f;
outlinable.BackParameters.BlurShift = 0.0f;
// Renderer가 있는 자식들을 OutlineTarget으로 등록
var renderers = obj.GetComponentsInChildren<Renderer>();
foreach (var renderer in renderers)
{
var target = new OutlineTarget(renderer)
{
CullMode = UnityEngine.Rendering.CullMode.Off // 양면 렌더링으로 Outline 연결
};
outlinable.AddTarget(target);
}
Debug.Log($"[StudioSideTabBarAccordion] Added Outlinable to: {obj.name} with {renderers.Length} targets");
}
/// <summary>
/// 캐시에서 모델을 가져오거나 새로 로드합니다
/// </summary>
private async UniTask<GameObject?> GetOrLoadModelAsync(string gltfPath)
{
// 캐시에 있으면 반환
if (gltfCache.TryGetValue(gltfPath, out var cached))
{
Debug.Log($"[StudioSideTabBarAccordion] Cache hit: {gltfPath}");
return cached;
}
// 이미 로딩 중이면 완료될 때까지 대기
if (loadingPaths.Contains(gltfPath))
{
Debug.Log($"[StudioSideTabBarAccordion] Waiting for loading: {gltfPath}");
await UniTask.WaitUntil(() => !loadingPaths.Contains(gltfPath));
return gltfCache.TryGetValue(gltfPath, out var loadedModel) ? loadedModel : null;
}
// 로딩 시작
loadingPaths.Add(gltfPath);
Debug.Log($"[StudioSideTabBarAccordion] Loading model: {gltfPath}");
try
{
// 캐시용 숨겨진 부모 생성 (처음 한 번만)
var cacheParent = GetOrCreateCacheParent();
// GLTF 로드
var model = await GLTFImporter.ImportFromFile(gltfPath, cacheParent);
if (model != null)
{
// 캐시에 저장 및 비활성화 (원본은 숨김)
model.SetActive(false);
gltfCache[gltfPath] = model;
Debug.Log($"[StudioSideTabBarAccordion] Cached model: {gltfPath}");
}
return model;
}
finally
{
loadingPaths.Remove(gltfPath);
}
}
/// <summary>
/// 캐시 부모 객체를 가져오거나 생성합니다
/// </summary>
private Transform GetOrCreateCacheParent()
{
var cacheName = "_GLTFCache";
var cacheObj = GameObject.Find(cacheName);
if (cacheObj == null)
{
cacheObj = new GameObject(cacheName);
cacheObj.SetActive(false);
DontDestroyOnLoad(cacheObj);
}
return cacheObj.transform;
}
public async void SetupData()
{
Debug.Log("StudioSideTabBarAccordion: SetupData called");
if (!isInitialized)
{
isInitialized = true;
Debug.Log($"StudioSideTabBarAccordion: Setting up data library != null:{library != null}");
if (library != null)
{
var data = new AccordionData();
var sec1 = new AccordionSectionData
{
Title = "Stacker Crane",
IsExpanded = true,
LayoutType = AccordionItemLayoutType.Grid
};
library.StakerCraneData.list.ForEach(stackerCrane =>
{
sec1.Items.Add(new AccordionGridItemData { Caption = stackerCrane.label, Image = library.LibraryPath + stackerCrane.image, Data = stackerCrane });
});
data.Sections.Add(sec1);
var sec2 = new AccordionSectionData
{
Title = "AGV",
IsExpanded = true,
LayoutType = AccordionItemLayoutType.Grid
};
library.AGVData.list.ForEach(agv =>
{
sec2.Items.Add(new AccordionGridItemData { Caption = agv.label, Image = library.LibraryPath + agv.image, Data = agv });
});
data.Sections.Add(sec2);
var sec3 = new AccordionSectionData
{
Title = "Ract",
IsExpanded = true,
LayoutType = AccordionItemLayoutType.Grid
};
library.RackSingleData.list.ForEach(ract =>
{
sec3.Items.Add(new AccordionGridItemData { Caption = ract.label, Image = library.LibraryPath + ract.image, Data = ract });
});
data.Sections.Add(sec3);
accordionWindow?.SetData(data);
}
else
{
Debug.LogError("Library is not injected.");
}
}
}
/// <summary>
/// 탭 콘텐츠에 데이터를 전달합니다.
/// </summary>
/// <param name="data">전달할 데이터 객체</param>
public void SetContentData(object? data)
{
}
/// <summary>
/// 탭 전환 시 데이터가 있는 경우 전달 되는 데이터. SetContentData 이후 호출 됨
/// </summary>
/// <param name="data">전달할 데이터 객체</param>
public void UpdateContentData(object? data)
{
}
/// <summary>
/// 닫힐 때 실행되는 로직을 처리합니다.
/// </summary>
/// <returns>비동기 닫기 작업을 나타내는 <see cref="UniTask"/>입니다.</returns>
public UniTask OnCloseAsync()
{
Debug.Log("StudioSideTabBarAccordion: OnClose called");
return UniTask.CompletedTask;
}
private void OnDestroy()
{
// 이벤트 해제
if (accordionWindow != null)
{
accordionWindow.AccordionList.OnGridItemBeginDrag -= OnGridItemBeginDragHandler;
accordionWindow.AccordionList.OnGridItemDrag -= OnGridItemDragHandler;
accordionWindow.AccordionList.OnGridItemEndDrag -= OnGridItemEndDragHandler;
}
// 드래그 프리뷰 정리
CancelDragPreview();
// 캐시 정리
ClearCache();
}
/// <summary>
/// GLTF 캐시를 정리합니다
/// </summary>
public void ClearCache()
{
foreach (var kvp in gltfCache)
{
if (kvp.Value != null)
{
Destroy(kvp.Value);
}
}
gltfCache.Clear();
loadingPaths.Clear();
Debug.Log("[StudioSideTabBarAccordion] Cache cleared");
}
/// <summary>
/// 특정 경로의 캐시를 제거합니다
/// </summary>
public void RemoveFromCache(string gltfPath)
{
if (gltfCache.TryGetValue(gltfPath, out var cached))
{
if (cached != null)
{
Destroy(cached);
}
gltfCache.Remove(gltfPath);
Debug.Log($"[StudioSideTabBarAccordion] Removed from cache: {gltfPath}");
}
}
}
}