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:
@@ -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.30–0.55;TARGET_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>
|
||||
</>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user