236 lines
6.2 KiB
TypeScript
236 lines
6.2 KiB
TypeScript
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<LayoutOptions, 'orientation' | 'printMargin'> & { 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;
|
||
}
|