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;
}
}
}
}