import { BaseDrawService } from '../../../core/draw/baseDraw'; import { drawTianZiGrid } from '../../shared/drawUtils'; import { getPenControlPattern } from '../data/penControlPatterns'; import type { PenControlSheetData } from '../generators/penControlGenerator'; import { drawPenControlInCell, type PenControlCellStyle, } from '../shared/penControlDrawUtils'; /** 控笔练习固定 8 列 × 10 行 */ const LAYOUT = { topGap: 17, leftMargin: 40, rightMargin: 40, bottomMargin: 40, targetCols: 8, targetRows: 10, minColGap: 6, rowGap: 8, } as const; export default class PenControlDrawService extends BaseDrawService { constructor( canvas: Canvas, ctx: RenderingContext, options?: Record, ) { super(canvas, ctx, { title: '控笔组合练习', subTitle: '每种图形占两行田字格', ...options, }); } getMaxGridLayout(): { maxRow: number; maxCol: number } { return { maxRow: LAYOUT.targetRows, maxCol: LAYOUT.targetCols }; } computeSheetLayout(maxRow: number, maxCol: number) { const content = this.getContentRect(); const { minColGap, rowGap } = LAYOUT; const cellByWidth = (content.width - (maxCol - 1) * minColGap) / maxCol; const cellByHeight = (content.height - (maxRow - 1) * rowGap) / maxRow; const cellSize = Math.max( 16, Math.floor(Math.min(cellByWidth, cellByHeight)), ); const actualColGap = maxCol > 1 ? (content.width - maxCol * cellSize) / (maxCol - 1) : 0; const actualRowGap = maxRow > 1 ? Math.min( rowGap, (content.height - maxRow * cellSize) / (maxRow - 1), ) : 0; return { maxRow, maxCol, cellSize, startX: content.left, startY: content.top, colGap: actualColGap, rowGap: actualRowGap, }; } async draw(data: PenControlSheetData) { this.prepareDraw(); await this.drawHeaderAndDivider(); const layout = this.computeSheetLayout( data.layout.maxRow, data.layout.maxCol, ); this.drawSheet({ ...data, layout }); this.drawPrintFooter(); } private getContentRect() { const topGap = LAYOUT.topGap; const leftMargin = LAYOUT.leftMargin; const rightMargin = LAYOUT.rightMargin; const bottomMargin = LAYOUT.bottomMargin; const contentTop = this.currentY + topGap; const contentWidth = this.canvasWidth - leftMargin - rightMargin; const contentHeight = this.canvasHeight - contentTop - bottomMargin; return { top: contentTop, left: leftMargin, width: contentWidth, height: contentHeight, }; } private drawSheet(data: PenControlSheetData) { const { ctx } = this; const { layout, blocks } = data; const { startX, startY, cellSize, colGap, rowGap, maxCol, maxRow, } = layout; for (let row = 0; row < maxRow; row++) { for (let col = 0; col < maxCol; col++) { const cx = startX + col * (cellSize + colGap) + cellSize / 2; const cy = startY + row * (cellSize + rowGap) + cellSize / 2; drawTianZiGrid({ ctx, cx, cy, size: cellSize }); } } let globalRow = 0; for (const block of blocks) { for (const rowSpec of block.rows) { if (globalRow >= maxRow) break; const pattern = getPenControlPattern(block.patternId); if (pattern) { for (let col = 0; col < maxCol; col++) { const cx = startX + col * (cellSize + colGap) + cellSize / 2; const cy = startY + globalRow * (cellSize + rowGap) + cellSize / 2; const style: PenControlCellStyle = rowSpec.cells[col] ?? 'guide'; const dashed = rowSpec.role === 'pair-second' && col >= 1; drawPenControlInCell( ctx, pattern, cx, cy, cellSize, style, { dashed }, ); } } globalRow++; } if (globalRow >= maxRow) break; } } /** 供页面在绘制前计算 layout */ buildLayoutForGenerate(): ReturnType< PenControlDrawService['computeSheetLayout'] > { const { maxRow, maxCol } = this.getMaxGridLayout(); return this.computeSheetLayout(maxRow, maxCol); } }