refactor: move code to plotcutter
This commit is contained in:
@@ -1,16 +1,4 @@
|
||||
export type CardShape = 'rectangle' | 'circle' | 'triangle' | 'hexagon';
|
||||
|
||||
export interface ContourPoint {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface ContourBounds {
|
||||
minX: number;
|
||||
minY: number;
|
||||
maxX: number;
|
||||
maxY: number;
|
||||
}
|
||||
import type { CardShape, ContourPoint, ContourBounds } from './types';
|
||||
|
||||
/**
|
||||
* 生成带圆角的矩形轮廓点
|
||||
|
||||
@@ -2,4 +2,6 @@ export * from "./bezier";
|
||||
export * from "./vector";
|
||||
export * from "./plotter";
|
||||
export * from "./contour";
|
||||
export * from "./layout";
|
||||
export * from "./layout";
|
||||
export * from "./types";
|
||||
export * from "./parser";
|
||||
+153
-13
@@ -1,18 +1,12 @@
|
||||
import { contourToSvgPath } from './contour';
|
||||
import { getCardShapePoints, calculateCenter } from './contour';
|
||||
import type { CardPath, SinglePageLayout, LayoutOptions, CardPosition } from './types';
|
||||
|
||||
/**
|
||||
* 卡片切割路径
|
||||
*/
|
||||
export interface CardPath {
|
||||
pageIndex: number;
|
||||
cardIndex: number;
|
||||
points: [number, number][];
|
||||
centerX: number;
|
||||
centerY: number;
|
||||
pathD: string;
|
||||
startPoint: [number, number];
|
||||
endPoint: [number, number];
|
||||
}
|
||||
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;
|
||||
|
||||
/**
|
||||
* 生成空走路径(抬刀移动路径)
|
||||
@@ -93,3 +87,149 @@ export function calculateTotalBounds(cardPaths: CardPath[]): {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
import type { ParsedPlt } from './types';
|
||||
|
||||
/**
|
||||
* HPGL 单位转换系数
|
||||
*/
|
||||
const HPGL_UNIT = 0.025; // 1 HPGL unit = 0.025mm
|
||||
|
||||
/**
|
||||
* 解析 HPGL/PLT 代码,提取路径信息
|
||||
*
|
||||
* 支持的命令:
|
||||
* - IN: 初始化
|
||||
* - TB: 定义绘图区域
|
||||
* - CT: 设置切线角度
|
||||
* - SP: 选择画笔
|
||||
* - PU: 抬笔移动
|
||||
* - PD: 下笔绘制
|
||||
* - D: 绘制到指定点(简写)
|
||||
* - U: 抬笔移动到指定点(简写)
|
||||
* - @: 结束命令
|
||||
*/
|
||||
export function parsePlt(pltCode: string): ParsedPlt {
|
||||
const result: ParsedPlt = {
|
||||
paths: [],
|
||||
startPoint: undefined,
|
||||
endPoint: undefined,
|
||||
width: undefined,
|
||||
height: undefined
|
||||
};
|
||||
|
||||
// 解析 TB 命令获取页面尺寸
|
||||
const tbMatch = pltCode.match(/TB(\d+),(\d+),(\d+)/);
|
||||
if (tbMatch) {
|
||||
const width = Number(tbMatch[2]) * HPGL_UNIT;
|
||||
const height = Number(tbMatch[3]) * HPGL_UNIT;
|
||||
result.width = width;
|
||||
result.height = height;
|
||||
}
|
||||
|
||||
// 提取所有下笔绘制的路径
|
||||
// 解析逻辑:跟踪 PU/PD/U/D 命令,收集 PD/D 命令的点
|
||||
const lines = pltCode.split('\n');
|
||||
let currentPath: [number, number][] = [];
|
||||
let isPenDown = false;
|
||||
let currentPosition: [number, number] = [0, 0];
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
|
||||
// 解析 PU 命令(抬笔移动)
|
||||
const puMatch = trimmed.match(/PU\s*(\d+(?:\.\d+)?),(\d+(?:\.\d+)?)/i);
|
||||
if (puMatch) {
|
||||
if (currentPath.length > 0) {
|
||||
result.paths.push([...currentPath]);
|
||||
currentPath = [];
|
||||
}
|
||||
isPenDown = false;
|
||||
currentPosition = [
|
||||
Number(puMatch[1]) * HPGL_UNIT,
|
||||
Number(puMatch[2]) * HPGL_UNIT
|
||||
];
|
||||
continue;
|
||||
}
|
||||
|
||||
// 解析 PD 命令(下笔绘制)
|
||||
const pdMatch = trimmed.match(/PD\s*(\d+(?:\.\d+)?),(\d+(?:\.\d+)?)/i);
|
||||
if (pdMatch) {
|
||||
const newPoint: [number, number] = [
|
||||
Number(pdMatch[1]) * HPGL_UNIT,
|
||||
Number(pdMatch[2]) * HPGL_UNIT
|
||||
];
|
||||
|
||||
if (!isPenDown) {
|
||||
// 新的路径起点
|
||||
currentPath = [newPoint];
|
||||
isPenDown = true;
|
||||
} else {
|
||||
currentPath.push(newPoint);
|
||||
}
|
||||
currentPosition = newPoint;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 解析 U 命令(抬笔移动,简写)
|
||||
const uMatch = trimmed.match(/U\s*(\d+(?:\.\d+)?),(\d+(?:\.\d+)?)/i);
|
||||
if (uMatch) {
|
||||
if (currentPath.length > 0) {
|
||||
result.paths.push([...currentPath]);
|
||||
currentPath = [];
|
||||
}
|
||||
isPenDown = false;
|
||||
currentPosition = [
|
||||
Number(uMatch[1]) * HPGL_UNIT,
|
||||
Number(uMatch[2]) * HPGL_UNIT
|
||||
];
|
||||
continue;
|
||||
}
|
||||
|
||||
// 解析 D 命令(下笔绘制,简写)
|
||||
const dMatch = trimmed.match(/D\s*(\d+(?:\.\d+)?),(\d+(?:\.\d+)?)/i);
|
||||
if (dMatch) {
|
||||
const newPoint: [number, number] = [
|
||||
Number(dMatch[1]) * HPGL_UNIT,
|
||||
Number(dMatch[2]) * HPGL_UNIT
|
||||
];
|
||||
|
||||
if (!isPenDown) {
|
||||
// 新的路径起点
|
||||
currentPath = [newPoint];
|
||||
isPenDown = true;
|
||||
} else {
|
||||
currentPath.push(newPoint);
|
||||
}
|
||||
currentPosition = newPoint;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 解析 PA 命令(绝对坐标移动)
|
||||
const paMatch = trimmed.match(/PA\s*(\d+(?:\.\d+)?),(\d+(?:\.\d+)?)/i);
|
||||
if (paMatch) {
|
||||
const newPoint: [number, number] = [
|
||||
Number(paMatch[1]) * HPGL_UNIT,
|
||||
Number(paMatch[2]) * HPGL_UNIT
|
||||
];
|
||||
|
||||
if (isPenDown) {
|
||||
currentPath.push(newPoint);
|
||||
} else {
|
||||
if (currentPath.length > 0) {
|
||||
result.paths.push([...currentPath]);
|
||||
currentPath = [];
|
||||
}
|
||||
currentPosition = newPoint;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查结束命令
|
||||
if (trimmed.includes('@')) {
|
||||
if (currentPath.length > 0) {
|
||||
result.paths.push([...currentPath]);
|
||||
currentPath = [];
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 处理最后一条路径
|
||||
if (currentPath.length > 0) {
|
||||
result.paths.push([...currentPath]);
|
||||
}
|
||||
|
||||
// 设置起点和终点
|
||||
if (result.paths.length > 0) {
|
||||
const firstPath = result.paths[0];
|
||||
const lastPath = result.paths[result.paths.length - 1];
|
||||
|
||||
if (firstPath.length > 0) {
|
||||
result.startPoint = firstPath[0];
|
||||
}
|
||||
if (lastPath.length > 0) {
|
||||
result.endPoint = lastPath[lastPath.length - 1];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从解析的 PLT 数据中提取切割路径(排除空走路径)
|
||||
*
|
||||
* 空走路径特征:
|
||||
* - 路径长度短(通常是直线移动)
|
||||
* - 连接两个切割路径的终点和起点
|
||||
*
|
||||
* @param parsedPlt 解析后的 PLT 数据
|
||||
* @param minCutLength 最小切割路径长度阈值(mm),小于此值的路径被视为空走路径
|
||||
*/
|
||||
export function extractCutPaths(parsedPlt: ParsedPlt, minCutLength: number = 5): [number, number][][] {
|
||||
const allPaths = parsedPlt.paths;
|
||||
const cutPaths: [number, number][][] = [];
|
||||
|
||||
for (const path of allPaths) {
|
||||
if (path.length < 2) continue;
|
||||
|
||||
// 计算路径长度
|
||||
let pathLength = 0;
|
||||
for (let i = 1; i < path.length; i++) {
|
||||
const [x1, y1] = path[i - 1];
|
||||
const [x2, y2] = path[i];
|
||||
const dx = x2 - x1;
|
||||
const dy = y2 - y1;
|
||||
pathLength += Math.sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
|
||||
// 如果路径长度大于阈值,认为是切割路径
|
||||
if (pathLength >= minCutLength) {
|
||||
cutPaths.push(path);
|
||||
}
|
||||
}
|
||||
|
||||
return cutPaths;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将解析的路径转换为 SVG path 命令
|
||||
*/
|
||||
export function parsedPltToSvg(paths: [number, number][]): string {
|
||||
if (paths.length === 0) return '';
|
||||
|
||||
const [startX, startY] = paths[0];
|
||||
let d = `M ${startX} ${startY}`;
|
||||
|
||||
for (let i = 1; i < paths.length; i++) {
|
||||
const [x, y] = paths[i];
|
||||
d += ` L ${x} ${y}`;
|
||||
}
|
||||
|
||||
d += ' Z';
|
||||
return d;
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* 卡片形状类型
|
||||
*/
|
||||
export type CardShape = 'rectangle' | 'circle' | 'triangle' | 'hexagon';
|
||||
|
||||
/**
|
||||
* 轮廓点类型
|
||||
*/
|
||||
export interface ContourPoint {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 轮廓边界框
|
||||
*/
|
||||
export interface ContourBounds {
|
||||
minX: number;
|
||||
minY: number;
|
||||
maxX: number;
|
||||
maxY: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 卡片切割路径
|
||||
*/
|
||||
export interface CardPath {
|
||||
pageIndex: number;
|
||||
cardIndex: number;
|
||||
points: [number, number][];
|
||||
centerX: number;
|
||||
centerY: number;
|
||||
pathD: string;
|
||||
startPoint: [number, number];
|
||||
endPoint: [number, number];
|
||||
}
|
||||
|
||||
/**
|
||||
* 单页排版结果
|
||||
*/
|
||||
export interface SinglePageLayout {
|
||||
/** 单页满排时的卡片位置 */
|
||||
cardPositions: CardPosition[];
|
||||
/** 对应的刀路路径 */
|
||||
cardPaths: CardPath[];
|
||||
/** A4 纸宽度 (mm) */
|
||||
a4Width: number;
|
||||
/** A4 纸高度 (mm) */
|
||||
a4Height: number;
|
||||
/** 每行卡片数 */
|
||||
cardsPerRow: number;
|
||||
/** 每页行数 */
|
||||
rowsPerPage: number;
|
||||
/** 每页总卡片数 */
|
||||
cardsPerPage: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 卡片位置
|
||||
*/
|
||||
export interface CardPosition {
|
||||
x: number;
|
||||
y: number;
|
||||
cardIndex: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 排版选项
|
||||
*/
|
||||
export interface LayoutOptions {
|
||||
/** 卡片宽度 (mm) */
|
||||
cardWidth: number;
|
||||
/** 卡片高度 (mm) */
|
||||
cardHeight: number;
|
||||
/** 卡片形状 */
|
||||
shape: CardShape;
|
||||
/** 出血 (mm) */
|
||||
bleed: number;
|
||||
/** 圆角半径 (mm) */
|
||||
cornerRadius: number;
|
||||
/** 打印方向 */
|
||||
orientation: 'portrait' | 'landscape';
|
||||
/** 打印边距 (mm) */
|
||||
printMargin?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* PLT 解析结果
|
||||
*/
|
||||
export interface ParsedPlt {
|
||||
/** 解析出的路径 */
|
||||
paths: [number, number][][];
|
||||
/** 起点坐标 (mm) */
|
||||
startPoint?: [number, number];
|
||||
/** 终点坐标 (mm) */
|
||||
endPoint?: [number, number];
|
||||
/** 页面宽度 (mm) */
|
||||
width?: number;
|
||||
/** 页面高度 (mm) */
|
||||
height?: number;
|
||||
}
|
||||
Reference in New Issue
Block a user