ThreeKingdomsSlingshot/Assets/Scripts/Editor/UIAtlasBuilder.cs

216 lines
9.5 KiB
C#
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. 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 System.IO;
using System.Linq;
using System.Text;
using UnityEditor;
using UnityEditor.U2D;
using UnityEngine;
using UnityEngine.U2D;
namespace TKS.EditorTools
{
/// <summary>
/// UI 스프라이트 아틀라스(PD 2026-11-10 「드로우콜 최소화 구조」) — 구성 SOT = Assets/Data/UI/ui_atlas.csv → Assets/UI/Atlases/&lt;이름&gt;.spriteatlasv2.
/// theme 줄 = UI 테마(UITheme.asset)가 쓰는 스프라이트를 match(경로 포함 · | = 또는 · * = 나머지 전부)로 나눔 · folder 줄 = 폴더 전체 ·
/// option single_area = 그보다 넓은 그림은 묶지 않음 · padding.
/// UI 테마 임포트가 끝날 때마다 자동으로 다시 맞춘다(새 납품 스프라이트 = 코드 변경 없이 아틀라스에 들어감) · 구성이 같으면 파일을 건드리지 않는다.
/// 패커 = Sprite Atlas V2(에디터 Play 에서도 묶음 · 프로젝트 설정 EditorSettings).
/// </summary>
public static class UIAtlasBuilder
{
public const string CsvPath = "Assets/Data/UI/ui_atlas.csv";
public const string OutDir = "Assets/UI/Atlases";
class Group
{
public string kind;
public string name;
public string[] match;
public int maxSize;
public readonly List<string> paths = new List<string>();
}
[MenuItem("TKS/Build UI Atlases (UI 아틀라스 갱신)")]
public static void Menu()
{
Build(AssetDatabase.LoadAssetAtPath<UITheme>(UIThemeImporter.AssetPath), true);
}
/// <summary>표대로 아틀라스를 맞춘다 · 반환 = 오류 없이 끝났는가(오류면 아틀라스 미변경).</summary>
public static bool Build(UITheme theme, bool log = false)
{
if (!File.Exists(CsvPath))
{
Debug.LogError("[TKS] UI 아틀라스 표 없음: " + CsvPath);
return false;
}
var groups = new List<Group>();
long singleArea = long.MaxValue;
int padding = 4;
var errors = new List<string>();
var lines = File.ReadAllText(CsvPath, Encoding.UTF8).TrimStart('').Replace("\r", "").Split('\n');
for (int n = 1; n < lines.Length; n++)
{
if (string.IsNullOrWhiteSpace(lines[n])) continue;
var f = lines[n].Split(',');
if (f.Length < 4)
{
errors.Add((n + 1) + "행: 열 부족");
continue;
}
string kind = f[0].Trim(), name = f[1].Trim(), match = f[2].Trim(), value = f[3].Trim();
int v;
switch (kind)
{
case "theme":
case "folder":
if (!int.TryParse(value, out v) || v < 128) errors.Add((n + 1) + "행 " + name + ": 최대 크기 " + value);
groups.Add(new Group { kind = kind, name = name, match = match.Split('|'), maxSize = v });
break;
case "option":
if (!int.TryParse(value, out v)) errors.Add((n + 1) + "행 " + name + ": 값 " + value);
else if (name == "single_area") singleArea = v;
else if (name == "padding") padding = v;
else errors.Add((n + 1) + "행: 모르는 옵션 " + name);
break;
default:
errors.Add((n + 1) + "행: 모르는 종류 " + kind);
break;
}
}
// theme 스프라이트 → 첫 번째로 맞는 theme 줄(* = 나머지)
var singles = new List<string>();
foreach (var path in ThemeTextures(theme))
{
var tex = AssetDatabase.LoadAssetAtPath<Texture2D>(path);
if (tex == null) continue;
if ((long)tex.width * tex.height > singleArea)
{
singles.Add(path);
continue;
}
Group hit = null;
foreach (var g in groups)
if (g.kind == "theme" && g.match.Any(m => m != "*" && path.Contains(m)))
{
hit = g;
break;
}
if (hit == null) hit = groups.FirstOrDefault(g => g.kind == "theme" && g.match.Contains("*"));
if (hit != null) hit.paths.Add(path);
}
foreach (var g in groups)
if (g.kind == "folder")
{
if (!AssetDatabase.IsValidFolder(g.match[0])) errors.Add(g.name + ": 폴더 없음 " + g.match[0]);
else g.paths.Add(g.match[0]);
}
if (errors.Count > 0)
{
Debug.LogError("[TKS] UI 아틀라스 표 오류 " + errors.Count + "건(아틀라스 미변경):\n" + string.Join("\n", errors));
return false;
}
// 패커 = Sprite Atlas V2(에디터 Play 에서도 묶음)
if (EditorSettings.spritePackerMode != SpritePackerMode.SpriteAtlasV2)
{
EditorSettings.spritePackerMode = SpritePackerMode.SpriteAtlasV2;
AssetDatabase.SaveAssets();
}
if (!AssetDatabase.IsValidFolder(OutDir)) AssetDatabase.CreateFolder(Path.GetDirectoryName(OutDir).Replace('\\', '/'), Path.GetFileName(OutDir));
int changed = 0;
var report = new StringBuilder();
foreach (var g in groups)
{
g.paths.Sort(System.StringComparer.Ordinal);
if (Write(g, padding)) changed++;
report.Append(" · ").Append(g.name).Append(' ').Append(g.paths.Count);
}
if (changed > 0) SpriteAtlasUtility.PackAllAtlases(EditorUserBuildSettings.activeBuildTarget);
if (log || changed > 0)
Debug.Log("[TKS] UI 아틀라스" + report + " · 큰 그림(묶지 않음) " + singles.Count + " · 갱신 " + changed + "장");
return true;
}
/// <summary>아틀라스 1장을 표 구성대로 · 구성(목록·크기·간격) 서명이 같으면 건드리지 않는다.</summary>
static bool Write(Group g, int padding)
{
string path = OutDir + "/" + g.name + ".spriteatlasv2";
string sig = Hash128.Compute(g.maxSize + ";" + padding + ";" + string.Join("|", g.paths.Select(AssetDatabase.AssetPathToGUID))).ToString();
var imp = AssetImporter.GetAtPath(path) as SpriteAtlasImporter;
if (imp != null && imp.userData == sig) return false;
var asset = new SpriteAtlasAsset();
var objs = new List<Object>();
foreach (var p in g.paths)
{
var o = AssetDatabase.LoadMainAssetAtPath(p);
if (o != null) objs.Add(o);
}
asset.Add(objs.ToArray());
SpriteAtlasAsset.Save(asset, path);
AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceSynchronousImport);
imp = (SpriteAtlasImporter)AssetImporter.GetAtPath(path);
imp.includeInBuild = true;
// UI = 사각 묶음(9-slice · Filled 가 그대로) · 회전 없음 · 밉맵 없음
imp.packingSettings = new SpriteAtlasPackingSettings
{
blockOffset = 1,
enableRotation = false,
enableTightPacking = false,
enableAlphaDilation = true,
padding = padding
};
imp.textureSettings = new SpriteAtlasTextureSettings
{
readable = false,
generateMipMaps = false,
sRGB = true,
filterMode = FilterMode.Bilinear,
anisoLevel = 0
};
var ps = imp.GetPlatformSettings("DefaultTexturePlatform");
ps.maxTextureSize = g.maxSize;
ps.format = TextureImporterFormat.Automatic;
ps.textureCompression = TextureImporterCompression.Compressed; // 원본 UI 텍스처와 같은 압축(Normal)
imp.SetPlatformSettings(ps);
imp.userData = sig;
imp.SaveAndReimport();
return true;
}
/// <summary>테마가 쓰는 스프라이트의 텍스처 경로(팩 · 신규 제작 · 덮개) — 중복 없음.</summary>
public static List<string> ThemeTextures(UITheme theme)
{
var set = new HashSet<string>();
if (theme != null)
{
foreach (var s in theme.skins)
{
Add(set, s.pack);
Add(set, s.samguk);
Add(set, s.overlay);
}
foreach (var i in theme.icons)
{
Add(set, i.pack);
Add(set, i.samguk);
}
}
var list = set.ToList();
list.Sort(System.StringComparer.Ordinal);
return list;
}
static void Add(HashSet<string> set, Sprite sp)
{
if (sp == null) return;
var p = AssetDatabase.GetAssetPath(sp);
// 팩 일부 = 확장자 대문자(.Png) → 확장자 대신 본 자산이 텍스처인지로 판정
if (!string.IsNullOrEmpty(p) && AssetDatabase.GetMainAssetTypeAtPath(p) == typeof(Texture2D)) set.Add(p);
}
}
}