#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; /// /// GLTF 모델 캐시 (gltf 경로 -> 로드된 원본 GameObject) /// private readonly Dictionary gltfCache = new(); /// /// 현재 로딩 중인 GLTF 경로 목록 (중복 로딩 방지) /// private readonly HashSet loadingPaths = new(); /// /// 현재 드래그 중인 3D 프리뷰 객체 /// private GameObject? dragPreview; /// /// 현재 드래그 중인 장비 정보 /// private EquipmentItem? draggingEquipment; private GameObject? stage; private void Awake() { if (accordionWindow == null) { accordionWindow = GetComponentInChildren(); } 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(this, ServiceLifetime.Scene); } public async void Start() { // 동적으로 로드되는 Prefab이므로 [Inject]가 자동으로 동작하지 않음 // InjectorAppContext에서 직접 Library를 가져옴 if (!InjectorAppContext.Instance.IsInitialized) { await InjectorAppContext.Instance.WaitForInitializationAsync(); } library = InjectorAppContext.Instance.Get(); stageObjectManager = InjectorAppContext.Instance.Get(); stage = InjectorAppContext.Instance.Get(); stageRoot = stage?.transform; SetupData(); SetupEvents(); } /// /// 이벤트 핸들러 설정 /// private void SetupEvents() { if (accordionWindow != null) { accordionWindow.AccordionList.OnGridItemBeginDrag += OnGridItemBeginDragHandler; accordionWindow.AccordionList.OnGridItemDrag += OnGridItemDragHandler; accordionWindow.AccordionList.OnGridItemEndDrag += OnGridItemEndDragHandler; } } /// /// 드래그 시작 핸들러 - 3D 프리뷰 생성 /// private void OnGridItemBeginDragHandler(AccordionGridItemData itemData, Vector2 screenPosition) { if (itemData.Data is EquipmentItem equipment) { draggingEquipment = equipment; CreateDragPreviewAsync(equipment, screenPosition).Forget(); } } /// /// 드래그 중 핸들러 - 3D 프리뷰 위치 업데이트 /// private void OnGridItemDragHandler(AccordionGridItemData itemData, Vector2 screenPosition) { if (dragPreview != null) { UpdatePreviewPosition(screenPosition); } } /// /// 드래그 종료 핸들러 - 프리뷰를 실제 객체로 전환 /// 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(); } } /// /// 드래그 프리뷰 생성 /// 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); } /// /// 프리뷰 위치 업데이트 /// private void UpdatePreviewPosition(Vector2 screenPosition) { if (dragPreview == null) return; var worldPosition = ScreenToWorldPosition(screenPosition); dragPreview.transform.position = worldPosition; } /// /// 스크린 좌표를 월드 좌표로 변환 /// 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); } /// /// 프리뷰 머티리얼 적용 (반투명) /// private void ApplyPreviewMaterials(GameObject obj) { var renderers = obj.GetComponentsInChildren(); 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; } } /// /// 프리뷰 머티리얼 복원 (불투명) /// private void RestorePreviewMaterials(GameObject obj) { var renderers = obj.GetComponentsInChildren(); 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; } } /// /// 드래그 프리뷰 취소 /// private void CancelDragPreview() { if (dragPreview != null) { Destroy(dragPreview); dragPreview = null; } draggingEquipment = null; } /// /// 모델에 Collider를 추가하고 레이어를 설정합니다 (Raycast로 클릭 감지용) /// private void AddCollidersToModel(GameObject obj) { if (obj == null) return; int modelLayer = LayerMask.NameToLayer("Model"); int colliderCount = 0; // 모든 자식 객체의 레이어를 Model로 설정 var allTransforms = obj.GetComponentsInChildren(true); foreach (var t in allTransforms) { t.gameObject.layer = modelLayer; } // MeshFilter가 있는 모든 자식에 MeshCollider 추가 var meshFilters = obj.GetComponentsInChildren(); foreach (var meshFilter in meshFilters) { // 이미 Collider가 있으면 스킵 if (meshFilter.GetComponent() != null) continue; // Mesh가 없으면 스킵 if (meshFilter.sharedMesh == null) continue; // MeshCollider 추가 var meshCollider = meshFilter.gameObject.AddComponent(); meshCollider.sharedMesh = meshFilter.sharedMesh; meshCollider.convex = false; // 정확한 충돌 감지를 위해 convex=false colliderCount++; } Debug.Log($"[StudioSideTabBarAccordion] Added {colliderCount} MeshColliders to: {obj.name} (total meshes: {meshFilters.Length})"); } /// /// Outlinable 컴포넌트를 추가합니다 (선택 시 아웃라인 표시용) /// private void AddOutlinableComponent(GameObject obj) { if (obj == null) return; // 이미 Outlinable이 있는지 확인 var existingOutlinable = obj.GetComponent(); if (existingOutlinable == null) { existingOutlinable = obj.GetComponentInChildren(); } if (existingOutlinable != null) { // 이미 있으면 비활성화만 existingOutlinable.enabled = false; return; } // Outlinable 추가 var outlinable = obj.AddComponent(); 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(); 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"); } /// /// 캐시에서 모델을 가져오거나 새로 로드합니다 /// private async UniTask 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); } } /// /// 캐시 부모 객체를 가져오거나 생성합니다 /// 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."); } } } /// /// 탭 콘텐츠에 데이터를 전달합니다. /// /// 전달할 데이터 객체 public void SetContentData(object? data) { } /// /// 탭 전환 시 데이터가 있는 경우 전달 되는 데이터. SetContentData 이후 호출 됨 /// /// 전달할 데이터 객체 public void UpdateContentData(object? data) { } /// /// 닫힐 때 실행되는 로직을 처리합니다. /// /// 비동기 닫기 작업을 나타내는 입니다. 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(); } /// /// GLTF 캐시를 정리합니다 /// public void ClearCache() { foreach (var kvp in gltfCache) { if (kvp.Value != null) { Destroy(kvp.Value); } } gltfCache.Clear(); loadingPaths.Clear(); Debug.Log("[StudioSideTabBarAccordion] Cache cleared"); } /// /// 특정 경로의 캐시를 제거합니다 /// 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}"); } } } }