B1: dedupe CJK/symbols merge via AddCjkAndSymbols, trim fallback range; FontsReady + report self-test

This commit is contained in:
2026-06-16 19:53:39 +02:00
parent 7d2fd1ab65
commit b048a51534
2 changed files with 141 additions and 61 deletions
+98 -61
View File
@@ -61,6 +61,17 @@ public sealed class FontManager : IDisposable
private ushort[] Ranges = [];
private ushort[] JpRange = [];
// B1: trimmed remainder the NotoSansCjk fallback is the sole source for
// (Hangul + Simplified-Han); excludes the Default/Latin block already merged
// by the global font, so the fallback no longer re-merges the full Ranges array.
private ushort[] CjkFallbackGlyphRange = [];
// Report accessor for the ctor self-test: built glyph-range array lengths so
// the step can show the B1 dedup effect (a small trimmed fallback vs the large
// primary range) in its on-disk report instead of a bare Pass.
internal (int Ranges, int JpRange, int CjkFallback) GlyphRangeLengths =>
(Ranges.Length, JpRange.Length, CjkFallbackGlyphRange.Length);
public static readonly HashSet<float> AxisFontSizeList =
[
9.6f,
@@ -170,6 +181,42 @@ public sealed class FontManager : IDisposable
// Instance method so Ranges / JpRange are reachable without parameter
// plumbing; PascalCase field names follow the existing class style.
// B1: shared CJK + symbols tail for both the regular and italic delegate
// fonts. Earlier-merged fonts win for shared codepoints (imgui MergeMode),
// so this runs AFTER the primary font is set as config.MergeFont. The CJK
// fallback is the sole Hangul/Simplified-Han source when UseHellionFont=true
// (global=Inter-Light), so it stays in the chain — only its glyph range is
// trimmed (CjkFallbackGlyphRange) to drop the Default-block/endonym overlap.
// basePt sizes the fallback to match the primary font; the Japanese merge
// keeps its own configured size and the full JpRange (which owns Traditional
// Han such as 體 U+9AD4), so japanese↔fallback no longer overlap.
private void AddCjkAndSymbols(
IFontAtlasBuildToolkitPreBuild tk,
SafeFontConfig config,
float basePt
)
{
config.SizePt = Plugin.Config.JapaneseFontV2.SizePt;
config.GlyphRanges = JpRange;
AddFontWithFallback(tk, Plugin.Config.JapaneseFontV2.FontId, config, "japanese");
// v1.5.3: NotoSansCjk fallback covers Hangul, Simplified-Chinese-specific
// Han (e.g. 简) and other CJK glyphs the primary (Inter Light / global font)
// and the FFXIV Japanese font do not ship. B1: trimmed to CjkFallbackGlyphRange
// so it no longer re-merges the Default block. Merged last so earlier fonts win.
config.SizePt = basePt;
config.GlyphRanges = CjkFallbackGlyphRange;
AddFontWithFallback(
tk,
new DalamudAssetFontAndFamilyId(DalamudAsset.NotoSansCjkRegular),
config,
"noto-cjk-fallback"
);
config.SizePt = ResolveSymbolsFontPt();
tk.AddGameSymbol(config);
}
private IFontHandle BuildRegularFontHandle(IFontAtlas atlas) =>
atlas.NewDelegateFontHandle(e =>
e.OnPreBuild(tk =>
@@ -183,25 +230,7 @@ public sealed class FontManager : IDisposable
? tk.AddFontFromMemory(bundledBytes, config, "Inter-Light")
: AddFontWithFallback(tk, Plugin.Config.GlobalFontV2.FontId, config, "global");
config.SizePt = Plugin.Config.JapaneseFontV2.SizePt;
config.GlyphRanges = JpRange;
AddFontWithFallback(tk, Plugin.Config.JapaneseFontV2.FontId, config, "japanese");
// v1.5.3: NotoSansCjk fallback covers Hangul, Simplified-Chinese
// -specific Han (e.g. 简) and other CJK glyphs that the primary
// (Inter Light / global font) and the FFXIV Japanese font do not
// ship. Merged last so earlier fonts win for shared codepoints.
config.SizePt = basePt;
config.GlyphRanges = Ranges;
AddFontWithFallback(
tk,
new DalamudAssetFontAndFamilyId(DalamudAsset.NotoSansCjkRegular),
config,
"noto-cjk-fallback"
);
config.SizePt = ResolveSymbolsFontPt();
tk.AddGameSymbol(config);
AddCjkAndSymbols(tk, config, basePt);
tk.Font = config.MergeFont;
})
@@ -223,22 +252,7 @@ public sealed class FontManager : IDisposable
"italic"
);
config.SizePt = Plugin.Config.JapaneseFontV2.SizePt;
config.GlyphRanges = JpRange;
AddFontWithFallback(tk, Plugin.Config.JapaneseFontV2.FontId, config, "japanese");
// v1.5.3: NotoSansCjk fallback (see BuildRegularFontHandle).
config.SizePt = Plugin.Config.ItalicFontV2.SizePt;
config.GlyphRanges = Ranges;
AddFontWithFallback(
tk,
new DalamudAssetFontAndFamilyId(DalamudAsset.NotoSansCjkRegular),
config,
"noto-cjk-fallback"
);
config.SizePt = ResolveSymbolsFontPt();
tk.AddGameSymbol(config);
AddCjkAndSymbols(tk, config, Plugin.Config.ItalicFontV2.SizePt);
tk.Font = config.MergeFont;
})
@@ -282,7 +296,11 @@ public sealed class FontManager : IDisposable
private unsafe void SetUpRanges()
{
ushort[] BuildRange(IReadOnlyList<ushort>? chars, params nint[] ranges)
ushort[] BuildRange(
IReadOnlyList<ushort>? chars,
bool includeCommonExtras,
params nint[] ranges
)
{
var builder = new ImFontGlyphRangesBuilderPtr(ImGuiNative.ImFontGlyphRangesBuilder());
foreach (var range in ranges)
@@ -300,33 +318,43 @@ public sealed class FontManager : IDisposable
}
}
// Ingame supported ranges
var reader = new FdtReader(Plugin.DataManager.GetFile("common/font/axis_12.fdt")!.Data);
foreach (var c in reader.Glyphs)
builder.AddChar(c.Char);
// Common extras (Axis ingame glyphs, endonyms, enclosed alphanumerics)
// belong to the primary/Japanese ranges only. The trimmed CJK fallback
// (B1) skips them so it stays a pure Hangul/Simplified-Han remainder and
// does not re-merge the Default-block work the global font already did.
if (includeCommonExtras)
{
// Ingame supported ranges
var reader = new FdtReader(
Plugin.DataManager.GetFile("common/font/axis_12.fdt")!.Data
);
foreach (var c in reader.Glyphs)
builder.AddChar(c.Char);
// French
// Romanian
builder.AddText("Œœ");
builder.AddText("ĂăÂâÎîȘșȚț");
// French
// Romanian
builder.AddText("Œœ");
builder.AddText("ĂăÂâÎîȘșȚț");
// v1.5.3: language-dropdown endonyms. The dropdown renders
// with the currently active font range; without these glyphs
// a user on an English UI cannot read non-Latin language names
// before switching. Auto-activation in Settings.Apply then
// pulls in the full ExtraGlyphRange for the chosen locale.
builder.AddText(
"Català Čeština Dansk Deutsch Ελληνικά English Español Suomi"
+ " Français Magyar Italiano 日本語 한국어 Norsk bokmål Nederlands"
+ " Polski Português Brasil (Portugal) Română Русский Svenska"
+ " Türkçe Українська 简体中文 繁體中文"
);
// v1.5.3: language-dropdown endonyms. The dropdown renders
// with the currently active font range; without these glyphs
// a user on an English UI cannot read non-Latin language names
// before switching. Auto-activation in Settings.Apply then
// pulls in the full ExtraGlyphRange for the chosen locale.
builder.AddText(
"Català Čeština Dansk Deutsch Ελληνικά English Español Suomi"
+ " Français Magyar Italiano 日本語 한국어 Norsk bokmål Nederlands"
+ " Polski Português Brasil (Portugal) Română Русский Svenska"
+ " Türkçe Українська 简体中文 繁體中文"
);
// "Enclosed Alphanumerics" (partial) https://www.compart.com/en/unicode/block/U+2460
for (var i = 0x2460; i <= 0x24B5; i++)
builder.AddChar((char)i);
// "Enclosed Alphanumerics" (partial) https://www.compart.com/en/unicode/block/U+2460
for (var i = 0x2460; i <= 0x24B5; i++)
builder.AddChar((char)i);
builder.AddChar('⓪');
}
builder.AddChar('⓪');
return builder.BuildRangesToArray();
}
@@ -356,8 +384,17 @@ public sealed class FontManager : IDisposable
}
}
Ranges = BuildRange(customChars.Count > 0 ? customChars : null, ranges.ToArray());
JpRange = BuildRange(GlyphRangesJapanese.GlyphRanges);
Ranges = BuildRange(
customChars.Count > 0 ? customChars : null,
includeCommonExtras: true,
ranges.ToArray()
);
JpRange = BuildRange(GlyphRangesJapanese.GlyphRanges, includeCommonExtras: true);
// B1: the fallback gets only the trimmed Hangul/Simplified-Han remainder.
// No Default block, no endonyms — those are already merged by the global and
// Japanese fonts, so re-merging them on the fallback was wasted atlas work.
CjkFallbackGlyphRange = BuildRange(CjkFallbackRange.Pairs, includeCommonExtras: false);
}
// Add font with fallback to NotoSansCjkRegular if unavailable
@@ -86,6 +86,49 @@ internal sealed class FontManagerCtorSmokeStep : ISelfTestStep
return SelfTestStepResult.Fail;
}
// B1: assert the atlas actually finished building all required handles,
// not just that the references are non-null. FontsReady is the observable
// state the trimmed-fallback rebuild must still reach; a half-built atlas
// would pass the null/exception checks above but fail here.
if (!fm.FontsReady)
{
ImGui.Text("FontManager.FontsReady is false (atlas not fully built).");
SelfTestReport.Append(
Name,
"FAIL",
new[] { "FontsReady is false — atlas not fully built." }
);
return SelfTestStepResult.Fail;
}
// Report what was actually verified (Flo's request: don't just show Pass).
// The glyph-range entry counts make the B1 dedup visible — the cjk-fallback
// range is now a small trimmed remainder next to the large primary range.
var counts = fm.GlyphRangeLengths;
var italicState =
fm.ItalicFont is null ? "disabled (null)"
: fm.ItalicFont.Available ? "available"
: "NOT available";
var path = SelfTestReport.Append(
Name,
"PASS",
new[]
{
$"Axis available: {fm.Axis.Available}",
$"AxisItalic available: {fm.AxisItalic.Available}",
$"FontAwesome available: {fm.FontAwesome.Available}",
$"RegularFont available: {fm.RegularFont.Available}",
$"ItalicFont: {italicState}",
$"FontsReady: {fm.FontsReady}",
$"Glyph-range entries: primary={counts.Ranges}, jp={counts.JpRange}, "
+ $"cjk-fallback={counts.CjkFallback} (B1 trimmed)",
$"UseHellionFont={Plugin.Config.UseHellionFont}, ItalicEnabled={Plugin.Config.ItalicEnabled}",
}
);
ImGui.Text(
$"PASS — FontsReady, ranges primary={counts.Ranges}/jp={counts.JpRange}/"
+ $"cjk-fallback={counts.CjkFallback}. Report: {path}"
);
return SelfTestStepResult.Pass;
}