Files
test/Assets/PerfectWorld/Scripts/Chat/Editor/EmotionAtlasConverterWindow.cs
2026-04-09 16:33:52 +07:00

338 lines
15 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections.Generic;
using BrewMonster.Scripts.Chat.EmotionData;
using TMPro;
using UnityEditor;
using UnityEngine;
namespace BrewMonster.Scripts.Editor
{
/// <summary>
/// Tool: atlas + txt → Sprite Mode Multiple (một PNG/bộ) + SO; batch linh hoạt số slot.
/// </summary>
public class EmotionAtlasConverterWindow : EditorWindow
{
private Texture2D _sourceAtlas;
private TextAsset _txtAsset;
private int _emotionSetIndex;
private int _cellW = 32;
private int _cellH = 32;
private string _outputFolder = "Assets/PerfectWorld/UI/Chat/GeneratedEmotions";
private bool _sliceInPlace;
private List<EmotionBatchSlot> _batchSlots = new List<EmotionBatchSlot>();
private Vector2 _batchScroll;
private string _libraryAssetPath = "Assets/PerfectWorld/UI/Chat/GeneratedEmotions/EmotionLibrary.asset";
// ── TMP Sprite Asset ──────────────────────────────────────────────────
private bool _genTmpOnConvert = true;
private string _tmpSpriteFolder = "Assets/TextMesh Pro/Resources/Sprite Assets";
private float _tmpCellSizePt = 0f; // 0 = lấy từ cellH
private bool _showTmpSection = true;
// Snapshot cuối cùng được convert (cho nút gen TMP standalone)
// Last converted snapshot (for standalone TMP gen button)
private EmotionSetSnapshot _lastSnapshot;
[MenuItem("Tools/Perfect World/ChatSystem/Emotion Atlas Converter…")]
public static void Open()
{
GetWindow<EmotionAtlasConverterWindow>(true, "Emotion Atlas Converter", true);
}
private void OnEnable()
{
if (_batchSlots == null)
_batchSlots = new List<EmotionBatchSlot>();
if (_batchSlots.Count == 0)
{
for (int i = 0; i < 8; i++)
_batchSlots.Add(new EmotionBatchSlot { EmotionSetIndex = i });
}
}
private void OnGUI()
{
EditorGUILayout.LabelField("Chung / Shared", EditorStyles.boldLabel);
_cellW = EditorGUILayout.IntField("Cell width (W)", _cellW);
_cellH = EditorGUILayout.IntField("Cell height (H)", _cellH);
_outputFolder = EditorGUILayout.TextField("Output folder", _outputFolder);
_sliceInPlace = EditorGUILayout.ToggleLeft(
"Slice tại asset nguồn (ghi đè import Multiple lên atlas đang chọn) — Slice in-place on source asset",
_sliceInPlace);
if (_sliceInPlace)
{
EditorGUILayout.HelpBox(
"Cảnh báo: Unity sẽ đổi import của file atlas gốc thành Sprite Multiple full lưới.\n" +
"Warning: overwrites source texture import settings.",
MessageType.Warning);
}
else
{
EditorGUILayout.HelpBox(
"Mặc định: copy atlas vào Output/Emotions{N}/Emotions{N}_atlas.png rồi Multiple slice (giữ nguyên file nguồn).\n" +
"Default: copy atlas to output folder then slice (source unchanged).",
MessageType.None);
}
EditorGUILayout.Space(8f);
EditorGUILayout.LabelField("Một bộ / Single set", EditorStyles.boldLabel);
_sourceAtlas = (Texture2D)EditorGUILayout.ObjectField("Atlas", _sourceAtlas, typeof(Texture2D), false);
_txtAsset = (TextAsset)EditorGUILayout.ObjectField("EmotionsN.txt", _txtAsset, typeof(TextAsset), false);
_emotionSetIndex = EditorGUILayout.IntField("Emotion set index (N)", _emotionSetIndex);
EditorGUILayout.Space();
if (GUILayout.Button("Convert → EmotionSetData + Atlas (Multiple)"))
{
RunConvertSingle();
}
EditorGUILayout.Space(12f);
EditorGUILayout.LabelField("Batch → EmotionLibrary", EditorStyles.boldLabel);
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("+ Thêm slot", GUILayout.Width(100)))
_batchSlots.Add(new EmotionBatchSlot { EmotionSetIndex = NextSuggestedSetIndex() });
if (GUILayout.Button(" Xóa slot cuối", GUILayout.Width(120)) && _batchSlots.Count > 0)
_batchSlots.RemoveAt(_batchSlots.Count - 1);
EditorGUILayout.LabelField($"Số slot: {_batchSlots.Count}");
EditorGUILayout.EndHorizontal();
_batchScroll = EditorGUILayout.BeginScrollView(_batchScroll, GUILayout.MinHeight(160f));
for (int i = 0; i < _batchSlots.Count; i++)
{
var slot = _batchSlots[i];
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField($"#{i}", GUILayout.Width(24));
slot.EmotionSetIndex = EditorGUILayout.IntField("Set N", slot.EmotionSetIndex, GUILayout.Width(120));
slot.Atlas = (Texture2D)EditorGUILayout.ObjectField(slot.Atlas, typeof(Texture2D), false);
slot.Txt = (TextAsset)EditorGUILayout.ObjectField(slot.Txt, typeof(TextAsset), false);
if (GUILayout.Button("×", GUILayout.Width(22)))
{
_batchSlots.RemoveAt(i);
i--;
EditorGUILayout.EndHorizontal();
EditorGUILayout.EndVertical();
continue;
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.EndVertical();
}
EditorGUILayout.EndScrollView();
_libraryAssetPath = EditorGUILayout.TextField("Library .asset path", _libraryAssetPath);
EditorGUILayout.Space();
if (GUILayout.Button("Convert batch → EmotionLibrary.asset"))
{
RunConvertLibrary();
}
EditorGUILayout.Space(12f);
DrawTMPSection();
EditorGUILayout.Space(8f);
EditorGUILayout.HelpBox(
"Mỗi bộ: một texture **Sprite Multiple**, tên sub-sprite `cell_0000` … theo chỉ số ô (giống C++).\n" +
"Each set: one **Sprite Multiple** texture; sub-sprite names `cell_0000` … by cell index.",
MessageType.Info);
}
// ─────────────────────────────────────────────────────────────────────
// TMP Sprite Asset section
// ─────────────────────────────────────────────────────────────────────
private void DrawTMPSection()
{
_showTmpSection = EditorGUILayout.Foldout(_showTmpSection, "TMP Sprite Asset", true, EditorStyles.foldoutHeader);
if (!_showTmpSection) return;
EditorGUI.indentLevel++;
_tmpSpriteFolder = EditorGUILayout.TextField(
new GUIContent("Sprite Assets folder",
"Thư mục lưu TMP_SpriteAsset (.asset). Mặc định: Assets/TextMesh Pro/Resources/Sprite Assets\n" +
"Folder for TMP_SpriteAsset files. Default: Assets/TextMesh Pro/Resources/Sprite Assets"),
_tmpSpriteFolder);
_tmpCellSizePt = EditorGUILayout.FloatField(
new GUIContent("Cell size (pt)",
"Chiều cao glyph theo đơn vị TMP point. 0 = dùng Cell height (pixel).\n" +
"Glyph height in TMP points. 0 = use Cell height in pixels."),
_tmpCellSizePt);
_genTmpOnConvert = EditorGUILayout.ToggleLeft(
"Tự động gen TMP Sprite Asset sau mỗi lần Convert — Auto-generate TMP Sprite Asset after Convert",
_genTmpOnConvert);
EditorGUILayout.HelpBox(
"Output: <Sprite Assets folder>/Emotions {N}.asset + Emotions {N} Material.mat\n" +
"Tên sub-sprite khớp với atlas: cell_0000 … (dùng cho <sprite name=\"cell_XXXX\"> trong TMP).\n" +
"Sub-sprite names match atlas: cell_0000 … (use <sprite name=\"cell_XXXX\"> in TMP).",
MessageType.Info);
EditorGUILayout.Space(4f);
// Nút gen standalone từ snapshot cuối — Standalone gen button from last snapshot
EditorGUI.BeginDisabledGroup(_lastSnapshot == null);
if (GUILayout.Button("Gen TMP Sprite Asset từ lần Convert cuối — from last Convert"))
RunGenTMPSingle(_lastSnapshot);
EditorGUI.EndDisabledGroup();
if (_lastSnapshot == null)
EditorGUILayout.HelpBox("Chạy Convert single ít nhất một lần để kích hoạt nút này.\nRun Convert (single) once to enable this button.", MessageType.None);
EditorGUI.indentLevel--;
}
private int NextSuggestedSetIndex()
{
int max = -1;
foreach (var s in _batchSlots)
{
if (s.EmotionSetIndex > max)
max = s.EmotionSetIndex;
}
return max + 1;
}
private void RunConvertSingle()
{
if (!EmotionAtlasConverterCore.ConvertOneSet(
_sourceAtlas, _txtAsset, _emotionSetIndex, _cellW, _cellH, _outputFolder, _sliceInPlace,
out var snapshot, out string err))
{
EditorUtility.DisplayDialog("Error", err, "OK");
return;
}
_lastSnapshot = snapshot;
var so = ScriptableObject.CreateInstance<EmotionSetDataSO>();
EmotionAtlasConverterCore.ApplySnapshotToSetSO(so, snapshot);
string setFolder = $"{_outputFolder}/Emotions{_emotionSetIndex}";
string soPath = $"{setFolder}/EmotionSetData_{_emotionSetIndex}.asset";
AssetDatabase.CreateAsset(so, soPath);
AssetDatabase.SaveAssets();
string msg = $"Đã tạo:\n{soPath}\nAtlas (Multiple) trong thư mục set.";
if (_genTmpOnConvert)
{
TMP_SpriteAsset tmpAsset = RunGenTMPSingle(snapshot);
if (tmpAsset != null)
msg += $"\nTMP Sprite Asset: {AssetDatabase.GetAssetPath(tmpAsset)}";
}
EditorUtility.DisplayDialog("Done", msg, "OK");
EditorGUIUtility.PingObject(so);
}
/// <summary>
/// Gen TMP_SpriteAsset cho một snapshot. Trả về asset nếu thành công, null nếu thất bại.
/// Generate TMP_SpriteAsset for one snapshot. Returns asset on success, null on failure.
/// </summary>
private TMP_SpriteAsset RunGenTMPSingle(EmotionSetSnapshot snapshot)
{
if (snapshot == null) return null;
string outPath = EmotionAtlasConverterCore.DefaultTMPSpriteAssetPath(_tmpSpriteFolder, snapshot.EmotionSetIndex);
if (!EmotionAtlasConverterCore.CreateTMPSpriteAsset(snapshot, outPath, _tmpCellSizePt, out var tmpAsset, out string err))
{
EditorUtility.DisplayDialog("TMP Gen Error", $"Set {snapshot.EmotionSetIndex}: {err}", "OK");
return null;
}
EditorGUIUtility.PingObject(tmpAsset);
return tmpAsset;
}
private void RunConvertLibrary()
{
if (!EmotionAtlasConverterCore.TryValidateBatchIndices(_batchSlots, out string dupErr))
{
EditorUtility.DisplayDialog("Error", dupErr, "OK");
return;
}
var library = ScriptableObject.CreateInstance<EmotionLibrarySO>();
library.Sets.Clear();
int ok = 0;
int total = _batchSlots.Count;
for (int i = 0; i < total; i++)
{
var slot = _batchSlots[i];
if (slot.Atlas == null && slot.Txt == null)
continue;
if (slot.Atlas == null || slot.Txt == null)
{
EditorUtility.DisplayDialog("Error",
$"Slot #{i} (Set N={slot.EmotionSetIndex}): cần cả Atlas và TXT.",
"OK");
Object.DestroyImmediate(library);
return;
}
EditorUtility.DisplayProgressBar("Emotion Library", $"Set {slot.EmotionSetIndex}…", (float)i / Mathf.Max(1, total));
if (!EmotionAtlasConverterCore.ConvertOneSet(
slot.Atlas, slot.Txt, slot.EmotionSetIndex, _cellW, _cellH, _outputFolder, _sliceInPlace,
out var snapshot, out string err))
{
EditorUtility.ClearProgressBar();
EditorUtility.DisplayDialog("Error", $"Set {slot.EmotionSetIndex}: {err}", "OK");
Object.DestroyImmediate(library);
return;
}
library.Sets.Add(snapshot);
ok++;
}
EditorUtility.ClearProgressBar();
if (ok == 0)
{
EditorUtility.DisplayDialog("Error", "Không có cặp Atlas+TXT hợp lệ.", "OK");
Object.DestroyImmediate(library);
return;
}
string dir = System.IO.Path.GetDirectoryName(_libraryAssetPath)?.Replace('\\', '/') ?? _outputFolder;
if (!string.IsNullOrEmpty(dir))
EmotionAtlasConverterCore.EnsureFolder(dir);
AssetDatabase.CreateAsset(library, _libraryAssetPath);
AssetDatabase.SaveAssets();
int tmpOk = 0;
if (_genTmpOnConvert)
{
for (int i = 0; i < library.Sets.Count; i++)
{
var snap = library.Sets[i];
EditorUtility.DisplayProgressBar("TMP Sprite Assets", $"Set {snap.EmotionSetIndex}…", (float)i / Mathf.Max(1, library.Sets.Count));
string outPath = EmotionAtlasConverterCore.DefaultTMPSpriteAssetPath(_tmpSpriteFolder, snap.EmotionSetIndex);
if (EmotionAtlasConverterCore.CreateTMPSpriteAsset(snap, outPath, _tmpCellSizePt, out _, out string tmpErr))
tmpOk++;
else
Debug.LogWarning($"[EmotionConverter] TMP gen Set {snap.EmotionSetIndex}: {tmpErr}");
}
EditorUtility.ClearProgressBar();
}
string doneMsg = $"EmotionLibrary: {ok} bộ.\n{_libraryAssetPath}";
if (_genTmpOnConvert)
doneMsg += $"\nTMP Sprite Assets: {tmpOk}/{ok} bộ → {_tmpSpriteFolder}";
EditorUtility.DisplayDialog("Done", doneMsg, "OK");
EditorGUIUtility.PingObject(library);
}
}
}