개발중

This commit is contained in:
logonkhi
2025-11-13 20:16:25 +09:00
parent 6920659ed9
commit c98c1d9d9a
42 changed files with 5008 additions and 1854 deletions

View File

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

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

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 5ec1e373110ba694f916a50093c32919

View File

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

View File

@@ -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 =>

View File

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

View File

@@ -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());
}
// 로딩 종료

View File

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

View File

@@ -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>

View File

@@ -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());
}
// 로딩 종료