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 { 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, ]; }