버그 수정

This commit is contained in:
logonkhi
2025-11-14 19:54:04 +09:00
parent 934fff54a7
commit 235b4bc28a
18 changed files with 669 additions and 293 deletions

View File

@@ -389,8 +389,8 @@ RectTransform:
m_ConstrainProportionsScale: 0 m_ConstrainProportionsScale: 0
m_Children: m_Children:
- {fileID: 3512955993103789164} - {fileID: 3512955993103789164}
- {fileID: 4162604190990658749}
- {fileID: 6704318869740062506} - {fileID: 6704318869740062506}
- {fileID: 4162604190990658749}
m_Father: {fileID: 1574318677252675885} m_Father: {fileID: 1574318677252675885}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1} m_AnchorMin: {x: 0, y: 1}

View File

@@ -243,7 +243,7 @@ GameObject:
m_Icon: {fileID: 0} m_Icon: {fileID: 0}
m_NavMeshLayer: 0 m_NavMeshLayer: 0
m_StaticEditorFlags: 0 m_StaticEditorFlags: 0
m_IsActive: 0 m_IsActive: 1
--- !u!224 &4162604190990658749 --- !u!224 &4162604190990658749
RectTransform: RectTransform:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
@@ -344,19 +344,7 @@ MonoBehaviour:
m_TargetGraphic: {fileID: 9209424671866392754} m_TargetGraphic: {fileID: 9209424671866392754}
m_OnClick: m_OnClick:
m_PersistentCalls: m_PersistentCalls:
m_Calls: m_Calls: []
- m_Target: {fileID: 0}
m_TargetAssemblyTypeName: UVC.UI.List.ComponentList.ComponentList, Assembly-CSharp
m_MethodName: OnClickClearText
m_Mode: 1
m_Arguments:
m_ObjectArgument: {fileID: 0}
m_ObjectArgumentAssemblyTypeName: UnityEngine.Object, UnityEngine
m_IntArgument: 0
m_FloatArgument: 0
m_StringArgument:
m_BoolArgument: 0
m_CallState: 2
--- !u!1 &1866422618894112705 --- !u!1 &1866422618894112705
GameObject: GameObject:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
@@ -390,8 +378,8 @@ RectTransform:
m_Children: m_Children:
- {fileID: 1487765936564688321} - {fileID: 1487765936564688321}
- {fileID: 3512955993103789164} - {fileID: 3512955993103789164}
- {fileID: 4162604190990658749}
- {fileID: 6704318869740062506} - {fileID: 6704318869740062506}
- {fileID: 4162604190990658749}
m_Father: {fileID: 1574318677252675885} m_Father: {fileID: 1574318677252675885}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1} m_AnchorMin: {x: 0, y: 1}
@@ -522,7 +510,7 @@ MonoBehaviour:
m_Calls: [] m_Calls: []
m_CaretColor: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} m_CaretColor: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1}
m_CustomCaretColor: 0 m_CustomCaretColor: 0
m_SelectionColor: {r: 0, g: 0, b: 0, a: 1} m_SelectionColor: {r: 0.54509807, g: 0.7764706, b: 0.9843137, a: 1}
m_Text: m_Text:
m_CaretBlinkRate: 0.85 m_CaretBlinkRate: 0.85
m_CaretWidth: 1 m_CaretWidth: 1
@@ -635,7 +623,7 @@ MonoBehaviour:
treeListSearch: {fileID: 7660962889100034149} treeListSearch: {fileID: 7660962889100034149}
inputField: {fileID: 7769071907284884258} inputField: {fileID: 7769071907284884258}
clearTextButton: {fileID: 5364685758092017862} clearTextButton: {fileID: 5364685758092017862}
loadingImage: {fileID: 1613511791166383164} loadingImage: {fileID: 196444072060293216}
loadingRotateSpeed: 360 loadingRotateSpeed: 360
loadingFillCycle: 0.5 loadingFillCycle: 0.5
--- !u!114 &5211500305101215241 --- !u!114 &5211500305101215241
@@ -1023,7 +1011,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5} m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5} m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 70, y: 0} m_AnchoredPosition: {x: 55, y: 0}
m_SizeDelta: {x: 10, y: 10} m_SizeDelta: {x: 10, y: 10}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &771756914789545659 --- !u!222 &771756914789545659
@@ -1334,7 +1322,7 @@ PrefabInstance:
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 4856300785373777908, guid: a030935af8c62d748b6fe3d01bd4851f, type: 3} - target: {fileID: 4856300785373777908, guid: a030935af8c62d748b6fe3d01bd4851f, type: 3}
propertyPath: m_Size propertyPath: m_Size
value: 0.8752399 value: 1
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 6081086258189437538, guid: a030935af8c62d748b6fe3d01bd4851f, type: 3} - target: {fileID: 6081086258189437538, guid: a030935af8c62d748b6fe3d01bd4851f, type: 3}
propertyPath: m_AnchorMax.x propertyPath: m_AnchorMax.x
@@ -1503,7 +1491,7 @@ PrefabInstance:
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 4856300785373777908, guid: a030935af8c62d748b6fe3d01bd4851f, type: 3} - target: {fileID: 4856300785373777908, guid: a030935af8c62d748b6fe3d01bd4851f, type: 3}
propertyPath: m_Size propertyPath: m_Size
value: 0.8771593 value: 1
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 6081086258189437538, guid: a030935af8c62d748b6fe3d01bd4851f, type: 3} - target: {fileID: 6081086258189437538, guid: a030935af8c62d748b6fe3d01bd4851f, type: 3}
propertyPath: m_AnchorMax.x propertyPath: m_AnchorMax.x

View File

@@ -33,7 +33,7 @@ Material:
- _QUADS: 1 - _QUADS: 1
- _WIREFRAME: 1 - _WIREFRAME: 1
m_Floats: m_Floats:
- _WireframeScale: 1 - _WireframeScale: 0.5
m_Colors: m_Colors:
- _WireframeColor: {r: 0.5333333, g: 0.5333333, b: 0.5333333, a: 1} - _WireframeColor: {r: 0.5333333, g: 0.5333333, b: 0.5333333, a: 1}
- _WireframeColour: {r: 0.53333336, g: 0.53333336, b: 0.53333336, a: 1} - _WireframeColour: {r: 0.53333336, g: 0.53333336, b: 0.53333336, a: 1}

View File

