Files
XRLib/Assets/Scripts/SHI/modal/ISOP/ISOPModal.cs
2025-11-27 13:41:37 +09:00

586 lines
21 KiB
C#

#nullable enable
using Cysharp.Threading.Tasks;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using UnityEngine;
using UnityEngine.UIElements;
namespace SHI.Modal.ISOP
{
public class ISOPModal : MonoBehaviour
{
[SerializeField]
public UIDocument uiDocument;
private VisualElement content;
private TreeList listView;
private ISOPModelView modelView;
private ISOPChart chartView;
private Button closeBtn;
private Button showTreeBtn;
private Button dragBtn;
private CancellationTokenSource? _cts;
private bool _suppressSelection = false;
// key<->id 매핑(차트-리스트/모델 동기화)
private readonly Dictionary<string, int> _keyToId = new Dictionary<string, int>();
private readonly Dictionary<int, string> _idToKey = new Dictionary<int, string>();
private int selectedItemId = -1;
private enum ExpandedSide { None, Model, Chart }
private ExpandedSide _expanded = ExpandedSide.None;
// 드래그 상태 저장
private bool _isDragging = false;
private int _activePointerId = -1;
private float _dragOffset = 0f; // 포인터와 drag 버튼 중심 간 오프셋
// 확장 전 비율 저장
private float _lastModelFlexGrow = 1f;
private float _lastChartFlexGrow = 1f;
private void OnEnable()
{
var root = uiDocument.rootVisualElement;
content = root.Q<VisualElement>("content");
// UXML에서 <TreeList> 태그로 추가했다면 Query로 찾음
listView = root.Q<TreeList>();
if (listView != null)
{
listView.OnSelectionChanged += OnListItemSelectionChanged;
listView.OnClosed += OnListClosed;
listView.OnVisibilityChanged += OnListVisibilityChanged;
// listView가 다른 요소 위에 표시되도록 설정
listView.style.unityOverflowClipBox = OverflowClipBox.ContentBox;
// 더미 데이터 생성
var data = GenerateDummyData();
listView.SetData(data);
}
modelView = root.Q<ISOPModelView>();
if (modelView != null)
{
// 선택 동기화: 모델 -> 리스트/차트
modelView.OnItemSelected += OnModelItemSelected;
modelView.OnExpand += ToggleExpandModel;
// modelView의 내용이 범위를 벗어나지 않도록 overflow 설정
modelView.style.overflow = Overflow.Hidden;
}
chartView = root.Q<ISOPChart>();
if (chartView != null)
{
chartView.OnExpand += ToggleExpandChart;
}
showTreeBtn = root.Q<Button>("show-tree-btn");
if (showTreeBtn != null)
{
showTreeBtn.clicked += OnClickShowTree;
showTreeBtn.style.display = DisplayStyle.None;
}
closeBtn = root.Q<Button>("closeButton");
if (closeBtn != null)
{
closeBtn.clicked += OnClickClose;
}
initDrag(root);
_expanded = ExpandedSide.None;
}
private void initDrag(VisualElement root)
{
dragBtn = root.Q<Button>("drag-btn");
if (dragBtn != null)
{
// modelView의 위치와 크기가 결정된 후에 dragBtn의 left를 계산해서 설정
if (modelView != null)
{
// dragBtn이 절대 위치로 움직일 수 있도록 설정
dragBtn.style.position = Position.Absolute;
dragBtn.pickingMode = PickingMode.Position;
//dragBtn.AddManipulator(new HorizontalDragManipulator());
// 드래그 시작
dragBtn.RegisterCallback<PointerDownEvent>((evt) =>
{
// 좌클릭만 처리 (0)
if (evt.button != 0) return;
Debug.Log("Drag Started (PointerDown) - captured");
// 포인터 캡처
_isDragging = true;
_activePointerId = evt.pointerId;
dragBtn.CapturePointer(_activePointerId);
// 포인터가 drag 버튼의 어느 위치를 눌렀는지 계산 (center 기준)
// evt.position은 content 기준 좌표
var dragCenterX = dragBtn.layout.x + dragBtn.layout.width * 0.5f;
_dragOffset = evt.position.x - dragCenterX;
evt.StopImmediatePropagation();
}, TrickleDown.TrickleDown);
// 전역 포인터 무브로 위치 추적 (root에 등록)
dragBtn.RegisterCallback<PointerMoveEvent>((evt) =>
{
if (!_isDragging) return;
if (evt.pointerId != _activePointerId) return;
//Debug.Log($"Dragging... evt.pointerId:{evt.pointerId}, _activePointerId:{_activePointerId}");
// evt.position은 content 기준 좌표
float pointerX = evt.position.x;
float centerX = pointerX - _dragOffset;
ApplyDragPosition(content, dragBtn, centerX);
evt.StopImmediatePropagation();
}, TrickleDown.TrickleDown);
// 드래그 종료 (마우스 업 또는 포인터 캔슬)
dragBtn.RegisterCallback<PointerUpEvent>((evt) =>
{
if (!_isDragging) return;
if (evt.pointerId != _activePointerId) return;
Debug.Log("Drag Ended");
_isDragging = false;
if (_activePointerId != -1)
{
try { dragBtn.ReleasePointer(_activePointerId); } catch { }
}
_activePointerId = -1;
evt.StopImmediatePropagation();
}, TrickleDown.TrickleDown);
dragBtn.RegisterCallback<PointerCancelEvent>((evt) =>
{
if (!_isDragging) return;
if (evt.pointerId != _activePointerId) return;
_isDragging = false;
if (_activePointerId != -1)
{
try { dragBtn.ReleasePointer(_activePointerId); } catch { }
}
_activePointerId = -1;
evt.StopImmediatePropagation();
}, TrickleDown.TrickleDown);
// 초기화 및 레이아웃 변경 시 재계산
bool initialized = false;
content.RegisterCallback<GeometryChangedEvent>((evt) =>
{
// 드래그 중에는 GeometryChanged 이벤트 무시
if (_isDragging) return;
// 초기화: treeList의 레이아웃이 계산될 때까지 대기
if (!initialized)
{
if (listView == null || listView.layout.width <= 0)
{
return; // 아직 레이아웃이 계산되지 않음
}
initialized = true;
}
UpdateDragAndPanels(content, dragBtn);
});
}
}
}
/// <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)
{
if(modelView == null)
{
//대기
while(modelView == null)
{
await UniTask.Yield();
}
}
Debug.Log($"ISOPModal: LoadData {gltfPath}");
// 이전 작업 취소
if (_cts != null)
{
try { _cts.Cancel(); } catch { }
_cts.Dispose();
}
_cts = CancellationTokenSource.CreateLinkedTokenSource(externalCt);
var ct = _cts.Token;
// 모델/리스트 로드
IEnumerable<TreeListItemData> items = Array.Empty<TreeListItemData>();
if (modelView != null)
{
try
{
items = await modelView.LoadModelAsync(gltfPath, ct);
}
catch (OperationCanceledException) { }
}
BuildKeyMaps(items);
if (listView != null) listView.SetData(items.ToList());
if (chartView != null) chartView.Load(ganttPath);
}
private void BuildKeyMaps(IEnumerable<TreeListItemData> items)
{
_keyToId.Clear();
_idToKey.Clear();
if (items == null) return;
var stack = new Stack<TreeListItemData>();
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 TreeListItemData;
if (child != null) stack.Push(child);
}
}
}
}
}
private void OnClickClose()
{
this.gameObject.SetActive(false);
}
private void OnClickShowTree()
{
if (listView != null)
{
listView.Show();
showTreeBtn.style.display = DisplayStyle.None;
//1frame 후에 위치 갱신
content.schedule.Execute(() =>
{
UpdateDragAndPanels(content, dragBtn);
}).ExecuteLater(1);
}
}
private void OnListItemSelectionChanged(TreeListItemData data)
{
if (data == null) return;
Debug.Log($"ISOPModal.OnListItemSelectionChanged: id={data.id}, isSelected={data.isSelected}");
if (data.isSelected)
{
HandleSelection(data.id, "ListView");
}
else
{
HandleDeselection(data.id, "ListView");
}
}
private void OnListClosed()
{
showTreeBtn.style.display = DisplayStyle.Flex;
UpdateDragAndPanels(content, dragBtn);
}
private void OnListVisibilityChanged(TreeListItemData itemData)
{
if (modelView != null) modelView.SetVisibility(itemData.id, itemData.IsVisible);
}
private void OnModelItemSelected(TreeListItemData data)
{
if (data == null) return;
HandleSelection(data.id, "ModelView");
}
private void HandleSelection(int itemId, string source)
{
if (_suppressSelection) return;
_suppressSelection = true;
try
{
selectedItemId = itemId;
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(int itemId, string source)
{
if (_suppressSelection) return;
if (source != "ModelView" && modelView != null) modelView.UnfocusItem();
}
private void ToggleExpandModel()
{
if (_expanded == ExpandedSide.Model)
{
// 확장 해제: 저장된 비율로 복원
_expanded = ExpandedSide.None;
modelView.style.display = DisplayStyle.Flex;
chartView.style.display = DisplayStyle.Flex;
dragBtn.style.display = DisplayStyle.Flex;
// 저장된 비율로 복원
modelView.style.flexGrow = _lastModelFlexGrow;
chartView.style.flexGrow = _lastChartFlexGrow;
// 1프레임 후 dragBtn 위치 업데이트
content.schedule.Execute(() =>
{
UpdateDragAndPanels(content, dragBtn);
}).ExecuteLater(1);
return;
}
// 확장 전 비율 저장
_lastModelFlexGrow = modelView.resolvedStyle.flexGrow;
_lastChartFlexGrow = chartView.resolvedStyle.flexGrow;
_expanded = ExpandedSide.Model;
//modelView 확장
modelView.style.display = DisplayStyle.Flex;
modelView.style.flexGrow = 1;
chartView.style.display = DisplayStyle.None;
dragBtn.style.display = DisplayStyle.None;
}
private void ToggleExpandChart()
{
if (_expanded == ExpandedSide.Chart)
{
// 확장 해제: 저장된 비율로 복원
_expanded = ExpandedSide.None;
modelView.style.display = DisplayStyle.Flex;
chartView.style.display = DisplayStyle.Flex;
dragBtn.style.display = DisplayStyle.Flex;
// 저장된 비율로 복원
modelView.style.flexGrow = _lastModelFlexGrow;
chartView.style.flexGrow = _lastChartFlexGrow;
// 1프레임 후 dragBtn 위치 업데이트
content.schedule.Execute(() =>
{
UpdateDragAndPanels(content, dragBtn);
}).ExecuteLater(1);
return;
}
// 확장 전 비율 저장
_lastModelFlexGrow = modelView.resolvedStyle.flexGrow;
_lastChartFlexGrow = chartView.resolvedStyle.flexGrow;
_expanded = ExpandedSide.Chart;
// chartView 확장
modelView.style.display = DisplayStyle.None;
chartView.style.display = DisplayStyle.Flex;
chartView.style.flexGrow = 1;
dragBtn.style.display = DisplayStyle.None;
}
// dragBtn 중심 X를 주면 flex-grow를 조절하여 modelView와 chartView 비율 조정
private void ApplyDragPosition(VisualElement root, VisualElement drag, float centerX)
{
if (modelView == null || chartView == null) return;
// 확장 모드일 때는 비율 조정하지 않음
if (_expanded != ExpandedSide.None) return;
float dragHalf = Mathf.Max(1f, drag.layout.width * 0.5f);
// 드래그 가능 범위 계산
float leftBound = GetLeftBound();
float rightBound = root.layout.width;
// centerX를 범위 내로 제한
centerX = Mathf.Clamp(centerX, leftBound, rightBound);
// 사용 가능한 너비 (treeList 제외)
float availableWidth = rightBound - leftBound;
// centerX를 leftBound 기준 상대 좌표로 변환
float relativeX = centerX - leftBound;
// flex 비율 계산 (relativeX : (availableWidth - relativeX))
float modelFlexGrow = relativeX;
float chartFlexGrow = availableWidth - relativeX;
// 양 끝 판정을 위한 threshold
float threshold = 2f;
bool isAtLeftEdge = relativeX < threshold;
bool isAtRightEdge = relativeX > (availableWidth - threshold);
// 가시성 및 flex 처리
if (isAtLeftEdge)
{
// 왼쪽 끝: modelView 숨김, chartView만 표시
modelView.style.display = DisplayStyle.None;
chartView.style.display = DisplayStyle.Flex;
chartView.style.flexGrow = 1;
// 비율 저장 (양 끝에서는 저장하지 않음)
}
else if (isAtRightEdge)
{
// 오른쪽 끝: chartView 숨김, modelView만 표시
modelView.style.display = DisplayStyle.Flex;
chartView.style.display = DisplayStyle.None;
modelView.style.flexGrow = 1;
// 비율 저장 (양 끝에서는 저장하지 않음)
}
else
{
// 중간: 둘 다 표시, flex 비율로 조정
modelView.style.display = DisplayStyle.Flex;
chartView.style.display = DisplayStyle.Flex;
modelView.style.flexGrow = modelFlexGrow;
chartView.style.flexGrow = chartFlexGrow;
// 비율 저장 (중간 위치에서만)
_lastModelFlexGrow = modelFlexGrow;
_lastChartFlexGrow = chartFlexGrow;
}
// dragBtn 위치 조정
float newDragLeft = centerX - dragHalf;
drag.style.left = new Length(newDragLeft, LengthUnit.Pixel);
}
// 왼쪽 경계 계산 (treeList 보이는 여부에 따라)
private float GetLeftBound()
{
// treeList가 null이 아니고, DisplayStyle.None이 아니면 보이는 것으로 간주
if (listView != null && listView.style.display != DisplayStyle.None)
{
// treeList가 보이는 경우: treeList의 오른쪽 끝
float treeWidth = listView.layout.width;
// layout이 아직 계산되지 않았으면 0이므로 체크
if (treeWidth > 0)
{
return listView.layout.x + treeWidth;
}
// layout이 아직 없으면 resolvedStyle에서 가져오기 시도
else if (listView.resolvedStyle.width > 0)
{
return listView.resolvedStyle.width;
}
}
// treeList가 안 보이거나 레이아웃 계산 안 된 경우: 화면 시작
return 0f;
}
private void UpdateDragAndPanels(VisualElement root, VisualElement drag)
{
if (modelView == null || chartView == null) return;
// modelView의 현재 layout을 사용해서 dragBtn을 배치
// treeList가 보이는 경우를 고려하여 leftBound부터 시작
float leftBound = GetLeftBound();
var mv = modelView.layout;
// modelView의 실제 위치 계산 (treeList 포함)
float centerX = leftBound + mv.width; // leftBound 기준 + modelView width
// 적용
ApplyDragPosition(root, drag, centerX);
}
// 테스트용 데이터 생성
List<TreeListItemData> GenerateDummyData()
{
var root1 = new TreeListItemData { id = 1, name = "모델", isExpanded = true };
root1.Add(new TreeListItemData { id = 2, name = "모델1", parent = root1 });
root1.Add(new TreeListItemData { id = 3, name = "모델2", parent = root1, IsVisible = false });
var child3 = new TreeListItemData { id = 4, name = "모델3", parent = root1 };
child3.Add(new TreeListItemData { id = 5, name = "메쉬 A", parent = child3 });
root1.Add(child3);
return new List<TreeListItemData> { root1 };
}
private void OnDestroy()
{
if (listView != null)
{
listView.OnSelectionChanged -= OnListItemSelectionChanged;
listView.OnClosed -= OnListClosed;
listView.OnVisibilityChanged -= OnListVisibilityChanged;
}
if (modelView != null)
{
modelView.OnItemSelected -= OnModelItemSelected;
modelView.OnExpand -= ToggleExpandModel;
}
if (chartView != null)
{
chartView.OnExpand -= ToggleExpandChart;
}
if (showTreeBtn != null) showTreeBtn.clicked -= OnClickShowTree;
if (closeBtn != null) closeBtn.clicked -= OnClickClose;
}
}
}