237 lines
10 KiB
C#
237 lines
10 KiB
C#
using System.Collections.Generic;
|
||
using System.IO;
|
||
using System.Text;
|
||
using UnityEditor;
|
||
using UnityEngine;
|
||
|
||
namespace TKS.EditorTools
|
||
{
|
||
/// <summary>
|
||
/// UI 테마 임포트(PD #1100) — Assets/Data/UI/ui_theme.csv(SOT) → Assets/Resources/UI/UITheme.asset.
|
||
/// 경로 = 팩 Components 기준 상대 경로(Assets/ 로 시작하면 그대로) · 오류가 있으면 자산을 바꾸지 않는다.
|
||
/// 신규 제작 납품(Assets/UI/Samguk/**/<id>.png)은 같은 id 의 스킨·아이콘을 치환하고 표에 없는 id 는 아이콘으로 추가한다(코드 변경 없이 교체).
|
||
/// </summary>
|
||
public static class UIThemeImporter
|
||
{
|
||
public const string CsvPath = "Assets/Data/UI/ui_theme.csv";
|
||
public const string AssetPath = "Assets/Resources/UI/UITheme.asset";
|
||
public const string PackRoot = "Assets/UI/GUIPro/ResourcesData/Sprites/Components/";
|
||
public const string SamgukRoot = "Assets/UI/Samguk";
|
||
|
||
[MenuItem("TKS/Import UI Theme (UI 테마 임포트)")]
|
||
public static void Menu()
|
||
{
|
||
Import();
|
||
}
|
||
|
||
public static bool Import()
|
||
{
|
||
var errors = new List<string>();
|
||
var theme = ScriptableObject.CreateInstance<UITheme>();
|
||
if (!File.Exists(CsvPath))
|
||
{
|
||
Debug.LogError("[TKS] UI 테마 표 없음: " + CsvPath);
|
||
return false;
|
||
}
|
||
var lines = File.ReadAllText(CsvPath, Encoding.UTF8).TrimStart('').Replace("\r", "").Split('\n');
|
||
for (int n = 1; n < lines.Length; n++)
|
||
{
|
||
var line = lines[n];
|
||
if (string.IsNullOrWhiteSpace(line)) continue;
|
||
var f = line.Split(',');
|
||
if (f.Length < 8)
|
||
{
|
||
errors.Add((n + 1) + "행: 열 부족");
|
||
continue;
|
||
}
|
||
string kind = f[0].Trim(), id = f[1].Trim(), path = f[2].Trim(), overlay = f[3].Trim(), color = f[7].Trim();
|
||
switch (kind)
|
||
{
|
||
case "color":
|
||
{
|
||
Color c;
|
||
if (!ColorUtility.TryParseHtmlString(color, out c)) errors.Add((n + 1) + "행 " + id + ": 색 형식 " + color);
|
||
else theme.colors.Add(new UITheme.ColorEntry { id = id, color = c });
|
||
break;
|
||
}
|
||
case "skin":
|
||
{
|
||
var s = new UITheme.SkinEntry { id = id, pack = LoadSprite(path, n, errors) };
|
||
if (overlay.Length > 0) s.overlay = LoadSprite(overlay, n, errors);
|
||
s.ppu = ParseF(f[4], 1f);
|
||
s.inset = ParseInset(f[5]);
|
||
s.overlayAlpha = ParseF(f[6], 1f);
|
||
theme.skins.Add(s);
|
||
break;
|
||
}
|
||
case "icon":
|
||
{
|
||
var e = new UITheme.IconEntry { id = id, pack = LoadSprite(path, n, errors) };
|
||
Color c;
|
||
if (color.Length > 0 && ColorUtility.TryParseHtmlString(color, out c)) e.tint = c;
|
||
theme.icons.Add(e);
|
||
break;
|
||
}
|
||
case "font":
|
||
if (id == "title")
|
||
{
|
||
theme.titleFont = AssetDatabase.LoadAssetAtPath<Font>(path);
|
||
if (theme.titleFont == null) errors.Add((n + 1) + "행: 폰트 없음 " + path);
|
||
}
|
||
break;
|
||
default:
|
||
errors.Add((n + 1) + "행: 알 수 없는 kind " + kind);
|
||
break;
|
||
}
|
||
}
|
||
if (errors.Count > 0)
|
||
{
|
||
foreach (var e in errors) Debug.LogError("[TKS] UI 테마 표 오류 · " + e);
|
||
Debug.LogError("[TKS] UI 테마 임포트 중단(오류 " + errors.Count + ") · 자산 변경 없음");
|
||
Object.DestroyImmediate(theme);
|
||
return false;
|
||
}
|
||
|
||
// 신규 제작 치환(파일 이름 = id)
|
||
var samguk = SamgukSprites();
|
||
int replaced = 0, added = 0;
|
||
var used = new HashSet<string>();
|
||
foreach (var s in theme.skins)
|
||
{
|
||
Sprite sp;
|
||
if (samguk.TryGetValue(s.id, out sp))
|
||
{
|
||
s.samguk = sp;
|
||
used.Add(s.id);
|
||
replaced++;
|
||
}
|
||
}
|
||
foreach (var e in theme.icons)
|
||
{
|
||
Sprite sp;
|
||
if (samguk.TryGetValue(e.id, out sp))
|
||
{
|
||
e.samguk = sp;
|
||
used.Add(e.id);
|
||
replaced++;
|
||
}
|
||
}
|
||
foreach (var kv in samguk)
|
||
{
|
||
if (used.Contains(kv.Key)) continue;
|
||
theme.icons.Add(new UITheme.IconEntry { id = kv.Key, samguk = kv.Value });
|
||
added++;
|
||
}
|
||
|
||
if (!AssetDatabase.IsValidFolder("Assets/Resources")) AssetDatabase.CreateFolder("Assets", "Resources");
|
||
if (!AssetDatabase.IsValidFolder("Assets/Resources/UI")) AssetDatabase.CreateFolder("Assets/Resources", "UI");
|
||
var asset = AssetDatabase.LoadAssetAtPath<UITheme>(AssetPath);
|
||
if (asset == null)
|
||
{
|
||
AssetDatabase.CreateAsset(theme, AssetPath);
|
||
asset = theme;
|
||
}
|
||
else
|
||
{
|
||
EditorUtility.CopySerialized(theme, asset);
|
||
Object.DestroyImmediate(theme);
|
||
}
|
||
asset.Rebuild();
|
||
EditorUtility.SetDirty(asset);
|
||
AssetDatabase.SaveAssets();
|
||
UITheme.Use(asset);
|
||
Debug.Log("[TKS] UI 테마 임포트 · 색 " + asset.colors.Count + " · 스킨 " + asset.skins.Count + " · 아이콘 " + asset.icons.Count +
|
||
" · 신규 제작 치환 " + replaced + " · 추가 " + added + " → " + AssetPath);
|
||
// 테마가 쓰는 스프라이트 = UI 아틀라스 구성(PD 2026-11-10 · 새 스프라이트도 자동으로 묶임 · 구성이 같으면 그대로)
|
||
UIAtlasBuilder.Build(asset);
|
||
return true;
|
||
}
|
||
|
||
/// <summary>Assets/UI/Samguk/**/<id>.png → 스프라이트(같은 id 가 둘이면 경로 순 마지막).</summary>
|
||
public static Dictionary<string, Sprite> SamgukSprites()
|
||
{
|
||
var map = new Dictionary<string, Sprite>();
|
||
if (!AssetDatabase.IsValidFolder(SamgukRoot)) return map;
|
||
var guids = AssetDatabase.FindAssets("t:Sprite", new[] { SamgukRoot });
|
||
var paths = new List<string>();
|
||
foreach (var g in guids) paths.Add(AssetDatabase.GUIDToAssetPath(g));
|
||
paths.Sort(System.StringComparer.Ordinal);
|
||
foreach (var p in paths)
|
||
{
|
||
var sp = AssetDatabase.LoadAssetAtPath<Sprite>(p);
|
||
if (sp != null) map[Path.GetFileNameWithoutExtension(p)] = sp;
|
||
}
|
||
return map;
|
||
}
|
||
|
||
static Sprite LoadSprite(string path, int n, List<string> errors)
|
||
{
|
||
if (string.IsNullOrEmpty(path))
|
||
{
|
||
errors.Add((n + 1) + "행: 경로 없음");
|
||
return null;
|
||
}
|
||
string full = path.StartsWith("Assets/") ? path : PackRoot + path;
|
||
var sp = AssetDatabase.LoadAssetAtPath<Sprite>(full);
|
||
if (sp == null) errors.Add((n + 1) + "행: 스프라이트 없음 " + full);
|
||
return sp;
|
||
}
|
||
|
||
static float ParseF(string s, float def)
|
||
{
|
||
float v;
|
||
return float.TryParse(s.Trim(), System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out v) ? v : def;
|
||
}
|
||
|
||
static Vector4 ParseInset(string s)
|
||
{
|
||
s = s.Trim();
|
||
if (s.Length == 0) return Vector4.zero;
|
||
var p = s.Split(';');
|
||
if (p.Length == 1)
|
||
{
|
||
float v = ParseF(p[0], 0f);
|
||
return new Vector4(v, v, v, v);
|
||
}
|
||
return new Vector4(ParseF(p[0], 0f), p.Length > 1 ? ParseF(p[1], 0f) : 0f, p.Length > 2 ? ParseF(p[2], 0f) : 0f, p.Length > 3 ? ParseF(p[3], 0f) : 0f);
|
||
}
|
||
}
|
||
|
||
/// <summary>신규 제작 UI(Assets/UI/Samguk) · 로비 배경(Assets/UI/Lobby) 임포트 설정 = Sprite(2D) · 납품 파일이 들어오면 테마 치환 테이블 자동 갱신.</summary>
|
||
public class UIThemeAssetPostprocessor : AssetPostprocessor
|
||
{
|
||
static bool pending;
|
||
|
||
static bool Ours(string path)
|
||
{
|
||
return path.StartsWith(UIThemeImporter.SamgukRoot + "/") || path.StartsWith("Assets/UI/Lobby/") || path.StartsWith("Assets/UI/Symbols/"); // Symbols = 병과 단색 심볼(Tools/class_symbols.py · PD 2026-11-09)
|
||
}
|
||
|
||
void OnPreprocessTexture()
|
||
{
|
||
if (!Ours(assetPath)) return;
|
||
var ti = (TextureImporter)assetImporter;
|
||
ti.textureType = TextureImporterType.Sprite;
|
||
ti.spriteImportMode = SpriteImportMode.Single;
|
||
ti.alphaIsTransparency = true;
|
||
ti.mipmapEnabled = false;
|
||
ti.spritePixelsPerUnit = 100f;
|
||
ti.maxTextureSize = assetPath.StartsWith("Assets/UI/Lobby/") ? 2048 : 1024;
|
||
}
|
||
|
||
static void OnPostprocessAllAssets(string[] imported, string[] deleted, string[] moved, string[] movedFrom)
|
||
{
|
||
bool hit = false;
|
||
foreach (var p in imported) hit |= p.StartsWith(UIThemeImporter.SamgukRoot + "/") && p.EndsWith(".png");
|
||
foreach (var p in deleted) hit |= p.StartsWith(UIThemeImporter.SamgukRoot + "/") && p.EndsWith(".png");
|
||
if (!hit || pending) return;
|
||
pending = true;
|
||
EditorApplication.delayCall += () =>
|
||
{
|
||
pending = false;
|
||
UIThemeImporter.Import();
|
||
};
|
||
}
|
||
}
|
||
}
|