버그 수정
This commit is contained in:
@@ -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}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 로딩 종료
|
// 로딩 종료
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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로 설정
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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,// 향후 버튼 관련 변경 추가 가능
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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.
@@ -1,7 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 9a7772b617c3413428e185c2e5e08f49
|
|
||||||
DefaultImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
Reference in New Issue
Block a user