using System.Collections.Generic; using System.Globalization; using System.Text; namespace AibisDream.MiniGame.Language { /// /// 将玩家可见文字按 Unicode 文本元素拆分。 /// 空白保留在布局中但不生成粒子;随机字符池直接忽略空白。 /// public static class ExpressionTextTokenizer { public readonly struct UnitSequence { public UnitSequence(List units, List gapWeights) { Units = units; GapWeights = gapWeights; } public IReadOnlyList Units { get; } public IReadOnlyList GapWeights { get; } public int Count => Units?.Count ?? 0; } public readonly struct Layout { public Layout(List visibleElements, List offsets) { VisibleElements = visibleElements; Offsets = offsets; } public IReadOnlyList VisibleElements { get; } public IReadOnlyList Offsets { get; } public int Count => VisibleElements?.Count ?? 0; } public static List GetVisibleElements(string text) { var result = new List(); if (string.IsNullOrEmpty(text)) return result; TextElementEnumerator enumerator = StringInfo.GetTextElementEnumerator(text); while (enumerator.MoveNext()) { string element = enumerator.GetTextElement(); if (!string.IsNullOrWhiteSpace(element)) result.Add(element); } return result; } public static List BuildCharacterPool(IEnumerable texts) { return BuildUnitPool(texts, ExpressionParticleUnitMode.Grapheme); } public static List TokenizeText( string text, ExpressionParticleUnitMode mode) { return new List(BuildLayoutUnits(text, mode).Units); } public static bool TryTokenizePool( string pool, ExpressionParticleUnitMode mode, out List units) { units = new List(); if (string.IsNullOrWhiteSpace(pool) || pool.Contains("|") || pool.Contains("|")) { return false; } units = TokenizeText(pool, mode); return units.Count > 0; } public static List BuildUnitPool( IEnumerable texts, ExpressionParticleUnitMode mode) { var result = new List(); if (texts == null) return result; foreach (string text in texts) result.AddRange(TokenizeText(text, mode)); return result; } public static UnitSequence BuildLayoutUnits( string text, ExpressionParticleUnitMode mode) { return mode == ExpressionParticleUnitMode.Word ? BuildWordUnits(text) : BuildGraphemeUnits(text); } public static Layout BuildLayout( string text, float characterSpacing, float whitespaceSpacingMultiplier) { var visible = new List(); var positions = new List(); if (string.IsNullOrEmpty(text)) return new Layout(visible, positions); float spacing = characterSpacing > 0f ? characterSpacing : 1f; float whitespaceAdvance = spacing * whitespaceSpacingMultiplier; float cursor = 0f; TextElementEnumerator enumerator = StringInfo.GetTextElementEnumerator(text); while (enumerator.MoveNext()) { string element = enumerator.GetTextElement(); if (string.IsNullOrWhiteSpace(element)) { if (visible.Count > 0) cursor += whitespaceAdvance; continue; } visible.Add(element); positions.Add(cursor); cursor += spacing; } if (positions.Count > 0) { float center = (positions[0] + positions[positions.Count - 1]) * 0.5f; for (int i = 0; i < positions.Count; i++) positions[i] -= center; } return new Layout(visible, positions); } public static int FindVisibleSequence( IReadOnlyList source, IReadOnlyList fragment) { if (source == null || fragment == null || fragment.Count == 0 || fragment.Count > source.Count) return -1; int lastStart = source.Count - fragment.Count; for (int start = 0; start <= lastStart; start++) { bool matches = true; for (int i = 0; i < fragment.Count; i++) { if (!string.Equals(source[start + i], fragment[i], System.StringComparison.Ordinal)) { matches = false; break; } } if (matches) return start; } return -1; } public static int FindUnitSequence( IReadOnlyList source, IReadOnlyList fragment) { return FindVisibleSequence(source, fragment); } private static UnitSequence BuildGraphemeUnits(string text) { var units = new List(); var gaps = new List(); if (string.IsNullOrEmpty(text)) return new UnitSequence(units, gaps); float pendingGap = 0f; TextElementEnumerator enumerator = StringInfo.GetTextElementEnumerator(text); while (enumerator.MoveNext()) { string element = enumerator.GetTextElement(); if (string.IsNullOrWhiteSpace(element)) { if (units.Count > 0) pendingGap += 1f; continue; } units.Add(element); gaps.Add(units.Count == 1 ? 0f : pendingGap); pendingGap = 0f; } return new UnitSequence(units, gaps); } private static UnitSequence BuildWordUnits(string text) { var units = new List(); var gaps = new List(); if (string.IsNullOrEmpty(text)) return new UnitSequence(units, gaps); var word = new StringBuilder(); bool sawSeparatorAfterUnit = false; foreach (char value in text) { if (char.IsWhiteSpace(value)) { FlushWord(word, units, gaps, sawSeparatorAfterUnit); if (units.Count > 0) sawSeparatorAfterUnit = true; continue; } word.Append(value); } FlushWord(word, units, gaps, sawSeparatorAfterUnit); return new UnitSequence(units, gaps); } private static void FlushWord( StringBuilder word, List units, List gaps, bool hasLeadingSeparator) { if (word.Length == 0) return; units.Add(word.ToString()); gaps.Add(units.Count == 1 ? 0f : hasLeadingSeparator ? 1f : 0f); word.Clear(); } } }