추가 개발 전
This commit is contained in:
@@ -1,24 +1,25 @@
|
||||
using Cysharp.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UVC.UI.Buttons;
|
||||
using UVC.UI.Window;
|
||||
|
||||
namespace SHI.modal
|
||||
{
|
||||
public class BlockDetailModal: MonoBehaviour
|
||||
public class BlockDetailModal : MonoBehaviour
|
||||
{
|
||||
[Header("References")]
|
||||
[SerializeField]
|
||||
private Button closeButton;
|
||||
|
||||
[SerializeField]
|
||||
private HierarchyWindow hierarchyWindow;
|
||||
private ModelDetailListView listView;
|
||||
|
||||
[SerializeField]
|
||||
private ModelDetailView modelView;
|
||||
|
||||
[SerializeField]
|
||||
private Transform chartView;
|
||||
private ModelDetailChartView chartView;
|
||||
|
||||
[Header("UI Controls")]
|
||||
[SerializeField]
|
||||
private Button modelViewExpandButton;
|
||||
|
||||
@@ -29,11 +30,140 @@ namespace SHI.modal
|
||||
private Button dragButton;
|
||||
|
||||
[SerializeField]
|
||||
private ImageToggle showHierarchyButton;
|
||||
private Button showListButton;
|
||||
|
||||
// cached layout elements for split control
|
||||
private LayoutElement _modelLayout;
|
||||
private LayoutElement _chartLayout;
|
||||
|
||||
private enum ExpandedSide { None, Model, Chart }
|
||||
private ExpandedSide _expanded = ExpandedSide.None;
|
||||
|
||||
private RectTransform ModelRect => modelView != null ? modelView.GetComponent<RectTransform>() : null;
|
||||
private RectTransform ChartRect => chartView != null ? chartView.GetComponent<RectTransform>() : null;
|
||||
private HorizontalSplitDrag _splitter;
|
||||
|
||||
public void Start()
|
||||
{
|
||||
|
||||
// Close
|
||||
if (closeButton != null)
|
||||
{
|
||||
closeButton.onClick.AddListener(() => gameObject.SetActive(false));
|
||||
}
|
||||
|
||||
// list show 버튼
|
||||
if (showListButton != null && listView != null)
|
||||
showListButton.onClick.AddListener(() =>
|
||||
{
|
||||
Debug.Log("BlockDetailModal: Show List View");
|
||||
listView.gameObject.SetActive(true);
|
||||
showListButton.gameObject.SetActive(false);
|
||||
if (_splitter != null) _splitter.RefreshPosition();
|
||||
});
|
||||
showListButton.gameObject.SetActive(false);
|
||||
|
||||
// Selection wiring: list -> model/chart
|
||||
if (listView != null)
|
||||
{
|
||||
listView.OnItemSelected += data =>
|
||||
{
|
||||
if (modelView != null) modelView.FocusItem(data);
|
||||
if (chartView != null) chartView.SelectByItem(data.Name);
|
||||
};
|
||||
listView.OnClosed += () =>
|
||||
{
|
||||
if (showListButton != null) showListButton.gameObject.SetActive(true);
|
||||
if (_splitter != null) _splitter.RefreshPosition();
|
||||
};
|
||||
}
|
||||
|
||||
// Selection wiring: model -> list/chart
|
||||
if (modelView != null)
|
||||
{
|
||||
modelView.OnItemSelected += data =>
|
||||
{
|
||||
if (listView != null) listView.SelectItem(data.Name);
|
||||
if (chartView != null) chartView.SelectByItem(data.Name);
|
||||
};
|
||||
}
|
||||
|
||||
// Chart -> list/model
|
||||
if (chartView != null)
|
||||
{
|
||||
chartView.OnRowClicked += name =>
|
||||
{
|
||||
if (listView != null) listView.SelectItem(name);
|
||||
if (modelView != null) modelView.FocusItemName(name);
|
||||
};
|
||||
}
|
||||
|
||||
// Expand buttons
|
||||
if (modelViewExpandButton != null)
|
||||
modelViewExpandButton.onClick.AddListener(ToggleExpandModel);
|
||||
if (chartViewExpandButton != null)
|
||||
chartViewExpandButton.onClick.AddListener(ToggleExpandChart);
|
||||
|
||||
// Drag splitter
|
||||
SetupSplitControls();
|
||||
}
|
||||
|
||||
private void SetupSplitControls()
|
||||
{
|
||||
var modelRect = ModelRect;
|
||||
var chartRect = ChartRect;
|
||||
if (modelRect == null || chartRect == null || dragButton == null) return;
|
||||
|
||||
_modelLayout = modelRect.GetComponent<LayoutElement>();
|
||||
if (_modelLayout == null) _modelLayout = modelRect.gameObject.AddComponent<LayoutElement>();
|
||||
|
||||
_chartLayout = chartRect.GetComponent<LayoutElement>();
|
||||
if (_chartLayout == null) _chartLayout = chartView.gameObject.AddComponent<LayoutElement>();
|
||||
|
||||
// initial split50/50
|
||||
_modelLayout.flexibleWidth = 1f;
|
||||
_chartLayout.flexibleWidth = 1f;
|
||||
|
||||
// attach drag handler
|
||||
_splitter = dragButton.gameObject.GetComponent<HorizontalSplitDrag>();
|
||||
if (_splitter == null) _splitter = dragButton.gameObject.AddComponent<HorizontalSplitDrag>();
|
||||
var leftFixed = listView != null ? listView.GetComponent<RectTransform>() : null;
|
||||
_splitter.Initialize(modelRect, chartRect, leftFixed);
|
||||
//시간이 좀 필요 함
|
||||
UniTask.DelayFrame(1).ContinueWith(() => _splitter.RefreshPosition());
|
||||
}
|
||||
|
||||
private void ToggleExpandModel()
|
||||
{
|
||||
if (ModelRect == null || chartView == null) return;
|
||||
if (_expanded == ExpandedSide.Model) { ResetSplit(); return; }
|
||||
_expanded = ExpandedSide.Model;
|
||||
ModelRect.gameObject.SetActive(true);
|
||||
chartView.gameObject.SetActive(false);
|
||||
if (_splitter != null) _splitter.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
private void ToggleExpandChart()
|
||||
{
|
||||
if (ModelRect == null || chartView == null) return;
|
||||
if (_expanded == ExpandedSide.Chart) { ResetSplit(); return; }
|
||||
_expanded = ExpandedSide.Chart;
|
||||
ModelRect.gameObject.SetActive(false);
|
||||
chartView.gameObject.SetActive(true);
|
||||
if (_splitter != null) _splitter.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
private void ResetSplit()
|
||||
{
|
||||
_expanded = ExpandedSide.None;
|
||||
if (ModelRect != null) ModelRect.gameObject.SetActive(true);
|
||||
if (chartView != null) chartView.gameObject.SetActive(true);
|
||||
if (_modelLayout != null) _modelLayout.flexibleWidth = 1f;
|
||||
if (_chartLayout != null) _chartLayout.flexibleWidth = 1f;
|
||||
if (_splitter != null)
|
||||
{
|
||||
_splitter.gameObject.SetActive(true);
|
||||
_splitter.RefreshPosition();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
128
Assets/Scripts/SHI/modal/HorizontalSplitDrag.cs
Normal file
128
Assets/Scripts/SHI/modal/HorizontalSplitDrag.cs
Normal file
@@ -0,0 +1,128 @@
|
||||
#nullable enable
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace SHI.modal
|
||||
{
|
||||
/// <summary>
|
||||
/// 두 개의 RectTransform 가로 분할을 드래그 버튼으로 조절하는 간단한 스플리터.
|
||||
/// 레이아웃 그룹(수평) + LayoutElement.flexibleWidth 기반으로 동작합니다.
|
||||
/// </summary>
|
||||
public class HorizontalSplitDrag : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
|
||||
{
|
||||
private RectTransform _left;
|
||||
private RectTransform _right;
|
||||
private LayoutElement _leftLayout;
|
||||
private LayoutElement _rightLayout;
|
||||
private RectTransform _parent;
|
||||
private RectTransform _handleRect; // this splitter's rect
|
||||
private float _parentWidth; // cached on begin drag
|
||||
private float _handleY = 42; // keep original y
|
||||
private RectTransform _leftFixedPanel; // e.g., ModelDetailListView root
|
||||
|
||||
private bool _lastLeftPanelActive;
|
||||
private float _lastParentWidth;
|
||||
|
||||
public void Initialize(RectTransform left, RectTransform right, RectTransform? leftFixedPanel = null)
|
||||
{
|
||||
_left = left;
|
||||
_right = right;
|
||||
_leftFixedPanel = leftFixedPanel;
|
||||
_parent = left != null ? left.parent as RectTransform : null;
|
||||
_handleRect = transform as RectTransform;
|
||||
|
||||
_leftLayout = _left.GetComponent<LayoutElement>();
|
||||
if (_leftLayout == null) _leftLayout = _left.gameObject.AddComponent<LayoutElement>();
|
||||
_rightLayout = _right.GetComponent<LayoutElement>();
|
||||
if (_rightLayout == null) _rightLayout = _right.gameObject.AddComponent<LayoutElement>();
|
||||
|
||||
_lastLeftPanelActive = _leftFixedPanel != null && _leftFixedPanel.gameObject.activeInHierarchy;
|
||||
_lastParentWidth = _parent != null ? _parent.rect.width : 0f;
|
||||
RefreshPosition();
|
||||
}
|
||||
|
||||
public void OnBeginDrag(PointerEventData eventData)
|
||||
{
|
||||
if (_parent != null) _parentWidth = _parent.rect.width;
|
||||
if (_handleRect != null) _handleY = _handleRect.anchoredPosition.y;
|
||||
}
|
||||
|
||||
public void OnDrag(PointerEventData eventData)
|
||||
{
|
||||
if (_parent == null) return;
|
||||
Vector2 local;
|
||||
if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(_parent, eventData.position, eventData.pressEventCamera, out local))
|
||||
return;
|
||||
|
||||
float width = _parent.rect.width;
|
||||
if (width <= 0f) return;
|
||||
|
||||
// 계산 범위: 좌측 고정 패널(보이는 경우)의 폭만큼 좌측 경계를 오른쪽으로 이동
|
||||
float leftOffset = 0f;
|
||||
if (_leftFixedPanel != null && _leftFixedPanel.gameObject.activeSelf)
|
||||
{
|
||||
leftOffset = _leftFixedPanel.rect.width;
|
||||
}
|
||||
|
||||
float minX = -width * 0.5f + leftOffset; // 사용 가능한 작업 영역의 좌측 경계
|
||||
float maxX = width * 0.5f; // 우측 경계
|
||||
// 현재 포인터 위치를 작업 영역 비율[0..1]로 변환 후 범위 제한
|
||||
float t = Mathf.InverseLerp(minX, maxX, local.x);
|
||||
t = Mathf.Clamp01(t);
|
||||
|
||||
// LayoutElement 비율 (양 끝 과도값 방지하여10%~90% 사이 유지)
|
||||
float leftWeight = Mathf.Clamp(t, 0.1f, 0.9f);
|
||||
float rightWeight = 1f - leftWeight;
|
||||
_leftLayout.flexibleWidth = leftWeight;
|
||||
_rightLayout.flexibleWidth = rightWeight;
|
||||
|
||||
// 스플리터 핸들도 같은 좌표계에서 이동
|
||||
if (_handleRect != null)
|
||||
{
|
||||
float clampedX = Mathf.Lerp(minX, maxX, leftWeight);
|
||||
_handleRect.anchoredPosition = new Vector2(clampedX, _handleY);
|
||||
}
|
||||
}
|
||||
|
||||
// 외부에서 강제로 현재 레이아웃 기준으로 핸들 위치를 동기화합니다.
|
||||
public void RefreshPosition()
|
||||
{
|
||||
if (_parent == null || _handleRect == null || _leftLayout == null || _rightLayout == null)
|
||||
return;
|
||||
|
||||
float width = _parent.rect.width;
|
||||
if (width <= 0f) return;
|
||||
|
||||
float leftOffset = 0f;
|
||||
if (_leftFixedPanel != null && _leftFixedPanel.gameObject.activeSelf)
|
||||
leftOffset = _leftFixedPanel.rect.width;
|
||||
|
||||
float minX = -width * 0.5f + leftOffset;
|
||||
float maxX = width * 0.5f;
|
||||
|
||||
float totalFlex = Mathf.Max(0.0001f, _leftLayout.flexibleWidth + _rightLayout.flexibleWidth);
|
||||
float leftWeight = Mathf.Clamp01(_leftLayout.flexibleWidth / totalFlex);
|
||||
leftWeight = Mathf.Clamp(leftWeight, 0.1f, 0.9f);
|
||||
|
||||
if (_handleRect != null)
|
||||
{
|
||||
_handleRect.anchoredPosition = new Vector2(Mathf.Lerp(minX, maxX, leftWeight), _handleY);
|
||||
}
|
||||
}
|
||||
|
||||
private void LateUpdate()
|
||||
{
|
||||
bool nowActive = _leftFixedPanel != null && _leftFixedPanel.gameObject.activeInHierarchy;
|
||||
float nowWidth = _parent != null ? _parent.rect.width : 0f;
|
||||
if (nowActive != _lastLeftPanelActive || !Mathf.Approximately(nowWidth, _lastParentWidth))
|
||||
{
|
||||
_lastLeftPanelActive = nowActive;
|
||||
_lastParentWidth = nowWidth;
|
||||
RefreshPosition();
|
||||
}
|
||||
}
|
||||
|
||||
public void OnEndDrag(PointerEventData eventData) { }
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/SHI/modal/HorizontalSplitDrag.cs.meta
Normal file
2
Assets/Scripts/SHI/modal/HorizontalSplitDrag.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4cdfc0facccb5164e87bd49a9a9be1e3
|
||||
37
Assets/Scripts/SHI/modal/ModelDetailChartView.cs
Normal file
37
Assets/Scripts/SHI/modal/ModelDetailChartView.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace SHI.modal
|
||||
{
|
||||
/// <summary>
|
||||
/// 차트 패널의 최소 동기화 컴포넌트.
|
||||
/// 실제 UI Toolkit 기반 간트 컴포넌트가 준비되면 이 클래스를 연결하세요.
|
||||
/// 현재는 항목 선택 신호만 송수신합니다.
|
||||
/// </summary>
|
||||
public class ModelDetailChartView : MonoBehaviour
|
||||
{
|
||||
/// <summary>
|
||||
/// 차트의 행을 클릭했을 때 선택된 항목의 이름을 알립니다.
|
||||
/// </summary>
|
||||
public Action<string>? OnRowClicked;
|
||||
|
||||
/// <summary>
|
||||
/// 외부(리스트/모델)에서 항목이 선택되었을 때 차트에서 해당 행을 강조합니다.
|
||||
/// 실제 구현은 프로젝트의 차트 위젯에 맞게 교체하세요.
|
||||
/// </summary>
|
||||
public void SelectByItem(string name)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name)) return;
|
||||
Debug.Log($"ModelDetailChartView.SelectByItem: {name}");
|
||||
// TODO: 차트에서 해당 행 스크롤/하이라이트
|
||||
}
|
||||
|
||||
// 임시: UI 이벤트 바인딩에서 호출 가능한 샘플
|
||||
public void SimulateRowClick(string name)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name)) return;
|
||||
OnRowClicked?.Invoke(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/SHI/modal/ModelDetailChartView.cs.meta
Normal file
2
Assets/Scripts/SHI/modal/ModelDetailChartView.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1af5017c83fc5cf4d8fbd1d2a801a095
|
||||
@@ -1,6 +1,7 @@
|
||||
#nullable enable
|
||||
using Cysharp.Threading.Tasks;
|
||||
using DG.Tweening;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using TMPro;
|
||||
@@ -52,16 +53,19 @@ namespace SHI.modal
|
||||
/// <summary>
|
||||
/// 메인/검색 리스트에서 항목이 선택될 때 발생합니다.
|
||||
/// </summary>
|
||||
public System.Action<TreeListItemData>? OnItemSelected;
|
||||
public Action<TreeListItemData>? OnItemSelected;
|
||||
|
||||
/// <summary>
|
||||
/// 메인/검색 리스트에서 항목이 선택 해제될 때 발생합니다.
|
||||
/// </summary>
|
||||
public System.Action<TreeListItemData>? OnItemDeselected;
|
||||
public Action<TreeListItemData>? OnItemDeselected;
|
||||
|
||||
public Action? OnClosed;
|
||||
|
||||
// 검색 목록에서 선택된 항목(클론된 데이터)
|
||||
protected TreeListItemData? selectedSearchItem;
|
||||
|
||||
|
||||
// 검색 작업 상태
|
||||
protected CancellationTokenSource? searchCts;
|
||||
protected bool isSearching = false;
|
||||
@@ -79,6 +83,7 @@ namespace SHI.modal
|
||||
protected Tween? loadingRotationTween;
|
||||
protected Tween? loadingFillTween;
|
||||
|
||||
|
||||
protected void Awake()
|
||||
{
|
||||
loadingImage.gameObject.SetActive(false);
|
||||
@@ -373,6 +378,12 @@ namespace SHI.modal
|
||||
}
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
gameObject.SetActive(false);
|
||||
OnClosed?.Invoke();
|
||||
}
|
||||
|
||||
|
||||
protected void OnDestroy()
|
||||
{
|
||||
|
||||
@@ -1,13 +1,49 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
using UVC.UI.List.Tree;
|
||||
|
||||
namespace SHI.modal
|
||||
{
|
||||
public class ModelDetailView: MonoBehaviour
|
||||
{
|
||||
/// <summary>
|
||||
/// 모델 뷰 내에서 항목이 선택되었을 때 알림.
|
||||
/// 외부에서 구독하여 리스트/차트를 동기화합니다.
|
||||
/// </summary>
|
||||
public Action<TreeListItemData>? OnItemSelected;
|
||||
|
||||
private TreeListItemData? _focused;
|
||||
|
||||
/// <summary>
|
||||
/// 트리 아이템에 해당하는 모델 요소를 강조하거나 카메라를 이동합니다.
|
||||
/// 실제 구현은 프로젝트 요구에 맞게 교체하세요.
|
||||
/// </summary>
|
||||
public void FocusItem(TreeListItemData data)
|
||||
{
|
||||
if (data == null) return;
|
||||
_focused = data;
|
||||
// TODO: 실제 GLTF/모델에서 data에 해당하는 노드를 찾아 강조/프레임 인
|
||||
// 디버그 표시로 대체
|
||||
Debug.Log($"ModelDetailView.FocusItem: {data.Name}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 주어진 이름에 해당하는 항목을 강조합니다.
|
||||
/// </summary>
|
||||
public void FocusItemName(string name)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name)) return;
|
||||
var data = new TreeListItemData(name);
|
||||
FocusItem(data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 모델에서 사용자가 어떤 요소를 클릭했을 때 외부로 통지하려면 이 메서드를 호출하세요.
|
||||
/// </summary>
|
||||
public void RaiseSelected(TreeListItemData data)
|
||||
{
|
||||
OnItemSelected?.Invoke(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user