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 }; }