692 lines
22 KiB
TypeScript
692 lines
22 KiB
TypeScript
import { BaseDrawService } from '../../core/draw/baseDraw';
|
||
import ColorShapeMatchDraw from '../shared/service/colorShapeMatchDraw';
|
||
import ShapeSymbolDraw from '../shared/service/shapeSymbolDraw';
|
||
import PositionColoringDraw from '../shared/service/positionColoringDraw';
|
||
import ColorPatternDraw from '../shared/service/colorPatternDraw';
|
||
import MatchConnectDraw from '../shared/service/matchConnectDraw';
|
||
import LineRecognitionDraw from '../shared/service/lineRecognitionDraw';
|
||
import GridReasoningDraw, {
|
||
type GridPosition,
|
||
} from '../shared/service/gridReasoningDraw';
|
||
import CodeConnectDraw from '../shared/service/codeConnectDraw';
|
||
import DotConnectDraw from '../shared/service/dotConnectDraw';
|
||
import GridDraw, { type GridGroup } from '../shared/service/gridDraw';
|
||
|
||
import {
|
||
getRandomUniqueNumberColors,
|
||
getNumberColors,
|
||
getRandomNumberColor,
|
||
NUMBER_COLORS,
|
||
WATER_COLORS,
|
||
} from '../../constants/colors';
|
||
import { SHAPE_SYMBOL_SHAPES } from '../shared/shapes/shapeSymbolShapes';
|
||
import { generateCompletePattern } from '../shared/utils/gridUtils';
|
||
import { ALL_3X3_SHAPES } from '../shared/shapes/gridShapes3x3';
|
||
import { ALL_5X5_SHAPES } from '../shared/shapes/gridShapes5x5';
|
||
import { ALL_7X7_SHAPES } from '../shared/shapes/gridShapes7x7';
|
||
import type {
|
||
ShapeTemplate,
|
||
GridCell,
|
||
GridConfig,
|
||
} from '../shared/types/gridTypes';
|
||
|
||
// ─── Types ───
|
||
|
||
export interface FocusTypeAction {
|
||
value: string;
|
||
label: string;
|
||
}
|
||
|
||
export interface FocusTypeConfig {
|
||
id: string;
|
||
title: string;
|
||
subTitle: string;
|
||
icon: string;
|
||
actionsTitle?: string;
|
||
actions?: FocusTypeAction[];
|
||
defaultMode?: string;
|
||
getTitle?: (mode: string) => string;
|
||
getSubTitle?: (mode: string) => string;
|
||
createDrawService: (
|
||
canvas: Canvas,
|
||
ctx: RenderingContext,
|
||
options?: Record<string, any>,
|
||
) => BaseDrawService;
|
||
generateData: (mode?: string) => any;
|
||
}
|
||
|
||
// ─── Helper: dotConnect ───
|
||
|
||
function generateNonBacktrackingSequence(length: number): number[] {
|
||
const getPosition = (num: number): [number, number] => {
|
||
const row = Math.floor((num - 1) / 3);
|
||
const col = (num - 1) % 3;
|
||
return [row, col];
|
||
};
|
||
|
||
const sequence: number[] = [];
|
||
const used = new Set<number>();
|
||
|
||
const startNum = Math.floor(Math.random() * 9) + 1;
|
||
sequence.push(startNum);
|
||
used.add(startNum);
|
||
|
||
let prevPos = getPosition(startNum);
|
||
let prevPrevPos: [number, number] | null = null;
|
||
|
||
while (sequence.length < length) {
|
||
const candidates: number[] = [];
|
||
for (let num = 1; num <= 9; num++) {
|
||
if (used.has(num)) continue;
|
||
const currentPos = getPosition(num);
|
||
if (
|
||
prevPrevPos &&
|
||
currentPos[0] === prevPrevPos[0] &&
|
||
currentPos[1] === prevPrevPos[1]
|
||
) {
|
||
continue;
|
||
}
|
||
candidates.push(num);
|
||
}
|
||
|
||
const pool =
|
||
candidates.length > 0
|
||
? candidates
|
||
: Array.from({ length: 9 }, (_, i) => i + 1).filter(
|
||
(n) => !used.has(n),
|
||
);
|
||
if (pool.length === 0) break;
|
||
|
||
const nextNum = pool[Math.floor(Math.random() * pool.length)];
|
||
sequence.push(nextNum);
|
||
used.add(nextNum);
|
||
prevPrevPos = prevPos;
|
||
prevPos = getPosition(nextNum);
|
||
}
|
||
|
||
return sequence;
|
||
}
|
||
|
||
// ─── Helpers: gridReasoning ───
|
||
|
||
function generateRandomPositions(count: number): GridPosition[] {
|
||
const allPositions: GridPosition[] = [];
|
||
for (let y = 0; y < 3; y++) {
|
||
for (let x = 0; x < 3; x++) {
|
||
allPositions.push({ x, y });
|
||
}
|
||
}
|
||
const shuffled = [...allPositions].sort(() => Math.random() - 0.5);
|
||
return shuffled.slice(0, count);
|
||
}
|
||
|
||
function unionPositions(
|
||
pos1: GridPosition[],
|
||
pos2: GridPosition[],
|
||
): GridPosition[] {
|
||
const set = new Set<string>();
|
||
const result: GridPosition[] = [];
|
||
for (const pos of pos1) {
|
||
const key = `${pos.x},${pos.y}`;
|
||
if (!set.has(key)) {
|
||
set.add(key);
|
||
result.push(pos);
|
||
}
|
||
}
|
||
for (const pos of pos2) {
|
||
const key = `${pos.x},${pos.y}`;
|
||
if (!set.has(key)) {
|
||
set.add(key);
|
||
result.push(pos);
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
|
||
function subtractPositions(
|
||
pos1: GridPosition[],
|
||
pos2: GridPosition[],
|
||
): GridPosition[] {
|
||
const set2 = new Set(pos2.map((p) => `${p.x},${p.y}`));
|
||
return pos1.filter((p) => !set2.has(`${p.x},${p.y}`));
|
||
}
|
||
|
||
// ─── Helpers: gridDrawing ───
|
||
|
||
function generateUniqueGroups(
|
||
allShapes: ShapeTemplate[],
|
||
count: number,
|
||
config: GridConfig,
|
||
): GridGroup[] {
|
||
const groups: GridGroup[] = [];
|
||
const usedPatterns = new Set<string>();
|
||
|
||
const availableShapes = [...allShapes];
|
||
for (let i = availableShapes.length - 1; i > 0; i--) {
|
||
const j = Math.floor(Math.random() * (i + 1));
|
||
[availableShapes[i], availableShapes[j]] = [
|
||
availableShapes[j],
|
||
availableShapes[i],
|
||
];
|
||
}
|
||
|
||
for (let i = 0; i < count; i++) {
|
||
const shapeIndex = i % availableShapes.length;
|
||
const shape = availableShapes[shapeIndex];
|
||
const cells = generateCompletePattern(shape, config);
|
||
const sig = cells
|
||
.map((c: GridCell) => `${c.x},${c.y},${c.color}`)
|
||
.sort()
|
||
.join('|');
|
||
|
||
if (
|
||
usedPatterns.has(sig) &&
|
||
usedPatterns.size < Math.min(count, allShapes.length)
|
||
) {
|
||
let found = false;
|
||
for (const nextShape of availableShapes) {
|
||
const nextCells = generateCompletePattern(nextShape, config);
|
||
const nextSig = nextCells
|
||
.map((c: GridCell) => `${c.x},${c.y},${c.color}`)
|
||
.sort()
|
||
.join('|');
|
||
if (!usedPatterns.has(nextSig)) {
|
||
groups.push({ filledCells: nextCells, emptyCells: [] });
|
||
usedPatterns.add(nextSig);
|
||
found = true;
|
||
break;
|
||
}
|
||
}
|
||
if (!found) {
|
||
groups.push({ filledCells: cells, emptyCells: [] });
|
||
}
|
||
} else {
|
||
groups.push({ filledCells: cells, emptyCells: [] });
|
||
usedPatterns.add(sig);
|
||
}
|
||
}
|
||
|
||
return groups;
|
||
}
|
||
|
||
// ─── Data Generators ───
|
||
|
||
function generateColorShapeMatchData() {
|
||
const shapes = [...SHAPE_SYMBOL_SHAPES].filter((s) => s.id !== 'cross');
|
||
const selectedShapes = shapes.sort(() => Math.random() - 0.5).slice(0, 4);
|
||
const colors = getRandomUniqueNumberColors(4);
|
||
|
||
const legendItems = selectedShapes.map((shape, i) => ({
|
||
shapeId: shape.id,
|
||
color: colors[i],
|
||
}));
|
||
|
||
const allColors: (string | null)[] = new Array(25).fill(null);
|
||
const positions = Array.from({ length: 25 }, (_, i) => i);
|
||
const shuffledPos = [...positions].sort(() => Math.random() - 0.5);
|
||
for (let i = 0; i < 4; i++) allColors[shuffledPos[i]] = colors[i];
|
||
for (let i = 4; i < 25; i++)
|
||
allColors[shuffledPos[i]] =
|
||
colors[Math.floor(Math.random() * colors.length)];
|
||
|
||
const practiceGrid: Array<Array<string | null>> = [];
|
||
for (let row = 0; row < 5; row++) {
|
||
practiceGrid.push(
|
||
Array.from({ length: 5 }, (_, col) => allColors[row * 5 + col]),
|
||
);
|
||
}
|
||
|
||
return { legendItems, practiceGrid };
|
||
}
|
||
|
||
function generateShapeSymbolData() {
|
||
const shuffled = [...SHAPE_SYMBOL_SHAPES].sort(() => Math.random() - 0.5);
|
||
const selectedShapes = shuffled.slice(0, 4);
|
||
const symbols = ['+', '-', '×', '✓'];
|
||
const shuffledSymbols = [...symbols].sort(() => Math.random() - 0.5);
|
||
|
||
const shapeColorMap = new Map<string, string>();
|
||
const usedColors = new Set<string>();
|
||
const legendMapping: Array<{
|
||
shapeId: string;
|
||
symbol: string;
|
||
color: string;
|
||
}> = [];
|
||
|
||
for (let i = 0; i < 4; i++) {
|
||
const shape = selectedShapes[i];
|
||
let color: string;
|
||
if (shapeColorMap.has(shape.id)) {
|
||
color = shapeColorMap.get(shape.id)!;
|
||
} else {
|
||
do {
|
||
color = getRandomNumberColor();
|
||
} while (usedColors.has(color));
|
||
shapeColorMap.set(shape.id, color);
|
||
usedColors.add(color);
|
||
}
|
||
legendMapping.push({
|
||
shapeId: shape.id,
|
||
symbol: shuffledSymbols[i],
|
||
color,
|
||
});
|
||
}
|
||
|
||
const practiceRows: Array<
|
||
Array<{ shapeId: string | null; symbol: string | null }>
|
||
> = [];
|
||
for (let g = 0; g < 4; g++) {
|
||
const shapeRow = Array.from({ length: 8 }, () => {
|
||
const rs =
|
||
selectedShapes[
|
||
Math.floor(Math.random() * selectedShapes.length)
|
||
];
|
||
return { shapeId: rs.id, symbol: null };
|
||
});
|
||
practiceRows.push(shapeRow);
|
||
practiceRows.push(
|
||
Array.from({ length: 8 }, () => ({ shapeId: null, symbol: null })),
|
||
);
|
||
}
|
||
|
||
return {
|
||
legendMapping,
|
||
practiceRows,
|
||
shapeColorMap: Object.fromEntries(shapeColorMap),
|
||
};
|
||
}
|
||
|
||
function generatePositionColoringData() {
|
||
const allImageIndices = Array.from({ length: 33 }, (_, i) => i + 1);
|
||
const shuffled = [...allImageIndices].sort(() => Math.random() - 0.5);
|
||
const selectedImages = shuffled.slice(0, 9);
|
||
|
||
const referenceGrid: number[] = [...selectedImages];
|
||
|
||
const taskImageIndices = [...selectedImages].sort(
|
||
() => Math.random() - 0.5,
|
||
);
|
||
const tasks = taskImageIndices.map((imageIndex) => {
|
||
const gridIndex = referenceGrid.indexOf(imageIndex);
|
||
return {
|
||
imageIndex,
|
||
position: { x: gridIndex % 3, y: Math.floor(gridIndex / 3) },
|
||
};
|
||
});
|
||
|
||
return { referenceGrid, tasks };
|
||
}
|
||
|
||
function generateColorPatternData() {
|
||
const allShapes = [...SHAPE_SYMBOL_SHAPES].sort(() => Math.random() - 0.5);
|
||
const selectedShapes = allShapes.slice(0, 8);
|
||
const allColors = getNumberColors();
|
||
|
||
const groups: Array<{ shapeId: string; colors: Array<string | null> }> = [];
|
||
for (let gi = 0; gi < 8; gi++) {
|
||
const shuffledColors = [...allColors].sort(() => Math.random() - 0.5);
|
||
const selectedColors = shuffledColors.slice(0, 2);
|
||
|
||
const patternColors: string[] = [];
|
||
for (let i = 0; i < 4; i++) {
|
||
patternColors.push(
|
||
selectedColors[
|
||
Math.floor(Math.random() * selectedColors.length)
|
||
],
|
||
);
|
||
}
|
||
if (new Set(patternColors).size === 1) {
|
||
const other = selectedColors.filter((c) => c !== patternColors[0]);
|
||
if (other.length > 0) {
|
||
patternColors[Math.floor(Math.random() * 4)] =
|
||
other[Math.floor(Math.random() * other.length)];
|
||
}
|
||
}
|
||
|
||
groups.push({
|
||
shapeId: selectedShapes[gi].id,
|
||
colors: [...patternColors, ...new Array(6).fill(null)],
|
||
});
|
||
}
|
||
|
||
return { groups };
|
||
}
|
||
|
||
function generateMatchConnectData() {
|
||
const allImageIndices = Array.from({ length: 33 }, (_, i) => i + 1);
|
||
const shuffled = [...allImageIndices].sort(() => Math.random() - 0.5);
|
||
const selectedImages = shuffled.slice(0, 5);
|
||
|
||
const referenceSequence = [...selectedImages];
|
||
const boxes = Array.from({ length: 6 }, () => ({
|
||
imageIndices: [...selectedImages],
|
||
}));
|
||
|
||
return { referenceSequence, boxes };
|
||
}
|
||
|
||
function generateLineRecognitionData() {
|
||
const LINE_TYPES: Array<{ type: string; name: string }> = [
|
||
{ type: 'straight', name: '直线' },
|
||
{ type: 'dashed', name: '虚线' },
|
||
{ type: 'wavy', name: '波浪线' },
|
||
{ type: 'zigzag', name: '锯齿线' },
|
||
{ type: 'spiral', name: '电话线' },
|
||
];
|
||
|
||
const colorKeys = Object.keys(NUMBER_COLORS)
|
||
.map(Number)
|
||
.filter((k) => k !== 11 && k !== 12);
|
||
const shuffledColors = [...colorKeys].sort(() => Math.random() - 0.5);
|
||
const selectedColorKeys = shuffledColors.slice(0, 5);
|
||
|
||
const shuffledLineTypes = [...LINE_TYPES].sort(() => Math.random() - 0.5);
|
||
const referenceLines = shuffledLineTypes.map((lt, i) => ({
|
||
type: lt.type,
|
||
name: lt.name,
|
||
color: NUMBER_COLORS[selectedColorKeys[i]],
|
||
}));
|
||
|
||
const practiceBlocks: Array<{
|
||
color: string;
|
||
lineType: string;
|
||
x: number;
|
||
y: number;
|
||
}> = [];
|
||
for (let row = 0; row < 8; row++) {
|
||
for (let col = 0; col < 2; col++) {
|
||
const sel =
|
||
referenceLines[
|
||
Math.floor(Math.random() * referenceLines.length)
|
||
];
|
||
practiceBlocks.push({
|
||
color: sel.color,
|
||
lineType: sel.type,
|
||
x: (Math.random() - 0.5) * 40,
|
||
y: (Math.random() - 0.5) * 16,
|
||
});
|
||
}
|
||
}
|
||
|
||
return { referenceLines, practiceBlocks };
|
||
}
|
||
|
||
function generateGridReasoningData(mode?: string) {
|
||
const operatorType = mode || 'mixed';
|
||
const colorKeys = Object.keys(NUMBER_COLORS).map(Number);
|
||
const selectedColor =
|
||
NUMBER_COLORS[colorKeys[Math.floor(Math.random() * colorKeys.length)]];
|
||
|
||
const rows: Array<{
|
||
grid1: GridPosition[];
|
||
grid2: GridPosition[];
|
||
operator: '+' | '-';
|
||
result: GridPosition[];
|
||
}> = [];
|
||
|
||
for (let i = 0; i < 5; i++) {
|
||
let operator: '+' | '-';
|
||
if (operatorType === 'addition') operator = '+';
|
||
else if (operatorType === 'subtraction') operator = '-';
|
||
else operator = Math.random() > 0.5 ? '+' : '-';
|
||
|
||
const grid1Count =
|
||
operator === '-'
|
||
? Math.floor(Math.random() * 8) + 2
|
||
: Math.floor(Math.random() * 9) + 1;
|
||
const grid1 = generateRandomPositions(grid1Count);
|
||
|
||
let grid2: GridPosition[];
|
||
let result: GridPosition[];
|
||
if (operator === '+') {
|
||
grid2 = generateRandomPositions(Math.floor(Math.random() * 9) + 1);
|
||
result = unionPositions(grid1, grid2);
|
||
} else {
|
||
const maxG2 = grid1Count - 1;
|
||
grid2 = generateRandomPositions(
|
||
Math.floor(Math.random() * maxG2) + 1,
|
||
);
|
||
result = subtractPositions(grid1, grid2);
|
||
}
|
||
|
||
rows.push({ grid1, grid2, operator, result });
|
||
}
|
||
|
||
const answerOptions = rows.map((r, i) => ({
|
||
positions: r.result,
|
||
rowIndex: i,
|
||
}));
|
||
answerOptions.sort(() => Math.random() - 0.5);
|
||
|
||
return { rows, answerOptions, color: selectedColor };
|
||
}
|
||
|
||
function generateCodeConnectData() {
|
||
const startOptions = [1, 2, 3];
|
||
const startNumber =
|
||
startOptions[Math.floor(Math.random() * startOptions.length)];
|
||
const numbers = Array.from({ length: 8 }, (_, i) => startNumber + i);
|
||
|
||
const colorIndices = Array.from(
|
||
{ length: WATER_COLORS.extended24.length },
|
||
(_, i) => i,
|
||
);
|
||
const shuffledIdx = [...colorIndices]
|
||
.sort(() => Math.random() - 0.5)
|
||
.slice(0, 8);
|
||
|
||
const colorMap = numbers.map((num, i) => ({
|
||
number: num,
|
||
color: WATER_COLORS.extended24[shuffledIdx[i]].hex,
|
||
}));
|
||
|
||
const groups: Array<{
|
||
sequence: number[];
|
||
dots: Array<{ number: number; color: string; angle: number }>;
|
||
}> = [];
|
||
|
||
for (let i = 0; i < 4; i++) {
|
||
const sequence = [...numbers]
|
||
.sort(() => Math.random() - 0.5)
|
||
.slice(0, 5);
|
||
const dots = numbers.map((num, j) => {
|
||
const angle = (j / 8) * Math.PI * 2 - Math.PI / 2;
|
||
const info = colorMap.find((m) => m.number === num);
|
||
return { number: num, color: info?.color || '#000', angle };
|
||
});
|
||
groups.push({ sequence, dots });
|
||
}
|
||
|
||
return { colorMap, startNumber, groups };
|
||
}
|
||
|
||
function generateDotConnectData() {
|
||
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9];
|
||
const colors = getRandomUniqueNumberColors(9);
|
||
const colorMap = numbers.map((num, i) => ({
|
||
number: num,
|
||
color: colors[i],
|
||
}));
|
||
|
||
const groups = Array.from({ length: 9 }, () => {
|
||
const seqLen = Math.floor(Math.random() * 3) + 4;
|
||
return { sequence: generateNonBacktrackingSequence(seqLen) };
|
||
});
|
||
|
||
return { colorMap, groups };
|
||
}
|
||
|
||
function generateGridDrawingData(mode?: string) {
|
||
const m = mode || '3x3';
|
||
let allShapes: ShapeTemplate[];
|
||
let config: GridConfig;
|
||
let groupsPerRow: number;
|
||
let totalRows: number;
|
||
|
||
if (m === '5x5') {
|
||
allShapes = ALL_5X5_SHAPES;
|
||
config = { rows: 5, cols: 5 };
|
||
groupsPerRow = 2;
|
||
totalRows = 4;
|
||
} else if (m === '7x7') {
|
||
allShapes = ALL_7X7_SHAPES;
|
||
config = { rows: 7, cols: 7 };
|
||
groupsPerRow = 1;
|
||
totalRows = 2;
|
||
} else {
|
||
allShapes = ALL_3X3_SHAPES;
|
||
config = { rows: 3, cols: 3 };
|
||
groupsPerRow = 2;
|
||
totalRows = 4;
|
||
}
|
||
|
||
const totalGroups = groupsPerRow * totalRows;
|
||
const groups = generateUniqueGroups(allShapes, totalGroups, config);
|
||
|
||
return { config, groups, groupsPerRow, totalRows };
|
||
}
|
||
|
||
// ─── Registry ───
|
||
|
||
const GRID_DRAWING_SUBTITLES: Record<string, string> = {
|
||
'3x3': '简单有趣,培养专注力',
|
||
'5x5': '创意挑战,提升观察力',
|
||
'7x7': '大师挑战,锻炼耐心',
|
||
};
|
||
|
||
export const FOCUS_TYPE_CONFIGS: FocusTypeConfig[] = [
|
||
{
|
||
id: 'color-shape-match',
|
||
title: '根据颜色画图形',
|
||
subTitle: '根据颜色画出对应的图形',
|
||
icon: '🎯',
|
||
createDrawService: (canvas, ctx, opts) =>
|
||
new ColorShapeMatchDraw(canvas, ctx, opts),
|
||
generateData: generateColorShapeMatchData,
|
||
},
|
||
{
|
||
id: 'shape-symbol',
|
||
title: '图形符号配对',
|
||
subTitle: '根据图形画对应的符号',
|
||
icon: '🔗',
|
||
createDrawService: (canvas, ctx, opts) =>
|
||
new ShapeSymbolDraw(canvas, ctx, opts),
|
||
generateData: generateShapeSymbolData,
|
||
},
|
||
{
|
||
id: 'position-coloring',
|
||
title: '方位涂涂乐',
|
||
subTitle: '观察卡片位置,在对应的方格中涂上颜色',
|
||
icon: '📍',
|
||
createDrawService: (canvas, ctx, opts) =>
|
||
new PositionColoringDraw(canvas, ctx, opts),
|
||
generateData: generatePositionColoringData,
|
||
},
|
||
{
|
||
id: 'color-pattern',
|
||
title: '颜色找规律',
|
||
subTitle: '观察颜色规律,在空白图形中涂上颜色',
|
||
icon: '🎨',
|
||
createDrawService: (canvas, ctx, opts) =>
|
||
new ColorPatternDraw(canvas, ctx, opts),
|
||
generateData: generateColorPatternData,
|
||
},
|
||
{
|
||
id: 'match-connect',
|
||
title: '连连看',
|
||
subTitle: '快来根据物品连一连吧!',
|
||
icon: '🔗',
|
||
createDrawService: (canvas, ctx, opts) =>
|
||
new MatchConnectDraw(canvas, ctx, opts),
|
||
generateData: generateMatchConnectData,
|
||
},
|
||
{
|
||
id: 'line-recognition',
|
||
title: '线条识别',
|
||
subTitle: '认识不同线条,画出颜色对应的线条',
|
||
icon: '📏',
|
||
createDrawService: (canvas, ctx, opts) =>
|
||
new LineRecognitionDraw(canvas, ctx, opts),
|
||
generateData: generateLineRecognitionData,
|
||
},
|
||
{
|
||
id: 'grid-reasoning',
|
||
title: '方格推理',
|
||
subTitle: '仔细观察,推理出合并方格并连线',
|
||
icon: '🧩',
|
||
actionsTitle: '选择运算',
|
||
actions: [
|
||
{ value: 'addition', label: '加法运算' },
|
||
{ value: 'subtraction', label: '减法运算' },
|
||
{ value: 'mixed', label: '混合运算' },
|
||
],
|
||
defaultMode: 'mixed',
|
||
createDrawService: (canvas, ctx, opts) =>
|
||
new GridReasoningDraw(canvas, ctx, opts),
|
||
generateData: generateGridReasoningData,
|
||
},
|
||
{
|
||
id: 'code-connect',
|
||
title: '译码连线',
|
||
subTitle: '按照数字顺序,将数字对应的颜色连起来',
|
||
icon: '🔢',
|
||
createDrawService: (canvas, ctx, opts) =>
|
||
new CodeConnectDraw(canvas, ctx, opts),
|
||
generateData: generateCodeConnectData,
|
||
},
|
||
{
|
||
id: 'dot-connect',
|
||
title: '数字点连线',
|
||
subTitle: '按数字顺序连点成图',
|
||
icon: '🔗',
|
||
createDrawService: (canvas, ctx, opts) =>
|
||
new DotConnectDraw(canvas, ctx, opts),
|
||
generateData: generateDotConnectData,
|
||
},
|
||
{
|
||
id: 'grid-drawing',
|
||
title: '格子仿画',
|
||
subTitle: '在网格中填充颜色,形成各种形状',
|
||
icon: '🎨',
|
||
actionsTitle: '选择难度',
|
||
actions: [
|
||
{ value: '3x3', label: '3×3' },
|
||
{ value: '5x5', label: '5×5' },
|
||
{ value: '7x7', label: '7×7' },
|
||
],
|
||
defaultMode: '3x3',
|
||
getTitle: (mode: string) => {
|
||
const labels: Record<string, string> = {
|
||
'3x3': '3×3',
|
||
'5x5': '5×5',
|
||
'7x7': '7×7',
|
||
};
|
||
return `格子仿画 ${labels[mode] || '3×3'}`;
|
||
},
|
||
getSubTitle: (mode: string) =>
|
||
GRID_DRAWING_SUBTITLES[mode] || GRID_DRAWING_SUBTITLES['3x3'],
|
||
createDrawService: (canvas, ctx, opts) =>
|
||
new GridDraw(canvas, ctx, opts),
|
||
generateData: generateGridDrawingData,
|
||
},
|
||
];
|
||
|
||
/**
|
||
* 通过路由 ID 查找类型配置
|
||
* 处理 grid-drawing-3x3 → grid-drawing + mode=3x3 等别名
|
||
*/
|
||
export function findTypeByRouteId(
|
||
routeId: string,
|
||
): { typeConfig: FocusTypeConfig; mode?: string } | null {
|
||
const direct = FOCUS_TYPE_CONFIGS.find((t) => t.id === routeId);
|
||
if (direct) return { typeConfig: direct };
|
||
|
||
if (routeId.startsWith('grid-drawing-')) {
|
||
const mode = routeId.replace('grid-drawing-', '');
|
||
const cfg = FOCUS_TYPE_CONFIGS.find((t) => t.id === 'grid-drawing');
|
||
if (cfg) return { typeConfig: cfg, mode };
|
||
}
|
||
|
||
return { typeConfig: FOCUS_TYPE_CONFIGS[0] };
|
||
}
|