import { contourToSvgPath } from './contour'; import { getCardShapePoints, calculateCenter } from './contour'; import type { CardPath, SinglePageLayout, LayoutOptions, CardPosition } from './types'; const A4_WIDTH_PORTRAIT = 210; const A4_HEIGHT_PORTRAIT = 297; const A4_WIDTH_LANDSCAPE = 297; const A4_HEIGHT_LANDSCAPE = 210; const DEFAULT_PRINT_MARGIN = 5; /** * 生成空走路径(抬刀移动路径) * @param cardPaths 卡片切割路径 * @param a4Height A4 纸高度(用于坐标转换) */ export function generateTravelPaths( cardPaths: CardPath[], a4Height: number ): [number, number][][] { const travelPaths: [number, number][][] = []; // 起点:左上角 (0, a4Height) - 注意 SVG 坐标 Y 向下,plotter 坐标 Y 向上 const startPoint: [number, number] = [0, a4Height]; if (cardPaths.length === 0) { return travelPaths; } // 从起点到第一张卡的起点 travelPaths.push([startPoint, cardPaths[0].startPoint]); // 卡片之间的移动 for (let i = 0; i < cardPaths.length - 1; i++) { const currentEnd = cardPaths[i].endPoint; const nextStart = cardPaths[i + 1].startPoint; travelPaths.push([currentEnd, nextStart]); } // 从最后一张卡返回起点 travelPaths.push([cardPaths[cardPaths.length - 1].endPoint, startPoint]); return travelPaths; } /** * 将旅行路径转换为 SVG path 命令 */ export function travelPathsToSvg(travelPaths: [number, number][][]): string { return travelPaths.map(path => contourToSvgPath(path, false)).join(' '); } /** * 计算所有卡片轮廓的总边界框 */ export function calculateTotalBounds(cardPaths: CardPath[]): { minX: number; minY: number; maxX: number; maxY: number; width: number; height: number; } { if (cardPaths.length === 0) { return { minX: 0, minY: 0, maxX: 0, maxY: 0, width: 0, height: 0 }; } let minX = Infinity; let minY = Infinity; let maxX = -Infinity; let maxY = -Infinity; for (const cardPath of cardPaths) { for (const [x, y] of cardPath.points) { minX = Math.min(minX, x); minY = Math.min(minY, y); maxX = Math.max(maxX, x); maxY = Math.max(maxY, y); } } return { minX, minY, maxX, maxY, width: maxX - minX, height: maxY - minY }; } /** * 计算单页满排时的排版布局和刀路 * * 此函数只关心单页排满的情况,不考虑实际牌组的张数。 * 返回的布局信息可以用于: * - 预览单页最大容量时的刀路 * - 计算排版参数 * - 生成 PLT 文件 */ export function calculateSinglePageLayout(options: LayoutOptions): SinglePageLayout { const { cardWidth, cardHeight, shape, bleed, cornerRadius, orientation, printMargin = DEFAULT_PRINT_MARGIN } = options; // 确定 A4 尺寸 const a4Width = orientation === 'landscape' ? A4_WIDTH_LANDSCAPE : A4_WIDTH_PORTRAIT; const a4Height = orientation === 'landscape' ? A4_HEIGHT_LANDSCAPE : A4_HEIGHT_PORTRAIT; // 计算可用区域 const usableWidth = a4Width - printMargin * 2; const usableHeight = a4Height - printMargin * 2; // 计算每行/每页的卡片数 const cardsPerRow = Math.floor(usableWidth / cardWidth); const rowsPerPage = Math.floor(usableHeight / cardHeight); const cardsPerPage = cardsPerRow * rowsPerPage; // 计算网格居中偏移 const maxGridWidth = cardsPerRow * cardWidth; const maxGridHeight = rowsPerPage * cardHeight; const offsetX = (a4Width - maxGridWidth) / 2; const offsetY = (a4Height - maxGridHeight) / 2; // 计算切割尺寸(排版尺寸减去出血) const cutWidth = cardWidth - bleed * 2; const cutHeight = cardHeight - bleed * 2; // 生成卡片位置和刀路 const cardPositions: CardPosition[] = []; const cardPaths: CardPath[] = []; for (let i = 0; i < cardsPerPage; i++) { const row = Math.floor(i / cardsPerRow); const col = i % cardsPerRow; // 卡片位置(左下角坐标) const x = offsetX + col * cardWidth; const y = offsetY + row * cardHeight; cardPositions.push({ x, y, cardIndex: i }); // 生成形状轮廓点(相对于卡片左下角) const shapePoints = getCardShapePoints(shape, cutWidth, cutHeight, cornerRadius); // 平移到页面坐标并翻转 Y 轴(SVG Y 向下,plotter Y 向上) const pagePoints = shapePoints.map(([px, py]) => [ x + bleed + px, a4Height - (y + bleed + py) ] as [number, number]); const center = calculateCenter(pagePoints); const pathD = contourToSvgPath(pagePoints); const startPoint = pagePoints[0]; const endPoint = pagePoints[pagePoints.length - 1]; cardPaths.push({ pageIndex: 0, cardIndex: i, points: pagePoints, centerX: center.x, centerY: center.y, pathD, startPoint, endPoint }); } return { cardPositions, cardPaths, a4Width, a4Height, cardsPerRow, rowsPerPage, cardsPerPage }; } /** * 根据卡片位置生成刀路(用于自定义布局) */ export function generateCardPathsFromPositions( positions: CardPosition[], options: Omit & { a4Height: number } ): CardPath[] { const { cardWidth, cardHeight, shape, bleed, cornerRadius, a4Height } = options; // 计算切割尺寸 const cutWidth = cardWidth - bleed * 2; const cutHeight = cardHeight - bleed * 2; const cardPaths: CardPath[] = []; for (const pos of positions) { // 生成形状轮廓点(相对于卡片左下角) const shapePoints = getCardShapePoints(shape, cutWidth, cutHeight, cornerRadius); // 平移到页面坐标并翻转 Y 轴 const pagePoints = shapePoints.map(([px, py]) => [ pos.x + bleed + px, a4Height - (pos.y + bleed + py) ] as [number, number]); const center = calculateCenter(pagePoints); const pathD = contourToSvgPath(pagePoints); const startPoint = pagePoints[0]; const endPoint = pagePoints[pagePoints.length - 1]; cardPaths.push({ pageIndex: 0, cardIndex: pos.cardIndex, points: pagePoints, centerX: center.x, centerY: center.y, pathD, startPoint, endPoint }); } return cardPaths; }