1차 완료
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
using RTG;
|
||||
using System.Collections.Concurrent;
|
||||
using UnityEngine;
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#nullable enable
|
||||
using EPOOutline;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
@@ -46,7 +48,7 @@ namespace UVC.Factory.Component
|
||||
private float teleportRotationThreshold = 45.0f; // 5도 이상 차이나면 순간이동
|
||||
|
||||
|
||||
private Renderer? renderer;
|
||||
private Renderer? modelRenderer;
|
||||
|
||||
private bool isRed = false;
|
||||
|
||||
@@ -56,13 +58,19 @@ namespace UVC.Factory.Component
|
||||
private Vector3 velocity = Vector3.zero;
|
||||
private float angularVelocity = 0;
|
||||
|
||||
private Outlinable? outlinable;
|
||||
|
||||
/// <summary>
|
||||
/// AGV 객체가 생성될 때 처음 한 번 호출되는 초기화 메서드입니다.
|
||||
/// </summary>
|
||||
private void Start()
|
||||
{
|
||||
|
||||
renderer = modelObject.GetComponent<Renderer>();
|
||||
if (modelObject != null)
|
||||
{
|
||||
modelRenderer = modelObject.GetComponent<Renderer>();
|
||||
outlinable = modelObject.GetComponent<Outlinable>();
|
||||
if (outlinable != null) outlinable.enabled = false;
|
||||
}
|
||||
|
||||
// 시작 시에는 현재 위치를 목표 위치로 설정하여 의도치 않은 움직임을 방지합니다.
|
||||
targetPosition = transform.position;
|
||||
@@ -89,6 +97,23 @@ namespace UVC.Factory.Component
|
||||
base.OnDestroy();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 선택 된 효과로 외곽선을 표시합니다.
|
||||
/// </summary>
|
||||
public override void ShowOutLine()
|
||||
{
|
||||
if (outlinable != null) outlinable.enabled = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 외곽선을 숨깁니다.
|
||||
/// </summary>
|
||||
public override void HideOutLine()
|
||||
{
|
||||
if (outlinable != null) outlinable.enabled = false;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// AGVManager로부터 새로운 데이터를 받았을 때 호출되는 핵심 메서드입니다.
|
||||
/// 받은 데이터를 기반으로 AGV의 내부 상태와 목표 위치를 갱신합니다.
|
||||
@@ -222,9 +247,9 @@ namespace UVC.Factory.Component
|
||||
|
||||
private void ChangeColor(Color color)
|
||||
{
|
||||
if(color == Color.red && isRed && renderer != null) return; // 이미 빨간색이면 변경하지 않음
|
||||
if(color == Color.red && isRed && modelRenderer != null) return; // 이미 빨간색이면 변경하지 않음
|
||||
isRed = color == Color.red;
|
||||
if(renderer != null) renderer!.material.color = color;
|
||||
if(modelRenderer != null) modelRenderer!.material.color = color;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -227,6 +227,8 @@ namespace UVC.Factory.Component
|
||||
// dataOrderedMask가 설정되어 있지 않으면 모든 데이터를 사용합니다.
|
||||
info = new OrderedDictionary<string, object>(data!);
|
||||
}
|
||||
//순서 바꾸지 말것. FactoryObjectSelectionManager.Instance.Select 호출 후 InfoWindow.Instance.Show
|
||||
FactoryObjectSelectionManager.Instance.Select(this);
|
||||
InfoWindow.Instance.Show(transform, info);
|
||||
}
|
||||
}
|
||||
@@ -266,6 +268,16 @@ namespace UVC.Factory.Component
|
||||
/// <param name="newData">처리할 데이터 객체입니다. null일 수 없습니다.</param>
|
||||
protected virtual void ProcessData(DataObject newData) { }
|
||||
|
||||
/// <summary>
|
||||
/// 선택 된 효과로 외곽선을 표시합니다.
|
||||
/// </summary>
|
||||
public virtual void ShowOutLine() { }
|
||||
|
||||
/// <summary>
|
||||
/// 외곽선을 숨깁니다.
|
||||
/// </summary>
|
||||
public virtual void HideOutLine() { }
|
||||
|
||||
/// <summary>
|
||||
/// 객체의 위치를 가져옵니다. 월드 좌표 또는 로컬 좌표로 반환할 수 있습니다.
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UVC.Core;
|
||||
using UVC.Factory.Modal;
|
||||
|
||||
namespace UVC.Factory.Component
|
||||
{
|
||||
/// <summary>
|
||||
/// 씬에 배치된 FactoryObject의 선택을 관리하는 싱글톤 클래스입니다.
|
||||
/// 단일 선택, 다중 선택(추후 확장), 선택 해제 로직을 처리합니다.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 이 관리자는 다음 기능을 제공합니다:
|
||||
/// - FactoryObject 클릭 시 선택 및 외곽선 표시.
|
||||
/// - 다른 객체 선택 시 이전에 선택된 객체의 외곽선 숨김.
|
||||
/// - UI가 아닌 빈 공간 클릭 시 모든 선택 해제.
|
||||
/// - 다중 선택을 위한 기반 제공.
|
||||
///
|
||||
/// 이 클래스가 올바르게 작동하려면 씬에 Unity의 EventSystem이 존재해야 합니다.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// // FactoryObjectSelectionManager는 자동으로 씬에 생성되므로 별도의 인스턴스화가 필요 없습니다.
|
||||
///
|
||||
/// // FactoryObject에서 선택을 요청하는 방법:
|
||||
/// public class MyFactoryObject : FactoryObject
|
||||
/// {
|
||||
/// public override void OnPointerClick(PointerEventData eventData)
|
||||
/// {
|
||||
/// // base.OnPointerClick(eventData); // InfoWindow를 표시하려면 기본 로직 호출
|
||||
///
|
||||
/// // 다중 선택을 원하면 (예: Shift 키 누름) isMultiSelect를 true로 설정
|
||||
/// bool isMultiSelect = Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift);
|
||||
///
|
||||
/// // FactoryObjectSelectionManager에 선택 요청
|
||||
/// FactoryObjectSelectionManager.Instance.Select(this, isMultiSelect);
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// // 외부에서 선택된 객체를 가져오는 방법:
|
||||
/// public class UIManager : MonoBehaviour
|
||||
/// {
|
||||
/// void Update()
|
||||
/// {
|
||||
/// if (Input.GetKeyDown(KeyCode.I))
|
||||
/// {
|
||||
/// var selectedObjects = FactoryObjectSelectionManager.Instance.GetSelectedObjects();
|
||||
/// if (selectedObjects.Any())
|
||||
/// {
|
||||
/// Debug.Log($"현재 {selectedObjects.Count}개의 객체가 선택되었습니다.");
|
||||
/// foreach (var obj in selectedObjects)
|
||||
/// {
|
||||
/// Debug.Log($"- {obj.Info?.Name}");
|
||||
/// }
|
||||
/// }
|
||||
/// else
|
||||
/// {
|
||||
/// Debug.Log("선택된 객체가 없습니다.");
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </example>
|
||||
public class FactoryObjectSelectionManager : MonoBehaviour
|
||||
{
|
||||
private static FactoryObjectSelectionManager? _instance;
|
||||
private static readonly object _lock = new object();
|
||||
private static bool _applicationIsQuitting = false;
|
||||
|
||||
/// <summary>
|
||||
/// SelectionManager의 싱글톤 인스턴스를 가져옵니다.
|
||||
/// 씬에 인스턴스가 없으면 자동으로 생성합니다.
|
||||
/// </summary>
|
||||
public static FactoryObjectSelectionManager Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_applicationIsQuitting)
|
||||
{
|
||||
// 애플리케이션 종료 시점에 이미 파괴된 싱글톤에 접근하는 것을 방지합니다.
|
||||
Debug.LogWarning("[Singleton] Instance 'FactoryObjectSelectionManager' already destroyed on application quit. Won't create again - returning null.");
|
||||
return null!;
|
||||
}
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
if (_instance == null)
|
||||
{
|
||||
// 씬에서 기존 인스턴스를 찾아봅니다.
|
||||
_instance = FindFirstObjectByType<FactoryObjectSelectionManager>();
|
||||
|
||||
if (_instance == null)
|
||||
{
|
||||
// 씬에 인스턴스가 없으면 새로 생성합니다.
|
||||
var singletonObject = new GameObject();
|
||||
_instance = singletonObject.AddComponent<FactoryObjectSelectionManager>();
|
||||
singletonObject.name = typeof(FactoryObjectSelectionManager).ToString() + " (Singleton)";
|
||||
|
||||
// 씬 전환 시 파괴되지 않도록 설정합니다.
|
||||
DontDestroyOnLoad(singletonObject);
|
||||
}
|
||||
}
|
||||
return _instance;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 현재 선택된 객체들을 저장하는 리스트입니다.
|
||||
private readonly List<FactoryObject> _selectedObjects = new List<FactoryObject>();
|
||||
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
// 싱글톤 인스턴스가 중복으로 생성되는 것을 방지합니다.
|
||||
if (_instance != null && _instance != this)
|
||||
{
|
||||
Debug.LogWarning("Another instance of FactoryObjectSelectionManager exists, destroying this one.");
|
||||
Destroy(gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
// 마우스 왼쪽 버튼 클릭을 감지합니다.
|
||||
if (Input.GetMouseButtonDown(0))
|
||||
{
|
||||
// 포인터가 UI 요소 위에 있는지 확인합니다. UI 클릭 시에는 선택/해제 로직을 무시합니다.
|
||||
Debug.Log($"IsPointerOverUIObject() : {IsPointerOverUIObject()}");
|
||||
if (IsPointerOverUIObject())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 메인 카메라에서 마우스 위치로 레이를 쏩니다.
|
||||
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
|
||||
|
||||
bool raycastHit = Physics.Raycast(ray, out RaycastHit hit);
|
||||
Debug.Log($"Raycast : {raycastHit}");
|
||||
// 레이캐스트로 무언가 감지되었는지 확인합니다.
|
||||
if (raycastHit)
|
||||
{
|
||||
// 감지된 객체 또는 그 부모 중에 FactoryObject 컴포넌트가 있는지 확인합니다.
|
||||
if (hit.collider.GetComponentInParent<FactoryObject>() == null)
|
||||
{
|
||||
// FactoryObject가 아닌 다른 객체를 클릭한 경우, 모든 선택을 해제합니다.
|
||||
DeselectAll();
|
||||
}
|
||||
// FactoryObject를 클릭한 경우는 FactoryObject의 OnPointerClick에서 처리하므로 여기서는 별도 처리를 하지 않습니다.
|
||||
}
|
||||
else
|
||||
{
|
||||
// 레이캐스트로 아무것도 감지되지 않았을 경우 (빈 공간 클릭), 모든 선택을 해제합니다.
|
||||
DeselectAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 포인터가 UI 객체 위에 있는지 확인합니다.
|
||||
/// </summary>
|
||||
/// <returns>UI 객체 위에 있으면 true, 그렇지 않으면 false를 반환합니다.</returns>
|
||||
private bool IsPointerOverUIObject()
|
||||
{
|
||||
// EventSystem이 없는 경우 false를 반환합니다.
|
||||
if (EventSystem.current == null) return false;
|
||||
|
||||
// 현재 포인터 위치에 대한 이벤트 데이터를 생성합니다.
|
||||
PointerEventData eventData = new PointerEventData(EventSystem.current)
|
||||
{
|
||||
position = Input.mousePosition
|
||||
};
|
||||
|
||||
// 레이캐스트 결과를 저장할 리스트를 생성합니다.
|
||||
List<RaycastResult> results = new List<RaycastResult>();
|
||||
|
||||
// 현재 포인터 위치에 있는 모든 UI 객체를 가져옵니다.
|
||||
EventSystem.current.RaycastAll(eventData, results);
|
||||
|
||||
bool hasCanvas = false;
|
||||
|
||||
foreach (var result in results)
|
||||
{
|
||||
if(result.gameObject.GetComponentInParent<Canvas>() != null)
|
||||
{
|
||||
// UI 객체가 Canvas의 자식인 경우, UI 위에 있는 것으로 간주합니다.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 결과가 하나라도 있으면 UI 위에 있는 것으로 간주합니다.
|
||||
return hasCanvas;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 지정된 FactoryObject를 선택합니다.
|
||||
/// </summary>
|
||||
/// <param name="factoryObject">선택할 객체입니다.</param>
|
||||
/// <param name="isMultiSelect">다중 선택 모드 여부입니다. true이면 기존 선택에 추가하고, false이면 기존 선택을 해제하고 새로 선택합니다.</param>
|
||||
public void Select(FactoryObject factoryObject, bool isMultiSelect = false)
|
||||
{
|
||||
if (!isMultiSelect)
|
||||
{
|
||||
// 단일 선택 모드일 경우, 기존에 선택된 모든 객체를 해제합니다.
|
||||
DeselectAll();
|
||||
}
|
||||
Debug.Log($"Selecting FactoryObject: {factoryObject.Info?.Name}, MultiSelect: {isMultiSelect}");
|
||||
if (!_selectedObjects.Contains(factoryObject))
|
||||
{
|
||||
// 객체가 아직 선택되지 않았다면 선택 목록에 추가하고 외곽선을 표시합니다.
|
||||
_selectedObjects.Add(factoryObject);
|
||||
factoryObject.ShowOutLine();
|
||||
}
|
||||
else if (isMultiSelect)
|
||||
{
|
||||
// 다중 선택 모드에서 이미 선택된 객체를 다시 클릭하면 선택을 해제합니다.
|
||||
Deselect(factoryObject);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 지정된 FactoryObject의 선택을 해제합니다.
|
||||
/// </summary>
|
||||
/// <param name="factoryObject">선택 해제할 객체입니다.</param>
|
||||
public void Deselect(FactoryObject factoryObject)
|
||||
{
|
||||
if (_selectedObjects.Contains(factoryObject))
|
||||
{
|
||||
// 객체가 선택 목록에 있으면 외곽선을 숨기고 목록에서 제거합니다.
|
||||
factoryObject.HideOutLine();
|
||||
_selectedObjects.Remove(factoryObject);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 선택된 모든 FactoryObject의 선택을 해제합니다.
|
||||
/// </summary>
|
||||
public void DeselectAll()
|
||||
{
|
||||
// 성능을 위해 ToList()를 사용하여 반복 중 컬렉션 변경 문제를 방지합니다.
|
||||
foreach (var selectedObject in _selectedObjects.ToList())
|
||||
{
|
||||
Deselect(selectedObject);
|
||||
}
|
||||
_selectedObjects.Clear();
|
||||
if(InfoWindow.Instance.IsVisible) InfoWindow.Instance.Hide();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 선택된 모든 FactoryObject의 리스트를 가져옵니다.
|
||||
/// </summary>
|
||||
/// <returns>선택된 객체들의 읽기 전용 리스트입니다.</returns>
|
||||
public IReadOnlyList<FactoryObject> GetSelectedObjects()
|
||||
{
|
||||
return _selectedObjects.AsReadOnly();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
_applicationIsQuitting = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: baf549ca71e7de94aa69fa6459ba51f1
|
||||
@@ -8,12 +8,14 @@ using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UVC.Core;
|
||||
using UVC.Extention;
|
||||
using UVC.Factory;
|
||||
using UVC.Factory.Component;
|
||||
using UVC.Locale;
|
||||
using UVC.UI.Commands;
|
||||
using UVC.UI.Menu;
|
||||
using UVC.UI.Modal;
|
||||
|
||||
namespace UVC.Factory.Modal.ComponentList
|
||||
namespace UVC.UI.List.ComponentList
|
||||
{
|
||||
/// <summary>
|
||||
/// 컴포넌트 목록 UI를 관리하는 싱글톤 클래스입니다.
|
||||
@@ -26,14 +28,15 @@ namespace UVC.Factory.Modal.ComponentList
|
||||
{
|
||||
[Tooltip("데이터를 표시할 InfiniteScroll 컴포넌트입니다.")]
|
||||
[SerializeField]
|
||||
private InfiniteScroll? scrollList = null;
|
||||
protected InfiniteScroll? scrollList = null;
|
||||
|
||||
[Tooltip("검색어 입력을 위한 TMP_InputField 컴포넌트입니다.")]
|
||||
[SerializeField]
|
||||
private TMP_InputField? inputField = null;
|
||||
protected TMP_InputField? inputField = null;
|
||||
|
||||
// InfiniteScroll에 표시될 원본 데이터 리스트입니다.
|
||||
private List<ComponentListItemData>? data;
|
||||
protected List<ComponentListItemData>? data;
|
||||
protected List<ComponentListItemData>? filteredData;
|
||||
|
||||
/// <summary>
|
||||
/// SingletonScene 초기화 과정에서 호출됩니다.
|
||||
@@ -57,7 +60,9 @@ namespace UVC.Factory.Modal.ComponentList
|
||||
if (inputField == null)
|
||||
{
|
||||
inputField = GetComponentInChildren<TMP_InputField>();
|
||||
|
||||
}
|
||||
if (inputField != null)
|
||||
{
|
||||
// 사용자가 검색어를 입력하고 Enter 키를 누르거나 입력을 완료했을 때 OnInputFieldChanged 메서드가 호출되도록 이벤트를 등록합니다.
|
||||
//inputField.onEndEdit.AddListener(OnInputFieldChanged);
|
||||
inputField.onSubmit.AddListener(OnInputFieldChanged);
|
||||
@@ -78,13 +83,11 @@ namespace UVC.Factory.Modal.ComponentList
|
||||
base.OnDestroy();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// FactoryObjectManager에서 데이터를 가져와 스크롤 리스트를 설정합니다.
|
||||
/// 데이터를 카테고리별로 그룹화하고, 각 카테고리와 해당 항목들을 리스트에 추가합니다.
|
||||
/// </summary>
|
||||
public void SetupData()
|
||||
public virtual void SetupData()
|
||||
{
|
||||
if (scrollList == null)
|
||||
{
|
||||
@@ -92,7 +95,10 @@ namespace UVC.Factory.Modal.ComponentList
|
||||
return;
|
||||
}
|
||||
|
||||
if(inputField != null) inputField.text = string.Empty;
|
||||
|
||||
//transform.SetAsFirstSibling();
|
||||
scrollList.ClearData(); // 스크롤 리스트의 기존 데이터를 모두 비웁니다.
|
||||
|
||||
data?.Clear();
|
||||
data = new List<ComponentListItemData>();
|
||||
@@ -133,57 +139,63 @@ namespace UVC.Factory.Modal.ComponentList
|
||||
/// 카테고리의 확장/축소 상태를 토글하고, 그에 따라 자식 아이템들의 표시 여부를 업데이트합니다.
|
||||
/// </summary>
|
||||
/// <param name="categoryData">상태를 변경할 카테고리 데이터</param>
|
||||
public void ToggleCategory(ComponentListItemData categoryData)
|
||||
public virtual void ToggleCategory(ComponentListItemData categoryData)
|
||||
{
|
||||
if (scrollList == null) return;
|
||||
if (scrollList == null || data == null) return;
|
||||
|
||||
// 1. 현재 스크롤에 표시된 데이터들을 가져옵니다.
|
||||
var currentData = new List<InfiniteScrollData>();
|
||||
// 1. 현재 스크롤 리스트에서 토글할 카테고리의 인덱스를 찾습니다.
|
||||
int categoryIndex = -1;
|
||||
for (int i = 0; i < scrollList.GetDataCount(); i++)
|
||||
{
|
||||
currentData.Add(scrollList.GetData(i));
|
||||
if (scrollList.GetData(i) == categoryData)
|
||||
{
|
||||
categoryIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 카테고리 데이터의 확장 상태를 변경합니다.
|
||||
if (categoryIndex == -1) return; // 카테고리를 찾지 못하면 중단
|
||||
|
||||
List<ComponentListItemData> list = filteredData != null ? filteredData : data;
|
||||
|
||||
// 2. 카테고리의 확장 상태를 변경합니다.
|
||||
categoryData.isExpanded = !categoryData.isExpanded;
|
||||
|
||||
// 3. 새로운 표시 리스트를 만듭니다.
|
||||
var newData = new List<InfiniteScrollData>();
|
||||
bool categoryFound = false;
|
||||
bool isChildrenVisible = true;
|
||||
|
||||
foreach (ComponentListItemData itemData in currentData.Cast<ComponentListItemData>())
|
||||
// 3. 확장/축소 상태에 따라 자식 아이템을 추가하거나 제거합니다.
|
||||
if (categoryData.isExpanded) // 카테고리 확장
|
||||
{
|
||||
if (itemData.isCategory)
|
||||
// 원본 데이터 리스트에서 이 카테고리에 속한 자식들을 찾습니다.
|
||||
int originalDataIndex = list.IndexOf(categoryData);
|
||||
if (originalDataIndex != -1)
|
||||
{
|
||||
// 현재 카테고리를 찾았으면, 그 카테고리의 확장 상태를 자식들의 표시 여부로 사용합니다.
|
||||
if (itemData == categoryData)
|
||||
var childrenToAdd = list.Skip(originalDataIndex + 1)
|
||||
.Take(categoryData.categoryBadgeCount)
|
||||
.ToArray();
|
||||
|
||||
// 카테고리 바로 다음 위치에 자식 아이템들을 삽입합니다.
|
||||
if (childrenToAdd.Length > 0)
|
||||
{
|
||||
categoryFound = true;
|
||||
isChildrenVisible = itemData.isExpanded;
|
||||
scrollList.InsertData(childrenToAdd, categoryIndex + 1, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
else // 카테고리 축소
|
||||
{
|
||||
// 카테고리 바로 다음에 있는 자식 아이템들을 제거합니다.
|
||||
// 한 번에 하나씩 제거하므로, 항상 categoryIndex + 1 위치의 아이템을 제거합니다.
|
||||
for (int i = 0; i < categoryData.categoryBadgeCount; i++)
|
||||
{
|
||||
// 제거할 아이템이 리스트 범위 내에 있는지 확인
|
||||
if (categoryIndex + 1 < scrollList.GetDataCount())
|
||||
{
|
||||
scrollList.RemoveData(categoryIndex + 1, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 다른 카테고리를 만나면, 다시 자식들을 보이게 설정합니다.
|
||||
// (이 로직은 현재 구조에서 크게 의미 없지만, 중첩 카테고리를 고려하면 유용할 수 있습니다.)
|
||||
categoryFound = false;
|
||||
isChildrenVisible = true;
|
||||
}
|
||||
newData.Add(itemData); // 카테고리 아이템은 항상 추가
|
||||
}
|
||||
else // 일반 아이템일 경우
|
||||
{
|
||||
// 해당 카테고리의 자식이거나, 다른 카테고리에 속해있다면 표시
|
||||
if (!categoryFound || isChildrenVisible)
|
||||
{
|
||||
newData.Add(itemData);
|
||||
break; // 범위를 벗어나면 중단
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 스크롤 리스트를 비우고 새로운 데이터 리스트로 갱신합니다.
|
||||
scrollList.Clear();
|
||||
scrollList.InsertData(newData.ToArray(), true);
|
||||
}
|
||||
|
||||
|
||||
@@ -192,8 +204,9 @@ namespace UVC.Factory.Modal.ComponentList
|
||||
/// 입력된 텍스트를 기반으로 scrollList의 내용을 필터링합니다.
|
||||
/// </summary>
|
||||
/// <param name="text">사용자가 입력한 검색어</param>
|
||||
private void OnInputFieldChanged(string text)
|
||||
protected virtual void OnInputFieldChanged(string text)
|
||||
{
|
||||
Debug.Log($"Searching1 for: {text}");
|
||||
if (scrollList == null || inputField == null) return;
|
||||
// 검색어가 비어있으면 필터링을 수행하지 않습니다.
|
||||
if (string.IsNullOrEmpty(text)) return;
|
||||
@@ -207,55 +220,73 @@ namespace UVC.Factory.Modal.ComponentList
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.Log($"Searching2 for: {text}");
|
||||
|
||||
// 특수 필터 키워드가 있는지 확인합니다.
|
||||
// 예: "@Category 전기" -> '전기' 카테고리 필터
|
||||
bool hasCategoryFilter = text.StartsWith("@Category ");
|
||||
bool hasAreaFilter = text.StartsWith("@Area ");
|
||||
bool hasFloorFilter = text.StartsWith("@Floor ");
|
||||
|
||||
// InfiniteScroll의 SetFilter 메서드에 전달할 Predicate(판별 함수)를 정의합니다.
|
||||
// 이 함수는 각 데이터 아이템에 대해 호출되며, 'true'를 반환하면 해당 아이템을 숨깁니다.
|
||||
Predicate<InfiniteScrollData> func = (data) =>
|
||||
// 원본 데이터(data)를 기반으로 필터링된 새로운 리스트를 생성합니다.
|
||||
List<ComponentListItemData> filteredItems = data.Where(itemData =>
|
||||
{
|
||||
if (data is ComponentListItemData itemData)
|
||||
if (itemData.isCategory) return false; // 카테고리 항목은 항상 제외
|
||||
|
||||
if (hasCategoryFilter)
|
||||
{
|
||||
// 필터 조건에 맞지 *않으면* true를 반환하여 아이템을 숨깁니다.
|
||||
// 반대로, 필터 조건에 맞으면 false를 반환하여 아이템을 표시합니다.
|
||||
|
||||
// 카테고리 필터링
|
||||
if (hasCategoryFilter)
|
||||
{
|
||||
string categoryText = text.Substring("@Category ".Length).Trim();
|
||||
// 아이템이 일반 항목이고, 그 아이템의 카테고리 이름에 검색어가 포함되어 있으면 표시 (false 반환)
|
||||
return !(!itemData.isCategory && itemData.categoryName.Contains(categoryText));
|
||||
}
|
||||
else if (hasAreaFilter)
|
||||
{
|
||||
string areaText = text.Substring("@Area ".Length).Trim();
|
||||
// 아이템이 일반 항목이고, factoryObjectInfo의 Area에 검색어가 포함되어 있으면 표시 (false 반환)
|
||||
return !(!itemData.isCategory && itemData.factoryObjectInfo?.Area.Contains(areaText) == true);
|
||||
}
|
||||
else if (hasFloorFilter)
|
||||
{
|
||||
string floorText = text.Substring("@Floor ".Length).Trim();
|
||||
// 아이템이 일반 항목이고, factoryObjectInfo의 Floor에 검색어가 포함되어 있으면 표시 (false 반환)
|
||||
return !(!itemData.isCategory && itemData.factoryObjectInfo?.Floor.Contains(floorText) == true);
|
||||
}
|
||||
// 일반 이름 검색
|
||||
// 아이템이 일반 항목이고, generalName에 검색어가 포함되어 있으면 표시 (false 반환)
|
||||
return !(!itemData.isCategory && itemData.generalName.Contains(text));
|
||||
string categoryText = text.Substring("@Category ".Length).Trim();
|
||||
return itemData.categoryName.Contains(categoryText, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
else if (hasAreaFilter)
|
||||
{
|
||||
string areaText = text.Substring("@Area ".Length).Trim();
|
||||
return itemData.factoryObjectInfo?.Area.Contains(areaText, StringComparison.OrdinalIgnoreCase) == true;
|
||||
}
|
||||
else if (hasFloorFilter)
|
||||
{
|
||||
string floorText = text.Substring("@Floor ".Length).Trim();
|
||||
return itemData.factoryObjectInfo?.Floor.Contains(floorText, StringComparison.OrdinalIgnoreCase) == true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return itemData.generalName.Contains(text, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}).ToList();
|
||||
|
||||
scrollList.SetFilter(func);
|
||||
if (filteredData == null)
|
||||
{
|
||||
filteredData = new List<ComponentListItemData>();
|
||||
}
|
||||
else
|
||||
{
|
||||
filteredData.Clear();
|
||||
}
|
||||
string resultText = LocalizationManager.Instance.GetString("검색결과");
|
||||
|
||||
// 검색 결과 카테고리 아이템을 추가합니다.
|
||||
filteredData.Add(new ComponentListItemData
|
||||
{
|
||||
isCategory = true,
|
||||
categoryName = $"{resultText}: {filteredItems.Count}건",
|
||||
categoryBadgeCount = filteredItems.Count,
|
||||
isExpanded = true, // 검색 결과는 항상 펼쳐진 상태로 시작
|
||||
OnCategoryExpendAction = ToggleCategory // 액션 할당
|
||||
});
|
||||
|
||||
// 필터링된 아이템들을 결과 리스트에 추가합니다.
|
||||
filteredData.AddRange(filteredItems);
|
||||
|
||||
// 스크롤 리스트를 비우고 필터링된 결과로 새로 채웁니다.
|
||||
scrollList.Clear();
|
||||
scrollList.InsertData(filteredData.ToArray(), true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 필터 버튼 클릭 시 호출됩니다.
|
||||
/// 검색에 사용할 수 있는 특수 필터 키워드를 보여주는 컨텍스트 메뉴를 표시합니다.
|
||||
/// </summary>
|
||||
public void OnClickFilter()
|
||||
public virtual void OnClickFilter()
|
||||
{
|
||||
Debug.Log("Filter button clicked.");
|
||||
|
||||
@@ -279,7 +310,7 @@ namespace UVC.Factory.Modal.ComponentList
|
||||
/// 새로고침 버튼 클릭 시 호출됩니다.
|
||||
/// 리스트의 모든 데이터를 지우고 SetupData를 다시 호출하여 목록을 갱신합니다.
|
||||
/// </summary>
|
||||
public void OnClickRefresh()
|
||||
public virtual void OnClickRefresh()
|
||||
{
|
||||
if (scrollList != null && data != null)
|
||||
{
|
||||
@@ -292,7 +323,7 @@ namespace UVC.Factory.Modal.ComponentList
|
||||
/// 닫기 버튼 클릭 시 호출됩니다.
|
||||
/// 컴포넌트 목록 UI를 비활성화하고, 카메라 컨트롤을 다시 활성화합니다.
|
||||
/// </summary>
|
||||
public void OnClickClose()
|
||||
public virtual void OnClickClose()
|
||||
{
|
||||
gameObject.SetActive(false);
|
||||
FactoryCameraController.Instance.Enable = true; // 카메라 컨트롤러 활성화
|
||||
@@ -302,14 +333,16 @@ namespace UVC.Factory.Modal.ComponentList
|
||||
/// 검색창의 'X' 버튼 클릭 시 호출됩니다.
|
||||
/// 입력 필드를 비우고, 적용되었던 모든 필터를 제거합니다.
|
||||
/// </summary>
|
||||
public void OnClickClearText()
|
||||
public virtual void OnClickClearText()
|
||||
{
|
||||
if (inputField != null)
|
||||
if (inputField != null && scrollList != null && data != null)
|
||||
{
|
||||
inputField.text = string.Empty; // 입력 필드 초기화
|
||||
inputField.ActivateInputField(); // 입력 필드에 다시 포커스 설정
|
||||
// scrollList에 설정된 필터를 제거(null)하여 모든 항목이 보이도록 합니다.
|
||||
if (scrollList != null) scrollList.SetFilter(null);
|
||||
scrollList.SetFilter(null);
|
||||
scrollList.ClearData(); // 스크롤 리스트의 내용을 비웁니다.
|
||||
scrollList.InsertData(data.ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,7 +350,7 @@ namespace UVC.Factory.Modal.ComponentList
|
||||
/// 마우스 포인터가 이 UI 요소의 영역 안으로 들어왔을 때 호출됩니다.
|
||||
/// UI와 상호작용하는 동안 3D 뷰의 카메라가 움직이지 않도록 컨트롤러를 비활성화합니다.
|
||||
/// </summary>
|
||||
public void OnPointerEnter(PointerEventData eventData)
|
||||
public virtual void OnPointerEnter(PointerEventData eventData)
|
||||
{
|
||||
FactoryCameraController.Instance.Enable = false; // 카메라 컨트롤러 비활성화
|
||||
}
|
||||
@@ -326,7 +359,7 @@ namespace UVC.Factory.Modal.ComponentList
|
||||
/// 마우스 포인터가 이 UI 요소의 영역 밖으로 나갔을 때 호출됩니다.
|
||||
/// 카메라 컨트롤을 다시 활성화하여 3D 뷰를 조작할 수 있도록 합니다.
|
||||
/// </summary>
|
||||
public void OnPointerExit(PointerEventData eventData)
|
||||
public virtual void OnPointerExit(PointerEventData eventData)
|
||||
{
|
||||
FactoryCameraController.Instance.Enable = true; // 카메라 컨트롤러 활성화
|
||||
}
|
||||
@@ -3,12 +3,13 @@ using DG.Tweening;
|
||||
using Gpm.Ui;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UVC.Factory;
|
||||
using UVC.Factory.Component;
|
||||
using UVC.Locale;
|
||||
using UVC.UI.Modal;
|
||||
|
||||
|
||||
namespace UVC.Factory.Modal.ComponentList
|
||||
namespace UVC.UI.List.ComponentList
|
||||
{
|
||||
/// <summary>
|
||||
/// InfiniteScroll에 표시될 개별 아이템을 정의하는 클래스입니다.
|
||||
@@ -20,36 +21,36 @@ namespace UVC.Factory.Modal.ComponentList
|
||||
// [카테고리 UI 요소]
|
||||
[Header("Category UI")]
|
||||
[SerializeField]
|
||||
private GameObject categoryRoot; // 카테리 UI의 최상위 GameObject
|
||||
protected GameObject categoryRoot; // 카테리 UI의 최상위 GameObject
|
||||
|
||||
[SerializeField]
|
||||
private TMPro.TextMeshProUGUI categoryText; // 카테고리 이름을 표시할 텍스트
|
||||
protected TMPro.TextMeshProUGUI categoryText; // 카테고리 이름을 표시할 텍스트
|
||||
|
||||
[SerializeField]
|
||||
private TMPro.TextMeshProUGUI categoryBageText; // 카테고리에 속한 항목 수를 표시할 텍스트
|
||||
protected TMPro.TextMeshProUGUI categoryBageText; // 카테고리에 속한 항목 수를 표시할 텍스트
|
||||
|
||||
[SerializeField]
|
||||
private Button categoryExtendButton; // 카테고리 확장/축소 버튼
|
||||
protected Button categoryExtendButton; // 카테고리 확장/축소 버튼
|
||||
|
||||
// [일반 항목 UI 요소]
|
||||
[Header("General Item UI")]
|
||||
[SerializeField]
|
||||
private GameObject generalRoot; // 일반 항목 UI의 최상위 GameObject
|
||||
protected GameObject generalRoot; // 일반 항목 UI의 최상위 GameObject
|
||||
|
||||
[SerializeField]
|
||||
private TMPro.TMP_InputField generalText; // 일반 항목의 이름을 표시할 입력 필드
|
||||
protected TMPro.TMP_InputField generalText; // 일반 항목의 이름을 표시할 입력 필드
|
||||
|
||||
[SerializeField]
|
||||
private TMPro.TextMeshProUGUI generalOptionText; // 일반 항목의 옵션을 표시할 텍스트
|
||||
protected TMPro.TextMeshProUGUI generalOptionText; // 일반 항목의 옵션을 표시할 텍스트
|
||||
|
||||
[SerializeField]
|
||||
private Button showButton; // 항목을 씬에 표시하는 버튼
|
||||
protected Button showButton; // 항목을 씬에 표시하는 버튼
|
||||
|
||||
[SerializeField]
|
||||
private Button hideButton; // 항목을 씬에서 숨기는 버튼
|
||||
protected Button hideButton; // 항목을 씬에서 숨기는 버튼
|
||||
|
||||
//자식들 보이는 상태인지
|
||||
private bool isExpanded = true;
|
||||
// 애니메이션 중복 실행을 방지하기 위한 플래그
|
||||
protected bool isAnimating = false;
|
||||
|
||||
/// <summary>
|
||||
/// InfiniteScroll에 의해 데이터가 이 아이템에 할당될 때 호출되는 핵심 메서드입니다.
|
||||
@@ -128,18 +129,34 @@ namespace UVC.Factory.Modal.ComponentList
|
||||
/// <summary>
|
||||
/// 카테고리 아이템의 '확장/축소' 버튼을 클릭했을 때 호출됩니다.
|
||||
/// </summary>
|
||||
public void OnExpendButtonClick()
|
||||
public virtual void OnExpendButtonClick()
|
||||
{
|
||||
isExpanded = !isExpanded; // 확장/축소 상태를 토글합니다.
|
||||
// 버튼 아이콘을 0.5초 동안 회전시켜 시각적인 피드백을 줍니다.
|
||||
// 확장 상태(isExpanded=true)이면 0도, 축소 상태(isExpanded=false)이면 90도로 회전합니다.
|
||||
categoryExtendButton.transform.DORotate(new Vector3(0, 0, isExpanded ? 0 : 90), 0.5f);
|
||||
// 애니메이션이 진행 중이면 중복 호출을 방지합니다.
|
||||
if (isAnimating)
|
||||
{
|
||||
return;
|
||||
}
|
||||
isAnimating = true;
|
||||
|
||||
ComponentListItemData itemData = (ComponentListItemData)scrollData;
|
||||
|
||||
// 애니메이션을 위해 현재 각도에서 목표 각도로 회전시킵니다.
|
||||
float targetAngle = itemData.isExpanded ? 90 : 0;
|
||||
categoryExtendButton.transform.DORotate(new Vector3(0, 0, targetAngle), 0.3f)
|
||||
.OnComplete(() =>
|
||||
{
|
||||
// 애니메이션이 완료되면 플래그를 초기화합니다.
|
||||
isAnimating = false;
|
||||
});
|
||||
|
||||
// 데이터에 할당된 Action을 호출하여 상태 변경을 상위 리스트에 알립니다.
|
||||
itemData.OnCategoryExpendAction?.Invoke(itemData);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 카테고리 아이템의 '설정' 버튼을 클릭했을 때 호출됩니다.
|
||||
/// </summary>
|
||||
public async void OnSettingButtonClick()
|
||||
public virtual async void OnSettingButtonClick()
|
||||
{
|
||||
// 1. 이 아이템에 바인딩된 데이터를 가져옵니다.
|
||||
ComponentListItemData itemData = (ComponentListItemData)scrollData;
|
||||
@@ -164,7 +181,7 @@ namespace UVC.Factory.Modal.ComponentList
|
||||
/// <summary>
|
||||
/// 일반 아이템의 '보이기' 버튼을 클릭했을 때 호출됩니다.
|
||||
/// </summary>
|
||||
public void OnShowButtonClick()
|
||||
public virtual void OnShowButtonClick()
|
||||
{
|
||||
ComponentListItemData itemData = (ComponentListItemData)scrollData;
|
||||
if (itemData.factoryObjectInfo != null)
|
||||
@@ -185,7 +202,7 @@ namespace UVC.Factory.Modal.ComponentList
|
||||
/// <summary>
|
||||
/// 일반 아이템의 '숨기기' 버튼을 클릭했을 때 호출됩니다.
|
||||
/// </summary>
|
||||
public void OnHideButtonClick()
|
||||
public virtual void OnHideButtonClick()
|
||||
{
|
||||
ComponentListItemData itemData = (ComponentListItemData)scrollData;
|
||||
if (itemData.factoryObjectInfo != null)
|
||||
@@ -207,7 +224,7 @@ namespace UVC.Factory.Modal.ComponentList
|
||||
/// <summary>
|
||||
/// 일반 아이템의 '찾기/이동' 버튼을 클릭했을 때 호출됩니다.
|
||||
/// </summary>
|
||||
public void OnSearchButtonClick()
|
||||
public virtual void OnSearchButtonClick()
|
||||
{
|
||||
ComponentListItemData itemData = (ComponentListItemData)scrollData;
|
||||
if (itemData.factoryObjectInfo != null)
|
||||
@@ -3,7 +3,7 @@ using Gpm.Ui;
|
||||
using System;
|
||||
using UVC.Factory.Component;
|
||||
|
||||
namespace UVC.Factory.Modal.ComponentList
|
||||
namespace UVC.UI.List.ComponentList
|
||||
{
|
||||
public class ComponentListItemData : InfiniteScrollData
|
||||
{
|
||||
@@ -3,7 +3,6 @@ using System;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using static RTG.GizmoTransform;
|
||||
|
||||
namespace UVC.UI.List.Draggable
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using UnityEngine;
|
||||
using UVC.Factory.Modal.ComponentList;
|
||||
using UVC.UI.List.ComponentList;
|
||||
using UVC.Factory.Playback;
|
||||
using UVC.Locale;
|
||||
using UVC.UI.Commands;
|
||||
|
||||
Reference in New Issue
Block a user