diff --git a/src/components/md-deck/PrintPreview.tsx b/src/components/md-deck/PrintPreview.tsx index 18c6c54..7949dac 100644 --- a/src/components/md-deck/PrintPreview.tsx +++ b/src/components/md-deck/PrintPreview.tsx @@ -1,4 +1,4 @@ -import { Show, For, createMemo } from 'solid-js'; +import { For, createMemo } from 'solid-js'; import { marked } from '../../markdown'; import { getLayerStyle } from './hooks/dimensions'; import type { DeckStore } from './hooks/deckStore'; @@ -28,7 +28,7 @@ export function PrintPreview(props: PrintPreviewProps) { const A4_HEIGHT = 297; const PRINT_MARGIN = 5; // 打印边距 - // 计算每张卡牌在 A4 纸上的位置 + // 计算每张卡牌在 A4 纸上的位置(居中布局) const pages = createMemo(() => { const cards = store.state.cards; const cardWidth = store.state.dimensions?.cardWidth || 56; @@ -45,9 +45,21 @@ export function PrintPreview(props: PrintPreviewProps) { // 每页的卡牌数量 const cardsPerPage = cardsPerRow * rowsPerPage; + // 计算最大卡牌区域的尺寸(用于居中和外围框) + const maxGridWidth = cardsPerRow * cardWidth; + const maxGridHeight = rowsPerPage * cardHeight; + + // 居中偏移量(使卡牌区域在 A4 纸上居中) + const offsetX = (A4_WIDTH - maxGridWidth) / 2; + const offsetY = (A4_HEIGHT - maxGridHeight) / 2; + // 分页 - const result: { pageIndex: number; cards: Array<{ data: typeof cards[0]; x: number; y: number }> }[] = []; - let currentPage: typeof result[0] = { pageIndex: 0, cards: [] }; + const result: { + pageIndex: number; + cards: Array<{ data: typeof cards[0]; x: number; y: number }>; + bounds: { minX: number; minY: number; maxX: number; maxY: number }; + }[] = []; + let currentPage: typeof result[0] = { pageIndex: 0, cards: [], bounds: { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity } }; for (let i = 0; i < cards.length; i++) { const pageIndex = Math.floor(i / cardsPerPage); @@ -57,21 +69,91 @@ export function PrintPreview(props: PrintPreviewProps) { if (pageIndex !== currentPage.pageIndex) { result.push(currentPage); - currentPage = { pageIndex, cards: [] }; + currentPage = { pageIndex, cards: [], bounds: { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity } }; } + // 使用居中偏移量计算卡牌位置 + const cardX = offsetX + col * cardWidth; + const cardY = offsetY + row * cardHeight; + currentPage.cards.push({ data: cards[i], - x: PRINT_MARGIN + col * cardWidth, - y: PRINT_MARGIN + row * cardHeight + x: cardX, + y: cardY }); + + // 更新边界(含 1mm 边距) + 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; + // 为每页添加固定的外围框尺寸(基于最大网格) + return result.map(page => ({ + ...page, + frameBounds: { + minX: offsetX, + minY: offsetY, + maxX: offsetX + maxGridWidth, + maxY: offsetY + maxGridHeight + } + })); + }); + + // 计算裁切线和外围框位置 + 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; // 3mm + + // 生成水平裁切线(沿 Y 轴) + const horizontalLines = sortedY.map(y => ({ + y, + xStart: frameBounds.minX - OVERLAP, + xEnd: frameBounds.maxX + OVERLAP + })); + + // 生成垂直裁切线(沿 X 轴) + const verticalLines = sortedX.map(x => ({ + x, + yStart: frameBounds.minY - OVERLAP, + yEnd: frameBounds.maxY + OVERLAP + })); + + // 外围框边界(离卡牌区域边缘 1mm) + 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 }; + }); }); const visibleLayers = createMemo(() => store.state.layerConfigs.filter((l) => l.visible)); @@ -116,6 +198,71 @@ export function PrintPreview(props: PrintPreviewProps) { > {/* 渲染该页的所有卡牌 */}
+ {/* 裁切线和外围框层 */} + + {/* 外围边框:黑色 0.2mm */} + + + {/* 水平裁切线 */} + + {(line) => ( + <> + {/* 左侧裁切线(外围框外部) */} + + {/* 右侧裁切线(外围框外部) */} + + + )} + + {/* 垂直裁切线 */} + + {(line) => ( + <> + {/* 上方裁切线(外围框外部) */} + + {/* 下方裁切线(外围框外部) */} + + + )} + + + {(card) => (