import { ALPHABET_BY_LETTER, LETTERS_UPPER } from '../../../core/data/alphabet'; /** 字母描红的布局模式(与产品文档四类练习一致) */ export type LetterTracingMode = /** 4 看图描红:教学区 + 6 行描红(前三行大写、后三行小写) */ | 'picture-tracing' /** 1 大小写配对:左栏 A–M、右栏 N–Z,每行最多 4 组 Aa */ | 'case-pairing' /** 2 分栏对照:半组字母,左栏大写、右栏小写,每行 4 个 */ | 'column-compare' /** 3 字母总览:Uppercase / Lowercase 分区,每区 9+9+8 */ | 'overview'; /** 字母大小写模式(部分模式不使用) */ export type LetterCaseMode = 'upper' | 'lower' | 'both'; /** A–M / N–Z 半表(分栏对照) */ export type AlphabetHalf = 'A-M' | 'N-Z'; /** 字母描红生成器的配置项 */ export interface LetterTracingGeneratorConfig { mode: LetterTracingMode; letterCase: LetterCaseMode; selectedLetter?: string; repetitions: number; fadePattern: 'gradient' | 'first-only'; /** 分栏对照:练习 A–M 或 N–Z */ alphabetHalf?: AlphabetHalf; } /** 单个描红格子的数据 */ export interface TracingCell { char: string; opacity: number; isGuide: boolean; /** 与 char 组成大小写一对(如 A + a),用于大小写配对格内展示 */ pairChar?: string; } /** 一行描红数据,包含展示字符和若干格子 */ export interface TracingRow { displayChar: string; cells: TracingCell[]; } /** 看图描红教学区 */ export interface TeachingBlock { upper: string; lower: string; word: string; sentence: string; emoji: string; } export type LetterTracingData = | { mode: 'picture-tracing'; teaching: TeachingBlock; rows: TracingRow[]; } | { mode: 'case-pairing'; leftVisualRows: TracingRow[][]; rightVisualRows: TracingRow[][]; } | { mode: 'column-compare'; leftRows: TracingRow[]; rightRows: TracingRow[]; } | { mode: 'overview'; upperRows: string[][]; lowerRows: string[][]; }; /** 将用户输入的字母标准化为大写单字符,默认 'A' */ export function normalizeLetter(c: string): string { const u = c.trim().toUpperCase(); return u.length ? u[0] : 'A'; } function buildCells( char: string, count: number, fadePattern: 'gradient' | 'first-only', pairChar?: string, ): TracingCell[] { const n = Math.max(1, Math.min(12, count)); const cells: TracingCell[] = []; for (let i = 0; i < n; i++) { let opacity: number; if (fadePattern === 'first-only') { opacity = i === 0 ? 1 : 0; } else if (n <= 1) { opacity = 1; } else { opacity = Math.max(0.12, 1 - i / (n - 1)); } cells.push({ char, opacity, isGuide: i === 0, ...(pairChar !== undefined ? { pairChar } : {}), }); } return cells; } function rowForChar( ch: string, repetitions: number, fadePattern: 'gradient' | 'first-only', pairChar?: string, ): TracingRow { return { displayChar: ch, cells: buildCells(ch, repetitions, fadePattern, pairChar), }; } /** 一行内多个字母,各自占一段描红格(用于分栏对照等) */ function rowForManyChars( chars: string[], repetitions: number, fadePattern: 'gradient' | 'first-only', ): TracingRow { const cells: TracingCell[] = []; for (const ch of chars) { cells.push(...buildCells(ch, repetitions, fadePattern)); } return { displayChar: chars.join(''), cells, }; } function chunk(arr: T[], size: number): T[][] { const out: T[][] = []; for (let i = 0; i < arr.length; i += size) { out.push(arr.slice(i, i + size)); } return out; } /** 总览区 26 字母拆成 9 + 9 + 8 三行 */ function overviewRowsFrom(letters: string[]): string[][] { return [letters.slice(0, 9), letters.slice(9, 18), letters.slice(18, 26)]; } /** * 根据配置生成字母描红数据 * - picture-tracing:教学区 + 6 行描红 * - case-pairing:左 A–M / 右 N–Z,每行最多 4 组大小写配对 * - column-compare:半表,左大写右小写,每行 4 个 * - overview:Uppercase / Lowercase Letters,9+9+8 */ export function generateLetterTracing( config: LetterTracingGeneratorConfig, ): LetterTracingData { const { mode, repetitions, fadePattern } = config; if (mode === 'picture-tracing') { const key = normalizeLetter(config.selectedLetter ?? 'A'); const meta = ALPHABET_BY_LETTER[key] ?? ALPHABET_BY_LETTER.A; const upper = key; const lower = key.toLowerCase(); const teaching: TeachingBlock = { upper, lower, word: meta.word, sentence: `${upper} is for ${meta.word}`, emoji: meta.emoji, }; const rows: TracingRow[] = [ rowForChar(upper, repetitions, fadePattern), rowForChar(upper, repetitions, fadePattern), rowForChar(upper, repetitions, fadePattern), rowForChar(lower, repetitions, fadePattern), rowForChar(lower, repetitions, fadePattern), rowForChar(lower, repetitions, fadePattern), ]; return { mode: 'picture-tracing', teaching, rows }; } if (mode === 'case-pairing') { const leftLetters = LETTERS_UPPER.slice(0, 13); const rightLetters = LETTERS_UPPER.slice(13); const pack = (letters: string[]) => chunk(letters, 4).map((group) => group.map((L) => rowForChar( L, repetitions, fadePattern, L.toLowerCase(), ), ), ); return { mode: 'case-pairing', leftVisualRows: pack(leftLetters), rightVisualRows: pack(rightLetters), }; } if (mode === 'column-compare') { const half = config.alphabetHalf === 'N-Z' ? LETTERS_UPPER.slice(13) : LETTERS_UPPER.slice(0, 13); const rowsOf4 = chunk(half, 4); const leftRows = rowsOf4.map((g) => rowForManyChars(g, repetitions, fadePattern), ); const rightRows = rowsOf4.map((g) => rowForManyChars( g.map((L) => L.toLowerCase()), repetitions, fadePattern, ), ); return { mode: 'column-compare', leftRows, rightRows }; } // overview return { mode: 'overview', upperRows: overviewRowsFrom([...LETTERS_UPPER]), lowerRows: overviewRowsFrom( LETTERS_UPPER.map((L) => L.toLowerCase()), ), }; } /** 与 WorksheetDefinition.generatorConfig 合并后的运行时配置 */ export function mergeLetterTracingConfig( base: Partial, overrides: Partial, ): LetterTracingGeneratorConfig { return { mode: (overrides.mode ?? base.mode ?? 'picture-tracing') as LetterTracingMode, letterCase: (overrides.letterCase ?? base.letterCase ?? 'upper') as LetterCaseMode, selectedLetter: overrides.selectedLetter ?? base.selectedLetter, repetitions: Math.max( 1, (overrides.repetitions ?? base.repetitions ?? 5) as number, ), fadePattern: (overrides.fadePattern ?? base.fadePattern ?? 'gradient') as 'gradient' | 'first-only', alphabetHalf: (overrides.alphabetHalf ?? base.alphabetHalf ?? 'A-M') as AlphabetHalf, }; }