249 lines
7.8 KiB
C#
249 lines
7.8 KiB
C#
using System.Collections.Generic;
|
||
using System.Globalization;
|
||
using System.Text;
|
||
|
||
namespace AibisDream.MiniGame.Language
|
||
{
|
||
/// <summary>
|
||
/// 将玩家可见文字按 Unicode 文本元素拆分。
|
||
/// 空白保留在布局中但不生成粒子;随机字符池直接忽略空白。
|
||
/// </summary>
|
||
public static class ExpressionTextTokenizer
|
||
{
|
||
public readonly struct UnitSequence
|
||
{
|
||
public UnitSequence(List<string> units, List<float> gapWeights)
|
||
{
|
||
Units = units;
|
||
GapWeights = gapWeights;
|
||
}
|
||
|
||
public IReadOnlyList<string> Units { get; }
|
||
public IReadOnlyList<float> GapWeights { get; }
|
||
public int Count => Units?.Count ?? 0;
|
||
}
|
||
|
||
public readonly struct Layout
|
||
{
|
||
public Layout(List<string> visibleElements, List<float> offsets)
|
||
{
|
||
VisibleElements = visibleElements;
|
||
Offsets = offsets;
|
||
}
|
||
|
||
public IReadOnlyList<string> VisibleElements { get; }
|
||
public IReadOnlyList<float> Offsets { get; }
|
||
public int Count => VisibleElements?.Count ?? 0;
|
||
}
|
||
|
||
public static List<string> GetVisibleElements(string text)
|
||
{
|
||
var result = new List<string>();
|
||
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<string> BuildCharacterPool(IEnumerable<string> texts)
|
||
{
|
||
return BuildUnitPool(texts, ExpressionParticleUnitMode.Grapheme);
|
||
}
|
||
|
||
public static List<string> TokenizeText(
|
||
string text,
|
||
ExpressionParticleUnitMode mode)
|
||
{
|
||
return new List<string>(BuildLayoutUnits(text, mode).Units);
|
||
}
|
||
|
||
public static bool TryTokenizePool(
|
||
string pool,
|
||
ExpressionParticleUnitMode mode,
|
||
out List<string> units)
|
||
{
|
||
units = new List<string>();
|
||
if (string.IsNullOrWhiteSpace(pool) ||
|
||
pool.Contains("|") ||
|
||
pool.Contains("|"))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
units = TokenizeText(pool, mode);
|
||
return units.Count > 0;
|
||
}
|
||
|
||
public static List<string> BuildUnitPool(
|
||
IEnumerable<string> texts,
|
||
ExpressionParticleUnitMode mode)
|
||
{
|
||
var result = new List<string>();
|
||
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<string>();
|
||
var positions = new List<float>();
|
||
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<string> source,
|
||
IReadOnlyList<string> 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<string> source,
|
||
IReadOnlyList<string> fragment)
|
||
{
|
||
return FindVisibleSequence(source, fragment);
|
||
}
|
||
|
||
private static UnitSequence BuildGraphemeUnits(string text)
|
||
{
|
||
var units = new List<string>();
|
||
var gaps = new List<float>();
|
||
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<string>();
|
||
var gaps = new List<float>();
|
||
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<string> units,
|
||
List<float> gaps,
|
||
bool hasLeadingSeparator)
|
||
{
|
||
if (word.Length == 0)
|
||
return;
|
||
|
||
units.Add(word.ToString());
|
||
gaps.Add(units.Count == 1 ? 0f : hasLeadingSeparator ? 1f : 0f);
|
||
word.Clear();
|
||
}
|
||
}
|
||
}
|