playbar 추가, 모델 여러개 로드
This commit is contained in:
@@ -42,8 +42,12 @@ namespace SHI.Modal.ISOP
|
||||
/// </list>
|
||||
/// </summary>
|
||||
[UxmlElement]
|
||||
public partial class ISOPChart : VisualElement
|
||||
public partial class ISOPChart : VisualElement, IDisposable
|
||||
{
|
||||
#region IDisposable
|
||||
private bool _disposed = false;
|
||||
#endregion
|
||||
|
||||
#region 상수 (Constants)
|
||||
/// <summary>메인 UXML 파일 경로 (Resources 폴더 기준)</summary>
|
||||
private const string UXML_PATH = "SHI/Modal/ISOP/ISOPChart";
|
||||
@@ -525,5 +529,47 @@ namespace SHI.Modal.ISOP
|
||||
return 0;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region IDisposable
|
||||
/// <summary>
|
||||
/// 리소스를 해제하고 이벤트 핸들러를 정리합니다.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
|
||||
// 스크롤 이벤트 해제
|
||||
if (contentScroll != null)
|
||||
{
|
||||
contentScroll.horizontalScroller.valueChanged -= OnHorizontalScroll;
|
||||
}
|
||||
|
||||
// 외부 이벤트 정리
|
||||
OnExpand = null;
|
||||
|
||||
// 동적으로 생성된 UI 요소 정리
|
||||
monthsLayer?.Clear();
|
||||
weeksLayer?.Clear();
|
||||
daysLayer?.Clear();
|
||||
timelineContent?.Clear();
|
||||
|
||||
// 데이터 정리
|
||||
tasks?.Clear();
|
||||
tasks = null;
|
||||
|
||||
// UI 참조 정리
|
||||
_expandBtn = null;
|
||||
taskRowTemplate = null;
|
||||
root = null;
|
||||
headerTitle = null;
|
||||
headerTimeline = null;
|
||||
monthsLayer = null;
|
||||
weeksLayer = null;
|
||||
daysLayer = null;
|
||||
timelineContent = null;
|
||||
contentScroll = null;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,15 +48,15 @@ namespace SHI.Modal.ISOP
|
||||
[SerializeField]
|
||||
public UIDocument uiDocument;
|
||||
|
||||
private VisualElement content;
|
||||
private VisualElement? content;
|
||||
|
||||
private TreeList listView;
|
||||
private ISOPModelView modelView;
|
||||
private ISOPChart chartView;
|
||||
private TreeList? listView;
|
||||
private ISOPModelView? modelView;
|
||||
private ISOPChart? chartView;
|
||||
|
||||
private Button closeBtn;
|
||||
private Button showTreeBtn;
|
||||
private Button dragBtn;
|
||||
private Button? closeBtn;
|
||||
private Button? showTreeBtn;
|
||||
private Button? dragBtn;
|
||||
|
||||
private CancellationTokenSource? _cts;
|
||||
private bool _suppressSelection = false;
|
||||
@@ -79,6 +79,14 @@ namespace SHI.Modal.ISOP
|
||||
private float _lastModelFlexGrow = 1f;
|
||||
private float _lastChartFlexGrow = 1f;
|
||||
|
||||
// GeometryChangedEvent 콜백 (해제용)
|
||||
private EventCallback<GeometryChangedEvent>? _contentGeometryChangedCallback;
|
||||
|
||||
// 로딩 UI
|
||||
private VisualElement? _loadingOverlay;
|
||||
private VisualElement? _loadingSpinner;
|
||||
private IVisualElementScheduledItem? _spinnerAnimation;
|
||||
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
@@ -140,8 +148,12 @@ namespace SHI.Modal.ISOP
|
||||
}
|
||||
|
||||
initDrag(root);
|
||||
|
||||
|
||||
_expanded = ExpandedSide.None;
|
||||
|
||||
// 로딩 UI 참조
|
||||
_loadingOverlay = root.Q<VisualElement>("loading-overlay");
|
||||
_loadingSpinner = root.Q<VisualElement>("loading-spinner");
|
||||
}
|
||||
|
||||
private void initDrag(VisualElement root)
|
||||
@@ -158,103 +170,107 @@ namespace SHI.Modal.ISOP
|
||||
|
||||
//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);
|
||||
// 드래그 이벤트 등록
|
||||
dragBtn.RegisterCallback<PointerDownEvent>(OnDragPointerDown, TrickleDown.TrickleDown);
|
||||
dragBtn.RegisterCallback<PointerMoveEvent>(OnDragPointerMove, TrickleDown.TrickleDown);
|
||||
dragBtn.RegisterCallback<PointerUpEvent>(OnDragPointerUp, TrickleDown.TrickleDown);
|
||||
dragBtn.RegisterCallback<PointerCancelEvent>(OnDragPointerCancel, 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);
|
||||
});
|
||||
_contentGeometryChangedCallback = OnContentGeometryChanged;
|
||||
content.RegisterCallback(_contentGeometryChangedCallback);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDragPointerDown(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();
|
||||
}
|
||||
|
||||
private void OnDragPointerMove(PointerMoveEvent evt)
|
||||
{
|
||||
if (!_isDragging) return;
|
||||
if (evt.pointerId != _activePointerId) return;
|
||||
|
||||
// evt.position은 content 기준 좌표
|
||||
float pointerX = evt.position.x;
|
||||
float centerX = pointerX - _dragOffset;
|
||||
ApplyDragPosition(content, dragBtn, centerX);
|
||||
evt.StopImmediatePropagation();
|
||||
}
|
||||
|
||||
private void OnDragPointerUp(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();
|
||||
}
|
||||
|
||||
private void OnDragPointerCancel(PointerCancelEvent evt)
|
||||
{
|
||||
if (!_isDragging) return;
|
||||
if (evt.pointerId != _activePointerId) return;
|
||||
|
||||
_isDragging = false;
|
||||
if (_activePointerId != -1)
|
||||
{
|
||||
try { dragBtn.ReleasePointer(_activePointerId); } catch { }
|
||||
}
|
||||
_activePointerId = -1;
|
||||
evt.StopImmediatePropagation();
|
||||
}
|
||||
|
||||
private bool _geometryInitialized = false;
|
||||
private void OnContentGeometryChanged(GeometryChangedEvent evt)
|
||||
{
|
||||
// 드래그 중에는 GeometryChanged 이벤트 무시
|
||||
if (_isDragging) return;
|
||||
|
||||
// 초기화: treeList의 레이아웃이 계산될 때까지 대기
|
||||
if (!_geometryInitialized)
|
||||
{
|
||||
if (listView == null || listView.layout.width <= 0)
|
||||
{
|
||||
return; // 아직 레이아웃이 계산되지 않음
|
||||
}
|
||||
_geometryInitialized = 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)
|
||||
public async UniTask LoadData(List<string> gltfPaths, string ganttPath, CancellationToken externalCt = default)
|
||||
{
|
||||
if(modelView == null)
|
||||
{
|
||||
@@ -263,38 +279,103 @@ namespace SHI.Modal.ISOP
|
||||
{
|
||||
await UniTask.Yield();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
Debug.Log($"ISOPModal: LoadData {gltfPath}");
|
||||
|
||||
Debug.Log($"ISOPModal: LoadData {string.Join(", ", gltfPaths)}, {ganttPath}");
|
||||
|
||||
// 이전 작업 취소
|
||||
if (_cts != null)
|
||||
{
|
||||
try { _cts.Cancel(); } catch { }
|
||||
_cts.Dispose();
|
||||
}
|
||||
_cts = CancellationTokenSource.CreateLinkedTokenSource(externalCt);
|
||||
var ct = _cts.Token;
|
||||
// 로딩 표시
|
||||
ShowLoading(true);
|
||||
|
||||
// 모델/리스트 로드
|
||||
IEnumerable<TreeListItemData> items = Array.Empty<TreeListItemData>();
|
||||
if (modelView != null)
|
||||
try
|
||||
{
|
||||
try
|
||||
// 이전 작업 취소
|
||||
if (_cts != null)
|
||||
{
|
||||
items = await modelView.LoadModelAsync(gltfPath, ct);
|
||||
try { _cts.Cancel(); } catch { }
|
||||
_cts.Dispose();
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
_cts = CancellationTokenSource.CreateLinkedTokenSource(externalCt);
|
||||
var ct = _cts.Token;
|
||||
|
||||
// 모델/리스트 로드
|
||||
IEnumerable<TreeListItemData> items = Array.Empty<TreeListItemData>();
|
||||
if (modelView != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
items = await modelView.LoadModelAsync(gltfPaths, ct);
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
}
|
||||
|
||||
BuildKeyMaps(items);
|
||||
|
||||
if (listView != null) listView.SetData(items.ToList());
|
||||
if (chartView != null) chartView.Load(ganttPath);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 로딩 숨기기
|
||||
ShowLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
#region 로딩 UI (Loading UI)
|
||||
/// <summary>
|
||||
/// 로딩 오버레이와 스피너 애니메이션을 표시하거나 숨깁니다.
|
||||
/// </summary>
|
||||
private void ShowLoading(bool show)
|
||||
{
|
||||
if (_loadingOverlay == null) return;
|
||||
|
||||
if (show)
|
||||
{
|
||||
_loadingOverlay.AddToClassList("visible");
|
||||
StartSpinnerAnimation();
|
||||
}
|
||||
else
|
||||
{
|
||||
_loadingOverlay.RemoveFromClassList("visible");
|
||||
StopSpinnerAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 스피너 회전 애니메이션 시작
|
||||
/// </summary>
|
||||
private void StartSpinnerAnimation()
|
||||
{
|
||||
if (_loadingSpinner == null) return;
|
||||
|
||||
StopSpinnerAnimation();
|
||||
|
||||
float angle = 0f;
|
||||
_spinnerAnimation = _loadingSpinner.schedule.Execute(() =>
|
||||
{
|
||||
angle = (angle + 10f) % 360f;
|
||||
_loadingSpinner.style.rotate = new Rotate(Angle.Degrees(angle));
|
||||
}).Every(16); // ~60fps
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 스피너 회전 애니메이션 중지
|
||||
/// </summary>
|
||||
private void StopSpinnerAnimation()
|
||||
{
|
||||
if (_spinnerAnimation != null)
|
||||
{
|
||||
_spinnerAnimation.Pause();
|
||||
_spinnerAnimation = null;
|
||||
}
|
||||
|
||||
BuildKeyMaps(items);
|
||||
|
||||
if (listView != null) listView.SetData(items.ToList());
|
||||
if (chartView != null) chartView.Load(ganttPath);
|
||||
if (_loadingSpinner != null)
|
||||
{
|
||||
_loadingSpinner.style.rotate = new Rotate(Angle.Degrees(0));
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
private void BuildKeyMaps(IEnumerable<TreeListItemData> items)
|
||||
{
|
||||
@@ -595,13 +676,24 @@ namespace SHI.Modal.ISOP
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
// CancellationTokenSource 정리
|
||||
if (_cts != null)
|
||||
{
|
||||
try { _cts.Cancel(); } catch { }
|
||||
_cts.Dispose();
|
||||
_cts = null;
|
||||
}
|
||||
|
||||
// listView 이벤트 해제 및 Dispose
|
||||
if (listView != null)
|
||||
{
|
||||
listView.OnSelectionChanged -= OnListItemSelectionChanged;
|
||||
listView.OnClosed -= OnListClosed;
|
||||
listView.OnVisibilityChanged -= OnListVisibilityChanged;
|
||||
listView.Dispose();
|
||||
}
|
||||
|
||||
// modelView 이벤트 해제 및 Dispose
|
||||
if (modelView != null)
|
||||
{
|
||||
modelView.OnItemSelected -= OnModelItemSelected;
|
||||
@@ -609,13 +701,56 @@ namespace SHI.Modal.ISOP
|
||||
modelView.Dispose();
|
||||
}
|
||||
|
||||
// chartView 이벤트 해제 및 Dispose
|
||||
if (chartView != null)
|
||||
{
|
||||
chartView.OnExpand -= ToggleExpandChart;
|
||||
chartView.Dispose();
|
||||
}
|
||||
|
||||
// 버튼 이벤트 해제
|
||||
if (showTreeBtn != null) showTreeBtn.clicked -= OnClickShowTree;
|
||||
if (closeBtn != null) closeBtn.clicked -= OnClickClose;
|
||||
|
||||
// dragBtn 포인터 이벤트 해제
|
||||
if (dragBtn != null)
|
||||
{
|
||||
dragBtn.UnregisterCallback<PointerDownEvent>(OnDragPointerDown, TrickleDown.TrickleDown);
|
||||
dragBtn.UnregisterCallback<PointerMoveEvent>(OnDragPointerMove, TrickleDown.TrickleDown);
|
||||
dragBtn.UnregisterCallback<PointerUpEvent>(OnDragPointerUp, TrickleDown.TrickleDown);
|
||||
dragBtn.UnregisterCallback<PointerCancelEvent>(OnDragPointerCancel, TrickleDown.TrickleDown);
|
||||
}
|
||||
|
||||
// content GeometryChangedEvent 해제
|
||||
if (content != null && _contentGeometryChangedCallback != null)
|
||||
{
|
||||
content.UnregisterCallback(_contentGeometryChangedCallback);
|
||||
_contentGeometryChangedCallback = null;
|
||||
}
|
||||
|
||||
// 딕셔너리 정리
|
||||
_keyToId.Clear();
|
||||
_idToKey.Clear();
|
||||
|
||||
// 드래그 상태 초기화
|
||||
_isDragging = false;
|
||||
_activePointerId = -1;
|
||||
_dragOffset = 0f;
|
||||
_geometryInitialized = false;
|
||||
|
||||
// 로딩 UI 정리
|
||||
StopSpinnerAnimation();
|
||||
_loadingOverlay = null;
|
||||
_loadingSpinner = null;
|
||||
|
||||
// UI 참조 정리
|
||||
content = null;
|
||||
listView = null;
|
||||
modelView = null;
|
||||
chartView = null;
|
||||
closeBtn = null;
|
||||
showTreeBtn = null;
|
||||
dragBtn = null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -46,8 +46,12 @@ namespace SHI.Modal.ISOP
|
||||
/// </list>
|
||||
/// </summary>
|
||||
[UxmlElement]
|
||||
public partial class ISOPModelView : VisualElement
|
||||
public partial class ISOPModelView : VisualElement, IDisposable
|
||||
{
|
||||
#region IDisposable
|
||||
private bool _disposed = false;
|
||||
#endregion
|
||||
|
||||
#region 외부 이벤트 (Public Events)
|
||||
/// <summary>
|
||||
/// 뷰 내부에서 항목이 선택될 때 발생합니다.
|
||||
@@ -156,35 +160,55 @@ namespace SHI.Modal.ISOP
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 주어진 경로의 glTF 모델을 비동기로 로드하고, UI 트리에 사용할 계층 항목을 생성합니다.
|
||||
/// 주어진 경로들의 glTF 모델을 비동기로 로드하고, UI 트리에 사용할 계층 항목을 생성합니다.
|
||||
/// </summary>
|
||||
public async UniTask<IEnumerable<TreeListItemData>> LoadModelAsync(string path, CancellationToken ct)
|
||||
/// <param name="paths">로드할 glTF/glb 파일 경로 목록</param>
|
||||
/// <param name="ct">취소 토큰</param>
|
||||
/// <returns>로드된 모델들의 계층 항목 목록</returns>
|
||||
public async UniTask<IEnumerable<TreeListItemData>> LoadModelAsync(List<string> paths, CancellationToken ct)
|
||||
{
|
||||
Debug.Log($"ISOPModelView.LoadModelAsync: {path}");
|
||||
Dispose();
|
||||
Debug.Log($"ISOPModelView.LoadModelAsync: {paths?.Count ?? 0} files");
|
||||
|
||||
CleanupForReload();
|
||||
await UniTask.DelayFrame(1);
|
||||
EnsureCameraAndTargetTexture();
|
||||
|
||||
var items = new List<TreeListItemData>();
|
||||
var gltf = new GltfImport();
|
||||
var success = await gltf.Load(path, new ImportSettings(), ct);
|
||||
if (!success)
|
||||
|
||||
if (paths == null || paths.Count == 0)
|
||||
{
|
||||
Debug.LogError($"glTFast Load failed: {path}");
|
||||
Debug.LogWarning("ISOPModelView.LoadModelAsync: No paths provided");
|
||||
return items;
|
||||
}
|
||||
|
||||
if(_root == null) _root = new GameObject("ISOPModelViewRoot");
|
||||
if (_root == null) _root = new GameObject("ISOPModelViewRoot");
|
||||
_root.layer = modelLayer;
|
||||
var sceneOk = await gltf.InstantiateMainSceneAsync(_root.transform);
|
||||
if (!sceneOk)
|
||||
|
||||
// 각 파일을 순차적으로 로드
|
||||
foreach (var path in paths)
|
||||
{
|
||||
Debug.LogError("InstantiateMainSceneAsync failed");
|
||||
return items;
|
||||
if (string.IsNullOrEmpty(path)) continue;
|
||||
|
||||
Debug.Log($"ISOPModelView.LoadModelAsync: Loading {path}");
|
||||
var gltf = new GltfImport();
|
||||
var success = await gltf.Load(path, new ImportSettings(), ct);
|
||||
if (!success)
|
||||
{
|
||||
Debug.LogError($"glTFast Load failed: {path}");
|
||||
continue;
|
||||
}
|
||||
|
||||
var sceneOk = await gltf.InstantiateMainSceneAsync(_root.transform);
|
||||
if (!sceneOk)
|
||||
{
|
||||
Debug.LogError($"InstantiateMainSceneAsync failed: {path}");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
SetLayerRecursive(_root, modelLayer);
|
||||
|
||||
// 로드된 모든 자식에서 TreeListItemData 생성
|
||||
if (_root != null)
|
||||
{
|
||||
for (int i = 0; i < _root.transform.childCount; i++)
|
||||
@@ -600,8 +624,13 @@ namespace SHI.Modal.ISOP
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
/// <summary>
|
||||
/// 모델 재로드를 위한 리소스 정리.
|
||||
/// Dispose()와 달리 UI 참조(_renderContainer, _expandBtn)는 유지합니다.
|
||||
/// </summary>
|
||||
private void CleanupForReload()
|
||||
{
|
||||
// 머티리얼 정리 (인스턴스 머티리얼 삭제)
|
||||
foreach (var kv in _originalSharedByRenderer)
|
||||
{
|
||||
var r = kv.Key;
|
||||
@@ -616,11 +645,14 @@ namespace SHI.Modal.ISOP
|
||||
}
|
||||
_originalSharedByRenderer.Clear();
|
||||
_idToObject.Clear();
|
||||
|
||||
// 모델 루트 삭제
|
||||
if (_root != null) UnityEngine.Object.Destroy(_root);
|
||||
_root = null;
|
||||
_focusedId = null;
|
||||
_wireframeApplied = false;
|
||||
|
||||
// 카메라 및 렌더텍스처 정리
|
||||
if (_viewCamera != null)
|
||||
{
|
||||
if (_rt != null && _viewCamera.targetTexture == _rt)
|
||||
@@ -629,6 +661,7 @@ namespace SHI.Modal.ISOP
|
||||
}
|
||||
_viewCamera.enabled = false;
|
||||
}
|
||||
|
||||
if (_rt != null)
|
||||
{
|
||||
_rt.Release();
|
||||
@@ -636,13 +669,95 @@ namespace SHI.Modal.ISOP
|
||||
_rt = null;
|
||||
}
|
||||
|
||||
//ISOPModelViewRig란 이름의 게임오브젝트도 같이 삭제
|
||||
// 드래그 상태 초기화
|
||||
_mmbDragging = false;
|
||||
_rmbDragging = false;
|
||||
|
||||
// ID 시드 초기화
|
||||
itemIdSeed = 1;
|
||||
|
||||
// 게임오브젝트 정리
|
||||
var rigGo = GameObject.Find("ISOPModelViewRig");
|
||||
if (rigGo != null) UnityEngine.Object.Destroy(rigGo);
|
||||
_viewCamera = null;
|
||||
|
||||
var rootGo = GameObject.Find("ISOPModelViewRoot");
|
||||
if (rootGo != null) UnityEngine.Object.Destroy(rootGo);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
|
||||
// 마우스 이벤트 해제
|
||||
UnregisterCallback<MouseDownEvent>(OnMouseDown);
|
||||
UnregisterCallback<MouseUpEvent>(OnMouseUp);
|
||||
UnregisterCallback<MouseMoveEvent>(OnMouseMove);
|
||||
UnregisterCallback<WheelEvent>(OnWheel);
|
||||
UnregisterCallback<GeometryChangedEvent>(OnGeometryChanged);
|
||||
|
||||
// 외부 이벤트 정리
|
||||
OnItemSelected = null;
|
||||
OnExpand = null;
|
||||
|
||||
// 머티리얼 정리 (인스턴스 머티리얼 삭제)
|
||||
foreach (var kv in _originalSharedByRenderer)
|
||||
{
|
||||
var r = kv.Key;
|
||||
if (r == null) continue;
|
||||
var originals = kv.Value;
|
||||
var mats = r.materials;
|
||||
for (int m = 0; m < mats.Length; m++)
|
||||
{
|
||||
if (mats[m] != null) UnityEngine.Object.Destroy(mats[m]);
|
||||
}
|
||||
r.materials = originals;
|
||||
}
|
||||
_originalSharedByRenderer.Clear();
|
||||
_idToObject.Clear();
|
||||
|
||||
// 모델 루트 삭제
|
||||
if (_root != null) UnityEngine.Object.Destroy(_root);
|
||||
_root = null;
|
||||
_focusedId = null;
|
||||
_wireframeApplied = false;
|
||||
_wireframeMat = null; // Resources에서 로드한 것은 Destroy하지 않음
|
||||
|
||||
// 카메라 및 렌더텍스처 정리
|
||||
if (_viewCamera != null)
|
||||
{
|
||||
if (_rt != null && _viewCamera.targetTexture == _rt)
|
||||
{
|
||||
_viewCamera.targetTexture = null;
|
||||
}
|
||||
_viewCamera.enabled = false;
|
||||
}
|
||||
_viewCamera = null;
|
||||
|
||||
if (_rt != null)
|
||||
{
|
||||
_rt.Release();
|
||||
UnityEngine.Object.Destroy(_rt);
|
||||
_rt = null;
|
||||
}
|
||||
|
||||
// 드래그 상태 초기화
|
||||
_mmbDragging = false;
|
||||
_rmbDragging = false;
|
||||
|
||||
// ID 시드 초기화
|
||||
itemIdSeed = 1;
|
||||
|
||||
// UI 참조 정리
|
||||
_renderContainer = null;
|
||||
_expandBtn = null;
|
||||
|
||||
// 게임오브젝트 정리
|
||||
var rigGo = GameObject.Find("ISOPModelViewRig");
|
||||
if (rigGo != null) UnityEngine.Object.Destroy(rigGo);
|
||||
var rootGo = GameObject.Find("ISOPModelViewRoot");
|
||||
if (rootGo != null) UnityEngine.Object.Destroy(rootGo);
|
||||
|
||||
|
||||
}
|
||||
|
||||
// 유틸리티 메서드
|
||||
|
||||
@@ -64,8 +64,12 @@ namespace SHI.Modal.NW
|
||||
/// </list>
|
||||
/// </summary>
|
||||
[UxmlElement]
|
||||
public partial class NWChart : VisualElement
|
||||
public partial class NWChart : VisualElement, IDisposable
|
||||
{
|
||||
#region IDisposable
|
||||
private bool _disposed = false;
|
||||
#endregion
|
||||
|
||||
private const string UXML_PATH = "SHI/Modal/NW/NWChart";
|
||||
|
||||
public Action? OnExpand;
|
||||
@@ -123,7 +127,9 @@ namespace SHI.Modal.NW
|
||||
private bool isDragging;
|
||||
|
||||
private DateTime projectStartDate;
|
||||
public DateTime ProjectStartDate => projectStartDate;
|
||||
private DateTime projectEndDate;
|
||||
public DateTime ProjectEndDate => projectEndDate;
|
||||
private int totalDays;
|
||||
private float canvasHeight;
|
||||
|
||||
@@ -142,10 +148,15 @@ namespace SHI.Modal.NW
|
||||
_expandBtn = this.Q<Button>("expand-btn");
|
||||
if (_expandBtn != null)
|
||||
{
|
||||
_expandBtn.clicked += () => OnExpand?.Invoke();
|
||||
_expandBtn.clicked += OnExpandBtnClicked;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnExpandBtnClicked()
|
||||
{
|
||||
OnExpand?.Invoke();
|
||||
}
|
||||
|
||||
public void Load(string jsonFileName)
|
||||
{
|
||||
root = this;
|
||||
@@ -157,6 +168,45 @@ namespace SHI.Modal.NW
|
||||
RenderNetwork();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 지정한 날짜가 STDT~FNDT 범위에 포함되는 작업들의 BLK_NO 목록을 반환합니다.
|
||||
/// </summary>
|
||||
/// <param name="yyyymmdd">조회할 날짜 (yyyyMMdd 형식, 예: "20250818")</param>
|
||||
/// <returns>해당 날짜에 진행 중인 블록 번호 목록</returns>
|
||||
public List<string> GetModelNamesByDate(string yyyymmdd)
|
||||
{
|
||||
var result = new List<string>();
|
||||
if (tasks == null || string.IsNullOrEmpty(yyyymmdd)) return result;
|
||||
|
||||
foreach (var task in tasks)
|
||||
{
|
||||
// BLK_NO가 없으면 스킵
|
||||
if (string.IsNullOrEmpty(task.BLK_NO)) continue;
|
||||
|
||||
// STDT, FNDT 가져오기
|
||||
string stdt = task.STDT;
|
||||
string fndt = task.FNDT;
|
||||
|
||||
// STDT가 없으면 스킵
|
||||
if (string.IsNullOrEmpty(stdt) || stdt == "null") continue;
|
||||
|
||||
// FNDT가 없으면 STDT와 동일하게 처리
|
||||
if (string.IsNullOrEmpty(fndt) || fndt == "null") fndt = stdt;
|
||||
|
||||
// 문자열 비교로 범위 체크 (yyyyMMdd 형식은 문자열 비교로 날짜 비교 가능)
|
||||
if (string.CompareOrdinal(stdt, yyyymmdd) <= 0 && string.CompareOrdinal(yyyymmdd, fndt) <= 0)
|
||||
{
|
||||
// 중복 방지
|
||||
if (!result.Contains(task.BLK_NO))
|
||||
{
|
||||
result.Add(task.BLK_NO);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void LoadData(string jsonFileName)
|
||||
{
|
||||
var json = File.ReadAllText(jsonFileName);
|
||||
@@ -428,7 +478,7 @@ namespace SHI.Modal.NW
|
||||
var task = tasks!.Find( (item) => item.STDT == date.ToString("yyyyMMdd"));
|
||||
if(task != null)
|
||||
{
|
||||
Debug.Log($"Found task for date {date:yyyyMMdd}: REL_TP={task.REL_TP}, PROJ_TP={task.PROJ_TP}");
|
||||
// Debug.Log($"Found task for date {date:yyyyMMdd}: REL_TP={task.REL_TP}, PROJ_TP={task.PROJ_TP}");
|
||||
var lab = new Label($"{task.REL_TP}\n{task.PROJ_TP}");
|
||||
lab.style.unityTextAlign = TextAnchor.MiddleRight;
|
||||
lab.style.width = Length.Percent(100);
|
||||
@@ -940,5 +990,69 @@ namespace SHI.Modal.NW
|
||||
if (draggingActivityCode == null) return;
|
||||
UpdateConnectionsForNode(draggingActivityCode);
|
||||
}
|
||||
|
||||
#region IDisposable
|
||||
/// <summary>
|
||||
/// 리소스를 해제하고 이벤트 핸들러를 정리합니다.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
|
||||
// 버튼 이벤트 해제
|
||||
if (_expandBtn != null)
|
||||
{
|
||||
_expandBtn.clicked -= OnExpandBtnClicked;
|
||||
}
|
||||
|
||||
// 스크롤 이벤트 해제
|
||||
if (contentScroll != null)
|
||||
{
|
||||
contentScroll.horizontalScroller.valueChanged -= OnHorizontalScroll;
|
||||
}
|
||||
|
||||
// 노드 드래그 이벤트 해제
|
||||
foreach (var node in nodeElements.Values)
|
||||
{
|
||||
node.UnregisterCallback<PointerDownEvent>(OnNodePointerDown);
|
||||
node.UnregisterCallback<PointerMoveEvent>(OnNodePointerMove);
|
||||
node.UnregisterCallback<PointerUpEvent>(OnNodePointerUp);
|
||||
}
|
||||
|
||||
// 외부 이벤트 정리
|
||||
OnExpand = null;
|
||||
|
||||
// 동적으로 생성된 UI 요소 정리
|
||||
monthsLayer?.Clear();
|
||||
daysLayer?.Clear();
|
||||
nodesLayer?.Clear();
|
||||
linesLayer?.Clear();
|
||||
|
||||
// 데이터 정리
|
||||
tasks?.Clear();
|
||||
tasks = null;
|
||||
tasksByActivityCode.Clear();
|
||||
nodeElements.Clear();
|
||||
nodePositions.Clear();
|
||||
connectionCache.Clear();
|
||||
|
||||
// 드래그 상태 정리
|
||||
draggingNode = null;
|
||||
draggingActivityCode = null;
|
||||
isDragging = false;
|
||||
|
||||
// UI 참조 정리
|
||||
_expandBtn = null;
|
||||
root = null;
|
||||
headerTimeline = null;
|
||||
monthsLayer = null;
|
||||
daysLayer = null;
|
||||
networkCanvas = null;
|
||||
nodesLayer = null;
|
||||
linesLayer = null;
|
||||
contentScroll = null;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,13 +50,14 @@ namespace SHI.Modal.NW
|
||||
[SerializeField]
|
||||
public UIDocument uiDocument;
|
||||
|
||||
private TreeList listView;
|
||||
private NWModelView modelView;
|
||||
private NWChart chartView;
|
||||
private TreeList? listView;
|
||||
private NWModelView? modelView;
|
||||
private NWChart? chartView;
|
||||
private PlayBar? _playBar;
|
||||
|
||||
private Button closeBtn;
|
||||
private Button showTreeBtn;
|
||||
private Button dragBtn;
|
||||
private Button? closeBtn;
|
||||
private Button? showTreeBtn;
|
||||
private Button? dragBtn;
|
||||
|
||||
private VisualElement? rightPanel;
|
||||
|
||||
@@ -81,6 +82,13 @@ namespace SHI.Modal.NW
|
||||
private float _lastModelFlexGrow = 1f;
|
||||
private float _lastChartFlexGrow = 1f;
|
||||
|
||||
// GeometryChangedEvent 콜백 (해제용)
|
||||
private EventCallback<GeometryChangedEvent>? _rightPanelGeometryChangedCallback;
|
||||
|
||||
// 로딩 UI
|
||||
private VisualElement? _loadingOverlay;
|
||||
private VisualElement? _loadingSpinner;
|
||||
private IVisualElementScheduledItem? _spinnerAnimation;
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
@@ -141,11 +149,45 @@ namespace SHI.Modal.NW
|
||||
closeBtn.clicked += OnClickClose;
|
||||
}
|
||||
|
||||
_playBar = root.Q<PlayBar>("play-bar");
|
||||
if (_playBar != null)
|
||||
{
|
||||
_playBar.SetTimeRange(DateTime.Now, DateTime.Now.AddHours(1));
|
||||
_playBar.OnPlayProgress += OnPlayProgressHandler;
|
||||
_playBar.OnPositionChanged += OnPlayPositionChangedHandler;
|
||||
}
|
||||
|
||||
initDrag(root);
|
||||
|
||||
|
||||
_expanded = ExpandedSide.None;
|
||||
|
||||
// 로딩 UI 참조
|
||||
_loadingOverlay = root.Q<VisualElement>("loading-overlay");
|
||||
_loadingSpinner = root.Q<VisualElement>("loading-spinner");
|
||||
}
|
||||
|
||||
private void OnPlayProgressHandler(DateTime time)
|
||||
{
|
||||
if(chartView != null && listView != null)
|
||||
{
|
||||
List<string> models = chartView.GetModelNamesByDate(time.ToString("yyyyMMdd"));
|
||||
Debug.Log($"Models at {time:yyyyMMdd}: {string.Join(", ", models)}");
|
||||
if(models.Count > 0) listView.ShowItems(models);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPlayPositionChangedHandler(DateTime time)
|
||||
{
|
||||
if(chartView != null && listView != null)
|
||||
{
|
||||
List<string> models = chartView.GetModelNamesByDate(time.ToString("yyyyMMdd"));
|
||||
Debug.Log($"Models at {time:yyyyMMdd}: {string.Join(", ", models)}");
|
||||
if(models.Count > 0) listView.ShowItems(models);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void initDrag(VisualElement root)
|
||||
{
|
||||
dragBtn = root.Q<Button>("drag-btn");
|
||||
@@ -157,100 +199,114 @@ namespace SHI.Modal.NW
|
||||
dragBtn.style.position = Position.Absolute;
|
||||
dragBtn.pickingMode = PickingMode.Position;
|
||||
|
||||
// 드래그 시작
|
||||
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 기준, Y축)
|
||||
var dragCenterY = dragBtn.layout.y + dragBtn.layout.height * 0.5f;
|
||||
_dragOffset = evt.position.y - dragCenterY;
|
||||
|
||||
evt.StopImmediatePropagation();
|
||||
}, TrickleDown.TrickleDown);
|
||||
|
||||
// 전역 포인터 무브로 위치 추적
|
||||
dragBtn.RegisterCallback<PointerMoveEvent>((evt) =>
|
||||
{
|
||||
if (!_isDragging) return;
|
||||
if (evt.pointerId != _activePointerId) return;
|
||||
|
||||
// evt.position은 rightPanel 기준 좌표
|
||||
float pointerY = evt.position.y;
|
||||
float centerY = pointerY - _dragOffset;
|
||||
ApplyDragPosition(rightPanel, dragBtn, centerY);
|
||||
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);
|
||||
|
||||
// 드래그 이벤트 등록
|
||||
dragBtn.RegisterCallback<PointerDownEvent>(OnDragPointerDown, TrickleDown.TrickleDown);
|
||||
dragBtn.RegisterCallback<PointerMoveEvent>(OnDragPointerMove, TrickleDown.TrickleDown);
|
||||
dragBtn.RegisterCallback<PointerUpEvent>(OnDragPointerUp, TrickleDown.TrickleDown);
|
||||
dragBtn.RegisterCallback<PointerCancelEvent>(OnDragPointerCancel, TrickleDown.TrickleDown);
|
||||
|
||||
// 초기화 및 레이아웃 변경 시 재계산
|
||||
bool initialized = false;
|
||||
rightPanel.RegisterCallback<GeometryChangedEvent>((evt) =>
|
||||
{
|
||||
// 드래그 중에는 GeometryChanged 이벤트 무시
|
||||
if (_isDragging) return;
|
||||
|
||||
// 초기화: rightPanel의 레이아웃이 계산될 때까지 대기
|
||||
if (!initialized)
|
||||
{
|
||||
if (rightPanel.layout.height <= 0)
|
||||
{
|
||||
return; // 아직 레이아웃이 계산되지 않음
|
||||
}
|
||||
initialized = true;
|
||||
}
|
||||
UpdateDragAndPanels(rightPanel, dragBtn);
|
||||
});
|
||||
_rightPanelGeometryChangedCallback = OnRightPanelGeometryChanged;
|
||||
rightPanel.RegisterCallback(_rightPanelGeometryChangedCallback);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDragPointerDown(PointerDownEvent evt)
|
||||
{
|
||||
// 좌클릭만 처리 (0)
|
||||
if (evt.button != 0) return;
|
||||
|
||||
Debug.Log("Drag Started (PointerDown) - captured");
|
||||
|
||||
// 포인터 캡처
|
||||
_isDragging = true;
|
||||
_activePointerId = evt.pointerId;
|
||||
dragBtn?.CapturePointer(_activePointerId);
|
||||
|
||||
// 포인터가 drag 버튼의 어느 위치를 눌렀는지 계산 (center 기준, Y축)
|
||||
if (dragBtn != null)
|
||||
{
|
||||
var dragCenterY = dragBtn.layout.y + dragBtn.layout.height * 0.5f;
|
||||
_dragOffset = evt.position.y - dragCenterY;
|
||||
}
|
||||
|
||||
evt.StopImmediatePropagation();
|
||||
}
|
||||
|
||||
private void OnDragPointerMove(PointerMoveEvent evt)
|
||||
{
|
||||
if (!_isDragging) return;
|
||||
if (evt.pointerId != _activePointerId) return;
|
||||
|
||||
// evt.position은 rightPanel 기준 좌표
|
||||
float pointerY = evt.position.y;
|
||||
float centerY = pointerY - _dragOffset;
|
||||
if (rightPanel != null && dragBtn != null)
|
||||
{
|
||||
ApplyDragPosition(rightPanel, dragBtn, centerY);
|
||||
}
|
||||
evt.StopImmediatePropagation();
|
||||
}
|
||||
|
||||
private void OnDragPointerUp(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();
|
||||
}
|
||||
|
||||
private void OnDragPointerCancel(PointerCancelEvent evt)
|
||||
{
|
||||
if (!_isDragging) return;
|
||||
if (evt.pointerId != _activePointerId) return;
|
||||
|
||||
_isDragging = false;
|
||||
if (_activePointerId != -1)
|
||||
{
|
||||
try { dragBtn?.ReleasePointer(_activePointerId); } catch { }
|
||||
}
|
||||
_activePointerId = -1;
|
||||
evt.StopImmediatePropagation();
|
||||
}
|
||||
|
||||
private bool _geometryInitialized = false;
|
||||
private void OnRightPanelGeometryChanged(GeometryChangedEvent evt)
|
||||
{
|
||||
// 드래그 중에는 GeometryChanged 이벤트 무시
|
||||
if (_isDragging) return;
|
||||
|
||||
// 초기화: rightPanel의 레이아웃이 계산될 때까지 대기
|
||||
if (!_geometryInitialized)
|
||||
{
|
||||
if (rightPanel == null || rightPanel.layout.height <= 0)
|
||||
{
|
||||
return; // 아직 레이아웃이 계산되지 않음
|
||||
}
|
||||
_geometryInitialized = true;
|
||||
}
|
||||
if (rightPanel != null && dragBtn != null)
|
||||
{
|
||||
UpdateDragAndPanels(rightPanel, dragBtn);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// glTF 모델과 간트 데이터를 하위 뷰에 로드하고, 선택 동기화를 위한 매핑을 구축합니다.
|
||||
/// </summary>
|
||||
/// <param name="gltfPath">glTF/glb 파일 경로.</param>
|
||||
/// <param name="gltfPaths">glTF/glb 파일 경로 목록.</param>
|
||||
/// <param name="ganttPath">간트 데이터셋 경로.</param>
|
||||
/// <param name="externalCt">외부 취소 토큰.</param>
|
||||
public async UniTask LoadData(string gltfPath, string ganttPath, CancellationToken externalCt = default)
|
||||
public async UniTask LoadData(List<string> gltfPaths, string ganttPath, CancellationToken externalCt = default)
|
||||
{
|
||||
if(modelView == null)
|
||||
{
|
||||
@@ -259,38 +315,107 @@ namespace SHI.Modal.NW
|
||||
{
|
||||
await UniTask.Yield();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
Debug.Log($"NWModal: LoadData {gltfPath}");
|
||||
|
||||
Debug.Log($"NWModal.LoadData: gltfPath:{string.Join(", ", gltfPaths)}, ganttPath={ganttPath}");
|
||||
|
||||
// 이전 작업 취소
|
||||
if (_cts != null)
|
||||
{
|
||||
try { _cts.Cancel(); } catch { }
|
||||
_cts.Dispose();
|
||||
}
|
||||
_cts = CancellationTokenSource.CreateLinkedTokenSource(externalCt);
|
||||
var ct = _cts.Token;
|
||||
// 로딩 표시
|
||||
ShowLoading(true);
|
||||
|
||||
// 모델/리스트 로드
|
||||
IEnumerable<TreeListItemData> items = Array.Empty<TreeListItemData>();
|
||||
if (modelView != null)
|
||||
try
|
||||
{
|
||||
try
|
||||
// 이전 작업 취소
|
||||
if (_cts != null)
|
||||
{
|
||||
items = await modelView.LoadModelAsync(gltfPath, ct);
|
||||
try { _cts.Cancel(); } catch { }
|
||||
_cts.Dispose();
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
_cts = CancellationTokenSource.CreateLinkedTokenSource(externalCt);
|
||||
var ct = _cts.Token;
|
||||
|
||||
// 모델/리스트 로드
|
||||
IEnumerable<TreeListItemData> items = Array.Empty<TreeListItemData>();
|
||||
if (modelView != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
items = await modelView.LoadModelAsync(gltfPaths, ct);
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
}
|
||||
|
||||
BuildKeyMaps(items);
|
||||
|
||||
if (listView != null) listView.SetData(items.ToList());
|
||||
if (chartView != null) chartView.Load(ganttPath);
|
||||
if (_playBar != null && chartView != null)
|
||||
{
|
||||
_playBar.SetTimeRange(chartView.ProjectStartDate, chartView.ProjectEndDate);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 로딩 숨기기
|
||||
ShowLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
#region 로딩 UI (Loading UI)
|
||||
/// <summary>
|
||||
/// 로딩 오버레이와 스피너 애니메이션을 표시하거나 숨깁니다.
|
||||
/// </summary>
|
||||
private void ShowLoading(bool show)
|
||||
{
|
||||
if (_loadingOverlay == null) return;
|
||||
|
||||
if (show)
|
||||
{
|
||||
_loadingOverlay.AddToClassList("visible");
|
||||
StartSpinnerAnimation();
|
||||
}
|
||||
else
|
||||
{
|
||||
_loadingOverlay.RemoveFromClassList("visible");
|
||||
StopSpinnerAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 스피너 회전 애니메이션 시작
|
||||
/// </summary>
|
||||
private void StartSpinnerAnimation()
|
||||
{
|
||||
if (_loadingSpinner == null) return;
|
||||
|
||||
StopSpinnerAnimation();
|
||||
|
||||
float angle = 0f;
|
||||
_spinnerAnimation = _loadingSpinner.schedule.Execute(() =>
|
||||
{
|
||||
angle = (angle + 10f) % 360f;
|
||||
_loadingSpinner.style.rotate = new Rotate(Angle.Degrees(angle));
|
||||
}).Every(16); // ~60fps
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 스피너 회전 애니메이션 중지
|
||||
/// </summary>
|
||||
private void StopSpinnerAnimation()
|
||||
{
|
||||
if (_spinnerAnimation != null)
|
||||
{
|
||||
_spinnerAnimation.Pause();
|
||||
_spinnerAnimation = null;
|
||||
}
|
||||
|
||||
BuildKeyMaps(items);
|
||||
|
||||
if (listView != null) listView.SetData(items.ToList());
|
||||
if (chartView != null) chartView.Load(ganttPath);
|
||||
if (_loadingSpinner != null)
|
||||
{
|
||||
_loadingSpinner.style.rotate = new Rotate(Angle.Degrees(0));
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
private void BuildKeyMaps(IEnumerable<TreeListItemData> items)
|
||||
{
|
||||
@@ -566,13 +691,24 @@ namespace SHI.Modal.NW
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
// CancellationTokenSource 정리
|
||||
if (_cts != null)
|
||||
{
|
||||
try { _cts.Cancel(); } catch { }
|
||||
_cts.Dispose();
|
||||
_cts = null;
|
||||
}
|
||||
|
||||
// listView 이벤트 해제 및 Dispose
|
||||
if (listView != null)
|
||||
{
|
||||
listView.OnSelectionChanged -= OnListItemSelectionChanged;
|
||||
listView.OnClosed -= OnListClosed;
|
||||
listView.OnVisibilityChanged -= OnListVisibilityChanged;
|
||||
listView.Dispose();
|
||||
}
|
||||
|
||||
// modelView 이벤트 해제 및 Dispose
|
||||
if (modelView != null)
|
||||
{
|
||||
modelView.OnItemSelected -= OnModelItemSelected;
|
||||
@@ -580,13 +716,63 @@ namespace SHI.Modal.NW
|
||||
modelView.Dispose();
|
||||
}
|
||||
|
||||
// chartView 이벤트 해제 및 Dispose
|
||||
if (chartView != null)
|
||||
{
|
||||
chartView.OnExpand -= ToggleExpandChart;
|
||||
chartView.Dispose();
|
||||
}
|
||||
|
||||
// 버튼 이벤트 해제
|
||||
if (showTreeBtn != null) showTreeBtn.clicked -= OnClickShowTree;
|
||||
if (closeBtn != null) closeBtn.clicked -= OnClickClose;
|
||||
|
||||
// dragBtn 포인터 이벤트 해제
|
||||
if (dragBtn != null)
|
||||
{
|
||||
dragBtn.UnregisterCallback<PointerDownEvent>(OnDragPointerDown, TrickleDown.TrickleDown);
|
||||
dragBtn.UnregisterCallback<PointerMoveEvent>(OnDragPointerMove, TrickleDown.TrickleDown);
|
||||
dragBtn.UnregisterCallback<PointerUpEvent>(OnDragPointerUp, TrickleDown.TrickleDown);
|
||||
dragBtn.UnregisterCallback<PointerCancelEvent>(OnDragPointerCancel, TrickleDown.TrickleDown);
|
||||
}
|
||||
|
||||
// rightPanel GeometryChangedEvent 해제
|
||||
if (rightPanel != null && _rightPanelGeometryChangedCallback != null)
|
||||
{
|
||||
rightPanel.UnregisterCallback(_rightPanelGeometryChangedCallback);
|
||||
_rightPanelGeometryChangedCallback = null;
|
||||
}
|
||||
|
||||
if( _playBar != null)
|
||||
{
|
||||
_playBar.OnPlayProgress -= OnPlayProgressHandler;
|
||||
_playBar.OnPositionChanged -= OnPlayPositionChangedHandler;
|
||||
_playBar.Dispose();
|
||||
}
|
||||
|
||||
// 딕셔너리 정리
|
||||
_keyToId.Clear();
|
||||
_idToKey.Clear();
|
||||
|
||||
// 드래그 상태 초기화
|
||||
_isDragging = false;
|
||||
_activePointerId = -1;
|
||||
_dragOffset = 0f;
|
||||
_geometryInitialized = false;
|
||||
|
||||
// 로딩 UI 정리
|
||||
StopSpinnerAnimation();
|
||||
_loadingOverlay = null;
|
||||
_loadingSpinner = null;
|
||||
|
||||
// UI 참조 정리
|
||||
rightPanel = null;
|
||||
listView = null;
|
||||
modelView = null;
|
||||
chartView = null;
|
||||
closeBtn = null;
|
||||
showTreeBtn = null;
|
||||
dragBtn = null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -46,8 +46,12 @@ namespace SHI.Modal.NW
|
||||
/// </list>
|
||||
/// </summary>
|
||||
[UxmlElement]
|
||||
public partial class NWModelView : VisualElement
|
||||
public partial class NWModelView : VisualElement, IDisposable
|
||||
{
|
||||
#region IDisposable
|
||||
private bool _disposed = false;
|
||||
#endregion
|
||||
|
||||
#region 외부 이벤트 (Public Events)
|
||||
/// <summary>
|
||||
/// 뷰 내부에서 항목이 선택될 때 발생합니다.
|
||||
@@ -141,10 +145,7 @@ namespace SHI.Modal.NW
|
||||
_expandBtn = this.Q<Button>("expand-btn");
|
||||
if(_expandBtn != null)
|
||||
{
|
||||
_expandBtn.clicked += () =>
|
||||
{
|
||||
OnExpand?.Invoke();
|
||||
};
|
||||
_expandBtn.clicked += OnExpandBtnClicked;
|
||||
}
|
||||
|
||||
// 마우스 이벤트 등록
|
||||
@@ -155,36 +156,60 @@ namespace SHI.Modal.NW
|
||||
RegisterCallback<GeometryChangedEvent>(OnGeometryChanged);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 주어진 경로의 glTF 모델을 비동기로 로드하고, UI 트리에 사용할 계층 항목을 생성합니다.
|
||||
/// </summary>
|
||||
public async UniTask<IEnumerable<TreeListItemData>> LoadModelAsync(string path, CancellationToken ct)
|
||||
private void OnExpandBtnClicked()
|
||||
{
|
||||
Debug.Log($"NWModelView.LoadModelAsync: {path}");
|
||||
Dispose();
|
||||
OnExpand?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 주어진 경로들의 glTF 모델을 비동기로 로드하고, UI 트리에 사용할 계층 항목을 생성합니다.
|
||||
/// </summary>
|
||||
/// <param name="paths">로드할 glTF/glb 파일 경로 목록</param>
|
||||
/// <param name="ct">취소 토큰</param>
|
||||
/// <returns>로드된 모델들의 계층 항목 목록</returns>
|
||||
public async UniTask<IEnumerable<TreeListItemData>> LoadModelAsync(List<string> paths, CancellationToken ct)
|
||||
{
|
||||
Debug.Log($"NWModelView.LoadModelAsync: {paths?.Count ?? 0} files");
|
||||
CleanupForReload();
|
||||
await UniTask.DelayFrame(1);
|
||||
EnsureCameraAndTargetTexture();
|
||||
|
||||
var items = new List<TreeListItemData>();
|
||||
var gltf = new GltfImport();
|
||||
var success = await gltf.Load(path, new ImportSettings(), ct);
|
||||
if (!success)
|
||||
|
||||
if (paths == null || paths.Count == 0)
|
||||
{
|
||||
Debug.LogError($"glTFast Load failed: {path}");
|
||||
Debug.LogWarning("NWModelView.LoadModelAsync: No paths provided");
|
||||
return items;
|
||||
}
|
||||
|
||||
if(_root == null) _root = new GameObject("NWModelViewRoot");
|
||||
if (_root == null) _root = new GameObject("NWModelViewRoot");
|
||||
_root.layer = modelLayer;
|
||||
var sceneOk = await gltf.InstantiateMainSceneAsync(_root.transform);
|
||||
if (!sceneOk)
|
||||
|
||||
// 각 파일을 순차적으로 로드
|
||||
foreach (var path in paths)
|
||||
{
|
||||
Debug.LogError("InstantiateMainSceneAsync failed");
|
||||
return items;
|
||||
if (string.IsNullOrEmpty(path)) continue;
|
||||
|
||||
Debug.Log($"NWModelView.LoadModelAsync: Loading {path}");
|
||||
var gltf = new GltfImport();
|
||||
var success = await gltf.Load(path, new ImportSettings(), ct);
|
||||
if (!success)
|
||||
{
|
||||
Debug.LogError($"glTFast Load failed: {path}");
|
||||
continue;
|
||||
}
|
||||
|
||||
var sceneOk = await gltf.InstantiateMainSceneAsync(_root.transform);
|
||||
if (!sceneOk)
|
||||
{
|
||||
Debug.LogError($"InstantiateMainSceneAsync failed: {path}");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
SetLayerRecursive(_root, modelLayer);
|
||||
|
||||
// 로드된 모든 자식에서 TreeListItemData 생성
|
||||
if (_root != null)
|
||||
{
|
||||
for (int i = 0; i < _root.transform.childCount; i++)
|
||||
@@ -313,7 +338,11 @@ namespace SHI.Modal.NW
|
||||
|
||||
private void EnsureRenderTargetSize()
|
||||
{
|
||||
if (_viewCamera == null || _renderContainer == null) return;
|
||||
if (_viewCamera == null || _renderContainer == null)
|
||||
{
|
||||
Debug.LogWarning($"[NWModelView] EnsureRenderTargetSize: camera={_viewCamera != null}, container={_renderContainer != null}");
|
||||
return;
|
||||
}
|
||||
|
||||
// 고정 크기 사용
|
||||
int w = FIXED_RT_WIDTH;
|
||||
@@ -377,12 +406,10 @@ namespace SHI.Modal.NW
|
||||
|
||||
public void FocusItemById(int id)
|
||||
{
|
||||
Debug.Log($"NWModelView.FocusItemById: id={id}");
|
||||
_focusedId = id;
|
||||
if (_idToObject.TryGetValue(id, out var go))
|
||||
{
|
||||
Highlight(go, true);
|
||||
Debug.Log($"NWModelView.FocusItemById: {go.name}");
|
||||
_orbitTarget = go.transform.position;
|
||||
}
|
||||
}
|
||||
@@ -398,17 +425,14 @@ namespace SHI.Modal.NW
|
||||
|
||||
public void Export(int id)
|
||||
{
|
||||
Debug.Log($"NWModelView.Export: id={id}");
|
||||
if (_idToObject.TryGetValue(id, out var go))
|
||||
{
|
||||
Debug.Log($"Exporting object: {go.name}");
|
||||
UVC.GLTF.GLTFExporter.ExportNodeByExplorer(go);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetVisibility(int id, bool on)
|
||||
{
|
||||
Debug.Log($"NWModelView.SetVisibility: id={id} on={on}");
|
||||
{
|
||||
if (_idToObject.TryGetValue(id, out var go))
|
||||
{
|
||||
go.SetActive(on);
|
||||
@@ -601,8 +625,12 @@ namespace SHI.Modal.NW
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
/// <summary>
|
||||
/// 모델 재로드를 위한 정리 (UI 참조는 유지)
|
||||
/// </summary>
|
||||
private void CleanupForReload()
|
||||
{
|
||||
// 머티리얼 정리 (인스턴스 머티리얼 삭제)
|
||||
foreach (var kv in _originalSharedByRenderer)
|
||||
{
|
||||
var r = kv.Key;
|
||||
@@ -617,11 +645,14 @@ namespace SHI.Modal.NW
|
||||
}
|
||||
_originalSharedByRenderer.Clear();
|
||||
_idToObject.Clear();
|
||||
|
||||
// 모델 루트 삭제
|
||||
if (_root != null) UnityEngine.Object.Destroy(_root);
|
||||
_root = null;
|
||||
_focusedId = null;
|
||||
_wireframeApplied = false;
|
||||
|
||||
// 카메라 및 렌더텍스처 정리
|
||||
if (_viewCamera != null)
|
||||
{
|
||||
if (_rt != null && _viewCamera.targetTexture == _rt)
|
||||
@@ -630,6 +661,7 @@ namespace SHI.Modal.NW
|
||||
}
|
||||
_viewCamera.enabled = false;
|
||||
}
|
||||
|
||||
if (_rt != null)
|
||||
{
|
||||
_rt.Release();
|
||||
@@ -637,13 +669,101 @@ namespace SHI.Modal.NW
|
||||
_rt = null;
|
||||
}
|
||||
|
||||
//NWModelViewRig란 이름의 게임오브젝트도 같이 삭제
|
||||
// 드래그 상태 초기화
|
||||
_mmbDragging = false;
|
||||
_rmbDragging = false;
|
||||
|
||||
// ID 시드 초기화
|
||||
itemIdSeed = 1;
|
||||
|
||||
// 게임오브젝트 정리
|
||||
var rigGo = GameObject.Find("NWModelViewRig");
|
||||
if (rigGo != null) UnityEngine.Object.Destroy(rigGo);
|
||||
_viewCamera = null;
|
||||
|
||||
var rootGo = GameObject.Find("NWModelViewRoot");
|
||||
if (rootGo != null) UnityEngine.Object.Destroy(rootGo);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
|
||||
// 버튼 이벤트 해제
|
||||
if (_expandBtn != null)
|
||||
{
|
||||
_expandBtn.clicked -= OnExpandBtnClicked;
|
||||
}
|
||||
|
||||
// 마우스 이벤트 해제
|
||||
UnregisterCallback<MouseDownEvent>(OnMouseDown);
|
||||
UnregisterCallback<MouseUpEvent>(OnMouseUp);
|
||||
UnregisterCallback<MouseMoveEvent>(OnMouseMove);
|
||||
UnregisterCallback<WheelEvent>(OnWheel);
|
||||
UnregisterCallback<GeometryChangedEvent>(OnGeometryChanged);
|
||||
|
||||
// 외부 이벤트 정리
|
||||
OnItemSelected = null;
|
||||
OnExpand = null;
|
||||
|
||||
// 머티리얼 정리 (인스턴스 머티리얼 삭제)
|
||||
foreach (var kv in _originalSharedByRenderer)
|
||||
{
|
||||
var r = kv.Key;
|
||||
if (r == null) continue;
|
||||
var originals = kv.Value;
|
||||
var mats = r.materials;
|
||||
for (int m = 0; m < mats.Length; m++)
|
||||
{
|
||||
if (mats[m] != null) UnityEngine.Object.Destroy(mats[m]);
|
||||
}
|
||||
r.materials = originals;
|
||||
}
|
||||
_originalSharedByRenderer.Clear();
|
||||
_idToObject.Clear();
|
||||
|
||||
// 모델 루트 삭제
|
||||
if (_root != null) UnityEngine.Object.Destroy(_root);
|
||||
_root = null;
|
||||
_focusedId = null;
|
||||
_wireframeApplied = false;
|
||||
_wireframeMat = null; // Resources에서 로드한 것은 Destroy하지 않음
|
||||
|
||||
// 카메라 및 렌더텍스처 정리
|
||||
if (_viewCamera != null)
|
||||
{
|
||||
if (_rt != null && _viewCamera.targetTexture == _rt)
|
||||
{
|
||||
_viewCamera.targetTexture = null;
|
||||
}
|
||||
_viewCamera.enabled = false;
|
||||
}
|
||||
_viewCamera = null;
|
||||
|
||||
if (_rt != null)
|
||||
{
|
||||
_rt.Release();
|
||||
UnityEngine.Object.Destroy(_rt);
|
||||
_rt = null;
|
||||
}
|
||||
|
||||
// 드래그 상태 초기화
|
||||
_mmbDragging = false;
|
||||
_rmbDragging = false;
|
||||
|
||||
// ID 시드 초기화
|
||||
itemIdSeed = 1;
|
||||
|
||||
// UI 참조 정리
|
||||
_renderContainer = null;
|
||||
_expandBtn = null;
|
||||
|
||||
// 게임오브젝트 정리
|
||||
var rigGo = GameObject.Find("NWModelViewRig");
|
||||
if (rigGo != null) UnityEngine.Object.Destroy(rigGo);
|
||||
var rootGo = GameObject.Find("NWModelViewRoot");
|
||||
if (rootGo != null) UnityEngine.Object.Destroy(rootGo);
|
||||
|
||||
|
||||
}
|
||||
|
||||
// 유틸리티 메서드
|
||||
|
||||
472
Assets/Scripts/SHI/modal/PlayBar.cs
Normal file
472
Assets/Scripts/SHI/modal/PlayBar.cs
Normal file
@@ -0,0 +1,472 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace SHI.Modal
|
||||
{
|
||||
/// <summary>
|
||||
/// 시간 기반 재생을 제어하는 UI Toolkit 컴포넌트입니다.
|
||||
/// </summary>
|
||||
[UxmlElement]
|
||||
public partial class PlayBar : VisualElement, IDisposable
|
||||
{
|
||||
#region 상수 (Constants)
|
||||
private const string UXML_PATH = "SHI/Modal/PlayBar";
|
||||
private const int DEFAULT_INTERVAL_SECONDS = 2;
|
||||
#endregion
|
||||
|
||||
#region UI 컴포넌트 참조
|
||||
private Label? _startLabel;
|
||||
private Label? _endLabel;
|
||||
private Label? _currentTimeLabel;
|
||||
private VisualElement? _progressTrack;
|
||||
private VisualElement? _progressFill;
|
||||
private Button? _playBtn;
|
||||
private Button? _firstBtn;
|
||||
private Button? _lastBtn;
|
||||
private Button? _stopBtn;
|
||||
private DropdownField? _intervalDropdown;
|
||||
#endregion
|
||||
|
||||
#region 상태 (State)
|
||||
private DateTime _startTime;
|
||||
private DateTime _endTime;
|
||||
private DateTime _currentTime;
|
||||
private bool _isPlaying;
|
||||
private int _intervalSeconds = DEFAULT_INTERVAL_SECONDS;
|
||||
private IVisualElementScheduledItem? _playSchedule;
|
||||
private ProgressDragManipulator? _dragManipulator;
|
||||
#endregion
|
||||
|
||||
#region 외부 이벤트 (Public Events)
|
||||
/// <summary>재생이 시작될 때 발생</summary>
|
||||
public event Action? OnPlayStarted;
|
||||
|
||||
/// <summary>재생이 정지될 때 발생</summary>
|
||||
public event Action? OnPlayStopped;
|
||||
|
||||
/// <summary>재생 중 시간이 변경될 때 발생 (자동 재생 시)</summary>
|
||||
public event Action<DateTime>? OnPlayProgress;
|
||||
|
||||
/// <summary>사용자가 진행바를 드래그/클릭하여 위치를 변경할 때 발생</summary>
|
||||
public event Action<DateTime>? OnPositionChanged;
|
||||
#endregion
|
||||
|
||||
#region UxmlAttribute
|
||||
[UxmlAttribute]
|
||||
public bool IsVisible
|
||||
{
|
||||
get => style.display == DisplayStyle.Flex;
|
||||
set => style.display = value ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 생성자 (Constructor)
|
||||
public PlayBar()
|
||||
{
|
||||
var visualTree = Resources.Load<VisualTreeAsset>(UXML_PATH);
|
||||
Debug.Log("[PlayBar] UXML loaded and cloned." + (visualTree == null));
|
||||
if (visualTree == null)
|
||||
{
|
||||
Debug.LogError($"[PlayBar] UXML not found at: {UXML_PATH}");
|
||||
return;
|
||||
}
|
||||
visualTree.CloneTree(this);
|
||||
InitializeUIReferences();
|
||||
InitializeEventHandlers();
|
||||
|
||||
// 패널에 연결된 후 드롭다운 초기화
|
||||
RegisterCallback<AttachToPanelEvent>(OnAttachToPanel);
|
||||
}
|
||||
|
||||
private void OnAttachToPanel(AttachToPanelEvent evt)
|
||||
{
|
||||
UnregisterCallback<AttachToPanelEvent>(OnAttachToPanel);
|
||||
InitializeDropdown();
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 초기화 (Initialization)
|
||||
private void InitializeUIReferences()
|
||||
{
|
||||
_startLabel = this.Q<Label>("start-label");
|
||||
_endLabel = this.Q<Label>("end-label");
|
||||
_currentTimeLabel = this.Q<Label>("current-time-label");
|
||||
_progressTrack = this.Q<VisualElement>("progress-track");
|
||||
_progressFill = this.Q<VisualElement>("progress-fill");
|
||||
_playBtn = this.Q<Button>("play-btn");
|
||||
_firstBtn = this.Q<Button>("first-btn");
|
||||
_lastBtn = this.Q<Button>("last-btn");
|
||||
_stopBtn = this.Q<Button>("stop-btn");
|
||||
_intervalDropdown = this.Q<DropdownField>("interval-dropdown");
|
||||
}
|
||||
|
||||
private void InitializeEventHandlers()
|
||||
{
|
||||
_playBtn?.RegisterCallback<ClickEvent>(OnPlayClicked);
|
||||
_firstBtn?.RegisterCallback<ClickEvent>(OnFirstClicked);
|
||||
_lastBtn?.RegisterCallback<ClickEvent>(OnLastClicked);
|
||||
_stopBtn?.RegisterCallback<ClickEvent>(OnStopClicked);
|
||||
|
||||
// 진행바 드래그/클릭 설정
|
||||
if (_progressTrack != null)
|
||||
{
|
||||
_dragManipulator = new ProgressDragManipulator(this);
|
||||
_progressTrack.AddManipulator(_dragManipulator);
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeDropdown()
|
||||
{
|
||||
if (_intervalDropdown == null) return;
|
||||
|
||||
var choices = new List<string>();
|
||||
for (int i = 0; i <= 10; i++)
|
||||
{
|
||||
choices.Add($"{i}초");
|
||||
}
|
||||
_intervalDropdown.choices = choices;
|
||||
_intervalDropdown.value = $"{DEFAULT_INTERVAL_SECONDS}초";
|
||||
_intervalDropdown.RegisterValueChangedCallback(OnIntervalChanged);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 공개 메서드 (Public Methods)
|
||||
/// <summary>
|
||||
/// 시간 범위를 설정합니다.
|
||||
/// </summary>
|
||||
public void SetTimeRange(DateTime start, DateTime end)
|
||||
{
|
||||
_startTime = start;
|
||||
_endTime = end;
|
||||
_currentTime = start;
|
||||
|
||||
UpdateLabels();
|
||||
UpdateProgressBar();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 시간을 설정합니다.
|
||||
/// </summary>
|
||||
public void SetCurrentTime(DateTime time)
|
||||
{
|
||||
_currentTime = ClampTime(time);
|
||||
UpdateLabels();
|
||||
UpdateProgressBar();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 간격 선택 옵션을 설정합니다.
|
||||
/// </summary>
|
||||
public void SetIntervalChoices(List<int> seconds)
|
||||
{
|
||||
if (_intervalDropdown == null) return;
|
||||
|
||||
var choices = new List<string>();
|
||||
foreach (var sec in seconds)
|
||||
{
|
||||
choices.Add($"{sec}초");
|
||||
}
|
||||
_intervalDropdown.choices = choices;
|
||||
_intervalDropdown.index = 0;
|
||||
_intervalSeconds = seconds.Count > 0 ? seconds[0] : DEFAULT_INTERVAL_SECONDS;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 재생을 시작합니다.
|
||||
/// </summary>
|
||||
public void Play()
|
||||
{
|
||||
if (_isPlaying) return;
|
||||
_isPlaying = true;
|
||||
|
||||
UpdatePlayButtonState();
|
||||
StartPlayTimer();
|
||||
OnPlayStarted?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 재생을 일시정지합니다.
|
||||
/// </summary>
|
||||
public void Pause()
|
||||
{
|
||||
if (!_isPlaying) return;
|
||||
_isPlaying = false;
|
||||
|
||||
StopPlayTimer();
|
||||
UpdatePlayButtonState();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 재생을 정지하고 처음으로 돌아갑니다.
|
||||
/// </summary>
|
||||
public void Stop()
|
||||
{
|
||||
_isPlaying = false;
|
||||
_currentTime = _startTime;
|
||||
|
||||
StopPlayTimer();
|
||||
UpdatePlayButtonState();
|
||||
UpdateLabels();
|
||||
UpdateProgressBar();
|
||||
OnPlayStopped?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 리소스를 정리합니다.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
StopPlayTimer();
|
||||
UnregisterEventHandlers();
|
||||
|
||||
OnPlayStarted = null;
|
||||
OnPlayStopped = null;
|
||||
OnPlayProgress = null;
|
||||
OnPositionChanged = null;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 이벤트 핸들러 (Event Handlers)
|
||||
private void OnPlayClicked(ClickEvent evt)
|
||||
{
|
||||
if (_isPlaying)
|
||||
Pause();
|
||||
else
|
||||
Play();
|
||||
}
|
||||
|
||||
private void OnFirstClicked(ClickEvent evt)
|
||||
{
|
||||
SetCurrentTime(_startTime);
|
||||
OnPositionChanged?.Invoke(_currentTime);
|
||||
}
|
||||
|
||||
private void OnLastClicked(ClickEvent evt)
|
||||
{
|
||||
SetCurrentTime(_endTime);
|
||||
OnPositionChanged?.Invoke(_currentTime);
|
||||
}
|
||||
|
||||
private void OnStopClicked(ClickEvent evt)
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
|
||||
private void OnIntervalChanged(ChangeEvent<string> evt)
|
||||
{
|
||||
// "2초" -> 2 파싱
|
||||
if (int.TryParse(evt.newValue.Replace("초", ""), out int seconds))
|
||||
{
|
||||
_intervalSeconds = seconds;
|
||||
|
||||
// 재생 중이면 타이머 재시작
|
||||
if (_isPlaying)
|
||||
{
|
||||
StopPlayTimer();
|
||||
StartPlayTimer();
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 타이머 (Timer)
|
||||
private void StartPlayTimer()
|
||||
{
|
||||
_playSchedule = schedule.Execute(OnPlayTick).Every(_intervalSeconds * 1000);
|
||||
}
|
||||
|
||||
private void StopPlayTimer()
|
||||
{
|
||||
_playSchedule?.Pause();
|
||||
_playSchedule = null;
|
||||
}
|
||||
|
||||
private void OnPlayTick()
|
||||
{
|
||||
if (!_isPlaying) return;
|
||||
|
||||
_currentTime = _currentTime.AddDays(1);
|
||||
|
||||
if (_currentTime >= _endTime)
|
||||
{
|
||||
_currentTime = _endTime;
|
||||
Pause();
|
||||
}
|
||||
|
||||
UpdateLabels();
|
||||
UpdateProgressBar();
|
||||
OnPlayProgress?.Invoke(_currentTime);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region UI 업데이트 (UI Updates)
|
||||
private void UpdateLabels()
|
||||
{
|
||||
if (_startLabel != null)
|
||||
_startLabel.text = _startTime.ToString("yyyy-MM-dd");
|
||||
|
||||
if (_endLabel != null)
|
||||
_endLabel.text = _endTime.ToString("yyyy-MM-dd");
|
||||
|
||||
if (_currentTimeLabel != null)
|
||||
_currentTimeLabel.text = _currentTime.ToString("yyyy-MM-dd HH:mm");
|
||||
}
|
||||
|
||||
private void UpdateProgressBar()
|
||||
{
|
||||
if (_progressFill == null) return;
|
||||
|
||||
float progress = CalculateProgress();
|
||||
_progressFill.style.width = Length.Percent(progress * 100);
|
||||
}
|
||||
|
||||
private void UpdatePlayButtonState()
|
||||
{
|
||||
if (_playBtn == null) return;
|
||||
|
||||
if (_isPlaying)
|
||||
_playBtn.AddToClassList("playing");
|
||||
else
|
||||
_playBtn.RemoveFromClassList("playing");
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 유틸리티 (Utilities)
|
||||
private float CalculateProgress()
|
||||
{
|
||||
double total = (_endTime - _startTime).TotalDays;
|
||||
if (total <= 0) return 0;
|
||||
|
||||
double current = (_currentTime - _startTime).TotalDays;
|
||||
return Mathf.Clamp01((float)(current / total));
|
||||
}
|
||||
|
||||
private DateTime ClampTime(DateTime time)
|
||||
{
|
||||
if (time < _startTime) return _startTime;
|
||||
if (time > _endTime) return _endTime;
|
||||
return time;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 정규화된 위치(0~1)로부터 시간을 설정합니다. (드래그용)
|
||||
/// </summary>
|
||||
internal void SetProgressFromPosition(float normalizedPosition)
|
||||
{
|
||||
double totalDays = (_endTime - _startTime).TotalDays;
|
||||
double targetDays = totalDays * normalizedPosition;
|
||||
|
||||
_currentTime = _startTime.AddDays(targetDays);
|
||||
_currentTime = ClampTime(_currentTime);
|
||||
|
||||
UpdateLabels();
|
||||
UpdateProgressBar();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 드래그 완료 시 이벤트를 발생시킵니다.
|
||||
/// </summary>
|
||||
internal void NotifyPositionChanged()
|
||||
{
|
||||
OnPositionChanged?.Invoke(_currentTime);
|
||||
}
|
||||
|
||||
internal float GetTrackWidth()
|
||||
{
|
||||
return _progressTrack?.layout.width ?? 0;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 이벤트 해제 (Unregister)
|
||||
private void UnregisterEventHandlers()
|
||||
{
|
||||
_playBtn?.UnregisterCallback<ClickEvent>(OnPlayClicked);
|
||||
_firstBtn?.UnregisterCallback<ClickEvent>(OnFirstClicked);
|
||||
_lastBtn?.UnregisterCallback<ClickEvent>(OnLastClicked);
|
||||
_stopBtn?.UnregisterCallback<ClickEvent>(OnStopClicked);
|
||||
|
||||
if (_progressTrack != null && _dragManipulator != null)
|
||||
{
|
||||
_progressTrack.RemoveManipulator(_dragManipulator);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 진행바의 드래그/클릭을 처리하는 Manipulator입니다.
|
||||
/// </summary>
|
||||
public class ProgressDragManipulator : PointerManipulator
|
||||
{
|
||||
private readonly PlayBar _playBar;
|
||||
private bool _isActive;
|
||||
|
||||
public ProgressDragManipulator(PlayBar playBar)
|
||||
{
|
||||
_playBar = playBar;
|
||||
activators.Add(new ManipulatorActivationFilter { button = MouseButton.LeftMouse });
|
||||
}
|
||||
|
||||
protected override void RegisterCallbacksOnTarget()
|
||||
{
|
||||
target.RegisterCallback<PointerDownEvent>(OnPointerDown);
|
||||
target.RegisterCallback<PointerMoveEvent>(OnPointerMove);
|
||||
target.RegisterCallback<PointerUpEvent>(OnPointerUp);
|
||||
target.RegisterCallback<PointerCaptureOutEvent>(OnPointerCaptureOut);
|
||||
}
|
||||
|
||||
protected override void UnregisterCallbacksFromTarget()
|
||||
{
|
||||
target.UnregisterCallback<PointerDownEvent>(OnPointerDown);
|
||||
target.UnregisterCallback<PointerMoveEvent>(OnPointerMove);
|
||||
target.UnregisterCallback<PointerUpEvent>(OnPointerUp);
|
||||
target.UnregisterCallback<PointerCaptureOutEvent>(OnPointerCaptureOut);
|
||||
}
|
||||
|
||||
private void OnPointerDown(PointerDownEvent evt)
|
||||
{
|
||||
if (!CanStartManipulation(evt)) return;
|
||||
|
||||
_isActive = true;
|
||||
target.CapturePointer(evt.pointerId);
|
||||
UpdatePosition(evt.localPosition);
|
||||
evt.StopPropagation();
|
||||
}
|
||||
|
||||
private void OnPointerMove(PointerMoveEvent evt)
|
||||
{
|
||||
if (!_isActive || !target.HasPointerCapture(evt.pointerId)) return;
|
||||
|
||||
UpdatePosition(evt.localPosition);
|
||||
}
|
||||
|
||||
private void OnPointerUp(PointerUpEvent evt)
|
||||
{
|
||||
if (!_isActive) return;
|
||||
|
||||
_isActive = false;
|
||||
target.ReleasePointer(evt.pointerId);
|
||||
|
||||
// 드래그 종료 시에만 이벤트 발생
|
||||
_playBar.NotifyPositionChanged();
|
||||
evt.StopPropagation();
|
||||
}
|
||||
|
||||
private void OnPointerCaptureOut(PointerCaptureOutEvent evt)
|
||||
{
|
||||
_isActive = false;
|
||||
}
|
||||
|
||||
private void UpdatePosition(Vector2 localPosition)
|
||||
{
|
||||
float trackWidth = _playBar.GetTrackWidth();
|
||||
if (trackWidth <= 0) return;
|
||||
|
||||
float normalizedPosition = Mathf.Clamp01(localPosition.x / trackWidth);
|
||||
_playBar.SetProgressFromPosition(normalizedPosition);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/SHI/modal/PlayBar.cs.meta
Normal file
2
Assets/Scripts/SHI/modal/PlayBar.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ea6077d0bdca20a4e954f6ab8a93e349
|
||||
@@ -45,14 +45,15 @@ namespace SHI.Modal
|
||||
/// </list>
|
||||
/// </summary>
|
||||
[UxmlElement]
|
||||
public partial class TreeList : VisualElement
|
||||
public partial class TreeList : VisualElement, IDisposable
|
||||
{
|
||||
#region IDisposable
|
||||
private bool _disposed = false;
|
||||
#endregion
|
||||
#region 상수 (Constants)
|
||||
/// <summary>메인 UXML 파일 경로 (Resources 폴더 기준)</summary>
|
||||
private const string UXML_PATH = "SHI/Modal/TreeList";
|
||||
|
||||
/// <summary>개별 항목 템플릿 UXML 파일 경로 (Resources 폴더 기준)</summary>
|
||||
private const string ITEM_UXML_PATH = "SHI/Modal/TreeListItem";
|
||||
|
||||
#endregion
|
||||
|
||||
#region UI 컴포넌트 참조 (UI Component References)
|
||||
@@ -70,11 +71,7 @@ namespace SHI.Modal
|
||||
#endregion
|
||||
|
||||
#region 내부 데이터 (Internal Data)
|
||||
/// <summary>
|
||||
/// 개별 항목 UI를 생성할 때 사용하는 UXML 템플릿입니다.
|
||||
/// 생성자에서 한 번만 로드하여 재사용합니다.
|
||||
/// </summary>
|
||||
private VisualTreeAsset _itemTemplate;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 원본 루트 데이터입니다.
|
||||
@@ -106,19 +103,19 @@ namespace SHI.Modal
|
||||
/// 항목의 가시성(눈 아이콘)이 변경될 때 발생합니다.
|
||||
/// 3D 모델의 GameObject 활성화/비활성화에 연동합니다.
|
||||
/// </summary>
|
||||
public event Action<TreeListItemData> OnVisibilityChanged;
|
||||
public event Action<TreeListItemData>? OnVisibilityChanged;
|
||||
|
||||
/// <summary>
|
||||
/// 항목 선택 상태가 변경될 때 발생합니다.
|
||||
/// 선택 및 선택 해제 모두에서 발생하며, item.isSelected로 구분합니다.
|
||||
/// </summary>
|
||||
public event Action<TreeListItemData> OnSelectionChanged;
|
||||
public event Action<TreeListItemData>? OnSelectionChanged;
|
||||
|
||||
/// <summary>
|
||||
/// 트리 리스트가 닫힐 때(숨겨질 때) 발생합니다.
|
||||
/// 닫기 버튼 클릭 시 트리거됩니다.
|
||||
/// </summary>
|
||||
public event Action OnClosed;
|
||||
public event Action? OnClosed;
|
||||
#endregion
|
||||
|
||||
#region 생성자 (Constructor)
|
||||
@@ -138,21 +135,14 @@ namespace SHI.Modal
|
||||
}
|
||||
visualTree!.CloneTree(this);
|
||||
|
||||
// 2. 항목 템플릿 UXML 로드 (성능을 위해 미리 로드)
|
||||
_itemTemplate = Resources.Load<VisualTreeAsset>(ITEM_UXML_PATH);
|
||||
if (_itemTemplate == null)
|
||||
{
|
||||
Debug.LogError($"[TreeMenu] Item UXML not found at: {ITEM_UXML_PATH}");
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 자식 요소 참조 획득 (UXML의 name 속성으로 찾음)
|
||||
// 2. 자식 요소 참조 획득 (UXML의 name 속성으로 찾음)
|
||||
_searchField = this.Q<TextField>("search-field");
|
||||
_treeView = this.Q<TreeView>("main-tree-view");
|
||||
_closeButton = this.Q<Button>("hide-btn");
|
||||
_clearButton = this.Q<Button>("clear-btn");
|
||||
|
||||
// 4. 이벤트 연결 및 로직 초기화
|
||||
// 3. 이벤트 연결 및 로직 초기화
|
||||
InitializeLogic();
|
||||
}
|
||||
#endregion
|
||||
@@ -166,7 +156,7 @@ namespace SHI.Modal
|
||||
// 검색창 이벤트: 입력 값이 변경될 때마다 필터링 실행
|
||||
if (_searchField != null)
|
||||
{
|
||||
_searchField.RegisterValueChangedCallback(evt => OnSearch(evt.newValue));
|
||||
_searchField.RegisterValueChangedCallback(OnSearchValueChanged);
|
||||
}
|
||||
|
||||
// TreeView 설정
|
||||
@@ -175,7 +165,6 @@ namespace SHI.Modal
|
||||
// selectionChanged: 선택 변경 시 호출
|
||||
if (_treeView != null)
|
||||
{
|
||||
_treeView.makeItem = MakeTreeItem;
|
||||
_treeView.bindItem = BindTreeItem;
|
||||
_treeView.selectionChanged += OnTreeViewSelectionChanged;
|
||||
}
|
||||
@@ -249,6 +238,59 @@ namespace SHI.Modal
|
||||
{
|
||||
_treeView.SetSelection(new List<int> { itemId });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 지정된 이름 목록에 해당하는 항목만 표시하고 나머지는 숨깁니다.
|
||||
/// </summary>
|
||||
/// <param name="items">표시할 항목들의 이름 목록</param>
|
||||
/// <param name="depth">검색 깊이 (1=1뎁스 자식만, 2=2뎁스까지, 0이하=전체)</param>
|
||||
public void ShowItems(List<string> items, int depth = 1)
|
||||
{
|
||||
if (_originalRoots == null || _originalRoots.Count == 0) return;
|
||||
|
||||
var visibleNames = new HashSet<string>(items ?? new List<string>());
|
||||
|
||||
foreach (var root in _originalRoots)
|
||||
{
|
||||
SetVisibilityByNames(root, visibleNames, depth, 0);
|
||||
}
|
||||
|
||||
// UI 갱신
|
||||
_treeView?.RefreshItems();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 재귀적으로 항목의 가시성을 이름 목록에 따라 설정합니다.
|
||||
/// </summary>
|
||||
/// <param name="node">현재 노드</param>
|
||||
/// <param name="visibleNames">표시할 이름 목록</param>
|
||||
/// <param name="maxDepth">최대 검색 깊이 (0이하=무제한)</param>
|
||||
/// <param name="currentDepth">현재 깊이</param>
|
||||
private void SetVisibilityByNames(TreeListItemData node, HashSet<string> visibleNames, int maxDepth, int currentDepth)
|
||||
{
|
||||
if (node == null) return;
|
||||
|
||||
// maxDepth <= 0 이면 전체 검색, 아니면 깊이 제한 체크
|
||||
bool isWithinDepth = maxDepth <= 0 || currentDepth < maxDepth;
|
||||
|
||||
if (isWithinDepth)
|
||||
{
|
||||
// 이름이 목록에 있으면 visible, 없으면 hidden
|
||||
node.IsVisible = visibleNames.Contains(node.name);
|
||||
|
||||
// 가시성 변경 이벤트 발송
|
||||
OnVisibilityChanged?.Invoke(node);
|
||||
}
|
||||
|
||||
// 자식들도 재귀적으로 처리
|
||||
if (node.children != null)
|
||||
{
|
||||
foreach (var child in node.children)
|
||||
{
|
||||
SetVisibilityByNames(child, visibleNames, maxDepth, currentDepth + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 선택 처리 (Selection Handling)
|
||||
@@ -320,20 +362,8 @@ namespace SHI.Modal
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region TreeView 항목 생성/바인딩 (TreeView Item Creation/Binding)
|
||||
/// <summary>
|
||||
/// TreeView의 새 행(Row) UI를 생성합니다.
|
||||
/// TreeView의 makeItem 콜백으로 사용됩니다.
|
||||
/// 스크롤 시 재사용되는 풀링 시스템에서 호출됩니다.
|
||||
/// </summary>
|
||||
/// <returns>항목 템플릿에서 생성된 VisualElement</returns>
|
||||
private VisualElement MakeTreeItem()
|
||||
{
|
||||
// UXML 템플릿을 복제하여 새 항목 생성
|
||||
var templateContainer = _itemTemplate.Instantiate();
|
||||
return templateContainer;
|
||||
}
|
||||
|
||||
#region TreeView 항목 바인딩 (TreeView Item Creation/Binding)
|
||||
|
||||
/// <summary>
|
||||
/// 데이터를 UI 요소에 바인딩합니다.
|
||||
/// TreeView의 bindItem 콜백으로 사용됩니다.
|
||||
@@ -419,6 +449,15 @@ namespace SHI.Modal
|
||||
#endregion
|
||||
|
||||
#region 검색 기능 (Search Functionality)
|
||||
/// <summary>
|
||||
/// 검색 필드 값 변경 콜백입니다.
|
||||
/// </summary>
|
||||
/// <param name="evt">값 변경 이벤트</param>
|
||||
private void OnSearchValueChanged(ChangeEvent<string> evt)
|
||||
{
|
||||
OnSearch(evt.newValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 검색어에 따라 트리를 필터링합니다.
|
||||
/// 검색어가 비어있으면 원본 데이터로 복원됩니다.
|
||||
@@ -536,5 +575,50 @@ namespace SHI.Modal
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region IDisposable
|
||||
/// <summary>
|
||||
/// 리소스를 해제하고 이벤트 핸들러를 정리합니다.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
|
||||
// 검색 필드 이벤트 해제
|
||||
if (_searchField != null)
|
||||
{
|
||||
_searchField.UnregisterValueChangedCallback(OnSearchValueChanged);
|
||||
}
|
||||
|
||||
// TreeView 이벤트 핸들러 해제
|
||||
if (_treeView != null)
|
||||
{
|
||||
_treeView.selectionChanged -= OnTreeViewSelectionChanged;
|
||||
_treeView.bindItem = null;
|
||||
_treeView.makeItem = null;
|
||||
}
|
||||
|
||||
// 외부 이벤트 구독자 정리
|
||||
OnVisibilityChanged = null;
|
||||
OnSelectionChanged = null;
|
||||
OnClosed = null;
|
||||
|
||||
// 데이터 정리
|
||||
_originalRoots.Clear();
|
||||
_rootData?.Clear();
|
||||
_rootData = null;
|
||||
_previouslySelectedItem = null;
|
||||
|
||||
// ID 시드 초기화
|
||||
_idSeed = 1;
|
||||
|
||||
// UI 참조 정리
|
||||
_searchField = null;
|
||||
_treeView = null;
|
||||
_closeButton = null;
|
||||
_clearButton = null;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user