추가 개발 전

This commit is contained in:
logonkhi
2025-11-12 16:48:34 +09:00
parent 8850f51193
commit 6920659ed9
13 changed files with 1359 additions and 21 deletions

View File

@@ -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();
}
}
}
}

View 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) { }
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 4cdfc0facccb5164e87bd49a9a9be1e3

View 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);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 1af5017c83fc5cf4d8fbd1d2a801a095

View File

@@ -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()
{

View File

@@ -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);
}
}
}