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