472 lines
14 KiB
C#
472 lines
14 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 dimLabel;
|
||
[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 Transform tableContent; // ScrollRect Content
|
||
[SerializeField] private InitRow rowPrefab;
|
||
|
||
[Header("Bottom Buttons")]
|
||
[SerializeField] private Button btnApply; // Apply 버튼 (요구사항 추가)
|
||
[SerializeField] private Button btnCancel; // 선택: 닫기
|
||
[SerializeField] private Button btnReset; // 선택: 초기화
|
||
|
||
private ASRSDataClass 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 =====
|
||
public void Open(ASRSDataClass asrs)
|
||
{
|
||
gameObject.SetActive(true);
|
||
this.asrs = asrs;
|
||
|
||
BuildDimLabel();
|
||
BuildPrefabDropdown();
|
||
BuildRacks();
|
||
|
||
// 기존 initialize 로딩
|
||
LoadInitialize(asrs.initialize);
|
||
|
||
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()
|
||
{
|
||
if (dimLabel == null || asrs?.asrs_layout == null) return;
|
||
|
||
var l = asrs.asrs_layout;
|
||
|
||
float xM = l.x * l.x_length;
|
||
float yM = l.z * l.z_length;
|
||
float zM = l.y * l.y_length;
|
||
|
||
dimLabel.text = $"{xM:0.##} m (x) × {yM:0.##} m (y) × {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()
|
||
{
|
||
foreach (Transform child in rackContainer)
|
||
Destroy(child.gameObject);
|
||
rackViews.Clear();
|
||
|
||
var l = asrs.asrs_layout;
|
||
int rackCount = Mathf.Clamp(l.z, 1, 2);
|
||
|
||
for (int rack = 1; rack <= rackCount; rack++)
|
||
{
|
||
var view = Instantiate(rackSectionPrefab, rackContainer);
|
||
view.Build(this, rack, l.x, l.y);
|
||
rackViews.Add(view);
|
||
}
|
||
}
|
||
|
||
private void HookButtons()
|
||
{
|
||
if (btnApply != null)
|
||
{
|
||
btnApply.onClick.RemoveAllListeners();
|
||
btnApply.onClick.AddListener(ApplyToAsrsInitialize);
|
||
}
|
||
|
||
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();
|
||
});
|
||
}
|
||
}
|
||
|
||
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);
|
||
|
||
// ===== 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) 삭제"
|
||
// 사용 예: 1,3,1~4,4,1로 설정된 상태에서 3,4,1 클릭 -> 해당 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: 현재 변경사항을 asrs.initialize에 반영 =====
|
||
|
||
private void ApplyToAsrsInitialize()
|
||
{
|
||
if (asrs == null) return;
|
||
|
||
// 현재 엔트리 -> InitializeEntry 리스트로 변환
|
||
asrs.initialize = BuildInitializeEntries();
|
||
|
||
// 필요하면 여기서 닫거나, “저장됨” 토스트 등을 띄우면 됩니다.
|
||
// Close();
|
||
}
|
||
|
||
private List<InitializeEntry> BuildInitializeEntries()
|
||
{
|
||
// 중요: InitializeEntry의 from/to는 Position 타입이므로 변환 필요
|
||
// 사용자님 기존 코드 방식 그대로 사용
|
||
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();
|
||
}
|
||
|
||
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()
|
||
{
|
||
var occupiedSet = occupancy.Keys.Select(k => (k.rack, k.x, k.y)).ToHashSet();
|
||
foreach (var view in rackViews)
|
||
view.SetOccupiedCells(occupiedSet);
|
||
}
|
||
|
||
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.Bind 시그니처는 사용자님 프로젝트 기준
|
||
row.Bind(
|
||
e.id,
|
||
e.prefabDisplayName,
|
||
e.count,
|
||
e.from,
|
||
e.to,
|
||
onClickRow: OnClickRow
|
||
);
|
||
|
||
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)
|
||
{
|
||
// entries에서 엔트리 찾기
|
||
var entry = entries.FirstOrDefault(e => e.id == entryId);
|
||
if (entry == null) return;
|
||
|
||
// occupancy 해제 (범위 전체)
|
||
MarkOccupancy(entry.id, entry.from, entry.to, false);
|
||
|
||
// entries 제거
|
||
entries.Remove(entry);
|
||
|
||
// 테이블 선택 상태도 정리
|
||
if (selectedEntryId.HasValue && selectedEntryId.Value == entryId)
|
||
selectedEntryId = null;
|
||
}
|
||
} |