更新换行算法和APP-PUSH

This commit is contained in:
吕新雨
2026-02-10 11:39:33 +08:00
parent f03d36b5e9
commit ee2d9f44ea
105 changed files with 9967 additions and 233 deletions

View File

@@ -0,0 +1,96 @@
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,
];
}