216 lines
9.5 KiB
C#
216 lines
9.5 KiB
C#
|
|
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/<이름>.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);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|