97 lines
3.3 KiB
TypeScript
97 lines
3.3 KiB
TypeScript
import type { Lang } from '../core/types';
|
||
|
||
import type { LayoutCandidate, LayoutCandidateLine, ScoredLayout, TextWrapContext } from './types';
|
||
|
||
function asInt(n: number): number {
|
||
return n | 0;
|
||
}
|
||
|
||
function roundToInt(n: number): number {
|
||
return Math.round(n);
|
||
}
|
||
|
||
function lineWidthOrApprox(line: LayoutCandidateLine, lang: Lang): number {
|
||
if (typeof line.width === 'number' && Number.isFinite(line.width)) return roundToInt(line.width);
|
||
return lang === 'EN' ? asInt(line.tokenCount) : asInt(line.charCount);
|
||
}
|
||
|
||
function computeIdealBreakPositions(nTokens: number, lineCount: number): number[] {
|
||
const n = Math.max(0, nTokens | 0);
|
||
const targetLines = Math.max(1, Math.min(lineCount | 0, n === 0 ? 1 : n));
|
||
const ideals: number[] = [];
|
||
|
||
for (let i = 1; i <= targetLines - 1; i++) {
|
||
ideals.push(Math.round((n * i) / targetLines));
|
||
}
|
||
|
||
ideals.sort((a, b) => a - b);
|
||
return ideals.filter((v, idx) => idx === 0 || v !== ideals[idx - 1]);
|
||
}
|
||
|
||
function sumAbs(a: number[], b: number[]): number {
|
||
const m = Math.min(a.length, b.length);
|
||
let s = 0;
|
||
for (let i = 0; i < m; i++) s += Math.abs((a[i] ?? 0) - (b[i] ?? 0));
|
||
// 若长度不等,用一个确定性惩罚补齐(避免 NaN,并让更“匹配理想结构”的更优)
|
||
if (a.length !== b.length) s += 100000 * Math.abs(a.length - b.length);
|
||
return s;
|
||
}
|
||
|
||
function computeSpread(widths: number[]): number {
|
||
if (widths.length === 0) return 0;
|
||
let min = widths[0]!;
|
||
let max = widths[0]!;
|
||
for (const w of widths) {
|
||
if (w < min) min = w;
|
||
if (w > max) max = w;
|
||
}
|
||
return max - min;
|
||
}
|
||
|
||
export type BuildTieKeyInput = {
|
||
scoredLayout: ScoredLayout;
|
||
layoutCandidate: LayoutCandidate;
|
||
lang: Lang;
|
||
context: TextWrapContext;
|
||
/** tokens.length(用于理想切分点距离;必须与 breaks/lines 的 token 体系一致) */
|
||
tokenCount: number;
|
||
/** 可用宽度(用于理想切分点/尾行宽度判断的辅助项;tieKey 主要使用 width 值) */
|
||
availableWidth: number;
|
||
idealWidthRatio: { APP: number; WIDGET: number };
|
||
};
|
||
|
||
/**
|
||
* 构造 tieKey(按文档 11 节顺序,必须确定性):
|
||
* 1) emotionSplit=false 优先
|
||
* 2) overflowed=false 优先
|
||
* 3) lastLineWidth 更大优先
|
||
* 4) 行宽分布更均匀优先(max-min 更小)
|
||
* 5) 断点更接近理想切分点优先(距离之和更小)
|
||
* 6) breaks 字典序更靠前优先(在外部比较中按逐项数值比较即可)
|
||
*/
|
||
export function buildTieKey(args: BuildTieKeyInput): Array<number | string> {
|
||
const { scoredLayout, layoutCandidate } = args;
|
||
const lines = layoutCandidate.lines ?? [];
|
||
const breaks = (layoutCandidate.breaks ?? []).slice().sort((a, b) => a - b);
|
||
|
||
const widths = lines.map((l) => lineWidthOrApprox(l, args.lang));
|
||
const lastLineWidth = widths.length > 0 ? widths[widths.length - 1]! : 0;
|
||
const spread = computeSpread(widths);
|
||
|
||
const ideals = computeIdealBreakPositions(args.tokenCount, lines.length);
|
||
const idealDist = sumAbs(breaks, ideals);
|
||
|
||
const emotionSplitFlag = scoredLayout.flags.emotionSplit ? 1 : 0;
|
||
const overflowedFlag = scoredLayout.flags.overflowed ? 1 : 0;
|
||
|
||
return [
|
||
emotionSplitFlag,
|
||
overflowedFlag,
|
||
-lastLineWidth, // 更大优先 => 取负数实现“更小更优”
|
||
spread, // 更小更优
|
||
idealDist, // 更小更优
|
||
...breaks,
|
||
];
|
||
}
|
||
|