#nullable enable using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Text; using UnityEditor; using UnityEngine; namespace UVC.UIToolkit.Editor { /// /// MaterialSymbolsOutlinedCodepoints.txt 파일을 파싱하여 /// UTKMaterialIcons.cs 정적 클래스를 자동 생성하는 에디터 도구입니다. /// public class UTKMaterialIconsGenerator : EditorWindow { #region EditorPrefs Keys private const string PrefKeyCodepointsPath = "UTK_MaterialIcons_CodepointsPath"; private const string PrefKeyOutputPath = "UTK_MaterialIcons_OutputPath"; private const string PrefKeyClassName = "UTK_MaterialIcons_ClassName"; private const string PrefKeyNamespace = "UTK_MaterialIcons_Namespace"; private const string PrefKeyFontPath = "UTK_MaterialIcons_FontPath"; #endregion #region Default Values private const string DefaultCodepointsPath = "Assets/Resources/Fonts/Icons/MaterialSymbolsOutlinedCodepoints.txt"; private const string DefaultOutputPath = "Assets/Scripts/UVC/UIToolkit/Common/UTKMaterialIcons.cs"; private const string DefaultClassName = "UTKMaterialIcons"; private const string DefaultNamespace = "UVC.UIToolkit"; private const string DefaultFontPath = "Fonts/Icons/MaterialSymbolsOutlined"; #endregion #region Fields private string _codepointsPath = DefaultCodepointsPath; private string _outputPath = DefaultOutputPath; private string _className = DefaultClassName; private string _namespace = DefaultNamespace; private string _fontPath = DefaultFontPath; #endregion [MenuItem("Tools/UTK/Material Icons Generator")] public static void ShowWindow() { var window = GetWindow("Material Icons Generator"); window.minSize = new Vector2(450, 250); window.LoadSettings(); } [MenuItem("Tools/UTK/Generate Material Icons Class (Quick)")] public static void GenerateQuick() { var codepointsPath = EditorPrefs.GetString(PrefKeyCodepointsPath, DefaultCodepointsPath); var outputPath = EditorPrefs.GetString(PrefKeyOutputPath, DefaultOutputPath); var className = EditorPrefs.GetString(PrefKeyClassName, DefaultClassName); var namespaceName = EditorPrefs.GetString(PrefKeyNamespace, DefaultNamespace); var fontPath = EditorPrefs.GetString(PrefKeyFontPath, DefaultFontPath); Generate(codepointsPath, outputPath, className, namespaceName, fontPath); } private void OnEnable() { LoadSettings(); } private void LoadSettings() { _codepointsPath = EditorPrefs.GetString(PrefKeyCodepointsPath, DefaultCodepointsPath); _outputPath = EditorPrefs.GetString(PrefKeyOutputPath, DefaultOutputPath); _className = EditorPrefs.GetString(PrefKeyClassName, DefaultClassName); _namespace = EditorPrefs.GetString(PrefKeyNamespace, DefaultNamespace); _fontPath = EditorPrefs.GetString(PrefKeyFontPath, DefaultFontPath); } private void SaveSettings() { EditorPrefs.SetString(PrefKeyCodepointsPath, _codepointsPath); EditorPrefs.SetString(PrefKeyOutputPath, _outputPath); EditorPrefs.SetString(PrefKeyClassName, _className); EditorPrefs.SetString(PrefKeyNamespace, _namespace); EditorPrefs.SetString(PrefKeyFontPath, _fontPath); } private void OnGUI() { EditorGUILayout.Space(10); EditorGUILayout.LabelField("Material Icons Class Generator", EditorStyles.boldLabel); EditorGUILayout.Space(5); EditorGUILayout.HelpBox("Codepoints 파일을 파싱하여 C# 정적 클래스를 생성합니다.", MessageType.Info); EditorGUILayout.Space(10); // Codepoints 파일 경로 EditorGUILayout.BeginHorizontal(); _codepointsPath = EditorGUILayout.TextField("Codepoints File", _codepointsPath); if (GUILayout.Button("...", GUILayout.Width(30))) { var path = EditorUtility.OpenFilePanel("Select Codepoints File", "Assets", "txt"); if (!string.IsNullOrEmpty(path)) { // 상대 경로로 변환 if (path.StartsWith(Application.dataPath)) { path = "Assets" + path.Substring(Application.dataPath.Length); } _codepointsPath = path; } } EditorGUILayout.EndHorizontal(); // 출력 파일 경로 EditorGUILayout.BeginHorizontal(); _outputPath = EditorGUILayout.TextField("Output File", _outputPath); if (GUILayout.Button("...", GUILayout.Width(30))) { var directory = Path.GetDirectoryName(_outputPath) ?? "Assets"; var filename = Path.GetFileName(_outputPath); var path = EditorUtility.SaveFilePanel("Save Generated Class", directory, filename, "cs"); if (!string.IsNullOrEmpty(path)) { // 상대 경로로 변환 if (path.StartsWith(Application.dataPath)) { path = "Assets" + path.Substring(Application.dataPath.Length); } _outputPath = path; } } EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(5); // 클래스명 _className = EditorGUILayout.TextField("Class Name", _className); // 네임스페이스 _namespace = EditorGUILayout.TextField("Namespace", _namespace); // 폰트 경로 (Resources 기준) _fontPath = EditorGUILayout.TextField("Font Path (Resources)", _fontPath); EditorGUILayout.HelpBox("Resources.Load에 사용할 폰트 경로 (확장자 제외)", MessageType.None); EditorGUILayout.Space(15); // 파일 존재 여부 표시 var codepointsExists = File.Exists(_codepointsPath); var outputExists = File.Exists(_outputPath); EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField("Codepoints File:", GUILayout.Width(100)); EditorGUILayout.LabelField(codepointsExists ? "Found" : "Not Found", codepointsExists ? EditorStyles.label : EditorStyles.boldLabel); EditorGUILayout.EndHorizontal(); EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField("Output File:", GUILayout.Width(100)); EditorGUILayout.LabelField(outputExists ? "Exists (will overwrite)" : "New file", EditorStyles.label); EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(15); // 버튼들 EditorGUILayout.BeginHorizontal(); GUI.enabled = codepointsExists; if (GUILayout.Button("Generate", GUILayout.Height(30))) { SaveSettings(); Generate(_codepointsPath, _outputPath, _className, _namespace, _fontPath); } GUI.enabled = true; if (GUILayout.Button("Reset to Defaults", GUILayout.Height(30))) { _codepointsPath = DefaultCodepointsPath; _outputPath = DefaultOutputPath; _className = DefaultClassName; _namespace = DefaultNamespace; _fontPath = DefaultFontPath; SaveSettings(); } EditorGUILayout.EndHorizontal(); } public static void Generate(string codepointsPath, string outputPath, string className, string namespaceName, string fontPath = DefaultFontPath) { if (!File.Exists(codepointsPath)) { Debug.LogError($"Codepoints 파일을 찾을 수 없습니다: {codepointsPath}"); return; } var icons = ParseCodepoints(codepointsPath); if (icons.Count == 0) { Debug.LogError("파싱된 아이콘이 없습니다."); return; } var code = GenerateCode(icons, className, namespaceName, fontPath); // 디렉토리 생성 var directory = Path.GetDirectoryName(outputPath); if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) { Directory.CreateDirectory(directory); } File.WriteAllText(outputPath, code, Encoding.UTF8); AssetDatabase.Refresh(); Debug.Log($"{className}.cs 생성 완료: {icons.Count}개 아이콘 ({outputPath})"); } private static List<(string name, string unicode)> ParseCodepoints(string path) { var result = new List<(string, string)>(); var lines = File.ReadAllLines(path); foreach (var line in lines) { if (string.IsNullOrWhiteSpace(line)) continue; var parts = line.Split(' ', StringSplitOptions.RemoveEmptyEntries); if (parts.Length >= 2) { result.Add((parts[0], parts[1])); } } return result; } private static string GenerateCode(List<(string name, string unicode)> icons, string className, string namespaceName, string fontPath) { var sb = new StringBuilder(); var usedNames = new HashSet(); sb.AppendLine("// "); sb.AppendLine("// 이 파일은 UTKMaterialIconsGenerator에 의해 자동 생성되었습니다."); sb.AppendLine("// 직접 수정하지 마세요. Tools > UTK > Material Icons Generator 메뉴로 재생성하세요."); sb.AppendLine("// "); sb.AppendLine(); sb.AppendLine("#nullable enable"); sb.AppendLine("using System;"); sb.AppendLine("using System.Collections.Generic;"); sb.AppendLine("using System.Threading;"); sb.AppendLine("using Cysharp.Threading.Tasks;"); sb.AppendLine("using UnityEngine;"); sb.AppendLine("using UnityEngine.UIElements;"); sb.AppendLine(); sb.AppendLine($"namespace {namespaceName}"); sb.AppendLine("{"); sb.AppendLine(" /// "); sb.AppendLine(" /// Material Symbols Outlined 아이콘 유니코드 문자 상수 클래스입니다."); sb.AppendLine($" /// 총 {icons.Count}개의 아이콘을 포함합니다."); sb.AppendLine(" /// "); sb.AppendLine(" /// "); sb.AppendLine(" /// "); sb.AppendLine($" /// // 동기 폰트 로드 (캐싱됨)"); sb.AppendLine($" /// Font font = {className}.LoadFont();"); sb.AppendLine(" /// "); sb.AppendLine($" /// // 비동기 폰트 로드 (UniTask)"); sb.AppendLine($" /// Font? font = await {className}.LoadFontAsync(cancellationToken);"); sb.AppendLine(" /// "); sb.AppendLine($" /// // Label에 아이콘 적용"); sb.AppendLine($" /// var label = new Label({className}.Home);"); sb.AppendLine($" /// {className}.ApplyIconStyle(label);"); sb.AppendLine(" /// "); sb.AppendLine($" /// // 비동기로 아이콘 스타일 적용"); sb.AppendLine($" /// await {className}.ApplyIconStyleAsync(label, cancellationToken);"); sb.AppendLine(" /// "); sb.AppendLine($" /// // 또는 StyleFontDefinition 직접 사용"); sb.AppendLine($" /// label.style.unityFontDefinition = {className}.GetFontDefinition();"); sb.AppendLine(" /// "); sb.AppendLine($" /// // 이름으로 아이콘 조회"); sb.AppendLine($" /// string icon = {className}.GetIcon(\"settings\");"); sb.AppendLine(" /// "); sb.AppendLine($" /// // 아이콘 존재 여부 확인"); sb.AppendLine($" /// if ({className}.HasIcon(\"search\")) {{ }}"); sb.AppendLine(" /// "); sb.AppendLine($" /// // 존재하는 유니코드 문자인지 확인"); sb.AppendLine($" /// if ({className}.IsIconChar(\"□\")) {{ }}"); sb.AppendLine(" /// "); sb.AppendLine($" /// // 전체 아이콘 이름 순회"); sb.AppendLine($" /// foreach (var name in {className}.GetAllIconNames()) {{ }}"); sb.AppendLine(" /// "); sb.AppendLine($" /// // 아이콘 총 개수"); sb.AppendLine($" /// int count = {className}.Count;"); sb.AppendLine(" /// "); sb.AppendLine(" /// "); sb.AppendLine(" /// "); sb.AppendLine(" /// UXML에서 사용하기:"); sb.AppendLine(" /// UXML에서 Material Icons를 사용하려면 USS에서 폰트를 설정하고, Label의 text에 유니코드 문자를 직접 입력합니다."); sb.AppendLine(" /// "); sb.AppendLine(" /// /* USS 파일 */"); sb.AppendLine(" /// .material-icon {"); sb.AppendLine(" /// -unity-font: resource('Fonts/Icons/MaterialSymbolsOutlined');"); sb.AppendLine(" /// font-size: 24px;"); sb.AppendLine(" /// }"); sb.AppendLine(" /// "); sb.AppendLine(" /// "); sb.AppendLine(" /// "); sb.AppendLine(" /// "); sb.AppendLine(" /// "); sb.AppendLine(" /// "); sb.AppendLine(" /// C# 코드에서 UXML Label에 아이콘 적용:"); sb.AppendLine(" /// "); sb.AppendLine($" /// var iconLabel = root.Q"); sb.AppendLine(" /// "); sb.AppendLine($" public static class {className}"); sb.AppendLine(" {"); sb.AppendLine(); sb.AppendLine(" #region Font"); sb.AppendLine(); sb.AppendLine($" private const string FontResourcePath = \"{fontPath}\";"); sb.AppendLine(" private static Font? _cachedFont;"); sb.AppendLine(" private static StyleFontDefinition? _cachedFontDefinition;"); sb.AppendLine(); sb.AppendLine(" /// "); sb.AppendLine(" /// 아이콘 폰트를 동기로 로드합니다. (캐싱됨)"); sb.AppendLine(" /// "); sb.AppendLine(" /// 로드된 Font, 실패 시 null"); sb.AppendLine(" public static Font? LoadFont()"); sb.AppendLine(" {"); sb.AppendLine(" if (_cachedFont == null)"); sb.AppendLine(" {"); sb.AppendLine(" _cachedFont = Resources.Load(FontResourcePath);"); sb.AppendLine(" }"); sb.AppendLine(" return _cachedFont;"); sb.AppendLine(" }"); sb.AppendLine(); sb.AppendLine(" /// "); sb.AppendLine(" /// 아이콘 폰트를 비동기로 로드합니다. (캐싱됨)"); sb.AppendLine(" /// "); sb.AppendLine(" /// 취소 토큰"); sb.AppendLine(" /// 로드된 Font, 실패 시 null"); sb.AppendLine(" public static async UniTask LoadFontAsync(CancellationToken ct = default)"); sb.AppendLine(" {"); sb.AppendLine(" if (_cachedFont != null)"); sb.AppendLine(" {"); sb.AppendLine(" return _cachedFont;"); sb.AppendLine(" }"); sb.AppendLine(); sb.AppendLine(" var request = Resources.LoadAsync(FontResourcePath);"); sb.AppendLine(" await request.ToUniTask(cancellationToken: ct);"); sb.AppendLine(); sb.AppendLine(" _cachedFont = request.asset as Font;"); sb.AppendLine(" return _cachedFont;"); sb.AppendLine(" }"); sb.AppendLine(); sb.AppendLine(" /// "); sb.AppendLine(" /// UI Toolkit용 StyleFontDefinition을 동기로 반환합니다. (캐싱됨)"); sb.AppendLine(" /// "); sb.AppendLine(" /// StyleFontDefinition, 폰트 로드 실패 시 기본값"); sb.AppendLine(" public static StyleFontDefinition GetFontDefinition()"); sb.AppendLine(" {"); sb.AppendLine(" if (_cachedFontDefinition == null)"); sb.AppendLine(" {"); sb.AppendLine(" var font = LoadFont();"); sb.AppendLine(" if (font != null)"); sb.AppendLine(" {"); sb.AppendLine(" _cachedFontDefinition = new StyleFontDefinition(font);"); sb.AppendLine(" }"); sb.AppendLine(" }"); sb.AppendLine(" return _cachedFontDefinition ?? default;"); sb.AppendLine(" }"); sb.AppendLine(); sb.AppendLine(" /// "); sb.AppendLine(" /// UI Toolkit용 StyleFontDefinition을 비동기로 반환합니다. (캐싱됨)"); sb.AppendLine(" /// "); sb.AppendLine(" /// 취소 토큰"); sb.AppendLine(" /// StyleFontDefinition, 폰트 로드 실패 시 기본값"); sb.AppendLine(" public static async UniTask GetFontDefinitionAsync(CancellationToken ct = default)"); sb.AppendLine(" {"); sb.AppendLine(" if (_cachedFontDefinition == null)"); sb.AppendLine(" {"); sb.AppendLine(" var font = await LoadFontAsync(ct);"); sb.AppendLine(" if (font != null)"); sb.AppendLine(" {"); sb.AppendLine(" _cachedFontDefinition = new StyleFontDefinition(font);"); sb.AppendLine(" }"); sb.AppendLine(" }"); sb.AppendLine(" return _cachedFontDefinition ?? default;"); sb.AppendLine(" }"); sb.AppendLine(); sb.AppendLine(" /// "); sb.AppendLine(" /// VisualElement에 아이콘 폰트 스타일을 동기로 적용합니다."); sb.AppendLine(" /// "); sb.AppendLine(" /// 스타일을 적용할 요소"); sb.AppendLine(" /// 폰트 크기 (기본값: 24)"); sb.AppendLine(" public static void ApplyIconStyle(VisualElement element, int? fontSize = 24)"); sb.AppendLine(" {"); sb.AppendLine(" element.style.unityFontDefinition = GetFontDefinition();"); sb.AppendLine(" if(fontSize != null) element.style.fontSize = fontSize.Value;"); sb.AppendLine(" }"); sb.AppendLine(); sb.AppendLine(" /// "); sb.AppendLine(" /// VisualElement에 아이콘 폰트 스타일을 비동기로 적용합니다."); sb.AppendLine(" /// "); sb.AppendLine(" /// 스타일을 적용할 요소"); sb.AppendLine(" /// 취소 토큰"); sb.AppendLine(" /// 폰트 크기 (기본값: 24)"); sb.AppendLine(" public static async UniTask ApplyIconStyleAsync(VisualElement element, CancellationToken ct = default, int fontSize = 24)"); sb.AppendLine(" {"); sb.AppendLine(" element.style.unityFontDefinition = await GetFontDefinitionAsync(ct);"); sb.AppendLine(" element.style.fontSize = fontSize;"); sb.AppendLine(" }"); sb.AppendLine(); sb.AppendLine(" /// "); sb.AppendLine(" /// 폰트 캐시를 클리어합니다."); sb.AppendLine(" /// "); sb.AppendLine(" public static void ClearFontCache()"); sb.AppendLine(" {"); sb.AppendLine(" _cachedFont = null;"); sb.AppendLine(" _cachedFontDefinition = null;"); sb.AppendLine(" }"); sb.AppendLine(); sb.AppendLine(" #endregion"); sb.AppendLine(); // 아이콘 상수 생성 foreach (var (name, unicode) in icons) { var fieldName = ConvertToFieldName(name, usedNames); var character = GetCharacterFromUnicode(unicode); var escapedChar = EscapeForCSharp(character); sb.AppendLine($" /// {name} (U+{unicode.ToUpper()})"); sb.AppendLine($" public const string {fieldName} = \"{escapedChar}\";"); } sb.AppendLine(); sb.AppendLine(" #region Lookup"); sb.AppendLine(); // 이름으로 아이콘 조회 딕셔너리 sb.AppendLine(" private static readonly Dictionary _iconsByName = new()"); sb.AppendLine(" {"); foreach (var (name, unicode) in icons) { var character = GetCharacterFromUnicode(unicode); var escapedChar = EscapeForCSharp(character); sb.AppendLine($" [\"{name}\"] = \"{escapedChar}\","); } sb.AppendLine(" };"); sb.AppendLine(); // 조회 메서드 sb.AppendLine(" /// "); sb.AppendLine(" /// 아이콘 이름으로 유니코드 문자를 조회합니다."); sb.AppendLine(" /// "); sb.AppendLine(" /// 아이콘 이름 (예: \"home\", \"settings\")"); sb.AppendLine(" /// 아이콘 문자, 없으면 빈 문자열"); sb.AppendLine(" public static string GetIcon(string iconName)"); sb.AppendLine(" {"); sb.AppendLine(" // 실제 유니코드 문자가 아니라 이스케이프 문자열인 경우 변환"); sb.AppendLine(" if (iconName.StartsWith(\"\\u\") && iconName.Length == 6)"); sb.AppendLine(" {"); sb.AppendLine(" try"); sb.AppendLine(" {"); sb.AppendLine(" var code = Convert.ToInt32(iconName.Substring(2), 16);"); sb.AppendLine(" iconName = char.ConvertFromUtf32(code);"); sb.AppendLine(" }"); sb.AppendLine(" catch"); sb.AppendLine(" {"); sb.AppendLine(" Debug.LogWarning($\"Failed to convert escape sequence: {iconName}\");"); sb.AppendLine(" }"); sb.AppendLine(" }"); sb.AppendLine(" "); sb.AppendLine(" if(IsIconChar(iconName)) return iconName;"); sb.AppendLine(" return _iconsByName.TryGetValue(iconName.ToLower(), out var icon) ? icon : string.Empty;"); sb.AppendLine(" }"); sb.AppendLine(); sb.AppendLine(" /// "); sb.AppendLine(" /// 아이콘이 존재하는지 확인합니다."); sb.AppendLine(" /// "); sb.AppendLine(" public static bool HasIcon(string iconName) => _iconsByName.ContainsKey(iconName);"); sb.AppendLine(); sb.AppendLine(" /// "); sb.AppendLine(" /// 유니코드 문자로 아이콘이 존재하는지 확인합니다."); sb.AppendLine(" /// "); sb.AppendLine(" public static bool IsIconChar(string iconChar) => _iconsByName.Values.Contains(iconChar);"); sb.AppendLine(); sb.AppendLine(" /// "); sb.AppendLine(" /// 유니코드 문자로 아이콘 이름을 조회합니다."); sb.AppendLine(" /// "); sb.AppendLine(" public static string GetIconNameByChar(string iconChar)"); sb.AppendLine(" {"); sb.AppendLine(" foreach (var kvp in _iconsByName)"); sb.AppendLine(" {"); sb.AppendLine(" if (string.Equals(kvp.Value, iconChar, StringComparison.OrdinalIgnoreCase)) return kvp.Key;"); sb.AppendLine(" }"); sb.AppendLine(" return string.Empty;"); sb.AppendLine(" }"); sb.AppendLine(); sb.AppendLine(" /// "); sb.AppendLine(" /// 모든 아이콘 이름 목록을 반환합니다."); sb.AppendLine(" /// "); sb.AppendLine(" public static IEnumerable GetAllIconNames() => _iconsByName.Keys;"); sb.AppendLine(); sb.AppendLine(" /// "); sb.AppendLine(" /// 전체 아이콘 수를 반환합니다."); sb.AppendLine(" /// "); sb.AppendLine($" public static int Count => {icons.Count};"); sb.AppendLine(); sb.AppendLine(" #endregion"); sb.AppendLine(" }"); sb.AppendLine("}"); return sb.ToString(); } private static string ConvertToFieldName(string iconName, HashSet usedNames) { var sb = new StringBuilder(); var words = iconName.Split('_', StringSplitOptions.RemoveEmptyEntries); foreach (var word in words) { if (word.Length == 0) continue; // 첫 글자 대문자, 나머지 소문자 (PascalCase) sb.Append(char.ToUpper(word[0])); if (word.Length > 1) { sb.Append(word.Substring(1).ToLower()); } } var result = sb.ToString(); // 숫자로 시작하면 앞에 _ 추가 if (result.Length > 0 && char.IsDigit(result[0])) { result = "_" + result; } // 빈 문자열이면 기본값 if (string.IsNullOrEmpty(result)) { result = "_Icon"; } // 중복 처리 var originalResult = result; var counter = 2; while (usedNames.Contains(result)) { result = $"{originalResult}_{counter}"; counter++; } usedNames.Add(result); return result; } private static string GetCharacterFromUnicode(string hexCode) { try { var codepoint = int.Parse(hexCode, NumberStyles.HexNumber); return char.ConvertFromUtf32(codepoint); } catch { return "?"; } } private static string EscapeForCSharp(string str) { var sb = new StringBuilder(); foreach (var c in str) { // 유니코드 이스케이프 시퀀스로 변환 if (c > 127 || c < 32) { // Surrogate pair 처리 sb.Append($"\\u{(int)c:X4}"); } else if (c == '"') { sb.Append("\\\""); } else if (c == '\\') { sb.Append("\\\\"); } else { sb.Append(c); } } return sb.ToString(); } } }