From afccb0a8b781c7aa2bbb529dfaa2efa14d0b2397 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 17 Mar 2026 22:40:42 +0000 Subject: [PATCH 1/4] feat: add linear/log ring score mapping with tunable log strength Co-authored-by: TaylorHere --- archery/shared/layout.ts | 54 ++++++++++++++++++++++++++++-- archery/src/main.ts | 72 ++++++++++++++++++++++++++++++++++++++-- archery/src/pdf.ts | 17 ++++++++-- 3 files changed, 136 insertions(+), 7 deletions(-) diff --git a/archery/shared/layout.ts b/archery/shared/layout.ts index b770bb2..19b6677 100644 --- a/archery/shared/layout.ts +++ b/archery/shared/layout.ts @@ -2,6 +2,12 @@ export type PagePreset = 'A4' | 'A3' | 'custom'; export type TargetColorMode = 'bw' | 'color'; export type LayoutMode = 'auto_fill' | 'target_count'; export type SpacingMode = 'auto_min' | 'manual'; +export type RingScoreMappingMode = 'linear' | 'log'; + +export interface RingScoreMappingConfig { + mode?: RingScoreMappingMode; + logStrength?: number; +} export interface LayoutInput { pageWidthMm: number; @@ -36,6 +42,8 @@ export interface ValidationResult { const EPS = 1e-7; const COLOR_RING_OUTER = '#000000'; const COLOR_RING_CENTER = '#facc15'; +const DEFAULT_RING_SCORE_LOG_STRENGTH = 20; +export const RING_SCORE_LOG_STRENGTH_DEFAULT = DEFAULT_RING_SCORE_LOG_STRENGTH; export const PAGE_PRESETS_MM: Record, { width: number; height: number }> = { A4: { width: 210, height: 297 }, @@ -318,7 +326,11 @@ export function ringColorByIndex(ringIndexFromOuter: number, startWithBlack: boo return isBlack ? '#000000' : '#FFFFFF'; } -export function ringScoreByIndex(ringIndexFromOuter: number, ringCount: number): number { +export function ringScoreByIndex( + ringIndexFromOuter: number, + ringCount: number, + config?: RingScoreMappingConfig, +): number { const safeRingCount = Math.max(1, Math.round(ringCount)); const safeIndex = Math.min(safeRingCount - 1, Math.max(0, Math.round(ringIndexFromOuter))); if (safeRingCount === 1) { @@ -330,7 +342,11 @@ export function ringScoreByIndex(ringIndexFromOuter: number, ringCount: number): if (safeIndex === safeRingCount - 1) { return 10; } - return Math.floor(1 + (safeIndex / (safeRingCount - 1)) * 9); + + const progress = safeIndex / (safeRingCount - 1); + const mappedProgress = mapRingScoreProgress(progress, config); + const mappedScore = Math.floor(1 + mappedProgress * 9); + return Math.max(1, Math.min(9, mappedScore)); } export function ringFillColor( @@ -338,6 +354,7 @@ export function ringFillColor( ringCount: number, colorMode: TargetColorMode, startWithBlack: boolean, + scoreMappingConfig?: RingScoreMappingConfig, ): string { if (colorMode === 'bw') { return ringColorByIndex(ringIndexFromOuter, startWithBlack); @@ -358,7 +375,7 @@ export function ringFillColor( // 按“分数段 -> 箭靶色板”映射,不做插值: // 9-10 黄,7-8 红,5-6 蓝,1-4 黑。 - const score = ringScoreByIndex(safeIndex, safeRingCount); + const score = ringScoreByIndex(safeIndex, safeRingCount, scoreMappingConfig); if (score >= 9) { return '#facc15'; } @@ -420,6 +437,37 @@ function normalizeSpacingMode(mode?: SpacingMode): SpacingMode { return mode === 'manual' ? 'manual' : 'auto_min'; } +function mapRingScoreProgress(progress: number, config?: RingScoreMappingConfig): number { + const safeProgress = Math.max(0, Math.min(1, progress)); + const mode = normalizeRingScoreMappingMode(config?.mode); + if (mode === 'linear') { + return safeProgress; + } + + const logStrength = normalizeRingScoreLogStrength(config?.logStrength); + if (logStrength <= EPS) { + return safeProgress; + } + + const curve = (logStrength / 100) * 99; + if (curve <= EPS) { + return safeProgress; + } + return Math.log1p(safeProgress * curve) / Math.log1p(curve); +} + +function normalizeRingScoreMappingMode(mode?: RingScoreMappingMode): RingScoreMappingMode { + return mode === 'log' ? 'log' : 'linear'; +} + +function normalizeRingScoreLogStrength(raw?: number): number { + const value = Number(raw); + if (!Number.isFinite(value)) { + return DEFAULT_RING_SCORE_LOG_STRENGTH; + } + return Math.min(100, Math.max(0, value)); +} + function solveManualSpacingLayout(input: LayoutInput): LayoutSolution | null { const W = input.pageWidthMm; const H = input.pageHeightMm; diff --git a/archery/src/main.ts b/archery/src/main.ts index 94550fb..1b2a300 100644 --- a/archery/src/main.ts +++ b/archery/src/main.ts @@ -4,10 +4,13 @@ import { diagnoseNoLayoutReason, type LayoutMode, PAGE_PRESETS_MM, + RING_SCORE_LOG_STRENGTH_DEFAULT, getTargetCenterMm, ringFillColor, ringScoreByIndex, ringScoreTextColor, + type RingScoreMappingConfig, + type RingScoreMappingMode, solveOptimalLayout, suggestNearbyFeasibleLayouts, type LayoutInput, @@ -95,6 +98,20 @@ app.innerHTML = ` + + + + -