2026-11-06 14:20:57 +00:00
|
|
|
|
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);
|
2026-11-09 16:05:47 +00:00
|
|
|
|
// 테마가 쓰는 스프라이트 = UI 아틀라스 구성(PD 2026-11-10 · 새 스프라이트도 자동으로 묶임 · 구성이 같으면 그대로)
|
|
|
|
|
|
UIAtlasBuilder.Build(asset);
|
2026-11-06 14:20:57 +00:00
|
|
|
|
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)
|
|
|
|
|
|
{
|
2026-11-09 10:30:58 +00:00
|
|
|
|
return path.StartsWith(UIThemeImporter.SamgukRoot + "/") || path.StartsWith("Assets/UI/Lobby/") || path.StartsWith("Assets/UI/Symbols/"); // Symbols = 병과 단색 심볼(Tools/class_symbols.py · PD 2026-11-09)
|
2026-11-06 14:20:57 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|