import { createMemo } from "solid-js"; import type { DeckStore } from "./deckStore"; import type { PageData, CropMarkData } from "./usePDFExport"; import type { CardData } from "../types"; export interface A4Size { width: number; height: number; } export interface UsePageLayoutReturn { getA4Size: () => A4Size; pages: ReturnType>; cropMarks: ReturnType>; } 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 * @param store 主牌组的 store * @param extraCards 可选的额外卡牌列表(用于合并打印) */ export function usePageLayout( store: DeckStore, extraCards?: () => CardData[], ): UsePageLayoutReturn { const orientation = () => store.state.printOrientation; const doubleSided = () => store.state.printDoubleSided; const frontOddPageOffsetX = () => store.state.printFrontOddPageOffsetX; const frontOddPageOffsetY = () => store.state.printFrontOddPageOffsetY; 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(() => { const baseCards = store.state.cards as CardData[]; const extra = extraCards?.() ?? []; const cards = [...baseCards, ...extra]; 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[] = []; if (doubleSided()) { // 双面打印模式:每页多张卡牌,正面和背面分别在相邻的两页 const totalCards = cards.length; const totalPages = Math.ceil(totalCards / cardsPerPage); for (let pageIndex = 0; pageIndex < totalPages; pageIndex++) { const frontPageIndex = pageIndex * 2; const backPageIndex = pageIndex * 2 + 1; // 确保页面数组有足够长度 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, }, }); } const frontPage = result[frontPageIndex]; const backPage = result[backPageIndex]; // 计算当前正面页的卡牌范围 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, ); } } } 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, ); } if (currentPage.cards.length > 0) { result.push(currentPage); } } 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, }, }; }); }); const cropMarks = createMemo(() => { 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(); const yPositions = new Set(); 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 }; }