586 lines
21 KiB
C#
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;
|
|
}
|
|
|
|
}
|
|
|
|
|
|
} |