Files
XRLib/Assets/Scripts/Simulator/PropertyWindow/InitailizePopup/InitialInventoryWindow.cs
2026-02-03 11:40:26 +09:00

685 lines
20 KiB
C#

using Simulator.Data;
using System;
using System.Collections.Generic;
using System.Linq;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
public class InitialInventoryWindow : MonoBehaviour
{
[Header("Top/Left")]
[SerializeField] private TMP_Text xLabel;
[SerializeField] private TMP_Text yLabel;
[SerializeField] private TMP_Text zLabel;
[SerializeField] private Transform rackContainer; // ScrollRect Content
[SerializeField] private GridView rackSectionPrefab; // Rack 섹션 프리팹
[Header("Right Controls")]
[SerializeField] private TMP_Dropdown prefabDropdown;
[SerializeField] private TMP_Text countText;
[Header("Table")]
[SerializeField] private Toggle toggleSelectAll;
[SerializeField] private Button btnDeleteSelected;
[SerializeField] private Transform tableContent; // ScrollRect Content
[SerializeField] private InitRow rowPrefab;
[Header("Bottom Buttons")]
[SerializeField] private Button btnApply;
[SerializeField] private Button btnCancel;
[SerializeField] private Button btnReset;
// ===== 대상(ASRS/Rack) 공통 핸들 =====
private Func<List<InitializeEntry>> initGetter;
private Action<List<InitializeEntry>> initSetter;
// 필요 시 외부에서 "지금 어느 타입을 열었나"를 추적하고 싶다면 유지
private ASRSDataClass asrs; // 마지막으로 연 ASRS (선택)
private RackDataClass rack; // 마지막으로 연 Rack (선택) - 실제 클래스명에 맞게 변경
[Serializable]
private struct LayoutInfo
{
public int x, y, z;
public float x_length, y_length, z_length;
}
private enum DimLabelMode
{
Asrs, // 기존 ASRS 표기(스크린샷 기준 y/z 매핑)
Rack // 일반 표기(x,y,z 그대로)
}
private LayoutInfo currentLayout;
private DimLabelMode dimLabelMode = DimLabelMode.Asrs;
// rack views
private readonly List<GridView> rackViews = new();
// ====== Click + Hover selection state ======
private readonly Dictionary<int, Vector2Int> anchors = new();
private readonly Dictionary<int, AsrsRect> previewRects = new();
// ====== Entry / Occupancy ======
private sealed class Entry
{
public int id;
public string prefabId;
public string prefabDisplayName;
public int count;
public Vector3Int from; // inclusive
public Vector3Int to; // inclusive
}
private readonly List<Entry> entries = new();
private readonly Dictionary<(int rack, int x, int y), int> occupancy = new(); // (rack,x,y) -> entryId
private int nextEntryId = 1;
private int? selectedEntryId = null;
// prefab dropdown cache
private List<PrefabCatalogFromManager.Item> prefabItems = new();
private int count = 1;
// 외부 버튼으로 모드 토글 가능
public bool IsSelectionMode { get; private set; } = true;
// =====================================================================
// Public API
// =====================================================================
// 기존 호출부 호환: Open(ASRSDataClass) 유지 (원하시면 제거 가능)
public void Open(ASRSDataClass asrs) => OpenAsrs(asrs);
void SaveChange(object source, object value, string name)
{
var path = PathIndexer.GetNodePath(source);
Patch updateData = new Patch();
updateData.value = value;
UpdateValueStack.AddPatch($"{path}.{name}", value);
}
public void OpenAsrs(ASRSDataClass asrs)
{
if (asrs == null) return;
this.asrs = asrs;
this.rack = null;
var l = asrs.asrs_layout;
var layout = new LayoutInfo
{
x = l.x,
y = l.y,
z = Mathf.Clamp(l.z, 1, 2),
x_length = l.x_length,
y_length = l.y_length,
z_length = l.z_length
};
OpenInternal(
layout,
getter: () => asrs.initialize,
setter: list => asrs.initialize = list,
mode: DimLabelMode.Asrs
);
}
public void OpenRack(RackDataClass rack,string path) // 실제 클래스명에 맞게 변경
{
if (rack == null) return;
this.rack = rack;
this.asrs = null;
var l = rack.rack_layout; // 실제 필드명에 맞게 변경
var layout = new LayoutInfo
{
x = l.x,
y = l.y,
z = Mathf.Clamp(l.z, 1, 2),
x_length = l.x_length,
y_length = l.y_length,
z_length = l.z_length
};
// initialize가 set 가능하면 아래 그대로
OpenInternal(
layout,
getter: () => rack.initialize,
setter: list => { rack.initialize = list; SaveChange(rack,list, "initialize"); },
mode: DimLabelMode.Rack
);
// 만약 rack.initialize가 set 불가(읽기전용 리스트)라면, 위 setter 대신 아래로 바꾸세요.
/*
OpenInternal(
layout,
getter: () => rack.initialize,
setter: list =>
{
rack.initialize.Clear();
rack.initialize.AddRange(list);
},
mode: DimLabelMode.Rack
);
*/
}
private void OpenInternal(
LayoutInfo layout,
Func<List<InitializeEntry>> getter,
Action<List<InitializeEntry>> setter,
DimLabelMode mode
)
{
gameObject.SetActive(true);
currentLayout = layout;
initGetter = getter;
initSetter = setter;
dimLabelMode = mode;
// UI Build
BuildDimLabel(layout, mode);
BuildPrefabDropdown();
BuildRacks(layout);
// 기존 initialize 로딩
LoadInitialize(getter?.Invoke());
HookButtons();
RefreshAll();
}
public void Close()
{
gameObject.SetActive(false);
}
public void SetSelectionMode(bool on)
{
IsSelectionMode = on;
if (!on)
{
anchors.Clear();
previewRects.Clear();
RefreshSelectionVisuals();
}
}
// =====================================================================
// Build UI
// =====================================================================
private void BuildDimLabel(LayoutInfo l, DimLabelMode mode)
{
float xM, yM, zM;
if (mode == DimLabelMode.Asrs)
{
// 기존 ASRS 표기 유지:
// x => x*x_length
// y => z*z_length
// z => y*y_length
xM = l.x * l.x_length;
yM = l.z * l.z_length;
zM = l.y * l.y_length;
}
else
{
// Rack은 일반 표기(원하시면 Asrs처럼 바꿔도 됩니다)
xM = l.x * l.x_length;
yM = l.y * l.y_length;
zM = l.z * l.z_length;
}
if (xLabel != null) xLabel.text = $"{xM:0.##} m (x)";
if (yLabel != null) yLabel.text = $"{yM:0.##} m (y)";
if (zLabel != null) zLabel.text = $"{zM:0.##} m (z)";
}
private void BuildPrefabDropdown()
{
if (prefabDropdown == null) return;
prefabItems = PrefabCatalogFromManager.GetSortedItems();
prefabDropdown.ClearOptions();
prefabDropdown.AddOptions(prefabItems.Select(i => i.DisplayName).ToList());
prefabDropdown.value = 0;
prefabDropdown.RefreshShownValue();
SetCount(count);
}
private void BuildRacks(LayoutInfo l)
{
foreach (Transform child in rackContainer)
Destroy(child.gameObject);
rackViews.Clear();
int rackCount = Mathf.Clamp(l.z, 1, 2);
for (int rackIndex = 1; rackIndex <= rackCount; rackIndex++)
{
var view = Instantiate(rackSectionPrefab, rackContainer);
view.Build(this, rackIndex, l.x, l.y);
rackViews.Add(view);
}
}
private void HookButtons()
{
if (btnApply != null)
{
btnApply.onClick.RemoveAllListeners();
btnApply.onClick.AddListener(ApplyToTargetInitialize);
}
if (btnCancel != null)
{
btnCancel.onClick.RemoveAllListeners();
btnCancel.onClick.AddListener(Close);
}
if (btnReset != null)
{
btnReset.onClick.RemoveAllListeners();
btnReset.onClick.AddListener(() =>
{
entries.Clear();
occupancy.Clear();
anchors.Clear();
previewRects.Clear();
selectedEntryId = null;
nextEntryId = 1;
RefreshAll();
});
}
if (toggleSelectAll != null)
{
toggleSelectAll.onValueChanged.RemoveAllListeners();
toggleSelectAll.onValueChanged.AddListener(OnToggleSelectAllChanged);
}
if (btnDeleteSelected != null)
{
btnDeleteSelected.onClick.RemoveAllListeners();
btnDeleteSelected.onClick.AddListener(DeleteSelectedRows);
}
}
private void SetCount(int newCount)
{
count = Mathf.Clamp(newCount, 1, 999999);
if (countText != null) countText.text = count.ToString();
}
private string GetSelectedPrefabId()
{
if (prefabItems == null || prefabItems.Count == 0) return null;
int idx = prefabDropdown != null ? prefabDropdown.value : 0;
idx = Mathf.Clamp(idx, 0, prefabItems.Count - 1);
return prefabItems[idx].Id;
}
private string ResolvePrefabDisplayName(string prefabId)
=> PrefabCatalogFromManager.ResolveDisplayName(prefabId);
// =====================================================================
// Table: select all / delete selected
// =====================================================================
private void OnToggleSelectAllChanged(bool on)
{
var rows = tableContent.GetComponentsInChildren<InitRow>(true);
foreach (var r in rows)
r.SetSelected(on);
RefreshDeleteSelectedButtonState();
}
private void DeleteSelectedRows()
{
var rows = tableContent.GetComponentsInChildren<InitRow>(true);
var ids = rows.Where(r => r.IsSelected).Select(r => r.EntryId).ToHashSet();
if (ids.Count == 0) return;
for (int i = entries.Count - 1; i >= 0; i--)
{
if (!ids.Contains(entries[i].id)) continue;
var e = entries[i];
MarkOccupancy(e.id, e.from, e.to, false);
entries.RemoveAt(i);
}
selectedEntryId = null;
if (toggleSelectAll != null) toggleSelectAll.SetIsOnWithoutNotify(false);
RefreshAll();
}
private void RefreshDeleteSelectedButtonState()
{
if (btnDeleteSelected == null || tableContent == null) return;
var rows = tableContent.GetComponentsInChildren<InitRow>(true);
bool anySelected = rows.Any(r => r.IsSelected);
btnDeleteSelected.interactable = anySelected;
if (toggleSelectAll != null)
{
bool allOff = rows.Length > 0 && rows.All(r => !r.IsSelected);
bool allOn = rows.Length > 0 && rows.All(r => r.IsSelected);
if (allOff) toggleSelectAll.SetIsOnWithoutNotify(false);
else if (allOn) toggleSelectAll.SetIsOnWithoutNotify(true);
}
}
// =====================================================================
// initialize load
// =====================================================================
private void LoadInitialize(List<InitializeEntry> initList)
{
entries.Clear();
occupancy.Clear();
selectedEntryId = null;
nextEntryId = 1;
anchors.Clear();
previewRects.Clear();
if (initList == null) return;
foreach (var dto in initList)
{
if (dto == null) continue;
var fromV = AsrsPositionUtil.ToGridInt(dto.from_position);
var toV = AsrsPositionUtil.ToGridInt(dto.to_position);
(fromV, toV) = AsrsPositionUtil.Normalize(fromV, toV);
if (!CanPlace(fromV, toV))
continue;
var entry = new Entry
{
id = nextEntryId++,
prefabId = dto.prefab,
prefabDisplayName = ResolvePrefabDisplayName(dto.prefab),
count = dto.count,
from = fromV,
to = toV
};
entries.Add(entry);
MarkOccupancy(entry.id, entry.from, entry.to, true);
}
}
// =====================================================================
// GridView -> Window 콜백 (Click + Hover)
// =====================================================================
public void OnCellHover(int rackIndex, int x, int y)
{
if (!IsSelectionMode) return;
if (!anchors.TryGetValue(rackIndex, out var anchor)) return;
previewRects[rackIndex] = AsrsRect.FromTwoPoints(anchor, new Vector2Int(x, y));
RefreshSelectionVisuals();
}
public void OnCellClick(int rackIndex, int x, int y)
{
if (!IsSelectionMode) return;
// 앵커를 아직 안 잡은 상태에서, 이미 설정된 셀 클릭 시 -> 그 Entry 전체 삭제
if (!anchors.ContainsKey(rackIndex) && occupancy.TryGetValue((rackIndex, x, y), out int entryId))
{
DeleteEntryById(entryId);
anchors.Remove(rackIndex);
previewRects.Remove(rackIndex);
RefreshAll();
return;
}
// 1차 클릭: anchor 설정 + 1셀 미리보기 시작
if (!anchors.ContainsKey(rackIndex))
{
anchors[rackIndex] = new Vector2Int(x, y);
previewRects[rackIndex] = AsrsRect.FromTwoPoints(anchors[rackIndex], anchors[rackIndex]);
selectedEntryId = null;
RefreshSelectionVisuals();
RefreshTableSelection();
return;
}
// 2차 클릭: anchor ~ 현재까지 확정
var rect = AsrsRect.FromTwoPoints(anchors[rackIndex], new Vector2Int(x, y));
bool committed = CommitRectToEntries(rackIndex, rect);
if (committed)
{
anchors.Remove(rackIndex);
previewRects.Remove(rackIndex);
RefreshAll();
}
else
{
previewRects[rackIndex] = rect;
RefreshSelectionVisuals();
}
}
private bool CommitRectToEntries(int rackIndex, AsrsRect rect)
{
string prefabId = GetSelectedPrefabId();
if (string.IsNullOrWhiteSpace(prefabId))
return false;
var from = new Vector3Int(rect.MinX, rect.MinY, rackIndex);
var to = new Vector3Int(rect.MaxX, rect.MaxY, rackIndex);
(from, to) = AsrsPositionUtil.Normalize(from, to);
if (!CanPlace(from, to))
return false;
var entry = new Entry
{
id = nextEntryId++,
prefabId = prefabId,
prefabDisplayName = ResolvePrefabDisplayName(prefabId),
count = count,
from = from,
to = to
};
entries.Add(entry);
MarkOccupancy(entry.id, entry.from, entry.to, true);
return true;
}
private bool CanPlace(Vector3Int from, Vector3Int to)
{
(from, to) = AsrsPositionUtil.Normalize(from, to);
int minRack = Mathf.Min(from.z, to.z);
int maxRack = Mathf.Max(from.z, to.z);
for (int rack = minRack; rack <= maxRack; rack++)
{
for (int y = from.y; y <= to.y; y++)
{
for (int x = from.x; x <= to.x; x++)
{
if (occupancy.ContainsKey((rack, x, y)))
return false;
}
}
}
return true;
}
private void MarkOccupancy(int entryId, Vector3Int from, Vector3Int to, bool occupy)
{
(from, to) = AsrsPositionUtil.Normalize(from, to);
int minRack = Mathf.Min(from.z, to.z);
int maxRack = Mathf.Max(from.z, to.z);
for (int rack = minRack; rack <= maxRack; rack++)
{
for (int y = from.y; y <= to.y; y++)
{
for (int x = from.x; x <= to.x; x++)
{
var key = (rack, x, y);
if (occupy) occupancy[key] = entryId;
else occupancy.Remove(key);
}
}
}
}
// =====================================================================
// Apply: 현재 변경사항을 "현재 대상" initialize에 반영
// =====================================================================
private void ApplyToTargetInitialize()
{
if (initSetter == null) return;
initSetter.Invoke(BuildInitializeEntries());
Close();
}
private List<InitializeEntry> BuildInitializeEntries()
{
return entries.Select(e => new InitializeEntry
{
count = e.count,
prefab = e.prefabId,
from_position = AsrsPositionUtil.ToPosition(e.from),
to_position = AsrsPositionUtil.ToPosition(e.to)
}).ToList();
}
// =====================================================================
// UI Refresh
// =====================================================================
private void RefreshAll()
{
RefreshSelectionVisuals();
RefreshOccupiedVisuals();
RefreshTable();
RefreshTableSelection();
RefreshDeleteSelectedButtonState();
}
private void RefreshSelectionVisuals()
{
foreach (var view in rackViews)
{
if (previewRects.TryGetValue(view.RackIndex, out var rect))
view.SetSelectionVisual(rect);
else
view.ClearSelectionVisual();
}
}
private void RefreshOccupiedVisuals()
{
// (rack,x,y) -> sprite
var map = new Dictionary<(int rack, int x, int y), Sprite>();
foreach (var kv in occupancy)
{
var key = kv.Key; // (rack,x,y)
int entryId = kv.Value;
var entry = entries.FirstOrDefault(e => e.id == entryId);
if (entry == null) continue;
// prefabId -> shape
string shape = PrefabManager.Instance.GetPrefab(entry.prefabId).shape;
// shape -> sprite (SingletonScene 기반 Catalog 사용)
Sprite sprite = ShapeIconCatalog.Instance != null
? ShapeIconCatalog.Instance.GetIcon(shape)
: null;
map[key] = sprite;
}
foreach (var view in rackViews)
view.SetOccupiedCells(map);
}
private void RefreshTable()
{
if (tableContent == null || rowPrefab == null) return;
foreach (Transform child in tableContent)
Destroy(child.gameObject);
foreach (var e in entries)
{
var row = Instantiate(rowPrefab, tableContent);
// InitRow가 onSelectionChanged를 받는 버전이어야 합니다.
row.Bind(
e.id,
e.prefabDisplayName,
e.count,
e.from,
e.to,
onClickRow: OnClickRow,
onSelectionChanged: _ => RefreshDeleteSelectedButtonState()
);
if (selectedEntryId.HasValue && selectedEntryId.Value == e.id)
row.SetSelected(true);
}
}
private void OnClickRow(int entryId)
{
selectedEntryId = entryId;
RefreshTableSelection();
}
private void RefreshTableSelection()
{
if (tableContent == null) return;
var rows = tableContent.GetComponentsInChildren<InitRow>(true);
foreach (var r in rows)
r.SetSelected(selectedEntryId.HasValue && r.EntryId == selectedEntryId.Value);
}
private void DeleteEntryById(int entryId)
{
var entry = entries.FirstOrDefault(e => e.id == entryId);
if (entry == null) return;
MarkOccupancy(entry.id, entry.from, entry.to, false);
entries.Remove(entry);
if (selectedEntryId.HasValue && selectedEntryId.Value == entryId)
selectedEntryId = null;
}
}