Files
ttrpg-tools/src/components/md-deck/hooks/usePlotterExport.ts
T

127 lines
4.2 KiB
TypeScript

import type { DeckStore } from './deckStore';
import { calculateSinglePageLayout, generateTravelPaths } from '../../../plotcutter/layout';
import { pts2plotter } from '../../../plotcutter/plotter';
export interface PltExportData {
/** 单页满排时的 PLT 代码 */
pltCode: string;
/** 排版区域宽度 (mm) */
frameWidth: number;
/** 排版区域高度 (mm) */
frameHeight: number;
/** 每页卡片数 */
cardsPerPage: number;
}
export interface UsePlotterExportReturn {
/** 生成单页满排时的 PLT 数据 */
generatePltData: () => PltExportData | null;
/** 下载 PLT 文件 */
downloadPltFile: (pltCode: string) => void;
/** 直接导出 PLT(打开下载) */
exportToPlt: () => void;
}
/**
* PLT 导出 hook - 生成单页满排时的 HPGL 格式文件
*
* 刀路只关心单页排满的情况,不考虑实际牌组的张数。
* PLT 坐标系统以 frameBoundsWithMargin 左上角为原点 (0,0)。
*/
export function usePlotterExport(store: DeckStore): UsePlotterExportReturn {
const bleed = () => store.state.bleed || 1;
const cornerRadius = () => store.state.cornerRadius ?? 3;
const cardWidth = () => store.state.dimensions?.cardWidth || 56;
const cardHeight = () => store.state.dimensions?.cardHeight || 88;
const shape = () => store.state.shape;
const orientation = () => store.state.printOrientation || 'landscape';
const doubleCut = () => store.state.pltDoubleCut;
/**
* 生成单页满排时的 PLT 数据
*/
const generatePltData = (): PltExportData | null => {
const layout = calculateSinglePageLayout({
cardWidth: cardWidth(),
cardHeight: cardHeight(),
shape: shape(),
bleed: bleed(),
cornerRadius: cornerRadius(),
orientation: orientation()
});
if (layout.cardPaths.length === 0) {
return null;
}
// 使用 frameBoundsWithMargin 作为 PLT 的坐标系统
const frameBounds = layout.frameBoundsWithMargin;
const pltWidth = frameBounds.width;
const pltHeight = frameBounds.height;
// 将卡片路径转换为相对于 frameBoundsWithMargin 的坐标
// 原点在 frameBoundsWithMargin 的左上角
const relativePaths = layout.cardPaths.flatMap(cardPath => {
const relativePoints = cardPath.points.map(([x, y]) => {
// 转换为相对于 frameBounds 左上角的坐标
const relativeX = x - frameBounds.x;
const relativeY = y - frameBounds.y;
// 翻转 Y 轴(plotter 坐标 Y 向上,SVG 坐标 Y 向下)
// 在 frameBounds 坐标系中,原点在左上,Y 向下为正
// 在 plotter 坐标系中,原点在左下,Y 向上为正
// 所以 plotterY = pltHeight - relativeY
return [relativeX, pltHeight - relativeY] as [number, number];
});
if (doubleCut()) {
// 重复一次:某些切割机需要每张卡牌切两次
return [relativePoints, relativePoints];
}
return [relativePoints];
});
// 起点和终点都在 frameBoundsWithMargin 的左上角 (0, pltHeight)
// 在 plotter 坐标系中,左上角是 (0, height)
const startPoint: [number, number] = [0, pltHeight];
const endPoint: [number, number] = [0, pltHeight];
// 生成 HPGL 代码
const plotterCode = pts2plotter(relativePaths, pltWidth, pltHeight, 1, startPoint, endPoint);
return {
pltCode: plotterCode,
frameWidth: pltWidth,
frameHeight: pltHeight,
cardsPerPage: layout.cardsPerPage
};
};
/**
* 下载 PLT 文件
*/
const downloadPltFile = (pltCode: string) => {
const blob = new Blob([pltCode], { type: 'application/vnd.hp-HPGL' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `deck-plt-${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.plt`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
};
/**
* 直接导出 PLT(打开下载)
*/
const exportToPlt = () => {
const data = generatePltData();
if (!data) {
alert('没有可导出的卡片');
return;
}
downloadPltFile(data.pltCode);
};
return { generatePltData, downloadPltFile, exportToPlt };
}