@@ -1,15 +1,17 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using Cysharp.Threading.Tasks; using Cysharp.Threading.Tasks;
using SHI.modal; using SHI.modal;
using System;
using System.Globalization;
using System.IO;
using UnityEngine; using UnityEngine;
using UnityEngine.UI; using UnityEngine.UI;
/// <summary>
/// 샘플 장면 드라이버: 버튼 클릭으로 SHI BlockDetail 모달을 생성/표시하고,
/// StreamingAssets에서 glb/간트 JSON을 읽어 모달에 전달합니다.
/// </summary>
public class ShiPopupSample : MonoBehaviour public class ShiPopupSample : MonoBehaviour
{ {
[SerializeField] [SerializeField]
private GameObject blockDetailModalPrefab; private GameObject blockDetailModalPrefab;
@@ -18,14 +20,12 @@ public class ShiPopupSample : MonoBehaviour
private BlockDetailModal blockDetailModal; private BlockDetailModal blockDetailModal;
void Start() private void Start()
{ {
if (openModalButton != null) if (openModalButton != null)
{ {
openModalButton.onClick.AddListener(() => openModalButton.onClick.AddListener(() =>
{ {
if (blockDetailModal == null && blockDetailModalPrefab != null) if (blockDetailModal == null && blockDetailModalPrefab != null)
{ {
Canvas canvas = Canvas.FindFirstObjectByType<Canvas>(); Canvas canvas = Canvas.FindFirstObjectByType<Canvas>();
@@ -39,7 +39,7 @@ public class ShiPopupSample : MonoBehaviour
} }
[Serializable] [Serializable]
private class RawScheduleSegment public class RawScheduleSegment
{ {
public string ItemId; public string ItemId;
public string Start; public string Start;
@@ -49,11 +49,14 @@ public class ShiPopupSample : MonoBehaviour
} }
[Serializable] [Serializable]
private class RawGanttChartData public class RawGanttChartData
{ {
public RawScheduleSegment[] Segments; public RawScheduleSegment[] Segments;
} }
/// <summary>
/// StreamingAssets에서 샘플 간트 JSON과 모델을 읽어 모달에 적용합니다.
/// </summary>
private async UniTaskVoid SetupData() private async UniTaskVoid SetupData()
{ {
if (blockDetailModal == null) if (blockDetailModal == null)
@@ -66,7 +69,7 @@ public class ShiPopupSample : MonoBehaviour
string glbPath = Path.Combine(sa, "block.glb"); string glbPath = Path.Combine(sa, "block.glb");
string jsonPath = Path.Combine(sa, "sample_gantt_data.json"); string jsonPath = Path.Combine(sa, "sample_gantt_data.json");
// Load JSON (editor/standalone path). For Android/WebGL, use UnityWebRequest if needed. // 플랫폼에 따라 UnityWebRequest 사용이 필요할 수 있음(여기선 Editor/Standalone 가정)
RawGanttChartData raw = null; RawGanttChartData raw = null;
try try
{ {

View File

@@ -7,6 +7,10 @@ using UnityEngine.UI;
namespace SHI.modal namespace SHI.modal
{ {
/// <summary>
/// 모달 패널 내부에서 모델 뷰, 계층 리스트, 간트 차트를 조율하는 컨트롤러입니다.
/// 컴포넌트 간 선택 동기화, 레이아웃 분할 제어, 데이터 로딩을 담당합니다.
/// </summary>
public class BlockDetailModal : MonoBehaviour public class BlockDetailModal : MonoBehaviour
{ {
[Header("References")] [Header("References")]
@@ -21,7 +25,7 @@ namespace SHI.modal
[SerializeField] private Button dragButton; [SerializeField] private Button dragButton;
[SerializeField] private Button showListButton; [SerializeField] private Button showListButton;
// cached layout elements for split control // split 제어용 캐시
private LayoutElement _modelLayout; private LayoutElement _modelLayout;
private LayoutElement _chartLayout; private LayoutElement _chartLayout;
@@ -36,95 +40,168 @@ namespace SHI.modal
private CancellationTokenSource _cts; private CancellationTokenSource _cts;
private bool _suppressSelection; private bool _suppressSelection;
// key<->id 매핑(차트-리스트/모델 동기화를 위해 유지) // key<->id 매핑(차트-리스트/모델 동기화)
private readonly Dictionary<string, Guid> _keyToId = new Dictionary<string, Guid>(); private readonly Dictionary<string, Guid> _keyToId = new Dictionary<string, Guid>();
private readonly Dictionary<Guid, string> _idToKey = new Dictionary<Guid, string>(); private readonly Dictionary<Guid, string> _idToKey = new Dictionary<Guid, string>();
/// <summary>
/// UI 이벤트를 연결하고 스플리터를 준비합니다.
/// </summary>
public void Start() public void Start()
{ {
// Close // Close
if (closeButton != null) if (closeButton != null)
{ {
closeButton.onClick.AddListener(() => gameObject.SetActive(false)); closeButton.onClick.AddListener(OnCloseClicked);
} }
// list show 버튼 // 리스트 표시 버튼
if (showListButton != null && listView != null) if (showListButton != null && listView != null)
showListButton.onClick.AddListener(() => showListButton.onClick.AddListener(OnShowListClicked);
{
Debug.Log("BlockDetailModal: Show List View");
listView.gameObject.SetActive(true);
showListButton.gameObject.SetActive(false);
if (_splitter != null) _splitter.RefreshPosition();
});
if (showListButton != null) showListButton.gameObject.SetActive(false); if (showListButton != null) showListButton.gameObject.SetActive(false);
// Selection wiring: list -> model/chart // 선택 동기화: 리스트 -> 모델/차트
if (listView != null) if (listView != null)
{ {
listView.OnItemSelected += data => listView.OnItemSelected += OnListItemSelected;
{ listView.OnItemDeselected += OnListItemDeselected;
if (data == null) return; listView.OnClosed += OnListClosed;
HandleSelection(data.Id, "ListView"); listView.OnVisibilityChanged += OnListVisibilityChanged;
};
listView.OnItemDeselected += data =>
{
HandleDeselection(data.Id, "ListView");
};
listView.OnClosed += () =>
{
if (showListButton != null) showListButton.gameObject.SetActive(true);
if (_splitter != null) _splitter.RefreshPosition();
};
listView.OnVisibilityChanged += (id, vis) =>
{
if (modelView != null) modelView.SetVisibility(id, vis);
};
} }
// Selection wiring: model -> list/chart // 선택 동기화: 모델 -> 리스트/차트
if (modelView != null) if (modelView != null)
{ {
modelView.OnItemSelected += data => modelView.OnItemSelected += OnModelItemSelected;
{
if (data == null) return;
HandleSelection(data.Id, "ModelView");
};
} }
// Chart -> list/model // 선택 동기화: 차트 -> 리스트/모델
if (chartView != null) if (chartView != null)
{ {
// key 기반 클릭 우선 사용 chartView.OnRowClickedByKey += OnChartRowClickedByKey;
chartView.OnRowClickedByKey += key => chartView.OnRowClicked += OnChartRowClicked;
{
if (string.IsNullOrEmpty(key)) return;
if (_keyToId.TryGetValue(key, out var id)) HandleSelection(id, "ChartView");
};
// 호환: Guid 이벤트도 유지
chartView.OnRowClicked += id =>
{
HandleSelection(id, "ChartView");
};
} }
// Expand buttons // 확장 버튼
if (modelViewExpandButton != null) if (modelViewExpandButton != null)
modelViewExpandButton.onClick.AddListener(ToggleExpandModel); modelViewExpandButton.onClick.AddListener(ToggleExpandModel);
if (chartViewExpandButton != null) if (chartViewExpandButton != null)
chartViewExpandButton.onClick.AddListener(ToggleExpandChart); chartViewExpandButton.onClick.AddListener(ToggleExpandChart);
// Drag splitter // 드래그 스플리터
SetupSplitControls(); SetupSplitControls();
} }
private void OnCloseClicked()
{
gameObject.SetActive(false);
}
private void OnShowListClicked()
{
if (listView != null) listView.gameObject.SetActive(true);
if (showListButton != null) showListButton.gameObject.SetActive(false);
if (_splitter != null) _splitter.RefreshPosition();
}
private void OnListItemSelected(UVC.UI.List.Tree.TreeListItemData data)
{
if (data == null) return;
HandleSelection(data.Id, "ListView");
}
private void OnListItemDeselected(UVC.UI.List.Tree.TreeListItemData data)
{
HandleDeselection(data.Id, "ListView");
}
private void OnListClosed()
{
if (showListButton != null) showListButton.gameObject.SetActive(true);
if (_splitter != null) _splitter.RefreshPosition();
}
private void OnListVisibilityChanged(Guid id, bool vis)
{
if (modelView != null) modelView.SetVisibility(id, vis);
}
private void OnModelItemSelected(UVC.UI.List.Tree.TreeListItemData data)
{
if (data == null) return;
HandleSelection(data.Id, "ModelView");
}
private void OnChartRowClickedByKey(string key)
{
if (string.IsNullOrEmpty(key)) return;
if (_keyToId.TryGetValue(key, out var id)) HandleSelection(id, "ChartView");
}
private void OnChartRowClicked(Guid id)
{
HandleSelection(id, "ChartView");
}
/// <summary>
/// LoadData 호출 시 리스트/모델/차트를 활성화하고 분할 비율을0.5/0.5로 초기화합니다.
/// </summary>
private void InitializePanelsForLoad()
{
// 활성화 상태 설정
if (listView != null)
{
listView.gameObject.SetActive(true);
listView.Clear();
}
if (showListButton != null) showListButton.gameObject.SetActive(false);
if (ModelRect != null) ModelRect.gameObject.SetActive(true);
if (chartView != null) chartView.gameObject.SetActive(true);
_expanded = ExpandedSide.None;
// 레이아웃 요소 보장 및50/50 설정
var modelRect = ModelRect;
var chartRect = ChartRect;
if (modelRect != null)
{
_modelLayout = modelRect.GetComponent<LayoutElement>() ?? modelRect.gameObject.AddComponent<LayoutElement>();
_modelLayout.flexibleWidth = 1f;
}
if (chartRect != null)
{
_chartLayout = chartRect.GetComponent<LayoutElement>() ?? chartRect.gameObject.AddComponent<LayoutElement>();
_chartLayout.flexibleWidth = 1f;
}
// 스플리터 보장 및 동기화
if (_splitter == null && dragButton != null && modelRect != null && chartRect != null)
{
_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);
}
if (_splitter != null) _splitter.gameObject.SetActive(true);
Canvas.ForceUpdateCanvases();
_splitter?.RefreshPosition();
}
/// <summary>
/// glTF 모델과 간트 데이터를 하위 뷰에 로드하고, 선택 동기화를 위한 매핑을 구축합니다.
/// </summary>
/// <param name="gltfPath">glTF/glb 파일 경로.</param>
/// <param name="gantt">간트 데이터셋.</param>
/// <param name="externalCt">외부 취소 토큰.</param>
public async UniTask LoadData(string gltfPath, GanttChartData gantt, CancellationToken externalCt = default) public async UniTask LoadData(string gltfPath, GanttChartData gantt, CancellationToken externalCt = default)
{ {
Debug.Log($"BlockDetailModal: LoadData {gltfPath}"); Debug.Log($"BlockDetailModal: LoadData {gltfPath}");
// cancel previous
// 레이아웃/활성 상태 초기화 (리스트/모델/차트 활성,50/50 비율)
InitializePanelsForLoad();
// 이전 작업 취소
if (_cts != null) if (_cts != null)
{ {
try { _cts.Cancel(); } catch { } try { _cts.Cancel(); } catch { }
@@ -133,7 +210,7 @@ namespace SHI.modal
_cts = CancellationTokenSource.CreateLinkedTokenSource(externalCt); _cts = CancellationTokenSource.CreateLinkedTokenSource(externalCt);
var ct = _cts.Token; var ct = _cts.Token;
// load model and list // 모델/리스트 로드
IEnumerable<ModelDetailListItemData> items = Array.Empty<ModelDetailListItemData>(); IEnumerable<ModelDetailListItemData> items = Array.Empty<ModelDetailListItemData>();
if (modelView != null) if (modelView != null)
{ {
@@ -143,21 +220,44 @@ namespace SHI.modal
} }
catch (OperationCanceledException) { } catch (OperationCanceledException) { }
} }
// 매핑 초기화
_keyToId.Clear(); BuildKeyMaps(items);
_idToKey.Clear();
foreach (var it in items)
{
if (!string.IsNullOrEmpty(it.ExternalKey))
{
_keyToId[it.ExternalKey] = it.Id;
_idToKey[it.Id] = it.ExternalKey;
}
}
if (listView != null) listView.SetupData(items); if (listView != null) listView.SetupData(items);
if (chartView != null) chartView.LoadData(gantt); if (chartView != null) chartView.LoadData(gantt);
} }
private void BuildKeyMaps(IEnumerable<ModelDetailListItemData> items)
{
_keyToId.Clear();
_idToKey.Clear();
if (items == null) return;
var stack = new Stack<ModelDetailListItemData>();
foreach (var it in items)
{
if (it == null) continue;
stack.Push(it);
while (stack.Count > 0)
{
var cur = stack.Pop();
if (!string.IsNullOrEmpty(cur.ExternalKey))
{
_keyToId[cur.ExternalKey] = cur.Id;
_idToKey[cur.Id] = cur.ExternalKey;
}
if (cur.Children != null)
{
for (int i = 0; i < cur.Children.Count; i++)
{
var child = cur.Children[i] as ModelDetailListItemData;
if (child != null) stack.Push(child);
}
}
}
}
}
private void SetupSplitControls() private void SetupSplitControls()
{ {
var modelRect = ModelRect; var modelRect = ModelRect;
@@ -170,17 +270,18 @@ namespace SHI.modal
_chartLayout = chartRect.GetComponent<LayoutElement>(); _chartLayout = chartRect.GetComponent<LayoutElement>();
if (_chartLayout == null) _chartLayout = chartView.gameObject.AddComponent<LayoutElement>(); if (_chartLayout == null) _chartLayout = chartView.gameObject.AddComponent<LayoutElement>();
// initial split50/50 // 초기 분할50/50
_modelLayout.flexibleWidth =1f; _modelLayout.flexibleWidth = 1f;
_chartLayout.flexibleWidth =1f; _chartLayout.flexibleWidth = 1f;
// attach drag handler // 드래그 핸들 부착
_splitter = dragButton.gameObject.GetComponent<HorizontalSplitDrag>(); _splitter = dragButton.gameObject.GetComponent<HorizontalSplitDrag>();
if (_splitter == null) _splitter = dragButton.gameObject.AddComponent<HorizontalSplitDrag>(); if (_splitter == null) _splitter = dragButton.gameObject.AddComponent<HorizontalSplitDrag>();
var leftFixed = listView != null ? listView.GetComponent<RectTransform>() : null; var leftFixed = listView != null ? listView.GetComponent<RectTransform>() : null;
_splitter.Initialize(modelRect, chartRect, leftFixed); _splitter.Initialize(modelRect, chartRect, leftFixed);
//시간이 좀 필요 함 // 레이아웃 갱신 후 위치 보정
UniTask.DelayFrame(1).ContinueWith(() => _splitter.RefreshPosition()); Canvas.ForceUpdateCanvases();
_splitter.RefreshPosition();
} }
private void HandleSelection(Guid itemId, string source) private void HandleSelection(Guid itemId, string source)
@@ -234,8 +335,8 @@ namespace SHI.modal
_expanded = ExpandedSide.None; _expanded = ExpandedSide.None;
if (ModelRect != null) ModelRect.gameObject.SetActive(true); if (ModelRect != null) ModelRect.gameObject.SetActive(true);
if (chartView != null) chartView.gameObject.SetActive(true); if (chartView != null) chartView.gameObject.SetActive(true);
if (_modelLayout != null) _modelLayout.flexibleWidth =1f; if (_modelLayout != null) _modelLayout.flexibleWidth = 1f;
if (_chartLayout != null) _chartLayout.flexibleWidth =1f; if (_chartLayout != null) _chartLayout.flexibleWidth = 1f;
if (_splitter != null) if (_splitter != null)
{ {
_splitter.gameObject.SetActive(true); _splitter.gameObject.SetActive(true);
@@ -254,5 +355,38 @@ namespace SHI.modal
if (chartView != null) chartView.Dispose(); if (chartView != null) chartView.Dispose();
if (modelView != null) modelView.Dispose(); if (modelView != null) modelView.Dispose();
} }
private void OnDestroy()
{
// 이벤트 해제(메모리 누수 방지)
if (closeButton != null) closeButton.onClick.RemoveListener(OnCloseClicked);
if (showListButton != null && listView != null) showListButton.onClick.RemoveListener(OnShowListClicked);
if (modelViewExpandButton != null) modelViewExpandButton.onClick.RemoveListener(ToggleExpandModel);
if (chartViewExpandButton != null) chartViewExpandButton.onClick.RemoveListener(ToggleExpandChart);
if (listView != null)
{
listView.OnItemSelected -= OnListItemSelected;
listView.OnItemDeselected -= OnListItemDeselected;
listView.OnClosed -= OnListClosed;
listView.OnVisibilityChanged -= OnListVisibilityChanged;
}
if (modelView != null)
{
modelView.OnItemSelected -= OnModelItemSelected;
}
if (chartView != null)
{
chartView.OnRowClickedByKey -= OnChartRowClickedByKey;
chartView.OnRowClicked -= OnChartRowClicked;
}
if (_cts != null)
{
try { _cts.Cancel(); } catch { }
_cts.Dispose();
_cts = null;
}
}
} }
} }

View File

@@ -4,20 +4,31 @@ using System.Collections.Generic;
namespace SHI.modal namespace SHI.modal
{ {
/// <summary>
/// 간트 차트의 한 구간(세그먼트) 정보를 나타냅니다.
/// </summary>
public class ScheduleSegment public class ScheduleSegment
{ {
// Stable external mapping key (preferred) /// <summary>외부 연동용 안정 키(권장).</summary>
public string ItemKey { get; set; } = string.Empty; public string ItemKey { get; set; } = string.Empty;
// Backward compatibility GUID (optional) /// <summary>호환용 GUID(선택적).</summary>
public Guid ItemId { get; set; } public Guid ItemId { get; set; }
/// <summary>시작 시각(UTC 권장).</summary>
public DateTime Start { get; set; } public DateTime Start { get; set; }
/// <summary>종료 시각(UTC 권장).</summary>
public DateTime End { get; set; } public DateTime End { get; set; }
/// <summary>진행률 값([0..1] 또는 [0..100] 등 상위 시스템 규약 따름).</summary>
public float Progress { get; set; } public float Progress { get; set; }
/// <summary>유형/카테고리 등 사용자 지정 문자열.</summary>
public string Type { get; set; } = string.Empty; public string Type { get; set; } = string.Empty;
} }
/// <summary>
/// 간단한 간트 차트 데이터셋입니다.
/// </summary>
public class GanttChartData public class GanttChartData
{ {
/// <summary>표시 순서대로의 세그먼트 컬렉션.</summary>
public List<ScheduleSegment> Segments { get; set; } = new List<ScheduleSegment>(); public List<ScheduleSegment> Segments { get; set; } = new List<ScheduleSegment>();
} }
} }

View File

@@ -6,11 +6,13 @@ using UnityEngine.UI;
namespace SHI.modal namespace SHI.modal
{ {
/// <summary> /// <summary>
/// 두 개의 RectTransform 가로 분할을 드래그 버튼으로 조절하는 간단한 스플리터. /// 수평 레이아웃에서 두 패널의 가중치(LayoutElement.flexibleWidth)를 드래그 핸들로 조절하는 간단한 분할기입니다.
/// 레이아웃 그룹(수평) + LayoutElement.flexibleWidth 기반으로 동작합니다.
/// </summary> /// </summary>
public class HorizontalSplitDrag : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler public class HorizontalSplitDrag : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
{ {
[SerializeField, Range(0.0f, 0.5f)] private float minLeftWeight = 0.1f;
[SerializeField, Range(0.5f, 1.0f)] private float maxLeftWeight = 0.9f;
private RectTransform _left; private RectTransform _left;
private RectTransform _right; private RectTransform _right;
private LayoutElement _leftLayout; private LayoutElement _leftLayout;
@@ -24,6 +26,9 @@ namespace SHI.modal
private bool _lastLeftPanelActive; private bool _lastLeftPanelActive;
private float _lastParentWidth; private float _lastParentWidth;
/// <summary>
/// 좌/우 패널을 지정하고(필요 시 좌측 고정 패널 포함) 분할기를 초기화합니다.
/// </summary>
public void Initialize(RectTransform left, RectTransform right, RectTransform? leftFixedPanel = null) public void Initialize(RectTransform left, RectTransform right, RectTransform? leftFixedPanel = null)
{ {
_left = left; _left = left;
@@ -32,25 +37,27 @@ namespace SHI.modal
_parent = left != null ? left.parent as RectTransform : null; _parent = left != null ? left.parent as RectTransform : null;
_handleRect = transform as RectTransform; _handleRect = transform as RectTransform;
_leftLayout = _left.GetComponent<LayoutElement>(); _leftLayout = _left != null ? _left.GetComponent<LayoutElement>() : null;
if (_leftLayout == null) _leftLayout = _left.gameObject.AddComponent<LayoutElement>(); if (_left != null && _leftLayout == null) _leftLayout = _left.gameObject.AddComponent<LayoutElement>();
_rightLayout = _right.GetComponent<LayoutElement>(); _rightLayout = _right != null ? _right.GetComponent<LayoutElement>() : null;
if (_rightLayout == null) _rightLayout = _right.gameObject.AddComponent<LayoutElement>(); if (_right != null && _rightLayout == null) _rightLayout = _right.gameObject.AddComponent<LayoutElement>();
_lastLeftPanelActive = _leftFixedPanel != null && _leftFixedPanel.gameObject.activeInHierarchy; _lastLeftPanelActive = _leftFixedPanel != null && _leftFixedPanel.gameObject.activeInHierarchy;
_lastParentWidth = _parent != null ? _parent.rect.width : 0f; _lastParentWidth = _parent != null ? _parent.rect.width : 0f;
RefreshPosition(); RefreshPosition();
} }
/// <inheritdoc />
public void OnBeginDrag(PointerEventData eventData) public void OnBeginDrag(PointerEventData eventData)
{ {
if (_parent != null) _parentWidth = _parent.rect.width; if (_parent != null) _parentWidth = _parent.rect.width;
if (_handleRect != null) _handleY = _handleRect.anchoredPosition.y; if (_handleRect != null) _handleY = _handleRect.anchoredPosition.y;
} }
/// <inheritdoc />
public void OnDrag(PointerEventData eventData) public void OnDrag(PointerEventData eventData)
{ {
if (_parent == null) return; if (_parent == null || _leftLayout == null || _rightLayout == null) return;
Vector2 local; Vector2 local;
if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(_parent, eventData.position, eventData.pressEventCamera, out local)) if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(_parent, eventData.position, eventData.pressEventCamera, out local))
return; return;
@@ -58,34 +65,37 @@ namespace SHI.modal
float width = _parent.rect.width; float width = _parent.rect.width;
if (width <= 0f) return; if (width <= 0f) return;
// 계산 범위: 좌측 고정 패널(보이는 경우)의 폭만큼 좌측 경계를 오른쪽으로 이동 // 좌측 고정 패널(보이는 경우)의 폭만큼 좌측 경계를 오른쪽으로 이동
float leftOffset = 0f; float leftOffset = 0f;
if (_leftFixedPanel != null && _leftFixedPanel.gameObject.activeSelf) if (_leftFixedPanel != null && _leftFixedPanel.gameObject.activeSelf)
{ {
leftOffset = _leftFixedPanel.rect.width; leftOffset = _leftFixedPanel.rect.width;
} }
float minX = -width * 0.5f + leftOffset; // 사용 가능한 작업 영역 좌측 경계 float minX = -width * 0.5f + leftOffset; // 작업 영역 좌측 경계
float maxX = width * 0.5f; // 우측 경계 float maxX = width * 0.5f; // 작업 영역 우측 경계
// 현재 포인터 위치를 작업 영역 비율[0..1]로 변환 후 범위 제한 // 현재 포인터 위치를 작업 영역 비율[0..1]로 변환 후 범위 제한
float t = Mathf.InverseLerp(minX, maxX, local.x); float t = Mathf.InverseLerp(minX, maxX, local.x);
t = Mathf.Clamp01(t); t = Mathf.Clamp01(t);
// LayoutElement 비율 (양 끝 과도값 방지하여10%~90% 사이 유지) // LayoutElement 비율 (가변 범위)
float leftWeight = Mathf.Clamp(t, 0.1f, 0.9f); float leftWeight = Mathf.Lerp(minLeftWeight, maxLeftWeight, t);
float rightWeight = 1f - leftWeight; float rightWeight = Mathf.Max(0.0001f, 1f - leftWeight);
_leftLayout.flexibleWidth = leftWeight; _leftLayout.flexibleWidth = leftWeight;
_rightLayout.flexibleWidth = rightWeight; _rightLayout.flexibleWidth = rightWeight;
// 스플리터 핸들도 같은 좌표계에서 이동 // 스플리터 핸들도 같은 좌표계에서 이동
if (_handleRect != null) if (_handleRect != null)
{ {
float clampedX = Mathf.Lerp(minX, maxX, leftWeight); float clampedX = Mathf.Lerp(minX, maxX, Mathf.InverseLerp(minLeftWeight, maxLeftWeight, leftWeight));
_handleRect.anchoredPosition = new Vector2(clampedX, _handleY); _handleRect.anchoredPosition = new Vector2(clampedX, _handleY);
} }
} }
// 외부에서 강제로 현재 레이아웃 기준으로 핸들 위치를 동기화합니다. /// <summary>
/// 현재 레이아웃 상태에 맞게 드래그 핸들의 위치를 동기화합니다.
/// (레이아웃/가시성 변경 이후 호출 권장)
/// </summary>
public void RefreshPosition() public void RefreshPosition()
{ {
if (_parent == null || _handleRect == null || _leftLayout == null || _rightLayout == null) if (_parent == null || _handleRect == null || _leftLayout == null || _rightLayout == null)
@@ -103,11 +113,11 @@ namespace SHI.modal
float totalFlex = Mathf.Max(0.0001f, _leftLayout.flexibleWidth + _rightLayout.flexibleWidth); float totalFlex = Mathf.Max(0.0001f, _leftLayout.flexibleWidth + _rightLayout.flexibleWidth);
float leftWeight = Mathf.Clamp01(_leftLayout.flexibleWidth / totalFlex); float leftWeight = Mathf.Clamp01(_leftLayout.flexibleWidth / totalFlex);
leftWeight = Mathf.Clamp(leftWeight, 0.1f, 0.9f); leftWeight = Mathf.Clamp(leftWeight, minLeftWeight, maxLeftWeight);
if (_handleRect != null) if (_handleRect != null)
{ {
_handleRect.anchoredPosition = new Vector2(Mathf.Lerp(minX, maxX, leftWeight), _handleY); _handleRect.anchoredPosition = new Vector2(Mathf.Lerp(minX, maxX, Mathf.InverseLerp(minLeftWeight, maxLeftWeight, leftWeight)), _handleY);
} }
} }
@@ -123,6 +133,7 @@ namespace SHI.modal
} }
} }
/// <inheritdoc />
public void OnEndDrag(PointerEventData eventData) { } public void OnEndDrag(PointerEventData eventData) { }
} }
} }

View File

@@ -15,34 +15,47 @@ namespace SHI.modal
private GanttChartData? _data; private GanttChartData? _data;
/// <summary>
/// 간트 데이터를 바인딩합니다(스텁 구현).
/// </summary>
public void LoadData(GanttChartData data) public void LoadData(GanttChartData data)
{ {
_data = data; _data = data;
Debug.Log($"ModelDetailChartView.LoadData: segments={data?.Segments?.Count ??0}"); Debug.Log($"ModelDetailChartView.LoadData: segments={data?.Segments?.Count ??0}");
} }
/// <summary>
/// 외부 키로 행을 하이라이트합니다.
/// </summary>
public void SelectByItemKey(string key) public void SelectByItemKey(string key)
{ {
if (_data == null) { Debug.Log("ChartView.SelectByItemKey: no data"); return; } if (_data == null) { Debug.Log("ChartView.SelectByItemKey: no data"); return; }
Debug.Log($"Chart highlight by key: {key}"); Debug.Log($"Chart highlight by key: {key}");
} }
/// <summary>
/// Guid 식별자로 행을 하이라이트합니다.
/// </summary>
public void SelectByItemId(Guid id) public void SelectByItemId(Guid id)
{ {
if (_data == null) { Debug.Log("ChartView.SelectByItemId: no data"); return; } if (_data == null) { Debug.Log("ChartView.SelectByItemId: no data"); return; }
Debug.Log($"Chart highlight by id: {id}"); Debug.Log($"Chart highlight by id: {id}");
} }
// Simulate UI callbacks // UI 시뮬레이션 콜백
public void SimulateRowClickKey(string key) public void SimulateRowClickKey(string key)
{ {
OnRowClickedByKey?.Invoke(key); OnRowClickedByKey?.Invoke(key);
} }
public void SimulateRowClick(string id) public void SimulateRowClick(string id)
{ {
if (Guid.TryParse(id, out var guid)) OnRowClicked?.Invoke(guid); if (Guid.TryParse(id, out var guid)) OnRowClicked?.Invoke(guid);
} }
/// <summary>
/// 바인딩된 데이터를 해제합니다.
/// </summary>
public void Dispose() { _data = null; } public void Dispose() { _data = null; }
} }
} }

View File

@@ -1,16 +1,11 @@
#nullable enable #nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UnityEngine; using UnityEngine;
using UVC.UI.Buttons; using UVC.UI.Buttons;
using UVC.UI.List.Tree; using UVC.UI.List.Tree;
namespace SHI.modal namespace SHI.modal
{ {
public class ModelDetailListItem: TreeListItem public class ModelDetailListItem : TreeListItem
{ {
/// <summary> /// <summary>
/// 가시성 상태를 표시하는 배경 이미지. /// 가시성 상태를 표시하는 배경 이미지.
@@ -22,7 +17,11 @@ namespace SHI.modal
{ {
base.Init(data, control, dragDropManager); base.Init(data, control, dragDropManager);
if (visibleToggle != null) if (visibleToggle != null)
{ {
if (data is ModelDetailListItemData modelData) {
visibleToggle.isOn = modelData.IsVisible;
}
visibleToggle.OnValueChanged.AddListener(isOn => visibleToggle.OnValueChanged.AddListener(isOn =>
{ {
if (data is ModelDetailListItemData modelData) if (data is ModelDetailListItemData modelData)
@@ -34,6 +33,18 @@ namespace SHI.modal
} }
} }
protected override void OnDataChanged(ChangedType changedType, TreeListItemData changedData, int index)
{
if (changedType == ChangedType.TailButtons && changedData is ModelDetailListItemData modelData)
{
if (visibleToggle != null)
{
visibleToggle.isOn = modelData.IsVisible;
}
}
base.OnDataChanged(changedType, changedData, index);
}
protected override void OnDestroy() protected override void OnDestroy()
{ {
if (visibleToggle != null) visibleToggle.OnValueChanged.RemoveAllListeners(); if (visibleToggle != null) visibleToggle.OnValueChanged.RemoveAllListeners();

View File

@@ -20,5 +20,23 @@ namespace SHI.modal
/// 외부(간트/백엔드)와의 안정 매핑용 키. 예: GLTF 노드의 풀 경로("/Root/Level1/Beam023"). /// 외부(간트/백엔드)와의 안정 매핑용 키. 예: GLTF 노드의 풀 경로("/Root/Level1/Beam023").
/// </summary> /// </summary>
public string ExternalKey { get; set; } = string.Empty; public string ExternalKey { get; set; } = string.Empty;
public override TreeListItemData Clone()
{
ModelDetailListItemData clone = new ModelDetailListItemData();
clone._id = this.Id;
clone.Name = this.Name;
clone.Option = this.Option;
clone.IsExpanded = this.IsExpanded;
clone.IsSelected = this.IsSelected;
clone.OnClickAction = this.OnClickAction;
clone.OnSelectionChanged = this.OnSelectionChanged;
clone.OnDataChanged = this.OnDataChanged;
clone.IsVisible = this.IsVisible;
clone.ExternalKey = this.ExternalKey;
clone.OnClickVisibleAction = this.OnClickVisibleAction;
return clone;
}
} }
} }

View File

@@ -108,14 +108,10 @@ namespace SHI.modal
} }
clearTextButton.onClick.AddListener(() => clearTextButton.onClick.AddListener(() =>
{ {
inputField.text = string.Empty;
// 취소 // 취소
CancelSearch(); CancelSearch();
treeListSearch.gameObject.SetActive(false);
treeList.gameObject.SetActive(true);
// 검색에서 선택한 항목이 있으면 원본 트리에서 동일 항목을 선택하고 펼침 // 검색에서 선택한 항목이 있으면 원본 트리에서 동일 항목을 선택하고 펼침
if (selectedSearchItem != null && treeList != null) if (selectedSearchItem != null && treeList != null)
{ {
@@ -123,6 +119,8 @@ namespace SHI.modal
var target = treeList.AllItemDataFlattened.FirstOrDefault(i => i == selectedSearchItem); var target = treeList.AllItemDataFlattened.FirstOrDefault(i => i == selectedSearchItem);
if (target != null) if (target != null)
{ {
ClearSelection();
// 부모 체인을 펼치고 선택 처리 // 부모 체인을 펼치고 선택 처리
treeList.RevealAndSelectItem(target, true); treeList.RevealAndSelectItem(target, true);
} }
@@ -132,6 +130,7 @@ namespace SHI.modal
}); });
} }
/// <summary> /// <summary>
/// NEW: 데이터 주입 /// NEW: 데이터 주입
/// </summary> /// </summary>
@@ -166,7 +165,7 @@ namespace SHI.modal
} }
/// <summary> /// <summary>
/// NEW: Guid 기반 선택 /// Guid 기반 선택
/// </summary> /// </summary>
/// <param name="id"></param> /// <param name="id"></param>
public void SelectByItemId(Guid id) public void SelectByItemId(Guid id)
@@ -174,6 +173,7 @@ namespace SHI.modal
var target = treeList.AllItemDataFlattened.FirstOrDefault(t => t.Id == id); var target = treeList.AllItemDataFlattened.FirstOrDefault(t => t.Id == id);
if (target != null) if (target != null)
{ {
ClearSelection();
treeList.RevealAndSelectItem(target, true); treeList.RevealAndSelectItem(target, true);
} }
} }
@@ -232,8 +232,6 @@ namespace SHI.modal
{ {
//검색 중이면 취소 //검색 중이면 취소
CancelSearch(); CancelSearch();
treeListSearch.gameObject.SetActive(false);
treeList.gameObject.SetActive(true);
treeList.SelectItem(name); treeList.SelectItem(name);
} }
@@ -247,6 +245,24 @@ namespace SHI.modal
treeList.DeselectItem(name); treeList.DeselectItem(name);
} }
/// <summary>
/// 선택해제 및 검색 취소
/// </summary>
public void Clear()
{
ClearSelection();
CancelSearch();
}
/// <summary>
/// 선택 해제
/// </summary>
public void ClearSelection()
{
treeListSearch.ClearSelection();
treeList.ClearSelection();
}
protected void StartLoadingAnimation() protected void StartLoadingAnimation()
{ {
if (loadingImage == null) return; if (loadingImage == null) return;
@@ -295,8 +311,47 @@ namespace SHI.modal
} }
} }
protected void CancelSearch() protected void CancelSearch()
{ {
// 검색 중이면
if (treeListSearch?.gameObject.activeSelf == true)
{
//검색 시 데이터 변경 된 내용 원본 트리에 반영
treeListSearch.AllItemDataFlattened.ToList().ForEach(searchItem =>
{
var originalItem = treeList.AllItemDataFlattened.FirstOrDefault(i => i == searchItem);
if (originalItem != null)
{
bool changed = false;
//선택 상태 동기화
if (originalItem.IsSelected != searchItem.IsSelected)
{
originalItem.IsSelected = searchItem.IsSelected;
changed = true;
}
//가시성 상태 동기화 (ModelDetailListItemData 전용)
if (originalItem is ModelDetailListItemData originalModelItem &&
searchItem is ModelDetailListItemData searchModelItem &&
originalModelItem.IsVisible != searchModelItem.IsVisible)
{
originalModelItem.IsVisible = searchModelItem.IsVisible;
changed = true;
}
if (changed)
{
//데이터 변경 알림
originalItem.NotifyDataChanged(ChangedType.TailButtons, originalItem);
}
}
});
}
inputField.text = string.Empty;
treeListSearch?.gameObject.SetActive(false);
treeList?.gameObject.SetActive(true);
if (searchCts != null) if (searchCts != null)
{ {
try { searchCts.Cancel(); } catch { } try { searchCts.Cancel(); } catch { }
@@ -337,12 +392,11 @@ namespace SHI.modal
protected void OnInputFieldSubmit(string text) protected void OnInputFieldSubmit(string text)
{ {
// 기존 검색 취소
CancelSearch();
// 검색어가 있으면 검색 결과 목록 표시 // 검색어가 있으면 검색 결과 목록 표시
if (!string.IsNullOrEmpty(text)) if (!string.IsNullOrEmpty(text))
{ {
treeListSearch.ClearSelection();
treeListSearch.gameObject.SetActive(true); treeListSearch.gameObject.SetActive(true);
treeList.gameObject.SetActive(false); treeList.gameObject.SetActive(false);
@@ -355,6 +409,9 @@ namespace SHI.modal
} }
else else
{ {
// 기존 검색 취소
CancelSearch();
treeListSearch.gameObject.SetActive(false); treeListSearch.gameObject.SetActive(false);
treeList.gameObject.SetActive(true); treeList.gameObject.SetActive(true);
} }
@@ -423,7 +480,13 @@ namespace SHI.modal
treeListSearch.ClearItems(); treeListSearch.ClearItems();
foreach (var r in results) foreach (var r in results)
{ {
treeListSearch.AddItem<ModelDetailListItem>(r.Clone()); var cloned = r.Clone();
treeListSearch.AddItem<ModelDetailListItem>(cloned);
if(cloned.IsSelected)
{
//선택된 항목은 펼치기
treeListSearch.SelectItem(cloned);
}
} }
// 로딩 종료 // 로딩 종료

View File

@@ -11,8 +11,15 @@ using UVC.Util;
namespace SHI.modal namespace SHI.modal
{ {
/// <summary>
/// glTF 모델을 비동기로 로드해 전용 카메라로 오프스크린 렌더링하고, 결과를 <see cref="RawImage"/>에 출력하는 뷰입니다.
/// 마우스 조작(이동/확대/회전), 항목 하이라이트, 와이어프레임 토글 등을 제공합니다.
/// </summary>
public class ModelDetailView : MonoBehaviour public class ModelDetailView : MonoBehaviour
{ {
/// <summary>
/// 뷰 내부에서 항목이 선택될 때 발생합니다.
/// </summary>
public Action<TreeListItemData>? OnItemSelected; public Action<TreeListItemData>? OnItemSelected;
[Header("View Output (UI)")] [Header("View Output (UI)")]
@@ -57,11 +64,18 @@ namespace SHI.modal
private Vector3 _rmbPivot; // RMB 회전의 피벗(모델 중심) private Vector3 _rmbPivot; // RMB 회전의 피벗(모델 중심)
private readonly Dictionary<Guid, GameObject> _idToObject = new Dictionary<Guid, GameObject>(); private readonly Dictionary<Guid, GameObject> _idToObject = new Dictionary<Guid, GameObject>();
private readonly Dictionary<GameObject, (Renderer[] rends, UnityEngine.Material[][] originals)> _matCache = new Dictionary<GameObject, (Renderer[], UnityEngine.Material[][])>(); // 렌더러별 원본 sharedMaterials 캐시(서브트리 중복/충돌 방지)
private readonly Dictionary<Renderer, UnityEngine.Material[]> _originalSharedByRenderer = new Dictionary<Renderer, UnityEngine.Material[]>();
private GameObject? _root; private GameObject? _root;
private Guid? _focusedId; private Guid? _focusedId;
/// <summary>
/// 주어진 경로의 glTF 모델을 비동기로 로드하고, UI 트리에 사용할 계층 항목을 생성합니다.
/// 또한 오프스크린 카메라/RT를 준비하고 모델을 화면에 맞춰 프레이밍합니다.
/// </summary>
/// <param name="path">glTF/glb 파일 경로(절대/StreamingAssets 기반).</param>
/// <param name="ct">취소 토큰.</param>
/// <returns>인스턴스화된 노드에 해당하는 항목 계층.</returns>
public async UniTask<IEnumerable<ModelDetailListItemData>> LoadModelAsync(string path, CancellationToken ct) public async UniTask<IEnumerable<ModelDetailListItemData>> LoadModelAsync(string path, CancellationToken ct)
{ {
Debug.Log($"ModelDetailView.LoadModelAsync: {path}"); Debug.Log($"ModelDetailView.LoadModelAsync: {path}");
@@ -122,7 +136,8 @@ namespace SHI.modal
var data = new ModelDetailListItemData var data = new ModelDetailListItemData
{ {
Name = node.name, Name = node.name,
ExternalKey = BuildFullPath(node, root) ExternalKey = BuildFullPath(node, root),
IsExpanded = true
}; };
// map id -> object for selection/highlight and cache materials at this subtree root // map id -> object for selection/highlight and cache materials at this subtree root
@@ -168,14 +183,12 @@ namespace SHI.modal
private void RestoreAllOriginalMaterials() private void RestoreAllOriginalMaterials()
{ {
foreach (var kv in _matCache) foreach (var kv in _originalSharedByRenderer)
{ {
var (rends, originals) = kv.Value; var r = kv.Key;
for (int i = 0; i < rends.Length; i++) if (r == null) continue;
{ var originals = kv.Value;
if (rends[i] == null) continue; r.sharedMaterials = originals;
rends[i].sharedMaterials = originals[i];
}
} }
_wireframeApplied = false; _wireframeApplied = false;
} }
@@ -187,7 +200,7 @@ namespace SHI.modal
if (_viewCamera == null) if (_viewCamera == null)
{ {
// Create a world-space rig not parented to UI to avoid layout side effects // UI와 분리된 월드 공간 리그 생성(레이아웃 영향 차단)
var rig = new GameObject("ModelDetailViewRig"); var rig = new GameObject("ModelDetailViewRig");
rig.layer = modelLayer; rig.layer = modelLayer;
rig.transform.SetParent(null, false); rig.transform.SetParent(null, false);
@@ -201,12 +214,12 @@ namespace SHI.modal
_viewCamera.nearClipPlane = 0.01f; _viewCamera.nearClipPlane = 0.01f;
_viewCamera.farClipPlane = 5000f; _viewCamera.farClipPlane = 5000f;
_viewCamera.cullingMask = (modelLayer >= 0 && modelLayer <= 31) ? (1 << modelLayer) : ~0; _viewCamera.cullingMask = (modelLayer >= 0 && modelLayer <= 31) ? (1 << modelLayer) : ~0;
// Prevent drawing to main display when no RT assigned // RT 미할당 시 메인 디스플레이 출력 방지
_viewCamera.targetDisplay = 1; // secondary display _viewCamera.targetDisplay = 1; // secondary display
_viewCamera.enabled = false; // enable only when RT is bound _viewCamera.enabled = false; // RT가 바인딩될 때만 활성화
} }
// optional default light (independent of UI) // 기본 조명(옵션, UI와 독립)
if (createDefaultLight && _viewCamera.transform.parent != null && _viewCamera.transform.parent.Find("ModelDetailViewLight") == null) if (createDefaultLight && _viewCamera.transform.parent != null && _viewCamera.transform.parent.Find("ModelDetailViewLight") == null)
{ {
var lightGo = new GameObject("ModelDetailViewLight"); var lightGo = new GameObject("ModelDetailViewLight");
@@ -256,7 +269,7 @@ namespace SHI.modal
} }
if (outputImage == null) if (outputImage == null)
{ {
// No output target; keep camera disabled to avoid drawing to display // 출력 타겟이 없으면 메인 디스플레이 출력 방지
_viewCamera.enabled = false; _viewCamera.enabled = false;
return; return;
} }
@@ -280,7 +293,7 @@ namespace SHI.modal
}; };
_viewCamera.targetTexture = _rt; _viewCamera.targetTexture = _rt;
outputImage.texture = _rt; outputImage.texture = _rt;
// enable camera only when RT is assigned // RT 바인딩 시에만 카메라 활성화
_viewCamera.enabled = true; _viewCamera.enabled = true;
} }
else else
@@ -296,11 +309,14 @@ namespace SHI.modal
protected void OnRectTransformDimensionsChange() protected void OnRectTransformDimensionsChange()
{ {
// Keep RT in sync with UI size changes (e.g., parent resized) // UI 리사이즈에 따라 RT 크기 동기화
EnsureRenderTargetSize(); EnsureRenderTargetSize();
} }
// Allow external toggle /// <summary>
/// 와이어프레임 모드를 토글합니다.
/// </summary>
/// <param name="on">true면 와이어프레임 적용, false면 원본 머티리얼 복원.</param>
public void SetWireframe(bool on) public void SetWireframe(bool on)
{ {
wireframeMode = on; wireframeMode = on;
@@ -315,76 +331,18 @@ namespace SHI.modal
} }
} }
private static void SetLayerRecursive(GameObject go, int layer) /// <summary>
{ /// 지정한 항목을 포커스(하이라이트)합니다.
if (layer < 0 || layer > 31) return; /// </summary>
go.layer = layer;
foreach (Transform c in go.transform) SetLayerRecursive(c.gameObject, layer);
}
private static Bounds CalculateBounds(GameObject root)
{
var rends = root.GetComponentsInChildren<Renderer>(true);
var has = false;
var bounds = new Bounds(root.transform.position, Vector3.zero);
foreach (var r in rends)
{
if (r == null) continue;
if (!has) { bounds = r.bounds; has = true; }
else bounds.Encapsulate(r.bounds);
}
if (!has) bounds = new Bounds(root.transform.position, Vector3.one);
return bounds;
}
private void FrameToBounds(Bounds b)
{
if (_viewCamera == null) return;
var center = b.center;
var extents = b.extents;
float radius = Mathf.Max(extents.x, Mathf.Max(extents.y, Mathf.Max(extents.z, 0.001f)));
float fovRad = _viewCamera.fieldOfView * Mathf.Deg2Rad;
float dist = radius / Mathf.Sin(fovRad * 0.5f);
dist = Mathf.Clamp(dist, 1f, 1e4f);
_viewCamera.transform.position = center + new Vector3(1, 0.5f, 1).normalized * dist;
_viewCamera.transform.LookAt(center);
//_viewCamera.nearClipPlane = Mathf.Max(0.01f, dist - radius * 2f);
//_viewCamera.farClipPlane = dist + radius * 4f;
_orbitTarget = center;
_orbitDistance = Vector3.Distance(_viewCamera.transform.position, _orbitTarget);
var dir = (_viewCamera.transform.position - _orbitTarget).normalized;
_yaw = Mathf.Atan2(dir.x, dir.z) * Mathf.Rad2Deg;
_pitch = Mathf.Asin(dir.y) * Mathf.Rad2Deg;
}
private string BuildFullPath(Transform node, Transform root)
{
var stack = new Stack<string>();
var current = node;
while (current != null && current != root)
{
stack.Push(current.name);
current = current.parent;
}
return "/" + string.Join('/', stack);
}
private void CacheOriginalMaterials(GameObject go)
{
var rends = go.GetComponentsInChildren<Renderer>(true);
if (rends.Length == 0) return;
var originals = new UnityEngine.Material[rends.Length][];
for (int i = 0; i < rends.Length; i++) originals[i] = rends[i].sharedMaterials;
_matCache[go] = (rends, originals);
}
public void FocusItem(TreeListItemData data) public void FocusItem(TreeListItemData data)
{ {
if (data == null) return; if (data == null) return;
FocusItemById(data.Id); FocusItemById(data.Id);
} }
/// <summary>
/// Guid 식별자로 항목을 포커스(하이라이트)합니다.
/// </summary>
public void FocusItemById(Guid id) public void FocusItemById(Guid id)
{ {
_focusedId = id; _focusedId = id;
@@ -396,6 +354,9 @@ namespace SHI.modal
} }
} }
/// <summary>
/// 포커스를 해제하고 하이라이트를 제거합니다.
/// </summary>
public void UnfocusItem() public void UnfocusItem()
{ {
if (_focusedId.HasValue && _idToObject.TryGetValue(_focusedId.Value, out var go)) if (_focusedId.HasValue && _idToObject.TryGetValue(_focusedId.Value, out var go))
@@ -405,6 +366,9 @@ namespace SHI.modal
_focusedId = null; _focusedId = null;
} }
/// <summary>
/// 인스턴스화된 노드의 활성 상태(가시성)를 변경합니다.
/// </summary>
public void SetVisibility(Guid id, bool on) public void SetVisibility(Guid id, bool on)
{ {
Debug.Log($"ModelDetailView.SetVisibility: id={id} on={on}"); Debug.Log($"ModelDetailView.SetVisibility: id={id} on={on}");
@@ -416,9 +380,9 @@ namespace SHI.modal
// Common color property names across pipelines/shaders // Common color property names across pipelines/shaders
private static readonly string[] ColorProps = new[] { private static readonly string[] ColorProps = new[] {
"_WireframeColor", "_WireColor", // wireframe shader variants "_WireframeColor", "_WireColor", // wireframe shader variants
"_BaseColor", "_Color", "_LineColor", "_TintColor" "_BaseColor", "_Color", "_LineColor", "_TintColor"
}; };
private bool TrySetColor(Material mat, Color c) private bool TrySetColor(Material mat, Color c)
{ {
@@ -451,26 +415,24 @@ namespace SHI.modal
private void Highlight(GameObject go, bool on) private void Highlight(GameObject go, bool on)
{ {
if (!_matCache.TryGetValue(go, out var tuple)) return; // 런타임 인스턴스 머티리얼을 사용하고, 복원 시 렌더러별 캐시된 원본 색을 참고
var (rends, originals) = tuple; var rends = go.GetComponentsInChildren<Renderer>(true);
if (rends == null || rends.Length == 0) return;
for (int i = 0; i < rends.Length; i++) for (int i = 0; i < rends.Length; i++)
{ {
var r = rends[i]; var r = rends[i];
if (r == null) continue; if (r == null) continue;
// Work on instantiated materials to avoid touching shared assets var mats = r.materials; // instanced
var mats = r.materials;
for (int m = 0; m < mats.Length; m++) for (int m = 0; m < mats.Length; m++)
{ {
if (on) if (on)
{ {
// set to highlight color regardless of shader
TrySetColor(mats[m], ColorUtil.FromHex("#888814")); TrySetColor(mats[m], ColorUtil.FromHex("#888814"));
} }
else else
{ {
// When in wireframe mode (material replaced) restore to default wireframe color.
bool matIsWire = mats[m] != null && (mats[m].HasProperty("_WireframeColor") || mats[m].HasProperty("_WireColor")); bool matIsWire = mats[m] != null && (mats[m].HasProperty("_WireframeColor") || mats[m].HasProperty("_WireColor"));
if (_wireframeApplied || matIsWire) if (_wireframeApplied || matIsWire)
{ {
@@ -482,12 +444,19 @@ namespace SHI.modal
} }
else else
{ {
// restore to original color if known; otherwise white // 캐시된 원본 shared material에서 색 복원 시도
Color orig; Color orig;
if (i < originals.Length && m < originals[i].Length && originals[i][m] != null && TryGetColor(originals[i][m], out orig)) UnityEngine.Material[] originals;
if (_originalSharedByRenderer.TryGetValue(r, out originals)
&& m < originals.Length && originals[m] != null
&& TryGetColor(originals[m], out orig))
{
TrySetColor(mats[m], orig); TrySetColor(mats[m], orig);
}
else else
{
TrySetColor(mats[m], ColorUtil.FromHex("#888888")); TrySetColor(mats[m], ColorUtil.FromHex("#888888"));
}
} }
} }
} }
@@ -496,6 +465,9 @@ namespace SHI.modal
} }
/// <summary>
/// 외부 리스너에게 선택 이벤트를 전달합니다.
/// </summary>
public void RaiseSelected(TreeListItemData data) public void RaiseSelected(TreeListItemData data)
{ {
OnItemSelected?.Invoke(data); OnItemSelected?.Invoke(data);
@@ -589,33 +561,27 @@ namespace SHI.modal
} }
} }
private void UpdateOrbitPose()
{
if (_viewCamera == null) return;
_orbitDistance = Mathf.Max(0.01f, _orbitDistance);
var rot = Quaternion.Euler(_pitch, _yaw, 0f);
var offset = rot * new Vector3(0, 0, -_orbitDistance);
_viewCamera.transform.position = _orbitTarget + offset;
_viewCamera.transform.LookAt(_orbitTarget);
}
/// <summary>
/// 이 뷰가 보유한 런타임 리소스(RT, 인스턴스 머티리얼, 루트 GO)를 해제합니다.
/// 여러 번 호출해도 안전합니다.
/// </summary>
public void Dispose() public void Dispose()
{ {
foreach (var kv in _matCache) // Destroy instanced materials and restore original shared materials
foreach (var kv in _originalSharedByRenderer)
{ {
var (rends, originals) = kv.Value; var r = kv.Key;
for (int i = 0; i < rends.Length; i++) if (r == null) continue;
var originals = kv.Value;
var mats = r.materials;
for (int m = 0; m < mats.Length; m++)
{ {
if (rends[i] == null) continue; if (mats[m] != null) UnityEngine.Object.Destroy(mats[m]);
var mats = rends[i].materials;
for (int m = 0; m < mats.Length; m++)
{
if (mats[m] != null) UnityEngine.Object.Destroy(mats[m]);
}
rends[i].materials = originals[i];
} }
r.materials = originals; // restore to originals in one go
} }
_matCache.Clear(); _originalSharedByRenderer.Clear();
_idToObject.Clear(); _idToObject.Clear();
if (_root != null) UnityEngine.Object.Destroy(_root); if (_root != null) UnityEngine.Object.Destroy(_root);
_root = null; _root = null;
@@ -624,7 +590,7 @@ namespace SHI.modal
if (_viewCamera != null) if (_viewCamera != null)
{ {
// detach RT and keep camera disabled to avoid replacing main display // RT 바인딩 해제하고 메인 디스플레이로 대체 출력되지 않도록 비활성화
if (_rt != null && _viewCamera.targetTexture == _rt) if (_rt != null && _viewCamera.targetTexture == _rt)
{ {
_viewCamera.targetTexture = null; _viewCamera.targetTexture = null;
@@ -639,5 +605,93 @@ namespace SHI.modal
} }
} }
private void OnDestroy()
{
// 컴포넌트 파괴 시 리소스 정리 보장
Dispose();
}
// ===== 유틸리티 =====
/// <summary>
/// 지정한 GameObject와 모든 하위 오브젝트의 레이어를 일괄 설정합니다.
/// </summary>
private static void SetLayerRecursive(GameObject go, int layer)
{
if (layer < 0 || layer > 31) return;
go.layer = layer;
foreach (Transform c in go.transform) SetLayerRecursive(c.gameObject, layer);
}
/// <summary>
/// 루트 아래 모든 렌더러의 경계(bounds)를 계산합니다.
/// </summary>
private static Bounds CalculateBounds(GameObject root)
{
var rends = root.GetComponentsInChildren<Renderer>(true);
var has = false;
var bounds = new Bounds(root.transform.position, Vector3.zero);
foreach (var r in rends)
{
if (r == null) continue;
if (!has) { bounds = r.bounds; has = true; }
else bounds.Encapsulate(r.bounds);
}
if (!has) bounds = new Bounds(root.transform.position, Vector3.one);
return bounds;
}
/// <summary>
/// 카메라를 주어진 경계에 맞게 배치하고 오빗 타깃을 갱신합니다.
/// </summary>
private void FrameToBounds(Bounds b)
{
if (_viewCamera == null) return;
var center = b.center;
var extents = b.extents;
float radius = Mathf.Max(extents.x, Mathf.Max(extents.y, Mathf.Max(extents.z, 0.001f)));
float fovRad = _viewCamera.fieldOfView * Mathf.Deg2Rad;
float dist = radius / Mathf.Sin(fovRad * 0.5f);
dist = Mathf.Clamp(dist, 1f, 1e4f);
_viewCamera.transform.position = center + new Vector3(1, 0.5f, 1).normalized * dist;
_viewCamera.transform.LookAt(center);
_orbitTarget = center;
_orbitDistance = Vector3.Distance(_viewCamera.transform.position, _orbitTarget);
var dir = (_viewCamera.transform.position - _orbitTarget).normalized;
_yaw = Mathf.Atan2(dir.x, dir.z) * Mathf.Rad2Deg;
_pitch = Mathf.Asin(dir.y) * Mathf.Rad2Deg;
}
/// <summary>
/// 루트 기준으로 트랜스폼의 전체 경로 문자열을 생성합니다. 예: "/Root/PartA/Bolt01"
/// </summary>
private string BuildFullPath(Transform node, Transform root)
{
var stack = new Stack<string>();
var current = node;
while (current != null && current != root)
{
stack.Push(current.name);
current = current.parent;
}
return "/" + string.Join('/', stack);
}
/// <summary>
/// 서브트리의 모든 렌더러에 대해 원본 sharedMaterials를 캐싱합니다.
/// </summary>
private void CacheOriginalMaterials(GameObject go)
{
var rends = go.GetComponentsInChildren<Renderer>(true);
if (rends.Length == 0) return;
for (int i = 0; i < rends.Length; i++)
{
var r = rends[i];
if (r == null) continue;
if (_originalSharedByRenderer.ContainsKey(r)) continue; // 이미 상위에서 캐시됨
_originalSharedByRenderer[r] = r.sharedMaterials;
}
}
} }
} }

View File

@@ -974,7 +974,7 @@ namespace UVC.UI.List.Tree
public void SelectItem(TreeListItemData data) public void SelectItem(TreeListItemData data)
{ {
// 이미 선택되어 있으면 중복 선택 방지 // 이미 선택되어 있으면 중복 선택 방지
if (data.IsSelected) if (data.IsSelected && selectedItems.Contains(data))
{ {
return; return;
} }
@@ -983,8 +983,8 @@ namespace UVC.UI.List.Tree
// (새 선택만 유지) // (새 선택만 유지)
if (!allowMultipleSelection && selectedItems.Count > 0) if (!allowMultipleSelection && selectedItems.Count > 0)
{ {
// 첫 번째(유일한) 선택 항목 해제 //선택 항목 해제
DeselectItem(selectedItems[0]); ClearSelection();
} }
// 아이템의 선택 상태를 true로 설정 // 아이템의 선택 상태를 true로 설정

View File

@@ -201,7 +201,7 @@ namespace UVC.UI.List.Tree
/// - changedData: 변경 대상 데이터(자식 추가 등일 때 유효) /// - changedData: 변경 대상 데이터(자식 추가 등일 때 유효)
/// - index: 삽입/이동 시 기준 인덱스(해당되는 경우) /// - index: 삽입/이동 시 기준 인덱스(해당되는 경우)
/// </summary> /// </summary>
protected void OnDataChanged(ChangedType changedType, TreeListItemData changedData, int index) protected virtual void OnDataChanged(ChangedType changedType, TreeListItemData changedData, int index)
{ {
if (data == null) return; if (data == null) return;

View File

@@ -56,28 +56,28 @@ namespace UVC.UI.List.Tree
#endregion #endregion
#region (Private Fields) #region (protected Fields)
/// <summary>고유 식별자(Id).</summary> /// <summary>고유 식별자(Id).</summary>
private readonly Guid _id = Guid.NewGuid(); protected Guid _id = Guid.NewGuid();
/// <summary>아이템 이름.</summary> /// <summary>아이템 이름.</summary>
private string _name = string.Empty; protected string _name = string.Empty;
/// <summary>추가 옵션 문자열.</summary> /// <summary>추가 옵션 문자열.</summary>
private string _option = string.Empty; protected string _option = string.Empty;
/// <summary>자식 펼침 여부.</summary> /// <summary>자식 펼침 여부.</summary>
private bool _isExpanded = false; protected bool _isExpanded = false;
/// <summary>선택 여부.</summary> /// <summary>선택 여부.</summary>
private bool _isSelected = false; protected bool _isSelected = false;
/// <summary>부모</summary> /// <summary>부모</summary>
private TreeListItemData? _parent; protected TreeListItemData? _parent;
/// <summary>자식 리스트.</summary> /// <summary>자식 리스트.</summary>
private List<TreeListItemData> _children = new List<TreeListItemData>(); protected List<TreeListItemData> _children = new List<TreeListItemData>();
#endregion #endregion
@@ -121,7 +121,7 @@ namespace UVC.UI.List.Tree
/// 펼침 상태(같은 어셈블리 내 전용). /// 펼침 상태(같은 어셈블리 내 전용).
/// 변경 시 OnDataChanged(Expanded) 발생. /// 변경 시 OnDataChanged(Expanded) 발생.
/// </summary> /// </summary>
internal bool IsExpanded public bool IsExpanded
{ {
get => _isExpanded; get => _isExpanded;
set set
@@ -399,10 +399,14 @@ namespace UVC.UI.List.Tree
public TreeListItemData CloneWithChild() public TreeListItemData CloneWithChild()
{ {
TreeListItemData clone = new TreeListItemData(); TreeListItemData clone = new TreeListItemData();
clone._id = this.Id;
clone.Name = this.Name; clone.Name = this.Name;
clone.Option = this.Option; clone.Option = this.Option;
clone.IsExpanded = this.IsExpanded; clone.IsExpanded = this.IsExpanded;
clone.IsSelected = this.IsSelected; clone.IsSelected = this.IsSelected;
clone.OnClickAction = this.OnClickAction;
clone.OnSelectionChanged = this.OnSelectionChanged;
clone.OnDataChanged = this.OnDataChanged;
foreach (var child in this.Children) foreach (var child in this.Children)
{ {
clone.AddChild(child.CloneWithChild()); clone.AddChild(child.CloneWithChild());
@@ -414,13 +418,17 @@ namespace UVC.UI.List.Tree
/// 현재 인스턴스의 복사본인 <see cref="TreeListItemData"/>의 새 인스턴스를 생성합니다. /// 현재 인스턴스의 복사본인 <see cref="TreeListItemData"/>의 새 인스턴스를 생성합니다.
/// </summary> /// </summary>
/// <returns>현재 인스턴스와 동일한 속성 값을 가진 새 <see cref="TreeListItemData"/> 객체를 생성합니다.</returns> /// <returns>현재 인스턴스와 동일한 속성 값을 가진 새 <see cref="TreeListItemData"/> 객체를 생성합니다.</returns>
public TreeListItemData Clone() public virtual TreeListItemData Clone()
{ {
TreeListItemData clone = new TreeListItemData(); TreeListItemData clone = new TreeListItemData();
clone._id = this.Id;
clone.Name = this.Name; clone.Name = this.Name;
clone.Option = this.Option; clone.Option = this.Option;
clone.IsExpanded = this.IsExpanded; clone.IsExpanded = this.IsExpanded;
clone.IsSelected = this.IsSelected; clone.IsSelected = this.IsSelected;
clone.OnClickAction = this.OnClickAction;
clone.OnSelectionChanged = this.OnSelectionChanged;
clone.OnDataChanged = this.OnDataChanged;
return clone; return clone;
} }
@@ -442,5 +450,6 @@ namespace UVC.UI.List.Tree
AddCloneChild, AddCloneChild,
AddCloneAtChild, AddCloneAtChild,
SwapChild, SwapChild,
TailButtons,// 향후 버튼 관련 변경 추가 가능
} }
} }

View File

@@ -1,6 +1,8 @@
#nullable enable #nullable enable
using Cysharp.Threading.Tasks; using Cysharp.Threading.Tasks;
using DG.Tweening; using DG.Tweening;
using SHI.modal;
using System;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using TMPro; using TMPro;
@@ -100,13 +102,9 @@ namespace UVC.UI.Window
clearTextButton.onClick.AddListener(() => clearTextButton.onClick.AddListener(() =>
{ {
inputField.text = string.Empty;
// 취소 // 취소
CancelSearch(); CancelSearch();
treeListSearch.gameObject.SetActive(false);
treeList.gameObject.SetActive(true);
// 검색에서 선택한 항목이 있으면 원본 트리에서 동일 항목을 선택하고 펼침 // 검색에서 선택한 항목이 있으면 원본 트리에서 동일 항목을 선택하고 펼침
if (selectedSearchItem != null && treeList != null) if (selectedSearchItem != null && treeList != null)
{ {
@@ -114,6 +112,8 @@ namespace UVC.UI.Window
var target = treeList.AllItemDataFlattened.FirstOrDefault(i => i == selectedSearchItem); var target = treeList.AllItemDataFlattened.FirstOrDefault(i => i == selectedSearchItem);
if (target != null) if (target != null)
{ {
ClearSelection();
// 부모 체인을 펼치고 선택 처리 // 부모 체인을 펼치고 선택 처리
treeList.RevealAndSelectItem(target, true); treeList.RevealAndSelectItem(target, true);
} }
@@ -123,6 +123,20 @@ namespace UVC.UI.Window
}); });
} }
/// <summary>
/// Guid 기반 선택
/// </summary>
/// <param name="id"></param>
public void SelectByItemId(Guid id)
{
var target = treeList.AllItemDataFlattened.FirstOrDefault(t => t.Id == id);
if (target != null)
{
ClearSelection();
treeList.RevealAndSelectItem(target, true);
}
}
/// <summary> /// <summary>
/// 메인 트리에 항목을 추가합니다. /// 메인 트리에 항목을 추가합니다.
/// </summary> /// </summary>
@@ -178,8 +192,6 @@ namespace UVC.UI.Window
//검색 중이면 취소 //검색 중이면 취소
CancelSearch(); CancelSearch();
treeListSearch.gameObject.SetActive(false);
treeList.gameObject.SetActive(true);
treeList.SelectItem(name); treeList.SelectItem(name);
} }
@@ -193,6 +205,24 @@ namespace UVC.UI.Window
treeList.DeselectItem(name); treeList.DeselectItem(name);
} }
/// <summary>
/// 선택해제 및 검색 취소
/// </summary>
public void Clear()
{
ClearSelection();
CancelSearch();
}
/// <summary>
/// 선택 해제
/// </summary>
public void ClearSelection()
{
treeListSearch.ClearSelection();
treeList.ClearSelection();
}
protected void StartLoadingAnimation() protected void StartLoadingAnimation()
{ {
if (loadingImage == null) return; if (loadingImage == null) return;
@@ -243,6 +273,36 @@ namespace UVC.UI.Window
protected void CancelSearch() protected void CancelSearch()
{ {
// 검색 중이면
if (treeListSearch?.gameObject.activeSelf == true)
{
//검색 시 데이터 변경 된 내용 원본 트리에 반영
treeListSearch.AllItemDataFlattened.ToList().ForEach(searchItem =>
{
var originalItem = treeList.AllItemDataFlattened.FirstOrDefault(i => i == searchItem);
if (originalItem != null)
{
bool changed = false;
//선택 상태 동기화
if (originalItem.IsSelected != searchItem.IsSelected)
{
originalItem.IsSelected = searchItem.IsSelected;
changed = true;
}
if (changed)
{
//데이터 변경 알림
originalItem.NotifyDataChanged(ChangedType.TailButtons, originalItem);
}
}
});
}
inputField.text = string.Empty;
treeListSearch?.gameObject.SetActive(false);
treeList?.gameObject.SetActive(true);
if (searchCts != null) if (searchCts != null)
{ {
try { searchCts.Cancel(); } catch { } try { searchCts.Cancel(); } catch { }
@@ -283,12 +343,11 @@ namespace UVC.UI.Window
protected void OnInputFieldSubmit(string text) protected void OnInputFieldSubmit(string text)
{ {
// 기존 검색 취소
CancelSearch();
// 검색어가 있으면 검색 결과 목록 표시 // 검색어가 있으면 검색 결과 목록 표시
if (!string.IsNullOrEmpty(text)) if (!string.IsNullOrEmpty(text))
{ {
treeListSearch.ClearSelection();
treeListSearch.gameObject.SetActive(true); treeListSearch.gameObject.SetActive(true);
treeList.gameObject.SetActive(false); treeList.gameObject.SetActive(false);
@@ -301,6 +360,9 @@ namespace UVC.UI.Window
} }
else else
{ {
// 기존 검색 취소
CancelSearch();
treeListSearch.gameObject.SetActive(false); treeListSearch.gameObject.SetActive(false);
treeList.gameObject.SetActive(true); treeList.gameObject.SetActive(true);
} }
@@ -369,7 +431,13 @@ namespace UVC.UI.Window
treeListSearch.ClearItems(); treeListSearch.ClearItems();
foreach (var r in results) foreach (var r in results)
{ {
treeListSearch.AddItem<TreeListItem>(r.Clone()); var cloned = r.Clone();
treeListSearch.AddItem<ModelDetailListItem>(cloned);
if (cloned.IsSelected)
{
//선택된 항목은 펼치기
treeListSearch.SelectItem(cloned);
}
} }
// 로딩 종료 // 로딩 종료

Binary file not shown.

View File

@@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: 9a7772b617c3413428e185c2e5e08f49
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant: