2026-02-27 21:12:23 +08:00
|
|
|
import { createMemo } from 'solid-js';
|
|
|
|
|
import type { DeckStore } from './deckStore';
|
|
|
|
|
import type { PageData, CropMarkData } from './usePDFExport';
|
|
|
|
|
|
|
|
|
|
export interface A4Size {
|
|
|
|
|
width: number;
|
|
|
|
|
height: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface UsePageLayoutReturn {
|
|
|
|
|
getA4Size: () => A4Size;
|
|
|
|
|
pages: ReturnType<typeof createMemo<PageData[]>>;
|
|
|
|
|
cropMarks: ReturnType<typeof createMemo<CropMarkData[]>>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const A4_WIDTH_PORTRAIT = 210;
|
|
|
|
|
const A4_HEIGHT_PORTRAIT = 297;
|
|
|
|
|
const A4_WIDTH_LANDSCAPE = 297;
|
|
|
|
|
const A4_HEIGHT_LANDSCAPE = 210;
|
|
|
|
|
const PRINT_MARGIN = 5;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 页面布局计算 hook
|
|
|
|
|
*/
|
|
|
|
|
export function usePageLayout(store: DeckStore): UsePageLayoutReturn {
|
|
|
|
|
const orientation = () => store.state.printOrientation;
|
2026-03-13 17:26:00 +08:00
|
|
|
const doubleSided = () => store.state.printDoubleSided;
|
|
|
|
|
const frontOddPageOffsetX = () => store.state.printFrontOddPageOffsetX;
|
|
|
|
|
const frontOddPageOffsetY = () => store.state.printFrontOddPageOffsetY;
|
2026-02-27 21:12:23 +08:00
|
|
|
|
|
|
|
|
const getA4Size = () => {
|
|
|
|
|
if (orientation() === 'landscape') {
|
|
|
|
|
return { width: A4_WIDTH_LANDSCAPE, height: A4_HEIGHT_LANDSCAPE };
|
|
|
|
|
}
|
|
|
|
|
return { width: A4_WIDTH_PORTRAIT, height: A4_HEIGHT_PORTRAIT };
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const pages = createMemo<PageData[]>(() => {
|
|
|
|
|
const cards = store.state.cards;
|
|
|
|
|
const cardWidth = store.state.dimensions?.cardWidth || 56;
|
|
|
|
|
const cardHeight = store.state.dimensions?.cardHeight || 88;
|
|
|
|
|
const { width: a4Width, height: a4Height } = getA4Size();
|
|
|
|
|
|
|
|
|
|
const usableWidth = a4Width - PRINT_MARGIN * 2;
|
|
|
|
|
const cardsPerRow = Math.floor(usableWidth / cardWidth);
|
|
|
|
|
const usableHeight = a4Height - PRINT_MARGIN * 2;
|
|
|
|
|
const rowsPerPage = Math.floor(usableHeight / cardHeight);
|
|
|
|
|
const cardsPerPage = cardsPerRow * rowsPerPage;
|
|
|
|
|
|
|
|
|
|
const maxGridWidth = cardsPerRow * cardWidth;
|
|
|
|
|
const maxGridHeight = rowsPerPage * cardHeight;
|
|
|
|
|
const baseOffsetX = (a4Width - maxGridWidth) / 2;
|
|
|
|
|
const baseOffsetY = (a4Height - maxGridHeight) / 2;
|
|
|
|
|
|
|
|
|
|
const result: PageData[] = [];
|
2026-03-13 17:42:51 +08:00
|
|
|
|
2026-03-13 17:26:00 +08:00
|
|
|
if (doubleSided()) {
|
2026-03-13 17:42:51 +08:00
|
|
|
// 双面打印模式:每页多张卡牌,正面和背面分别在相邻的两页
|
2026-03-13 17:26:00 +08:00
|
|
|
const totalCards = cards.length;
|
2026-03-13 17:42:51 +08:00
|
|
|
const totalPages = Math.ceil(totalCards / cardsPerPage);
|
|
|
|
|
|
|
|
|
|
for (let pageIndex = 0; pageIndex < totalPages; pageIndex++) {
|
|
|
|
|
const frontPageIndex = pageIndex * 2;
|
|
|
|
|
const backPageIndex = pageIndex * 2 + 1;
|
|
|
|
|
|
2026-03-13 17:26:00 +08:00
|
|
|
// 确保页面数组有足够长度
|
|
|
|
|
while (result.length <= backPageIndex) {
|
|
|
|
|
result.push({
|
|
|
|
|
pageIndex: result.length,
|
|
|
|
|
cards: [],
|
|
|
|
|
bounds: { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity },
|
|
|
|
|
frameBounds: { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity }
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-03-13 17:42:51 +08:00
|
|
|
|
2026-03-13 17:26:00 +08:00
|
|
|
const frontPage = result[frontPageIndex];
|
|
|
|
|
const backPage = result[backPageIndex];
|
2026-03-13 17:42:51 +08:00
|
|
|
|
|
|
|
|
// 计算当前正面页的卡牌范围
|
|
|
|
|
const startCardIndex = pageIndex * cardsPerPage;
|
|
|
|
|
const endCardIndex = Math.min(startCardIndex + cardsPerPage, totalCards);
|
|
|
|
|
|
|
|
|
|
for (let i = startCardIndex; i < endCardIndex; i++) {
|
|
|
|
|
// 正面:正常顺序排列
|
|
|
|
|
const indexInPage = i - startCardIndex;
|
|
|
|
|
const row = Math.floor(indexInPage / cardsPerRow);
|
|
|
|
|
const col = indexInPage % cardsPerRow;
|
|
|
|
|
// 双面打印时,所有正面页都在奇数物理页上,所以都应用偏移
|
|
|
|
|
const pageOffsetX = frontOddPageOffsetX();
|
|
|
|
|
const pageOffsetY = frontOddPageOffsetY();
|
|
|
|
|
|
|
|
|
|
const frontX = baseOffsetX + col * cardWidth + pageOffsetX;
|
|
|
|
|
const frontY = baseOffsetY + row * cardHeight + pageOffsetY;
|
|
|
|
|
|
|
|
|
|
frontPage.cards.push({ data: cards[i], x: frontX, y: frontY, side: 'front' as const });
|
|
|
|
|
frontPage.bounds.minX = Math.min(frontPage.bounds.minX, frontX);
|
|
|
|
|
frontPage.bounds.minY = Math.min(frontPage.bounds.minY, frontY);
|
|
|
|
|
frontPage.bounds.maxX = Math.max(frontPage.bounds.maxX, frontX + cardWidth);
|
|
|
|
|
frontPage.bounds.maxY = Math.max(frontPage.bounds.maxY, frontY + cardHeight);
|
|
|
|
|
|
|
|
|
|
// 背面:逆转顺序排列(长边方向)
|
|
|
|
|
// 对于竖向打印,长边是垂直方向,所以逆转行
|
|
|
|
|
// 对于横向打印,长边是水平方向,所以逆转列
|
|
|
|
|
const backRow = orientation() === 'portrait'
|
|
|
|
|
? (rowsPerPage - 1 - row)
|
|
|
|
|
: row;
|
|
|
|
|
const backCol = orientation() === 'portrait'
|
|
|
|
|
? col
|
|
|
|
|
: (cardsPerRow - 1 - col);
|
|
|
|
|
|
|
|
|
|
const backX = baseOffsetX + backCol * cardWidth;
|
|
|
|
|
const backY = baseOffsetY + backRow * cardHeight;
|
|
|
|
|
|
|
|
|
|
backPage.cards.push({ data: cards[i], x: backX, y: backY, side: 'back' as const });
|
|
|
|
|
backPage.bounds.minX = Math.min(backPage.bounds.minX, backX);
|
|
|
|
|
backPage.bounds.minY = Math.min(backPage.bounds.minY, backY);
|
|
|
|
|
backPage.bounds.maxX = Math.max(backPage.bounds.maxX, backX + cardWidth);
|
|
|
|
|
backPage.bounds.maxY = Math.max(backPage.bounds.maxY, backY + cardHeight);
|
|
|
|
|
}
|
2026-03-13 17:26:00 +08:00
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// 单面打印模式:原有逻辑
|
|
|
|
|
let currentPage: PageData = {
|
|
|
|
|
pageIndex: 0,
|
|
|
|
|
cards: [],
|
|
|
|
|
bounds: { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity },
|
|
|
|
|
frameBounds: { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity }
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < cards.length; i++) {
|
|
|
|
|
const pageIndex = Math.floor(i / cardsPerPage);
|
|
|
|
|
const indexInPage = i % cardsPerPage;
|
|
|
|
|
const row = Math.floor(indexInPage / cardsPerRow);
|
|
|
|
|
const col = indexInPage % cardsPerRow;
|
|
|
|
|
|
|
|
|
|
if (pageIndex !== currentPage.pageIndex) {
|
|
|
|
|
result.push(currentPage);
|
|
|
|
|
currentPage = {
|
|
|
|
|
pageIndex,
|
|
|
|
|
cards: [],
|
|
|
|
|
bounds: { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity },
|
|
|
|
|
frameBounds: { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity }
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const isOddPage = pageIndex % 2 === 0;
|
|
|
|
|
const pageOffsetX = isOddPage ? frontOddPageOffsetX() : 0;
|
|
|
|
|
const pageOffsetY = isOddPage ? frontOddPageOffsetY() : 0;
|
|
|
|
|
|
|
|
|
|
const cardX = baseOffsetX + col * cardWidth + pageOffsetX;
|
|
|
|
|
const cardY = baseOffsetY + row * cardHeight + pageOffsetY;
|
|
|
|
|
|
|
|
|
|
currentPage.cards.push({ data: cards[i], x: cardX, y: cardY, side: 'front' as const });
|
|
|
|
|
currentPage.bounds.minX = Math.min(currentPage.bounds.minX, cardX);
|
|
|
|
|
currentPage.bounds.minY = Math.min(currentPage.bounds.minY, cardY);
|
|
|
|
|
currentPage.bounds.maxX = Math.max(currentPage.bounds.maxX, cardX + cardWidth);
|
|
|
|
|
currentPage.bounds.maxY = Math.max(currentPage.bounds.maxY, cardY + cardHeight);
|
2026-02-27 21:12:23 +08:00
|
|
|
}
|
|
|
|
|
|
2026-03-13 17:26:00 +08:00
|
|
|
if (currentPage.cards.length > 0) {
|
|
|
|
|
result.push(currentPage);
|
|
|
|
|
}
|
2026-02-27 21:12:23 +08:00
|
|
|
}
|
|
|
|
|
|
2026-03-13 17:26:00 +08:00
|
|
|
return result.map(page => {
|
|
|
|
|
const offsetX = doubleSided() && page.pageIndex % 2 === 0 ? frontOddPageOffsetX() : 0;
|
|
|
|
|
const offsetY = doubleSided() && page.pageIndex % 2 === 0 ? frontOddPageOffsetY() : 0;
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...page,
|
|
|
|
|
frameBounds: {
|
|
|
|
|
minX: baseOffsetX + offsetX,
|
|
|
|
|
minY: baseOffsetY + offsetY,
|
|
|
|
|
maxX: baseOffsetX + maxGridWidth + offsetX,
|
|
|
|
|
maxY: baseOffsetY + maxGridHeight + offsetY
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
});
|
2026-02-27 21:12:23 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const cropMarks = createMemo<CropMarkData[]>(() => {
|
|
|
|
|
const pagesData = pages();
|
|
|
|
|
return pagesData.map(page => {
|
|
|
|
|
const { frameBounds, cards } = page;
|
|
|
|
|
const cardWidth = store.state.dimensions?.cardWidth || 56;
|
|
|
|
|
const cardHeight = store.state.dimensions?.cardHeight || 88;
|
|
|
|
|
|
|
|
|
|
const xPositions = new Set<number>();
|
|
|
|
|
const yPositions = new Set<number>();
|
|
|
|
|
|
|
|
|
|
cards.forEach(card => {
|
|
|
|
|
xPositions.add(card.x);
|
|
|
|
|
xPositions.add(card.x + cardWidth);
|
|
|
|
|
yPositions.add(card.y);
|
|
|
|
|
yPositions.add(card.y + cardHeight);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const sortedX = Array.from(xPositions).sort((a, b) => a - b);
|
|
|
|
|
const sortedY = Array.from(yPositions).sort((a, b) => a - b);
|
|
|
|
|
|
|
|
|
|
const OVERLAP = 3;
|
|
|
|
|
|
|
|
|
|
const horizontalLines = sortedY.map(y => ({
|
|
|
|
|
y,
|
|
|
|
|
xStart: frameBounds.minX - OVERLAP,
|
|
|
|
|
xEnd: frameBounds.maxX + OVERLAP
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
const verticalLines = sortedX.map(x => ({
|
|
|
|
|
x,
|
|
|
|
|
yStart: frameBounds.minY - OVERLAP,
|
|
|
|
|
yEnd: frameBounds.maxY + OVERLAP
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
const frameBoundsWithMargin = {
|
|
|
|
|
x: frameBounds.minX - 1,
|
|
|
|
|
y: frameBounds.minY - 1,
|
|
|
|
|
width: frameBounds.maxX - frameBounds.minX + 2,
|
|
|
|
|
height: frameBounds.maxY - frameBounds.minY + 2
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return { horizontalLines, verticalLines, frameBounds, frameBoundsWithMargin };
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return { getA4Size, pages, cropMarks };
|
|
|
|
|
}
|