using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.TextCore.Text; using UnityEngine.UIElements; namespace KernelPanic.UI { /// /// Fits monospace ASCII art into a fixed UI Toolkit label region. /// public sealed class AsciiArtFitter { private const float ReferenceFontSize = 20f; private const float LineHeightRatio = 1.05f; private const float MinFontSize = 6f; private const float MaxFontSize = 24f; private const float MonospaceTolerance = 0.02f; private static readonly Dictionary AdvanceRatioByFont = new(); private static readonly HashSet WarnedFonts = new(); private static bool hasDefaultAdvanceRatio; private static float defaultAdvanceRatio; private readonly Label label; private readonly FontAsset font; private string art; public AsciiArtFitter(Label label, FontAsset font) { this.label = label; this.font = font; label.RegisterCallback(_ => Fit()); } public void SetArt(string value) { art = value; Fit(); } private void Fit() { if (string.IsNullOrWhiteSpace(art)) { return; } Rect region = label.layout; if (!IsUsable(region.width) || !IsUsable(region.height)) { return; } GetArtDimensions(art, out int rows, out int columns); if (rows <= 0 || columns <= 0) { return; } float advanceRatio = GetAdvanceRatio(); float widthFit = region.width / (columns * advanceRatio); float heightFit = region.height / (rows * LineHeightRatio); float fontSize = Mathf.Clamp(Mathf.Floor(Mathf.Min(widthFit, heightFit)), MinFontSize, MaxFontSize); float blockWidth = columns * fontSize * advanceRatio; float blockHeight = rows * fontSize * LineHeightRatio; label.style.fontSize = fontSize; label.style.paddingLeft = Mathf.Max(0f, (region.width - blockWidth) * 0.5f); label.style.paddingTop = Mathf.Max(0f, (region.height - blockHeight) * 0.5f); label.style.paddingRight = 0f; label.style.paddingBottom = 0f; } private float GetAdvanceRatio() { if (font == null && hasDefaultAdvanceRatio) { return defaultAdvanceRatio; } if (font != null && AdvanceRatioByFont.TryGetValue(font, out float cached)) { return cached; } float previousFontSize = label.resolvedStyle.fontSize; label.style.fontSize = ReferenceFontSize; float mWidth = label.MeasureTextSize("MMMMMMMMMM", 0f, VisualElement.MeasureMode.Undefined, 0f, VisualElement.MeasureMode.Undefined).x; float iWidth = label.MeasureTextSize("iiiiiiiiii", 0f, VisualElement.MeasureMode.Undefined, 0f, VisualElement.MeasureMode.Undefined).x; label.style.fontSize = previousFontSize; float advanceRatio = mWidth > 0f ? mWidth / (10f * ReferenceFontSize) : 0.6f; if (font == null) { defaultAdvanceRatio = advanceRatio; hasDefaultAdvanceRatio = true; } else { AdvanceRatioByFont[font] = advanceRatio; } if (font != null && mWidth > 0f && iWidth > 0f) { float difference = Mathf.Abs(mWidth - iWidth) / Mathf.Max(mWidth, iWidth); if (difference > MonospaceTolerance && WarnedFonts.Add(font)) { Debug.LogWarning($"ASCII art font '{font.name}' is not monospace; art may misalign."); } } return advanceRatio; } private static void GetArtDimensions(string value, out int rows, out int columns) { string normalized = value.Replace("\r\n", "\n").Replace('\r', '\n'); if (normalized.EndsWith("\n", StringComparison.Ordinal)) { normalized = normalized[..^1]; } rows = 0; columns = 0; string[] lines = normalized.Split('\n'); for (int i = 0; i < lines.Length; i++) { rows++; columns = Mathf.Max(columns, lines[i].Length); } } private static bool IsUsable(float value) { return value > 0f && !float.IsNaN(value) && !float.IsInfinity(value); } } }