397 lines
15 KiB
C#
397 lines
15 KiB
C#
using Best.HTTP.JSON.LitJson;
|
|
using Cysharp.Threading.Tasks;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Threading;
|
|
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
using UVC.Json;
|
|
|
|
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="ganttPath">간트 데이터셋 경로.</param>
|
|
/// <param name="externalCt">외부 취소 토큰.</param>
|
|
public async UniTask LoadData(string gltfPath, string ganttPath, 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.LoadFromStreamingAssets(ganttPath);
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
}
|