Files
XRLib/Assets/Scripts/SHI/modal/BlockDetailModal.cs
2025-11-14 19:54:04 +09:00

393 lines
15 KiB
C#

using Cysharp.Threading.Tasks;
using System;
using System.Collections.Generic;
using System.Threading;
using UnityEngine;
using UnityEngine.UI;
namespace SHI.modal
{
/// <summary>
/// 모달 패널 내부에서 모델 뷰, 계층 리스트, 간트 차트를 조율하는 컨트롤러입니다.
/// 컴포넌트 간 선택 동기화, 레이아웃 분할 제어, 데이터 로딩을 담당합니다.
/// </summary>
public class BlockDetailModal : MonoBehaviour
{
[Header("References")]
[SerializeField] private Button closeButton;
[SerializeField] private ModelDetailListView listView;
[SerializeField] private ModelDetailView modelView;
[SerializeField] private ModelDetailChartView chartView;
[Header("UI Controls")]
[SerializeField] private Button modelViewExpandButton;
[SerializeField] private Button chartViewExpandButton;
[SerializeField] private Button dragButton;
[SerializeField] private Button showListButton;
// split 제어용 캐시
private LayoutElement _modelLayout;
private LayoutElement _chartLayout;
private enum ExpandedSide { None, Model, Chart }
private ExpandedSide _expanded = ExpandedSide.None;
private RectTransform ModelRect => modelView != null ? modelView.GetComponent<RectTransform>() : null;
private RectTransform ChartRect => chartView != null ? chartView.GetComponent<RectTransform>() : null;
private HorizontalSplitDrag _splitter;
// lifecycle
private CancellationTokenSource _cts;
private bool _suppressSelection;
// key<->id 매핑(차트-리스트/모델 동기화)
private readonly Dictionary<string, Guid> _keyToId = new Dictionary<string, Guid>();
private readonly Dictionary<Guid, string> _idToKey = new Dictionary<Guid, string>();
/// <summary>
/// UI 이벤트를 연결하고 스플리터를 준비합니다.
/// </summary>
public void Start()
{
// Close
if (closeButton != null)
{
closeButton.onClick.AddListener(OnCloseClicked);
}
// 리스트 표시 버튼
if (showListButton != null && listView != null)
showListButton.onClick.AddListener(OnShowListClicked);
if (showListButton != null) showListButton.gameObject.SetActive(false);
// 선택 동기화: 리스트 -> 모델/차트
if (listView != null)
{
listView.OnItemSelected += OnListItemSelected;
listView.OnItemDeselected += OnListItemDeselected;
listView.OnClosed += OnListClosed;
listView.OnVisibilityChanged += OnListVisibilityChanged;
}
// 선택 동기화: 모델 -> 리스트/차트
if (modelView != null)
{
modelView.OnItemSelected += OnModelItemSelected;
}
// 선택 동기화: 차트 -> 리스트/모델
if (chartView != null)
{
chartView.OnRowClickedByKey += OnChartRowClickedByKey;
chartView.OnRowClicked += OnChartRowClicked;
}
// 확장 버튼
if (modelViewExpandButton != null)
modelViewExpandButton.onClick.AddListener(ToggleExpandModel);
if (chartViewExpandButton != null)
chartViewExpandButton.onClick.AddListener(ToggleExpandChart);
// 드래그 스플리터
SetupSplitControls();
}
private void OnCloseClicked()
{
gameObject.SetActive(false);
}
private void OnShowListClicked()
{
if (listView != null) listView.gameObject.SetActive(true);
if (showListButton != null) showListButton.gameObject.SetActive(false);
if (_splitter != null) _splitter.RefreshPosition();
}
private void OnListItemSelected(UVC.UI.List.Tree.TreeListItemData data)
{
if (data == null) return;
HandleSelection(data.Id, "ListView");
}
private void OnListItemDeselected(UVC.UI.List.Tree.TreeListItemData data)
{
HandleDeselection(data.Id, "ListView");
}
private void OnListClosed()
{
if (showListButton != null) showListButton.gameObject.SetActive(true);
if (_splitter != null) _splitter.RefreshPosition();
}
private void OnListVisibilityChanged(Guid id, bool vis)
{
if (modelView != null) modelView.SetVisibility(id, vis);
}
private void OnModelItemSelected(UVC.UI.List.Tree.TreeListItemData data)
{
if (data == null) return;
HandleSelection(data.Id, "ModelView");
}
private void OnChartRowClickedByKey(string key)
{
if (string.IsNullOrEmpty(key)) return;
if (_keyToId.TryGetValue(key, out var id)) HandleSelection(id, "ChartView");
}
private void OnChartRowClicked(Guid id)
{
HandleSelection(id, "ChartView");
}
/// <summary>
/// LoadData 호출 시 리스트/모델/차트를 활성화하고 분할 비율을0.5/0.5로 초기화합니다.
/// </summary>
private void InitializePanelsForLoad()
{
// 활성화 상태 설정
if (listView != null)
{
listView.gameObject.SetActive(true);
listView.Clear();
}
if (showListButton != null) showListButton.gameObject.SetActive(false);
if (ModelRect != null) ModelRect.gameObject.SetActive(true);
if (chartView != null) chartView.gameObject.SetActive(true);
_expanded = ExpandedSide.None;
// 레이아웃 요소 보장 및50/50 설정
var modelRect = ModelRect;
var chartRect = ChartRect;
if (modelRect != null)
{
_modelLayout = modelRect.GetComponent<LayoutElement>() ?? modelRect.gameObject.AddComponent<LayoutElement>();
_modelLayout.flexibleWidth = 1f;
}
if (chartRect != null)
{
_chartLayout = chartRect.GetComponent<LayoutElement>() ?? chartRect.gameObject.AddComponent<LayoutElement>();
_chartLayout.flexibleWidth = 1f;
}
// 스플리터 보장 및 동기화
if (_splitter == null && dragButton != null && modelRect != null && chartRect != null)
{
_splitter = dragButton.gameObject.GetComponent<HorizontalSplitDrag>();
if (_splitter == null) _splitter = dragButton.gameObject.AddComponent<HorizontalSplitDrag>();
var leftFixed = listView != null ? listView.GetComponent<RectTransform>() : null;
_splitter.Initialize(modelRect, chartRect, leftFixed);
}
if (_splitter != null) _splitter.gameObject.SetActive(true);
Canvas.ForceUpdateCanvases();
_splitter?.RefreshPosition();
}
/// <summary>
/// glTF 모델과 간트 데이터를 하위 뷰에 로드하고, 선택 동기화를 위한 매핑을 구축합니다.
/// </summary>
/// <param name="gltfPath">glTF/glb 파일 경로.</param>
/// <param name="gantt">간트 데이터셋.</param>
/// <param name="externalCt">외부 취소 토큰.</param>
public async UniTask LoadData(string gltfPath, GanttChartData gantt, CancellationToken externalCt = default)
{
Debug.Log($"BlockDetailModal: LoadData {gltfPath}");
// 레이아웃/활성 상태 초기화 (리스트/모델/차트 활성,50/50 비율)
InitializePanelsForLoad();
// 이전 작업 취소
if (_cts != null)
{
try { _cts.Cancel(); } catch { }
_cts.Dispose();
}
_cts = CancellationTokenSource.CreateLinkedTokenSource(externalCt);
var ct = _cts.Token;
// 모델/리스트 로드
IEnumerable<ModelDetailListItemData> items = Array.Empty<ModelDetailListItemData>();
if (modelView != null)
{
try
{
items = await modelView.LoadModelAsync(gltfPath, ct);
}
catch (OperationCanceledException) { }
}
BuildKeyMaps(items);
if (listView != null) listView.SetupData(items);
if (chartView != null) chartView.LoadData(gantt);
}
private void BuildKeyMaps(IEnumerable<ModelDetailListItemData> items)
{
_keyToId.Clear();
_idToKey.Clear();
if (items == null) return;
var stack = new Stack<ModelDetailListItemData>();
foreach (var it in items)
{
if (it == null) continue;
stack.Push(it);
while (stack.Count > 0)
{
var cur = stack.Pop();
if (!string.IsNullOrEmpty(cur.ExternalKey))
{
_keyToId[cur.ExternalKey] = cur.Id;
_idToKey[cur.Id] = cur.ExternalKey;
}
if (cur.Children != null)
{
for (int i = 0; i < cur.Children.Count; i++)
{
var child = cur.Children[i] as ModelDetailListItemData;
if (child != null) stack.Push(child);
}
}
}
}
}
private void SetupSplitControls()
{
var modelRect = ModelRect;
var chartRect = ChartRect;
if (modelRect == null || chartRect == null || dragButton == null) return;
_modelLayout = modelRect.GetComponent<LayoutElement>();
if (_modelLayout == null) _modelLayout = modelRect.gameObject.AddComponent<LayoutElement>();
_chartLayout = chartRect.GetComponent<LayoutElement>();
if (_chartLayout == null) _chartLayout = chartView.gameObject.AddComponent<LayoutElement>();
// 초기 분할50/50
_modelLayout.flexibleWidth = 1f;
_chartLayout.flexibleWidth = 1f;
// 드래그 핸들 부착
_splitter = dragButton.gameObject.GetComponent<HorizontalSplitDrag>();
if (_splitter == null) _splitter = dragButton.gameObject.AddComponent<HorizontalSplitDrag>();
var leftFixed = listView != null ? listView.GetComponent<RectTransform>() : null;
_splitter.Initialize(modelRect, chartRect, leftFixed);
// 레이아웃 갱신 후 위치 보정
Canvas.ForceUpdateCanvases();
_splitter.RefreshPosition();
}
private void HandleSelection(Guid itemId, string source)
{
if (_suppressSelection) return;
_suppressSelection = true;
try
{
if (source != "ListView" && listView != null) listView.SelectByItemId(itemId);
if (source != "ModelView" && modelView != null) modelView.FocusItemById(itemId);
if (source != "ChartView" && chartView != null)
{
if (_idToKey.TryGetValue(itemId, out var key)) chartView.SelectByItemKey(key);
else chartView.SelectByItemId(itemId);
}
}
finally
{
_suppressSelection = false;
}
}
private void HandleDeselection(Guid itemId, string source)
{
if (_suppressSelection) return;
if (source != "ModelView" && modelView != null) modelView.UnfocusItem();
}
private void ToggleExpandModel()
{
if (ModelRect == null || chartView == null) return;
if (_expanded == ExpandedSide.Model) { ResetSplit(); return; }
_expanded = ExpandedSide.Model;
ModelRect.gameObject.SetActive(true);
chartView.gameObject.SetActive(false);
if (_splitter != null) _splitter.gameObject.SetActive(false);
}
private void ToggleExpandChart()
{
if (ModelRect == null || chartView == null) return;
if (_expanded == ExpandedSide.Chart) { ResetSplit(); return; }
_expanded = ExpandedSide.Chart;
ModelRect.gameObject.SetActive(false);
chartView.gameObject.SetActive(true);
if (_splitter != null) _splitter.gameObject.SetActive(false);
}
private void ResetSplit()
{
_expanded = ExpandedSide.None;
if (ModelRect != null) ModelRect.gameObject.SetActive(true);
if (chartView != null) chartView.gameObject.SetActive(true);
if (_modelLayout != null) _modelLayout.flexibleWidth = 1f;
if (_chartLayout != null) _chartLayout.flexibleWidth = 1f;
if (_splitter != null)
{
_splitter.gameObject.SetActive(true);
_splitter.RefreshPosition();
}
}
private void OnDisable()
{
if (_cts != null)
{
try { _cts.Cancel(); } catch { }
_cts.Dispose();
_cts = null;
}
if (chartView != null) chartView.Dispose();
if (modelView != null) modelView.Dispose();
}
private void OnDestroy()
{
// 이벤트 해제(메모리 누수 방지)
if (closeButton != null) closeButton.onClick.RemoveListener(OnCloseClicked);
if (showListButton != null && listView != null) showListButton.onClick.RemoveListener(OnShowListClicked);
if (modelViewExpandButton != null) modelViewExpandButton.onClick.RemoveListener(ToggleExpandModel);
if (chartViewExpandButton != null) chartViewExpandButton.onClick.RemoveListener(ToggleExpandChart);
if (listView != null)
{
listView.OnItemSelected -= OnListItemSelected;
listView.OnItemDeselected -= OnListItemDeselected;
listView.OnClosed -= OnListClosed;
listView.OnVisibilityChanged -= OnListVisibilityChanged;
}
if (modelView != null)
{
modelView.OnItemSelected -= OnModelItemSelected;
}
if (chartView != null)
{
chartView.OnRowClickedByKey -= OnChartRowClickedByKey;
chartView.OnRowClicked -= OnChartRowClicked;
}
if (_cts != null)
{
try { _cts.Cancel(); } catch { }
_cts.Dispose();
_cts = null;
}
}
}
}