개발중
This commit is contained in:
@@ -1,4 +1,7 @@
|
||||
using Cysharp.Threading.Tasks;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
@@ -7,30 +10,16 @@ namespace SHI.modal
|
||||
public class BlockDetailModal : MonoBehaviour
|
||||
{
|
||||
[Header("References")]
|
||||
[SerializeField]
|
||||
private Button closeButton;
|
||||
|
||||
[SerializeField]
|
||||
private ModelDetailListView listView;
|
||||
|
||||
[SerializeField]
|
||||
private ModelDetailView modelView;
|
||||
|
||||
[SerializeField]
|
||||
private ModelDetailChartView chartView;
|
||||
[SerializeField] private Button closeButton;
|
||||
[SerializeField] private ModelDetailListView listView;
|
||||
[SerializeField] private ModelDetailView modelView;
|
||||
[SerializeField] private ModelDetailChartView chartView;
|
||||
|
||||
[Header("UI Controls")]
|
||||
[SerializeField]
|
||||
private Button modelViewExpandButton;
|
||||
|
||||
[SerializeField]
|
||||
private Button chartViewExpandButton;
|
||||
|
||||
[SerializeField]
|
||||
private Button dragButton;
|
||||
|
||||
[SerializeField]
|
||||
private Button showListButton;
|
||||
[SerializeField] private Button modelViewExpandButton;
|
||||
[SerializeField] private Button chartViewExpandButton;
|
||||
[SerializeField] private Button dragButton;
|
||||
[SerializeField] private Button showListButton;
|
||||
|
||||
// cached layout elements for split control
|
||||
private LayoutElement _modelLayout;
|
||||
@@ -43,6 +32,14 @@ namespace SHI.modal
|
||||
private RectTransform ChartRect => chartView != null ? chartView.GetComponent<RectTransform>() : null;
|
||||
private HorizontalSplitDrag _splitter;
|
||||
|
||||
// lifecycle
|
||||
private CancellationTokenSource _cts;
|
||||
private bool _suppressSelection;
|
||||
|
||||
// key<->id 매핑(차트-리스트/모델 동기화를 위해 유지)
|
||||
private readonly Dictionary<string, Guid> _keyToId = new Dictionary<string, Guid>();
|
||||
private readonly Dictionary<Guid, string> _idToKey = new Dictionary<Guid, string>();
|
||||
|
||||
public void Start()
|
||||
{
|
||||
// Close
|
||||
@@ -60,21 +57,34 @@ namespace SHI.modal
|
||||
showListButton.gameObject.SetActive(false);
|
||||
if (_splitter != null) _splitter.RefreshPosition();
|
||||
});
|
||||
showListButton.gameObject.SetActive(false);
|
||||
if (showListButton != null) showListButton.gameObject.SetActive(false);
|
||||
|
||||
// Selection wiring: list -> model/chart
|
||||
if (listView != null)
|
||||
{
|
||||
listView.OnItemSelected += data =>
|
||||
{
|
||||
if (modelView != null) modelView.FocusItem(data);
|
||||
if (chartView != null) chartView.SelectByItem(data.Name);
|
||||
Debug.Log($"BlockDetailModal: ListView OnItemSelected, data:{data==null}");
|
||||
if (data == null) return;
|
||||
HandleSelection(data.Id, "ListView");
|
||||
};
|
||||
|
||||
listView.OnItemDeselected += data =>
|
||||
{
|
||||
Debug.Log("BlockDetailModal: ListView OnItemDeselected");
|
||||
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
|
||||
@@ -82,18 +92,24 @@ namespace SHI.modal
|
||||
{
|
||||
modelView.OnItemSelected += data =>
|
||||
{
|
||||
if (listView != null) listView.SelectItem(data.Name);
|
||||
if (chartView != null) chartView.SelectByItem(data.Name);
|
||||
if (data == null) return;
|
||||
HandleSelection(data.Id, "ModelView");
|
||||
};
|
||||
}
|
||||
|
||||
// Chart -> list/model
|
||||
if (chartView != null)
|
||||
{
|
||||
chartView.OnRowClicked += name =>
|
||||
// key 기반 클릭 우선 사용
|
||||
chartView.OnRowClickedByKey += key =>
|
||||
{
|
||||
if (listView != null) listView.SelectItem(name);
|
||||
if (modelView != null) modelView.FocusItemName(name);
|
||||
if (string.IsNullOrEmpty(key)) return;
|
||||
if (_keyToId.TryGetValue(key, out var id)) HandleSelection(id, "ChartView");
|
||||
};
|
||||
// 호환: Guid 이벤트도 유지
|
||||
chartView.OnRowClicked += id =>
|
||||
{
|
||||
HandleSelection(id, "ChartView");
|
||||
};
|
||||
}
|
||||
|
||||
@@ -107,6 +123,43 @@ namespace SHI.modal
|
||||
SetupSplitControls();
|
||||
}
|
||||
|
||||
public async UniTask LoadData(string gltfPath, GanttChartData gantt, CancellationToken externalCt = default)
|
||||
{
|
||||
Debug.Log($"BlockDetailModal: LoadData {gltfPath}");
|
||||
// cancel previous
|
||||
if (_cts != null)
|
||||
{
|
||||
try { _cts.Cancel(); } catch { }
|
||||
_cts.Dispose();
|
||||
}
|
||||
_cts = CancellationTokenSource.CreateLinkedTokenSource(externalCt);
|
||||
var ct = _cts.Token;
|
||||
|
||||
// load model and list
|
||||
IEnumerable<ModelDetailListItemData> items = Array.Empty<ModelDetailListItemData>();
|
||||
if (modelView != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
items = await modelView.LoadModelAsync(gltfPath, ct);
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
}
|
||||
// 매핑 초기화
|
||||
_keyToId.Clear();
|
||||
_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.Populate(items);
|
||||
if (chartView != null) chartView.LoadData(gantt);
|
||||
}
|
||||
|
||||
private void SetupSplitControls()
|
||||
{
|
||||
var modelRect = ModelRect;
|
||||
@@ -120,8 +173,8 @@ namespace SHI.modal
|
||||
if (_chartLayout == null) _chartLayout = chartView.gameObject.AddComponent<LayoutElement>();
|
||||
|
||||
// initial split50/50
|
||||
_modelLayout.flexibleWidth = 1f;
|
||||
_chartLayout.flexibleWidth = 1f;
|
||||
_modelLayout.flexibleWidth =1f;
|
||||
_chartLayout.flexibleWidth =1f;
|
||||
|
||||
// attach drag handler
|
||||
_splitter = dragButton.gameObject.GetComponent<HorizontalSplitDrag>();
|
||||
@@ -132,6 +185,32 @@ namespace SHI.modal
|
||||
UniTask.DelayFrame(1).ContinueWith(() => _splitter.RefreshPosition());
|
||||
}
|
||||
|
||||
private void HandleSelection(Guid itemId, string source)
|
||||
{
|
||||
if (_suppressSelection) return;
|
||||
_suppressSelection = true;
|
||||
try
|
||||
{
|
||||
if (source != "ListView" && listView != null) listView.SelectByItemId(itemId);
|
||||
if (source != "ModelView" && modelView != null) modelView.FocusItemById(itemId);
|
||||
if (source != "ChartView" && chartView != null)
|
||||
{
|
||||
if (_idToKey.TryGetValue(itemId, out var key)) chartView.SelectByItemKey(key);
|
||||
else chartView.SelectByItemId(itemId);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_suppressSelection = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleDeselection(Guid itemId, string source)
|
||||
{
|
||||
if (_suppressSelection) return;
|
||||
if (source != "ModelView" && modelView != null) modelView.UnfocusItem();
|
||||
}
|
||||
|
||||
private void ToggleExpandModel()
|
||||
{
|
||||
if (ModelRect == null || chartView == null) return;
|
||||
@@ -157,13 +236,25 @@ namespace SHI.modal
|
||||
_expanded = ExpandedSide.None;
|
||||
if (ModelRect != null) ModelRect.gameObject.SetActive(true);
|
||||
if (chartView != null) chartView.gameObject.SetActive(true);
|
||||
if (_modelLayout != null) _modelLayout.flexibleWidth = 1f;
|
||||
if (_chartLayout != null) _chartLayout.flexibleWidth = 1f;
|
||||
if (_modelLayout != null) _modelLayout.flexibleWidth =1f;
|
||||
if (_chartLayout != null) _chartLayout.flexibleWidth =1f;
|
||||
if (_splitter != null)
|
||||
{
|
||||
_splitter.gameObject.SetActive(true);
|
||||
_splitter.RefreshPosition();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (_cts != null)
|
||||
{
|
||||
try { _cts.Cancel(); } catch { }
|
||||
_cts.Dispose();
|
||||
_cts = null;
|
||||
}
|
||||
if (chartView != null) chartView.Dispose();
|
||||
if (modelView != null) modelView.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
23
Assets/Scripts/SHI/modal/GanttChartData.cs
Normal file
23
Assets/Scripts/SHI/modal/GanttChartData.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SHI.modal
|
||||
{
|
||||
public class ScheduleSegment
|
||||
{
|
||||
// Stable external mapping key (preferred)
|
||||
public string ItemKey { get; set; } = string.Empty;
|
||||
// Backward compatibility GUID (optional)
|
||||
public Guid ItemId { get; set; }
|
||||
public DateTime Start { get; set; }
|
||||
public DateTime End { get; set; }
|
||||
public float Progress { get; set; }
|
||||
public string Type { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class GanttChartData
|
||||
{
|
||||
public List<ScheduleSegment> Segments { get; set; } = new List<ScheduleSegment>();
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/SHI/modal/GanttChartData.cs.meta
Normal file
2
Assets/Scripts/SHI/modal/GanttChartData.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5ec1e373110ba694f916a50093c32919
|
||||
@@ -5,33 +5,44 @@ using UnityEngine;
|
||||
namespace SHI.modal
|
||||
{
|
||||
/// <summary>
|
||||
/// 차트 패널의 최소 동기화 컴포넌트.
|
||||
/// 실제 UI Toolkit 기반 간트 컴포넌트가 준비되면 이 클래스를 연결하세요.
|
||||
/// 현재는 항목 선택 신호만 송수신합니다.
|
||||
/// 차트 패널: 간트 데이터 바인딩/선택 동기화용 경량 래퍼.
|
||||
/// 실제 UI Toolkit 간트 구현 전까지 스텁 동작을 수행합니다.
|
||||
/// </summary>
|
||||
public class ModelDetailChartView : MonoBehaviour
|
||||
{
|
||||
/// <summary>
|
||||
/// 차트의 행을 클릭했을 때 선택된 항목의 이름을 알립니다.
|
||||
/// </summary>
|
||||
public Action<string>? OnRowClicked;
|
||||
public Action<string>? OnRowClickedByKey;
|
||||
public Action<Guid>? OnRowClicked; // backward compat
|
||||
|
||||
/// <summary>
|
||||
/// 외부(리스트/모델)에서 항목이 선택되었을 때 차트에서 해당 행을 강조합니다.
|
||||
/// 실제 구현은 프로젝트의 차트 위젯에 맞게 교체하세요.
|
||||
/// </summary>
|
||||
public void SelectByItem(string name)
|
||||
private GanttChartData? _data;
|
||||
|
||||
public void LoadData(GanttChartData data)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name)) return;
|
||||
Debug.Log($"ModelDetailChartView.SelectByItem: {name}");
|
||||
// TODO: 차트에서 해당 행 스크롤/하이라이트
|
||||
_data = data;
|
||||
Debug.Log($"ModelDetailChartView.LoadData: segments={data?.Segments?.Count ??0}");
|
||||
}
|
||||
|
||||
// 임시: UI 이벤트 바인딩에서 호출 가능한 샘플
|
||||
public void SimulateRowClick(string name)
|
||||
public void SelectByItemKey(string key)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name)) return;
|
||||
OnRowClicked?.Invoke(name);
|
||||
if (_data == null) { Debug.Log("ChartView.SelectByItemKey: no data"); return; }
|
||||
Debug.Log($"Chart highlight by key: {key}");
|
||||
}
|
||||
|
||||
public void SelectByItemId(Guid id)
|
||||
{
|
||||
if (_data == null) { Debug.Log("ChartView.SelectByItemId: no data"); return; }
|
||||
Debug.Log($"Chart highlight by id: {id}");
|
||||
}
|
||||
|
||||
// Simulate UI callbacks
|
||||
public void SimulateRowClickKey(string key)
|
||||
{
|
||||
OnRowClickedByKey?.Invoke(key);
|
||||
}
|
||||
public void SimulateRowClick(string id)
|
||||
{
|
||||
if (Guid.TryParse(id, out var guid)) OnRowClicked?.Invoke(guid);
|
||||
}
|
||||
|
||||
public void Dispose() { _data = null; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ namespace SHI.modal
|
||||
public virtual void Init(TreeListItemData data, TreeList control, TreeListDragDropManager dragDropManager)
|
||||
{
|
||||
base.Init(data, control, dragDropManager);
|
||||
Debug.Log($"ModelDetailListItem Init. visibleToggle:{visibleToggle==null}");
|
||||
if (visibleToggle != null)
|
||||
{
|
||||
visibleToggle.OnValueChanged.AddListener(isOn =>
|
||||
|
||||
@@ -11,6 +11,14 @@ namespace SHI.modal
|
||||
/// </summary>
|
||||
public Action<TreeListItemData, bool>? OnClickVisibleAction;
|
||||
|
||||
/// <summary>
|
||||
/// 리스트/모델 가시성 상태
|
||||
/// </summary>
|
||||
public bool IsVisible { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 외부(간트/백엔드)와의 안정 매핑용 키. 예: GLTF 노드의 풀 경로("/Root/Level1/Beam023").
|
||||
/// </summary>
|
||||
public string ExternalKey { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ using DG.Tweening;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Collections.Generic;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
@@ -62,6 +63,9 @@ namespace SHI.modal
|
||||
|
||||
public Action? OnClosed;
|
||||
|
||||
// NEW: 가시성 변경 이벤트(Guid, bool)
|
||||
public Action<Guid, bool>? OnVisibilityChanged;
|
||||
|
||||
// 검색 목록에서 선택된 항목(클론된 데이터)
|
||||
protected TreeListItemData? selectedSearchItem;
|
||||
|
||||
@@ -128,13 +132,51 @@ namespace SHI.modal
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// NEW: 데이터 주입
|
||||
/// </summary>
|
||||
/// <param name="items"></param>
|
||||
public void Populate(IEnumerable<ModelDetailListItemData> items)
|
||||
{
|
||||
// 기존 아이템 클리어
|
||||
treeList.ClearItems();
|
||||
foreach (var item in items)
|
||||
{
|
||||
// 가시성 아이콘 클릭 연동
|
||||
item.OnClickVisibleAction = (data, isVisible) =>
|
||||
{
|
||||
var md = data as ModelDetailListItemData;
|
||||
if (md != null)
|
||||
{
|
||||
md.IsVisible = isVisible;
|
||||
OnVisibilityChanged?.Invoke(md.Id, isVisible);
|
||||
}
|
||||
};
|
||||
treeList.AddItem<ModelDetailListItem>(item);
|
||||
}
|
||||
treeList.ScheduleFlattenedUpdate();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// NEW: Guid 기반 선택
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
public void SelectByItemId(Guid id)
|
||||
{
|
||||
var target = treeList.AllItemDataFlattened.FirstOrDefault(t => t.Id == id);
|
||||
if (target != null)
|
||||
{
|
||||
treeList.RevealAndSelectItem(target, true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 메인 트리에 항목을 추가합니다.
|
||||
/// </summary>
|
||||
/// <param name="data">추가할 데이터.</param>
|
||||
public void AddItem(TreeListItemData data)
|
||||
{
|
||||
treeList.AddItem(data);
|
||||
treeList.AddItem<ModelDetailListItem>(data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -144,7 +186,7 @@ namespace SHI.modal
|
||||
/// <param name="index">삽입 인덱스(0 기반).</param>
|
||||
public void AddItemAt(TreeListItemData data, int index)
|
||||
{
|
||||
treeList.AddItemAt(data, index);
|
||||
treeList.AddItemAt<ModelDetailListItem>(data, index);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -302,7 +344,7 @@ namespace SHI.modal
|
||||
isSearching = true;
|
||||
searchProgress = 0f;
|
||||
|
||||
var results = new System.Collections.Generic.List<TreeListItemData>();
|
||||
var results = new List<TreeListItemData>();
|
||||
|
||||
var sourceList = treeList?.AllItemDataFlattened;
|
||||
if (sourceList == null)
|
||||
@@ -356,7 +398,7 @@ namespace SHI.modal
|
||||
treeListSearch.ClearItems();
|
||||
foreach (var r in results)
|
||||
{
|
||||
treeListSearch.AddItem(r.Clone());
|
||||
treeListSearch.AddItem<ModelDetailListItem>(r.Clone());
|
||||
}
|
||||
|
||||
// 로딩 종료
|
||||
|
||||
@@ -1,49 +1,634 @@
|
||||
#nullable enable
|
||||
using Cysharp.Threading.Tasks;
|
||||
using GLTFast;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UVC.UI.List.Tree;
|
||||
using UVC.Util;
|
||||
|
||||
namespace SHI.modal
|
||||
{
|
||||
public class ModelDetailView: MonoBehaviour
|
||||
public class ModelDetailView : MonoBehaviour
|
||||
{
|
||||
/// <summary>
|
||||
/// 모델 뷰 내에서 항목이 선택되었을 때 알림.
|
||||
/// 외부에서 구독하여 리스트/차트를 동기화합니다.
|
||||
/// </summary>
|
||||
public Action<TreeListItemData>? OnItemSelected;
|
||||
|
||||
private TreeListItemData? _focused;
|
||||
[Header("View Output (UI)")]
|
||||
[SerializeField] private RawImage outputImage;
|
||||
[SerializeField] private Color backgroundColor = new Color(0.2f, 0.2f, 0.22f, 1f);
|
||||
[Tooltip("전용 카메라가 렌더링할 레이어. 모델 노드에 재귀적으로 적용됩니다.")]
|
||||
[SerializeField] private int modelLayer = 6;
|
||||
[SerializeField] private bool createDefaultLight = true;
|
||||
|
||||
[Header("Mouse Controls")]
|
||||
[SerializeField] private float panSpeed = 1.0f; // 픽셀→월드 스케일 보정 계수
|
||||
[SerializeField] private float rotateDegPerPixel = 0.2f; // RMB 회전(도/픽셀)
|
||||
[SerializeField] private float zoomSpeed = 5f; // 휠 전/후 모델 이동 속도
|
||||
|
||||
[Header("Output Image Setup")]
|
||||
[SerializeField] private bool autoCreateOutputImage = true;
|
||||
[SerializeField] private bool autoApplyRectMask = true;
|
||||
|
||||
[Header("Wireframe")]
|
||||
[SerializeField] private bool wireframeMode = false; // 켜면 라인만 렌더
|
||||
|
||||
private UnityEngine.Camera _viewCamera;
|
||||
private RenderTexture _rt;
|
||||
private Material _wireframeMat; // Resources에서 로드한 메터리얼
|
||||
private bool _wireframeApplied;
|
||||
|
||||
// Orbit controls state (카메라 위치 계산용)
|
||||
private Vector3 _orbitTarget;
|
||||
private float _orbitDistance = 5f;
|
||||
private float _yaw = 0f;
|
||||
private float _pitch = 20f;
|
||||
|
||||
// Drag state
|
||||
private bool _mmbDragging;
|
||||
private bool _rmbDragging;
|
||||
private Vector3 _mmbLastPos;
|
||||
private Vector3 _rmbStartPos;
|
||||
private float _yawStart;
|
||||
private float _pitchStart;
|
||||
private Quaternion _modelStartRot;
|
||||
|
||||
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[][])>();
|
||||
private GameObject? _root;
|
||||
private Guid? _focusedId;
|
||||
|
||||
public async UniTask<IEnumerable<ModelDetailListItemData>> LoadModelAsync(string path, CancellationToken ct)
|
||||
{
|
||||
Debug.Log($"ModelDetailView.LoadModelAsync: {path}");
|
||||
Dispose();
|
||||
await UniTask.DelayFrame(1); // Dispose 후1프레임 대기하여 리소스 해제 안정화
|
||||
EnsureCameraAndTargetTexture();
|
||||
|
||||
var items = new List<ModelDetailListItemData>();
|
||||
var gltf = new GltfImport();
|
||||
var success = await gltf.Load(path, new ImportSettings(), ct);
|
||||
if (!success)
|
||||
{
|
||||
Debug.LogError($"glTFast Load failed: {path}");
|
||||
return items;
|
||||
}
|
||||
|
||||
_root = new GameObject("ModelDetailViewRoot");
|
||||
_root.layer = modelLayer;
|
||||
var sceneOk = await gltf.InstantiateMainSceneAsync(_root.transform);
|
||||
if (!sceneOk)
|
||||
{
|
||||
Debug.LogError("InstantiateMainSceneAsync failed");
|
||||
return items;
|
||||
}
|
||||
|
||||
SetLayerRecursive(_root, modelLayer);
|
||||
|
||||
// Build hierarchical item data preserving parent-child relationships
|
||||
if (_root != null)
|
||||
{
|
||||
for (int i = 0; i < _root.transform.childCount; i++)
|
||||
{
|
||||
var child = _root.transform.GetChild(i);
|
||||
var topItem = BuildItemRecursive(child, _root.transform);
|
||||
if (topItem != null)
|
||||
{
|
||||
items.Add(topItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var bounds = CalculateBounds(_root);
|
||||
FrameToBounds(bounds);
|
||||
|
||||
// 요청: Resources/SHI/Shader/BasicWireframe 메터리얼 적용
|
||||
TryLoadWireframeMaterial();
|
||||
if (wireframeMode && _wireframeMat != null)
|
||||
{
|
||||
ApplyWireframeMaterialToRoot();
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
private ModelDetailListItemData BuildItemRecursive(Transform node, Transform root)
|
||||
{
|
||||
var data = new ModelDetailListItemData
|
||||
{
|
||||
Name = node.name,
|
||||
ExternalKey = BuildFullPath(node, root)
|
||||
};
|
||||
|
||||
// map id -> object for selection/highlight and cache materials at this subtree root
|
||||
_idToObject[data.Id] = node.gameObject;
|
||||
CacheOriginalMaterials(node.gameObject);
|
||||
|
||||
// children
|
||||
for (int i = 0; i < node.childCount; i++)
|
||||
{
|
||||
var child = node.GetChild(i);
|
||||
var childData = BuildItemRecursive(child, root);
|
||||
data.AddChild(childData);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
private void TryLoadWireframeMaterial()
|
||||
{
|
||||
if (_wireframeMat == null)
|
||||
{
|
||||
_wireframeMat = Resources.Load<Material>("SHI/Shader/BasicWireframe");
|
||||
if (_wireframeMat == null)
|
||||
{
|
||||
Debug.LogWarning("BasicWireframe material not found at Resources/SHI/Shader/BasicWireframe. Wireframe will be disabled.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyWireframeMaterialToRoot()
|
||||
{
|
||||
if (_root == null || _wireframeMat == null) return;
|
||||
var rends = _root.GetComponentsInChildren<Renderer>(true);
|
||||
foreach (var r in rends)
|
||||
{
|
||||
if (r == null) continue;
|
||||
var count = Mathf.Max(1, r.sharedMaterials.Length);
|
||||
var arr = new Material[count];
|
||||
for (int i = 0; i < count; i++) arr[i] = _wireframeMat;
|
||||
r.sharedMaterials = arr;
|
||||
}
|
||||
_wireframeApplied = true;
|
||||
}
|
||||
|
||||
private void RestoreAllOriginalMaterials()
|
||||
{
|
||||
foreach (var kv in _matCache)
|
||||
{
|
||||
var (rends, originals) = kv.Value;
|
||||
for (int i = 0; i < rends.Length; i++)
|
||||
{
|
||||
if (rends[i] == null) continue;
|
||||
rends[i].sharedMaterials = originals[i];
|
||||
}
|
||||
}
|
||||
_wireframeApplied = false;
|
||||
}
|
||||
|
||||
|
||||
private void EnsureCameraAndTargetTexture()
|
||||
{
|
||||
EnsureOutputImageUI();
|
||||
|
||||
if (_viewCamera == null)
|
||||
{
|
||||
// Create a world-space rig not parented to UI to avoid layout side effects
|
||||
var rig = new GameObject("ModelDetailViewRig");
|
||||
rig.layer = modelLayer;
|
||||
rig.transform.SetParent(null, false);
|
||||
|
||||
var camGo = new GameObject("ModelDetailViewCamera");
|
||||
camGo.layer = modelLayer;
|
||||
camGo.transform.SetParent(rig.transform, false);
|
||||
_viewCamera = camGo.AddComponent<UnityEngine.Camera>();
|
||||
_viewCamera.clearFlags = CameraClearFlags.SolidColor;
|
||||
_viewCamera.backgroundColor = backgroundColor;
|
||||
_viewCamera.nearClipPlane = 0.01f;
|
||||
_viewCamera.farClipPlane = 5000f;
|
||||
_viewCamera.cullingMask = (modelLayer >= 0 && modelLayer <= 31) ? (1 << modelLayer) : ~0;
|
||||
// Prevent drawing to main display when no RT assigned
|
||||
_viewCamera.targetDisplay = 1; // secondary display
|
||||
_viewCamera.enabled = false; // enable only when RT is bound
|
||||
}
|
||||
|
||||
// optional default light (independent of UI)
|
||||
if (createDefaultLight && _viewCamera.transform.parent != null && _viewCamera.transform.parent.Find("ModelDetailViewLight") == null)
|
||||
{
|
||||
var lightGo = new GameObject("ModelDetailViewLight");
|
||||
lightGo.layer = modelLayer;
|
||||
lightGo.transform.SetParent(_viewCamera.transform.parent, false);
|
||||
lightGo.transform.localPosition = Vector3.zero;
|
||||
lightGo.transform.localRotation = Quaternion.Euler(50f, -30f, 0f);
|
||||
var light = lightGo.AddComponent<Light>();
|
||||
light.type = LightType.Directional;
|
||||
light.intensity = 1.1f;
|
||||
light.shadows = LightShadows.Soft;
|
||||
}
|
||||
|
||||
EnsureRenderTargetSize();
|
||||
}
|
||||
|
||||
private void EnsureOutputImageUI()
|
||||
{
|
||||
var selfRt = GetComponent<RectTransform>();
|
||||
if (autoApplyRectMask && selfRt != null && selfRt.GetComponent<RectMask2D>() == null)
|
||||
{
|
||||
selfRt.gameObject.AddComponent<RectMask2D>();
|
||||
}
|
||||
|
||||
if (outputImage == null && autoCreateOutputImage)
|
||||
{
|
||||
var go = new GameObject("OutputImage", typeof(RectTransform), typeof(RawImage));
|
||||
go.transform.SetParent(transform, false);
|
||||
outputImage = go.GetComponent<RawImage>();
|
||||
}
|
||||
|
||||
if (outputImage != null)
|
||||
{
|
||||
var rt = outputImage.rectTransform;
|
||||
rt.anchorMin = Vector2.zero;
|
||||
rt.anchorMax = Vector2.one;
|
||||
rt.offsetMin = Vector2.zero;
|
||||
rt.offsetMax = Vector2.zero;
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureRenderTargetSize()
|
||||
{
|
||||
if (_viewCamera == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (outputImage == null)
|
||||
{
|
||||
// No output target; keep camera disabled to avoid drawing to display
|
||||
_viewCamera.enabled = false;
|
||||
return;
|
||||
}
|
||||
var rtf = outputImage.rectTransform;
|
||||
int w = Mathf.Max(64, Mathf.RoundToInt(rtf.rect.width));
|
||||
int h = Mathf.Max(64, Mathf.RoundToInt(rtf.rect.height));
|
||||
if (_rt == null || _rt.width != w || _rt.height != h)
|
||||
{
|
||||
// release old
|
||||
if (_rt != null)
|
||||
{
|
||||
if (_viewCamera.targetTexture == _rt) _viewCamera.targetTexture = null;
|
||||
_rt.Release();
|
||||
Destroy(_rt);
|
||||
}
|
||||
// create new
|
||||
_rt = new RenderTexture(w, h, 24, RenderTextureFormat.ARGB32)
|
||||
{
|
||||
name = "ModelDetailViewRT",
|
||||
antiAliasing = 2
|
||||
};
|
||||
_viewCamera.targetTexture = _rt;
|
||||
outputImage.texture = _rt;
|
||||
// enable camera only when RT is assigned
|
||||
_viewCamera.enabled = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// ensure binding and enable
|
||||
if (_viewCamera.targetTexture != _rt)
|
||||
{
|
||||
_viewCamera.targetTexture = _rt;
|
||||
}
|
||||
if (!_viewCamera.enabled) _viewCamera.enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected void OnRectTransformDimensionsChange()
|
||||
{
|
||||
// Keep RT in sync with UI size changes (e.g., parent resized)
|
||||
EnsureRenderTargetSize();
|
||||
}
|
||||
|
||||
// Allow external toggle
|
||||
public void SetWireframe(bool on)
|
||||
{
|
||||
wireframeMode = on;
|
||||
TryLoadWireframeMaterial();
|
||||
if (_wireframeMat != null)
|
||||
{
|
||||
if (on) ApplyWireframeMaterialToRoot(); else RestoreAllOriginalMaterials();
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("Wireframe material not found. Wireframe toggle ignored.");
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 트리 아이템에 해당하는 모델 요소를 강조하거나 카메라를 이동합니다.
|
||||
/// 실제 구현은 프로젝트 요구에 맞게 교체하세요.
|
||||
/// </summary>
|
||||
public void FocusItem(TreeListItemData data)
|
||||
{
|
||||
if (data == null) return;
|
||||
_focused = data;
|
||||
// TODO: 실제 GLTF/모델에서 data에 해당하는 노드를 찾아 강조/프레임 인
|
||||
// 디버그 표시로 대체
|
||||
Debug.Log($"ModelDetailView.FocusItem: {data.Name}");
|
||||
FocusItemById(data.Id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 주어진 이름에 해당하는 항목을 강조합니다.
|
||||
/// </summary>
|
||||
public void FocusItemName(string name)
|
||||
public void FocusItemById(Guid id)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name)) return;
|
||||
var data = new TreeListItemData(name);
|
||||
FocusItem(data);
|
||||
_focusedId = id;
|
||||
if (_idToObject.TryGetValue(id, out var go))
|
||||
{
|
||||
Highlight(go, true);
|
||||
Debug.Log($"ModelDetailView.FocusItemById: {go.name}");
|
||||
_orbitTarget = go.transform.position;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 모델에서 사용자가 어떤 요소를 클릭했을 때 외부로 통지하려면 이 메서드를 호출하세요.
|
||||
/// </summary>
|
||||
public void UnfocusItem()
|
||||
{
|
||||
if (_focusedId.HasValue && _idToObject.TryGetValue(_focusedId.Value, out var go))
|
||||
{
|
||||
Highlight(go, false);
|
||||
}
|
||||
_focusedId = null;
|
||||
}
|
||||
|
||||
public void SetVisibility(Guid id, bool on)
|
||||
{
|
||||
Debug.Log($"ModelDetailView.SetVisibility: id={id} on={on}");
|
||||
if (_idToObject.TryGetValue(id, out var go))
|
||||
{
|
||||
go.SetActive(on);
|
||||
}
|
||||
}
|
||||
|
||||
// Common color property names across pipelines/shaders
|
||||
private static readonly string[] ColorProps = new[] {
|
||||
"_WireframeColor", "_WireColor", // wireframe shader variants
|
||||
"_BaseColor", "_Color", "_LineColor", "_TintColor"
|
||||
};
|
||||
|
||||
private bool TrySetColor(Material mat, Color c)
|
||||
{
|
||||
for (int i = 0; i < ColorProps.Length; i++)
|
||||
{
|
||||
var prop = ColorProps[i];
|
||||
if (mat != null && mat.HasProperty(prop)) { mat.SetColor(prop, c); return true; }
|
||||
}
|
||||
// fallback to material.color if supported
|
||||
try { mat.color = c; return true; } catch { /* ignore */ }
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool TryGetColor(Material mat, out Color c)
|
||||
{
|
||||
for (int i = 0; i < ColorProps.Length; i++)
|
||||
{
|
||||
var prop = ColorProps[i];
|
||||
if (mat != null && mat.HasProperty(prop)) { c = mat.GetColor(prop); return true; }
|
||||
}
|
||||
try { c = mat.color; return true; } catch { /* ignore */ }
|
||||
c = ColorUtil.FromHex("#888888"); return false;
|
||||
}
|
||||
|
||||
private bool TryGetDefaultWireColor(out Color c)
|
||||
{
|
||||
if (_wireframeMat != null && TryGetColor(_wireframeMat, out c)) return true;
|
||||
c = ColorUtil.FromHex("#888888"); return false;
|
||||
}
|
||||
|
||||
private void Highlight(GameObject go, bool on)
|
||||
{
|
||||
if (!_matCache.TryGetValue(go, out var tuple)) return;
|
||||
var (rends, originals) = tuple;
|
||||
|
||||
for (int i = 0; i < rends.Length; i++)
|
||||
{
|
||||
var r = rends[i];
|
||||
if (r == null) continue;
|
||||
|
||||
// Work on instantiated materials to avoid touching shared assets
|
||||
var mats = r.materials;
|
||||
for (int m = 0; m < mats.Length; m++)
|
||||
{
|
||||
if (on)
|
||||
{
|
||||
// set to highlight color regardless of shader
|
||||
TrySetColor(mats[m], ColorUtil.FromHex("#888814"));
|
||||
}
|
||||
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"));
|
||||
if (_wireframeApplied || matIsWire)
|
||||
{
|
||||
Color baseWire;
|
||||
if (TryGetDefaultWireColor(out baseWire))
|
||||
TrySetColor(mats[m], baseWire);
|
||||
else
|
||||
TrySetColor(mats[m], ColorUtil.FromHex("#888888"));
|
||||
}
|
||||
else
|
||||
{
|
||||
// restore to original color if known; otherwise white
|
||||
Color orig;
|
||||
if (i < originals.Length && m < originals[i].Length && originals[i][m] != null && TryGetColor(originals[i][m], out orig))
|
||||
TrySetColor(mats[m], orig);
|
||||
else
|
||||
TrySetColor(mats[m], ColorUtil.FromHex("#888888"));
|
||||
}
|
||||
}
|
||||
}
|
||||
r.materials = mats;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void RaiseSelected(TreeListItemData data)
|
||||
{
|
||||
OnItemSelected?.Invoke(data);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (_viewCamera == null) return;
|
||||
|
||||
// 영역 히트테스트
|
||||
if (outputImage != null)
|
||||
{
|
||||
var cam = outputImage.canvas != null && outputImage.canvas.renderMode != RenderMode.ScreenSpaceOverlay
|
||||
? outputImage.canvas.worldCamera
|
||||
: null;
|
||||
if (!RectTransformUtility.RectangleContainsScreenPoint(outputImage.rectTransform, Input.mousePosition, cam))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var selfRt = GetComponent<RectTransform>();
|
||||
if (selfRt != null)
|
||||
{
|
||||
var parentCanvas = selfRt.GetComponentInParent<Canvas>();
|
||||
UnityEngine.Camera uiCam = null;
|
||||
if (parentCanvas != null && parentCanvas.renderMode != RenderMode.ScreenSpaceOverlay)
|
||||
uiCam = parentCanvas.worldCamera;
|
||||
if (!RectTransformUtility.RectangleContainsScreenPoint(selfRt, Input.mousePosition, uiCam))
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 드래그 상태 전이 처리 (점프 방지)
|
||||
if (Input.GetMouseButtonDown(2)) { _mmbDragging = true; _mmbLastPos = Input.mousePosition; }
|
||||
if (Input.GetMouseButtonUp(2)) { _mmbDragging = false; }
|
||||
if (Input.GetMouseButtonDown(1)) { _rmbDragging = true; _rmbStartPos = Input.mousePosition; _yawStart = _yaw; _pitchStart = _pitch; if (_root != null) _modelStartRot = _root.transform.rotation; }
|
||||
if (Input.GetMouseButtonUp(1)) { _rmbDragging = false; }
|
||||
|
||||
// 가운데 버튼: 모델 이동 (좌/우/상/하) - 카메라 기준, 픽셀 델타 기반
|
||||
if (_mmbDragging && _root != null)
|
||||
{
|
||||
Vector3 cur = Input.mousePosition;
|
||||
Vector2 dp = (Vector2)(cur - _mmbLastPos);
|
||||
_mmbLastPos = cur;
|
||||
// 뷰 사각 픽셀 크기 구하기
|
||||
float wPix, hPix;
|
||||
if (outputImage != null)
|
||||
{ var r = outputImage.rectTransform.rect; wPix = Mathf.Max(1f, r.width); hPix = Mathf.Max(1f, r.height); }
|
||||
else
|
||||
{ var rt = GetComponent<RectTransform>(); var r = (rt != null) ? rt.rect : new Rect(0, 0, Screen.width, Screen.height); wPix = Mathf.Max(1f, r.width); hPix = Mathf.Max(1f, r.height); }
|
||||
// 카메라에서의 월드/픽셀 스케일(거리 기반)
|
||||
float halfV = Mathf.Tan(_viewCamera.fieldOfView * Mathf.Deg2Rad * 0.5f) * _orbitDistance;
|
||||
float halfH = halfV * _viewCamera.aspect;
|
||||
float worldPerPixelX = (halfH * 2f) / wPix * panSpeed;
|
||||
float worldPerPixelY = (halfV * 2f) / hPix * panSpeed;
|
||||
Vector3 deltaModel = _viewCamera.transform.right * (dp.x * worldPerPixelX) + _viewCamera.transform.up * (dp.y * worldPerPixelY);
|
||||
_root.transform.position += deltaModel;
|
||||
_orbitTarget += deltaModel;
|
||||
}
|
||||
|
||||
// 마우스 휠: 모델 전/후 이동(카메라 기준)
|
||||
float scroll = Input.GetAxis("Mouse ScrollWheel");
|
||||
if (Mathf.Abs(scroll) > 1e-5f && _root != null)
|
||||
{
|
||||
var forward = _viewCamera.transform.forward;
|
||||
Vector3 deltaZ = forward * (-scroll * zoomSpeed);
|
||||
_root.transform.position += deltaZ;
|
||||
_orbitTarget += deltaZ;
|
||||
}
|
||||
|
||||
// 오른쪽 버튼: 모델 회전 (카메라 기준 yaw/pitch)
|
||||
if (_rmbDragging && _root != null)
|
||||
{
|
||||
Vector3 cur = Input.mousePosition;
|
||||
Vector2 dpAbs = (Vector2)(cur - _rmbStartPos);
|
||||
// 반전: 좌우/상하 모두 반대 방향으로 회전
|
||||
float yaw = -dpAbs.x * rotateDegPerPixel; // 좌우 반전
|
||||
float pitch = dpAbs.y * rotateDegPerPixel; // 위아래 반전
|
||||
// 카메라 기준 축으로 모델 회전
|
||||
Quaternion yawQ = Quaternion.AngleAxis(yaw, _viewCamera.transform.up);
|
||||
Quaternion pitchQ = Quaternion.AngleAxis(pitch, _viewCamera.transform.right);
|
||||
_root.transform.rotation = yawQ * pitchQ * _modelStartRot;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var kv in _matCache)
|
||||
{
|
||||
var (rends, originals) = kv.Value;
|
||||
for (int i = 0; i < rends.Length; i++)
|
||||
{
|
||||
if (rends[i] == null) continue;
|
||||
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];
|
||||
}
|
||||
}
|
||||
_matCache.Clear();
|
||||
_idToObject.Clear();
|
||||
if (_root != null) UnityEngine.Object.Destroy(_root);
|
||||
_root = null;
|
||||
_focusedId = null;
|
||||
_wireframeApplied = false;
|
||||
|
||||
if (_viewCamera != null)
|
||||
{
|
||||
// detach RT and keep camera disabled to avoid replacing main display
|
||||
if (_rt != null && _viewCamera.targetTexture == _rt)
|
||||
{
|
||||
_viewCamera.targetTexture = null;
|
||||
}
|
||||
_viewCamera.enabled = false;
|
||||
}
|
||||
if (_rt != null)
|
||||
{
|
||||
_rt.Release();
|
||||
Destroy(_rt);
|
||||
_rt = null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,7 +244,7 @@ namespace UVC.UI.List.Tree
|
||||
}
|
||||
|
||||
// 디버그 로그
|
||||
Debug.Log($"Escape key pressed. Clearing {selectedItems.Count} selected item(s).");
|
||||
//Debug.Log($"Escape key pressed. Clearing {selectedItems.Count} selected item(s).");
|
||||
|
||||
// 모든 선택 해제
|
||||
ClearSelection();
|
||||
@@ -276,7 +276,7 @@ namespace UVC.UI.List.Tree
|
||||
var itemsToDelete = selectedItems.ToList();
|
||||
|
||||
// 디버그 로그
|
||||
Debug.Log($"Delete key pressed. Removing {itemsToDelete.Count} selected item(s).");
|
||||
//Debug.Log($"Delete key pressed. Removing {itemsToDelete.Count} selected item(s).");
|
||||
|
||||
// 각 선택된 아이템을 삭제
|
||||
foreach (var item in itemsToDelete)
|
||||
@@ -381,7 +381,7 @@ namespace UVC.UI.List.Tree
|
||||
lastSelectedItem = previousItem;
|
||||
|
||||
// 디버그 로그
|
||||
Debug.Log($"Up arrow: Selected '{previousItem.Name}' (visible index {previousIndex})");
|
||||
//Debug.Log($"Up arrow: Selected '{previousItem.Name}' (visible index {previousIndex})");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -412,7 +412,7 @@ namespace UVC.UI.List.Tree
|
||||
lastSelectedItem = firstItem;
|
||||
|
||||
// 디버그 로그
|
||||
Debug.Log($"Down arrow (no selection): Selected first visible item '{firstItem.Name}'");
|
||||
//Debug.Log($"Down arrow (no selection): Selected first visible item '{firstItem.Name}'");
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -448,7 +448,7 @@ namespace UVC.UI.List.Tree
|
||||
lastSelectedItem = nextItem;
|
||||
|
||||
// 디버그 로그
|
||||
Debug.Log($"Down arrow: Selected '{nextItem.Name}' (visible index {nextIndex})");
|
||||
//Debug.Log($"Down arrow: Selected '{nextItem.Name}' (visible index {nextIndex})");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -485,7 +485,7 @@ namespace UVC.UI.List.Tree
|
||||
selectedItem.IsExpanded = true;
|
||||
|
||||
// 디버그 로그
|
||||
Debug.Log($"Return key: Toggled '{selectedItem.Name}' IsExpanded to {selectedItem.IsExpanded}");
|
||||
//Debug.Log($"Return key: Toggled '{selectedItem.Name}' IsExpanded to {selectedItem.IsExpanded}");
|
||||
|
||||
// 펼침/접힘 상태 변경을 UI에 반영하기 위해 평탄화 리스트 업데이트
|
||||
UpdateFlattenedItemDataList();
|
||||
@@ -524,7 +524,7 @@ namespace UVC.UI.List.Tree
|
||||
selectedItem.IsExpanded = false;
|
||||
|
||||
// 디버그 로그
|
||||
Debug.Log($"Return key: Toggled '{selectedItem.Name}' IsExpanded to {selectedItem.IsExpanded}");
|
||||
//Debug.Log($"Return key: Toggled '{selectedItem.Name}' IsExpanded to {selectedItem.IsExpanded}");
|
||||
|
||||
// 펼침/접힘 상태 변경을 UI에 반영하기 위해 평탄화 리스트 업데이트
|
||||
UpdateFlattenedItemDataList();
|
||||
@@ -543,12 +543,12 @@ namespace UVC.UI.List.Tree
|
||||
/// - data: 추가할 아이템 데이터
|
||||
/// </summary>
|
||||
/// <param name="data">추가할 아이템 데이터</param>
|
||||
public void AddItem(TreeListItemData data)
|
||||
public void AddItem<T>(TreeListItemData data) where T : TreeListItem
|
||||
{
|
||||
data.Parent = null;
|
||||
// Instantiate(템플릿, 부모 Transform)
|
||||
// = 템플릿을 복제하고 부모의 자식으로 설정
|
||||
TreeListItem item = GameObject.Instantiate<TreeListItem>(ItemPrefab, root);
|
||||
T item = GameObject.Instantiate(ItemPrefab, root) as T;
|
||||
|
||||
// 생성된 아이템 초기화
|
||||
// 데이터를 UI에 바인딩하고 이벤트 리스너 등록
|
||||
@@ -571,11 +571,11 @@ namespace UVC.UI.List.Tree
|
||||
/// </summary>
|
||||
/// <param name="data">추가할 아이템 데이터</param>
|
||||
/// <param name="index">삽입 위치</param>
|
||||
public void AddItemAt(TreeListItemData data, int index)
|
||||
public void AddItemAt<T>(TreeListItemData data, int index) where T : TreeListItem
|
||||
{
|
||||
data.Parent = null;
|
||||
|
||||
TreeListItem item = GameObject.Instantiate<TreeListItem>(ItemPrefab, root);
|
||||
T item = GameObject.Instantiate(ItemPrefab, root) as T;
|
||||
item.Init(data, this, dragDropManager);
|
||||
index = Mathf.Clamp(index, 0, items.Count);
|
||||
items.Insert(index, data);
|
||||
@@ -614,8 +614,7 @@ namespace UVC.UI.List.Tree
|
||||
|
||||
if (item != null) item.Delete(true);
|
||||
|
||||
|
||||
AddItem(clone);
|
||||
AddItem<TreeListItem>(clone);
|
||||
|
||||
}
|
||||
|
||||
@@ -644,7 +643,7 @@ namespace UVC.UI.List.Tree
|
||||
|
||||
if (item != null) item.Delete(true);
|
||||
|
||||
AddItemAt(clone, index);
|
||||
AddItemAt<TreeListItem>(clone, index);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -800,7 +799,7 @@ namespace UVC.UI.List.Tree
|
||||
public void OnItemClicked(TreeListItemData data, bool ctrlPressed, bool shiftPressed)
|
||||
{
|
||||
// 디버그 로그: 클릭 정보를 콘솔에 출력 (개발 중 확인용)
|
||||
Debug.Log($"OnItemClicked {data.Name}, ctrlPressed:{ctrlPressed}, shiftPressed:{shiftPressed}, lastSelectedItem:{lastSelectedItem}");
|
||||
//Debug.Log($"OnItemClicked {data.Name}, ctrlPressed:{ctrlPressed}, shiftPressed:{shiftPressed}, lastSelectedItem:{lastSelectedItem}");
|
||||
|
||||
// 다중 선택 비활성화
|
||||
if (!allowMultipleSelection)
|
||||
@@ -1055,7 +1054,7 @@ namespace UVC.UI.List.Tree
|
||||
UpdateFlattenedItemDataList();
|
||||
|
||||
// 필요시 UI 재구성 (예: 자식 컨테이너 위치 변경 등)
|
||||
Debug.Log($"Item '{draggedItem.Name}' dropped on '{(targetItem?.Name ?? "Root")}'");
|
||||
//Debug.Log($"Item '{draggedItem.Name}' dropped on '{(targetItem?.Name ?? "Root")}'");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -129,7 +129,7 @@ namespace UVC.UI.Window
|
||||
/// <param name="data">추가할 데이터.</param>
|
||||
public void AddItem(TreeListItemData data)
|
||||
{
|
||||
treeList.AddItem(data);
|
||||
treeList.AddItem<TreeListItem>(data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -139,7 +139,7 @@ namespace UVC.UI.Window
|
||||
/// <param name="index">삽입 인덱스(0 기반).</param>
|
||||
public void AddItemAt(TreeListItemData data, int index)
|
||||
{
|
||||
treeList.AddItemAt(data, index);
|
||||
treeList.AddItemAt<TreeListItem>(data, index);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -351,7 +351,7 @@ namespace UVC.UI.Window
|
||||
treeListSearch.ClearItems();
|
||||
foreach (var r in results)
|
||||
{
|
||||
treeListSearch.AddItem(r.Clone());
|
||||
treeListSearch.AddItem<TreeListItem>(r.Clone());
|
||||
}
|
||||
|
||||
// 로딩 종료
|
||||
|
||||
Reference in New Issue
Block a user