feat: 添加控笔页

This commit is contained in:
R524809
2026-05-28 16:46:30 +08:00
parent 824e83c887
commit 2f0c4ab591
21 changed files with 1406 additions and 169 deletions
@@ -0,0 +1,169 @@
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<string, unknown>,
) {
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);
}
}