using System; using System.Collections.Generic; using System.IO; using Uigenx.Protocol; using UnityEditor; using UnityEngine; using UnityEngine.TextCore.LowLevel; using UnityEngine.TextCore.Text; namespace Uigenx.Editor { /// /// Bakes the FontAssets the export asked for. An SDF atlas is rendered by Unity's own text /// engine, so the compiler can only ship the font file and the recipe; this turns the recipe /// into the asset that `-unity-font-definition` points at. /// public static class UgxFontAssetBuilder { public sealed class Report { public List Created { get; } = new List(); public List Rebuilt { get; } = new List(); public List Unchanged { get; } = new List(); public List Warnings { get; } = new List(); } /// Reads the manifest on disk and bakes what it lists. Used after an offline export. public static Report BuildFromDisk(string assetsUiFolder) { var report = new Report(); if (!UgxManifestFile.TryLoad(assetsUiFolder, out var manifest, out var problem)) { report.Warnings.Add(problem); return report; } return Build(manifest.Fonts, assetsUiFolder, report); } public static Report Build(IEnumerable fonts, string assetsUiFolder, Report report = null) { report = report ?? new Report(); if (fonts == null) return report; bool touched = false; foreach (var font in fonts) { if (font == null || string.IsNullOrEmpty(font.Path) || string.IsNullOrEmpty(font.Source)) { report.Warnings.Add("a font entry has no source or path, skipped"); continue; } touched |= BuildOne(font, assetsUiFolder, report); } // A forced GUID only takes effect on the next import, and so does a fresh atlas. if (touched) AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport); return report; } static bool BuildOne(UgxManifestFont plan, string assetsUiFolder, Report report) { var sourcePath = UgxBundleImporter.CombineAssetPath(assetsUiFolder, plan.Source); var assetPath = UgxBundleImporter.CombineAssetPath(assetsUiFolder, plan.Path); var source = AssetDatabase.LoadAssetAtPath(sourcePath); if (source == null) { report.Warnings.Add(plan.Path + ": " + sourcePath + " is not an imported font, skipped"); return false; } var existing = AssetDatabase.LoadAssetAtPath(assetPath); if (existing != null && Matches(existing, plan)) { report.Unchanged.Add(assetPath); return false; } // Recreating replaces the file, so the GUID it already has is what references point at. var keepGuid = existing == null ? plan.Guid : AssetDatabase.AssetPathToGUID(assetPath); FontAsset baked; try { baked = FontAsset.CreateFontAsset( source, Math.Max(8, plan.SamplingPointSize), Math.Max(0, plan.Padding), RenderModeOf(plan.RenderMode), Math.Max(64, plan.AtlasSize), Math.Max(64, plan.AtlasSize), AtlasPopulationMode.Dynamic, true); } catch (Exception e) { report.Warnings.Add(plan.Path + ": Unity could not bake the atlas: " + e.Message); return false; } if (baked == null) { report.Warnings.Add(plan.Path + ": Unity returned no font asset"); return false; } var folder = Path.GetDirectoryName(assetPath); if (!string.IsNullOrEmpty(folder)) Directory.CreateDirectory(folder); baked.name = Path.GetFileNameWithoutExtension(assetPath); AssetDatabase.CreateAsset(baked, assetPath); AttachSubAssets(baked); AssetDatabase.SaveAssets(); UgxGuidPolicy.ForceGuid(assetPath, keepGuid); (existing == null ? report.Created : report.Rebuilt).Add(assetPath); return true; } /// /// The atlas texture and the material are created with the font asset and have no file of /// their own. Without this they are dropped on the next domain reload. /// static void AttachSubAssets(FontAsset baked) { var textures = baked.atlasTextures; if (textures != null) { for (int i = 0; i < textures.Length; i++) { if (textures[i] == null) continue; textures[i].name = baked.name + " Atlas"; if (AssetDatabase.Contains(textures[i])) continue; AssetDatabase.AddObjectToAsset(textures[i], baked); } } if (baked.material == null || AssetDatabase.Contains(baked.material)) return; baked.material.name = baked.name + " Material"; AssetDatabase.AddObjectToAsset(baked.material, baked); } /// Every setting the recipe pins. Anything else and the atlas has to be rendered again. static bool Matches(FontAsset asset, UgxManifestFont plan) { return asset.atlasRenderMode == RenderModeOf(plan.RenderMode) && asset.atlasPadding == Math.Max(0, plan.Padding) && asset.atlasWidth == Math.Max(64, plan.AtlasSize) && asset.atlasHeight == Math.Max(64, plan.AtlasSize) && Mathf.Approximately(asset.faceInfo.pointSize, Math.Max(8, plan.SamplingPointSize)); } public static GlyphRenderMode RenderModeOf(string name) { switch (name) { case "SDF16": return GlyphRenderMode.SDF16; case "SDF32": return GlyphRenderMode.SDF32; case "SMOOTH": return GlyphRenderMode.SMOOTH; case "RASTER": return GlyphRenderMode.RASTER; default: return GlyphRenderMode.SDFAA; } } } }