更新换行算法和APP-PUSH
This commit is contained in:
96
client/src/features/textWrap/scoring/tieKey.ts
Normal file
96
client/src/features/textWrap/scoring/tieKey.ts
Normal 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,
|
||||
];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user