685 lines
20 KiB
C#
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;
|
|
}
|
|
} |