Rework layout engine around exact-glyph collision, add tests and docs sync

Replace the old bbox/heuristic placement (scale search rounds, large-font
capping, stratified sampling, fill-retry ladders) with an area-model font
sizing pass feeding a C++ exact-glyph collision engine (centroid-biased
spiral + random probing, HD clearance refinement, density/hole
optimization). Simplify the frontend advanced-params panel and JobParams
type to match the surviving config surface, add a layout-constraints test
suite and a repeatable benchmark tool, and bring docs/*.md back in sync
with current code (plus new TESTING.md and DEPLOYMENT.md).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-26 18:32:25 +08:00
co-authored by Claude Sonnet 5
parent bf2b138007
commit 1d17b5e20d
24 changed files with 2600 additions and 1283 deletions
+8 -246
View File
@@ -196,7 +196,7 @@ export default function AdvancedPanel({
min={0.1}
max={1}
step={0.01}
onChange={v => onParamsChange({ packingEfficiency: v ?? 0.85 })}
onChange={v => onParamsChange({ packingEfficiency: v ?? 0.9 })}
/>
</div>
<Hint>SIZE_RATIO 2.0</Hint>
@@ -220,13 +220,6 @@ export default function AdvancedPanel({
/>
</div>
<div className="form-row">
<NumberField
label="字号下限 MIN_FONT_FLOOR"
value={params.minFontFloor}
min={1}
step={1}
onChange={v => onParamsChange({ minFontFloor: Math.max(1, Math.round(v ?? 2)) })}
/>
<NumberField
label="可读高度(px)"
value={params.minReadableHeightPx}
@@ -242,7 +235,7 @@ export default function AdvancedPanel({
min={0.05}
max={1}
step={0.01}
onChange={v => onParamsChange({ workScale: v ?? 0.25 })}
onChange={v => onParamsChange({ workScale: v ?? 0.2 })}
/>
<NumberField
label="目标填充率 TARGET_FILL"
@@ -250,10 +243,10 @@ export default function AdvancedPanel({
min={0}
max={1}
step={0.01}
onChange={v => onParamsChange({ targetFillRatio: v ?? 0 })}
onChange={v => onParamsChange({ targetFillRatio: v ?? 0.45 })}
/>
</div>
<Hint>TARGET_FILL_RATIO=0 </Hint>
<Hint> 0.300.55TARGET_FILL </Hint>
<div className="section-divider" />
<SectionTitle></SectionTitle>
@@ -263,229 +256,9 @@ export default function AdvancedPanel({
onChange={v => onParamsChange({ strokeWeights: v })}
hint="开启后笔画复杂的字更大;关闭则更接近均等字号"
/>
<div className="form-row">
<NumberField
label="对数权重 LOG_WEIGHT_RATIO"
value={params.logWeightRatio}
min={0}
max={1}
step={0.01}
onChange={v => onParamsChange({ logWeightRatio: v ?? 0.72 })}
/>
<NumberField
label="排序权重 RANK_WEIGHT_RATIO"
value={params.rankWeightRatio}
min={0}
max={1}
step={0.01}
onChange={v => onParamsChange({ rankWeightRatio: v ?? 0.28 })}
/>
</div>
<Hint> 1.0 0.72 / 0.28</Hint>
<div className="section-divider" />
<SectionTitle></SectionTitle>
<BoolField
label="智能减少大字数量"
checked={params.enableSmartLargeFontReduction}
onChange={v => onParamsChange({ enableSmartLargeFontReduction: v })}
hint="填不满时自动减少大字,给小词腾空间"
/>
<BoolField
label="限制大字比例 LIMIT_LARGE_FONTS"
checked={params.limitLargeFonts}
onChange={v => onParamsChange({ limitLargeFonts: v })}
/>
<div className="form-row">
<NumberField
label="大字允许占比"
value={params.largeFontLimitRatio}
min={0}
max={1}
step={0.01}
onChange={v => onParamsChange({ largeFontLimitRatio: v ?? 0.2 })}
/>
<NumberField
label="大字判定阈值"
value={params.largeFontThresholdRatio}
min={0}
max={1}
step={0.01}
onChange={v => onParamsChange({ largeFontThresholdRatio: v ?? 0.8 })}
/>
</div>
<NumberField
label="大字压缩系数 LARGE_FONT_CAP_RATIO"
value={params.largeFontCapRatio}
min={0.1}
max={1}
step={0.01}
onChange={v => onParamsChange({ largeFontCapRatio: v ?? 0.6 })}
/>
<Hint> 20% 80% 60%</Hint>
<div className="section-divider" />
<SectionTitle></SectionTitle>
<div className="form-row">
<NumberField
label="缩放下限 FONT_SCALE_MIN"
value={params.fontScaleMin}
min={0.05}
max={2}
step={0.05}
onChange={v => onParamsChange({ fontScaleMin: v ?? 0.5 })}
/>
<NumberField
label="缩放上限 FONT_SCALE_MAX"
value={params.fontScaleMax}
min={0.1}
max={3}
step={0.05}
onChange={v => onParamsChange({ fontScaleMax: v ?? 1.2 })}
/>
</div>
<div className="form-row">
<NumberField
label="搜索步数"
value={params.scaleSearchSteps}
min={1}
step={1}
onChange={v => onParamsChange({ scaleSearchSteps: Math.max(1, Math.round(v ?? 7)) })}
/>
<NumberField
label="搜索轮数"
value={params.scaleSearchRounds}
min={1}
step={1}
onChange={v => onParamsChange({ scaleSearchRounds: Math.max(1, Math.round(v ?? 5)) })}
/>
</div>
<div className="form-row">
<NumberField
label="衰减 SCALE_DECAY"
value={params.scaleDecay}
min={0.1}
max={1}
step={0.01}
onChange={v => onParamsChange({ scaleDecay: v ?? 0.85 })}
/>
<NumberField
label="地板 SCALE_FLOOR"
value={params.scaleFloor}
min={0.05}
max={1}
step={0.01}
onChange={v => onParamsChange({ scaleFloor: v ?? 0.25 })}
/>
</div>
<div className="form-row">
<NumberField
label="自动收缩轮数"
value={params.autoShrinkRounds}
min={0}
step={1}
onChange={v => onParamsChange({ autoShrinkRounds: Math.max(0, Math.round(v ?? 4)) })}
/>
<div className="form-group" style={{ justifyContent: 'center' }}>
<BoolField
label="要求全部词语放入"
checked={params.requireAllWords}
onChange={v => onParamsChange({ requireAllWords: v })}
/>
</div>
</div>
<div className="section-divider" />
<SectionTitle></SectionTitle>
<BoolField
label="分层采样(边缘覆盖)"
checked={params.enableStratifiedSampling}
onChange={v => onParamsChange({ enableStratifiedSampling: v })}
/>
<NumberField
label="分层条带数 STRATIFIED_BANDS"
value={params.stratifiedBands}
min={1}
max={10}
step={1}
onChange={v => onParamsChange({ stratifiedBands: Math.max(1, Math.round(v ?? 3)) })}
/>
<div className="form-row">
<NumberField
label="最低接受填充率"
value={params.minAcceptFillRatio}
min={0}
max={1}
step={0.01}
onChange={v => onParamsChange({ minAcceptFillRatio: v ?? 0.75 })}
/>
<NumberField
label="填充重试轮数"
value={params.fillRetryMaxRounds}
min={0}
step={1}
onChange={v => onParamsChange({ fillRetryMaxRounds: Math.max(0, Math.round(v ?? 3)) })}
/>
</div>
<div className="form-row">
<NumberField
label="重试最大缩放"
value={params.fillRetryMaxScale}
min={1}
max={3}
step={0.05}
onChange={v => onParamsChange({ fillRetryMaxScale: v ?? 1.5 })}
/>
<NumberField
label="低填充增字号步长"
value={params.growFontStep}
min={1}
max={2}
step={0.01}
onChange={v => onParamsChange({ growFontStep: v ?? 1.05 })}
/>
</div>
<BoolField
label="填充重试时放宽大字限制"
checked={params.fillRetryRelaxLargeCap}
onChange={v => onParamsChange({ fillRetryRelaxLargeCap: v })}
/>
<BoolField
label="低填充时略增字号 GROW_FONT_ON_LOW_FILL"
checked={params.growFontOnLowFill}
onChange={v => onParamsChange({ growFontOnLowFill: v })}
hint="后端默认关闭"
/>
<div className="section-divider" />
<SectionTitle></SectionTitle>
<BoolField
label="自动扩展画布 AUTO_EXPAND_CANVAS"
checked={params.autoExpandCanvas}
onChange={v => onParamsChange({ autoExpandCanvas: v })}
/>
<BoolField
label="螺旋填充扩展 EXPAND_FOR_SPIRAL"
checked={params.expandForSpiral}
onChange={v => onParamsChange({ expandForSpiral: v })}
/>
<div className="form-row">
<NumberField
label="扩展倍率 EXPAND_RATIO"
value={params.expandRatio}
min={1}
max={5}
step={0.1}
onChange={v => onParamsChange({ expandRatio: v ?? 2.5 })}
/>
<NumberField
label="最大尝试次数"
value={params.maxAttempts}
min={1}
step={1}
onChange={v => onParamsChange({ maxAttempts: Math.max(1, Math.round(v ?? 5)) })}
/>
</div>
<div className="form-row">
<NumberField
label="画布重试轮数"
@@ -494,6 +267,8 @@ export default function AdvancedPanel({
step={1}
onChange={v => onParamsChange({ canvasRetryMaxRounds: Math.max(0, Math.round(v ?? 1)) })}
/>
</div>
<div className="form-row">
<NumberField
label="画布重试增长"
value={params.canvasRetryGrowth}
@@ -506,19 +281,6 @@ export default function AdvancedPanel({
<div className="section-divider" />
<SectionTitle></SectionTitle>
<div className="form-group">
<label className="form-label"> LAYOUT_ORDER_MODE</label>
<select
className="form-input"
value={params.layoutOrderMode}
onChange={e =>
onParamsChange({ layoutOrderMode: e.target.value as JobParams['layoutOrderMode'] })
}
>
<option value="SORTED">SORTED</option>
<option value="INTERLEAVED_RANDOM">INTERLEAVED_RANDOM</option>
</select>
</div>
<NumberField
label="布局种子 LAYOUT_SEED"
value={params.layoutSeed}
@@ -531,8 +293,8 @@ export default function AdvancedPanel({
<div className="section-divider" />
<p className="text-xs text-muted" style={{ lineHeight: 1.6 }}>
<code style={{ fontSize: 10 }}>backend/core/config.py</code>
<strong>SIZE_RATIO</strong><strong></strong> {' '}
<strong></strong>
<strong>SIZE_RATIO</strong><strong></strong> {' '}
<strong></strong>
</p>
</div>
</>
+4 -72
View File
@@ -48,58 +48,19 @@ const DEFAULT_PARAMS: JobParams = {
// 字号与填充
sizeRatio: 2.0,
packingEfficiency: 0.85,
targetFillRatio: 0.0,
packingEfficiency: 0.9,
targetFillRatio: 0.45,
userMinFontSize: null,
userMaxFontSize: null,
minFontFloor: 2,
minReadableHeightPx: 25,
workScale: 0.25,
// 字号搜索
fontScaleMin: 0.5,
fontScaleMax: 1.2,
scaleSearchSteps: 7,
scaleSearchRounds: 5,
scaleDecay: 0.85,
scaleFloor: 0.25,
autoShrinkRounds: 4,
requireAllWords: true,
// 权重混合
logWeightRatio: 0.72,
rankWeightRatio: 0.28,
// 大字限制
enableSmartLargeFontReduction: true,
limitLargeFonts: true,
largeFontLimitRatio: 0.2,
largeFontThresholdRatio: 0.8,
largeFontCapRatio: 0.6,
// 分层采样
enableStratifiedSampling: true,
stratifiedBands: 3,
// 填充重试
minAcceptFillRatio: 0.75,
fillRetryRelaxLargeCap: true,
fillRetryMaxRounds: 3,
fillRetryMaxScale: 1.5,
growFontOnLowFill: false,
growFontStep: 1.05,
minReadableHeightPx: 22,
workScale: 0.18,
// 画布
autoExpandCanvas: true,
expandForSpiral: true,
expandRatio: 2.5,
fillOn: 'BLACK',
canvasRetryMaxRounds: 1,
canvasRetryGrowth: 1.12,
maxAttempts: 5,
// 布局可复现
layoutOrderMode: 'SORTED',
layoutSeed: null,
};
@@ -469,40 +430,11 @@ export default function TestWorkbench({
SIZE_RATIO: params.sizeRatio,
PACKING_EFFICIENCY: params.packingEfficiency,
TARGET_FILL_RATIO: params.targetFillRatio,
MIN_FONT_FLOOR: params.minFontFloor,
MIN_READABLE_HEIGHT_PX: params.minReadableHeightPx,
WORK_SCALE: params.workScale,
FONT_SCALE_MIN: params.fontScaleMin,
FONT_SCALE_MAX: params.fontScaleMax,
SCALE_SEARCH_STEPS: params.scaleSearchSteps,
SCALE_SEARCH_ROUNDS: params.scaleSearchRounds,
SCALE_DECAY: params.scaleDecay,
SCALE_FLOOR: params.scaleFloor,
AUTO_SHRINK_ROUNDS: params.autoShrinkRounds,
REQUIRE_ALL_WORDS: params.requireAllWords,
LOG_WEIGHT_RATIO: params.logWeightRatio,
RANK_WEIGHT_RATIO: params.rankWeightRatio,
ENABLE_SMART_LARGE_FONT_REDUCTION: params.enableSmartLargeFontReduction,
LIMIT_LARGE_FONTS: params.limitLargeFonts,
LARGE_FONT_LIMIT_RATIO: params.largeFontLimitRatio,
LARGE_FONT_THRESHOLD_RATIO: params.largeFontThresholdRatio,
LARGE_FONT_CAP_RATIO: params.largeFontCapRatio,
ENABLE_STRATIFIED_SAMPLING: params.enableStratifiedSampling,
STRATIFIED_BANDS: params.stratifiedBands,
MIN_ACCEPT_FILL_RATIO: params.minAcceptFillRatio,
FILL_RETRY_RELAX_LARGE_CAP: params.fillRetryRelaxLargeCap,
FILL_RETRY_MAX_ROUNDS: params.fillRetryMaxRounds,
FILL_RETRY_MAX_SCALE: params.fillRetryMaxScale,
GROW_FONT_ON_LOW_FILL: params.growFontOnLowFill,
GROW_FONT_STEP: params.growFontStep,
AUTO_EXPAND_CANVAS: params.autoExpandCanvas,
EXPAND_FOR_SPIRAL: params.expandForSpiral,
EXPAND_RATIO: params.expandRatio,
FILL_ON: params.fillOn,
CANVAS_RETRY_MAX_ROUNDS: params.canvasRetryMaxRounds,
CANVAS_RETRY_GROWTH: params.canvasRetryGrowth,
MAX_ATTEMPTS: params.maxAttempts,
LAYOUT_ORDER_MODE: params.layoutOrderMode,
};
if (params.seed !== null) {
paramsObj.SEED = params.seed;
-39
View File
@@ -25,54 +25,15 @@ export interface JobParams {
targetFillRatio: number;
userMinFontSize: number | null;
userMaxFontSize: number | null;
minFontFloor: number;
minReadableHeightPx: number;
workScale: number;
// 字号搜索
fontScaleMin: number;
fontScaleMax: number;
scaleSearchSteps: number;
scaleSearchRounds: number;
scaleDecay: number;
scaleFloor: number;
autoShrinkRounds: number;
requireAllWords: boolean;
// 权重混合
logWeightRatio: number;
rankWeightRatio: number;
// 大字限制
enableSmartLargeFontReduction: boolean;
limitLargeFonts: boolean;
largeFontLimitRatio: number;
largeFontThresholdRatio: number;
largeFontCapRatio: number;
// 分层采样
enableStratifiedSampling: boolean;
stratifiedBands: number;
// 填充重试
minAcceptFillRatio: number;
fillRetryRelaxLargeCap: boolean;
fillRetryMaxRounds: number;
fillRetryMaxScale: number;
growFontOnLowFill: boolean;
growFontStep: number;
// 画布
autoExpandCanvas: boolean;
expandForSpiral: boolean;
expandRatio: number;
fillOn: 'BLACK' | 'WHITE';
canvasRetryMaxRounds: number;
canvasRetryGrowth: number;
maxAttempts: number;
// 布局可复现
layoutOrderMode: 'SORTED' | 'INTERLEAVED_RANDOM';
layoutSeed: number | null;
}