diff --git a/src/components/md-deck/PltPreview.tsx b/src/components/md-deck/PltPreview.tsx
new file mode 100644
index 0000000..a58a879
--- /dev/null
+++ b/src/components/md-deck/PltPreview.tsx
@@ -0,0 +1,385 @@
+import { createSignal, For, Show, createMemo } from 'solid-js';
+import type { PageData } from './hooks/usePDFExport';
+import type { CardShape } from './types';
+import { pts2plotter } from '../../plotcutter';
+
+export interface PltPreviewProps {
+ pages: PageData[];
+ cardWidth: number;
+ cardHeight: number;
+ shape: CardShape;
+ bleed: number;
+ onClose: () => void;
+}
+
+export interface CardPath {
+ pageIndex: number;
+ cardIndex: number;
+ points: [number, number][];
+ centerX: number;
+ centerY: number;
+ pathD: string;
+}
+
+/**
+ * 根据形状生成卡片轮廓点(单位:mm,相对于卡片左下角)
+ */
+function getCardShapePoints(
+ shape: CardShape,
+ width: number,
+ height: number
+): [number, number][] {
+ const points: [number, number][] = [];
+
+ switch (shape) {
+ case 'circle': {
+ const radius = Math.min(width, height) / 2;
+ const centerX = width / 2;
+ const centerY = height / 2;
+ for (let i = 0; i < 36; i++) {
+ const angle = (i / 36) * Math.PI * 2;
+ points.push([
+ centerX + radius * Math.cos(angle),
+ centerY + radius * Math.sin(angle)
+ ]);
+ }
+ break;
+ }
+ case 'triangle': {
+ points.push([width / 2, 0]);
+ points.push([0, height]);
+ points.push([width, height]);
+ break;
+ }
+ case 'hexagon': {
+ const halfW = width / 2;
+ const quarterH = height / 4;
+ points.push([halfW, 0]);
+ points.push([width, quarterH]);
+ points.push([width, height - quarterH]);
+ points.push([halfW, height]);
+ points.push([0, height - quarterH]);
+ points.push([0, quarterH]);
+ break;
+ }
+ case 'rectangle':
+ default: {
+ points.push([0, 0]);
+ points.push([width, 0]);
+ points.push([width, height]);
+ points.push([0, height]);
+ break;
+ }
+ }
+
+ return points;
+}
+
+/**
+ * 计算多边形的中心点
+ */
+function calculateCenter(points: [number, number][]): { x: number; y: number } {
+ let sumX = 0;
+ let sumY = 0;
+ for (const [x, y] of points) {
+ sumX += x;
+ sumY += y;
+ }
+ return {
+ x: sumX / points.length,
+ y: sumY / points.length
+ };
+}
+
+/**
+ * 根据进度计算小球在路径上的位置
+ */
+function getPointOnPath(points: [number, number][], progress: number): [number, number] {
+ if (points.length === 0) return [0, 0];
+ if (points.length === 1) return points[0];
+
+ const totalSegments = points.length;
+ const scaledProgress = progress * totalSegments;
+ const segmentIndex = Math.floor(scaledProgress);
+ const segmentProgress = scaledProgress - segmentIndex;
+
+ const currentIndex = Math.min(segmentIndex, points.length - 1);
+ const nextIndex = (currentIndex + 1) % points.length;
+
+ const [x1, y1] = points[currentIndex];
+ const [x2, y2] = points[nextIndex];
+
+ return [
+ x1 + (x2 - x1) * segmentProgress,
+ y1 + (y2 - y1) * segmentProgress
+ ];
+}
+
+/**
+ * PLT 预览组件 - 显示切割路径预览
+ */
+export function PltPreview(props: PltPreviewProps) {
+ const a4Width = 297; // 横向 A4
+ const a4Height = 210;
+
+ // 收集所有卡片路径
+ const cardPaths: CardPath[] = [];
+ let pathIndex = 0;
+
+ // 计算切割尺寸(排版尺寸减去出血)
+ const cutWidth = props.cardWidth - props.bleed * 2;
+ const cutHeight = props.cardHeight - props.bleed * 2;
+
+ for (const page of props.pages) {
+ for (const card of page.cards) {
+ if (card.side !== 'front') continue;
+
+ const shapePoints = getCardShapePoints(props.shape, cutWidth, cutHeight);
+ const pagePoints = shapePoints.map(([x, y]) => [
+ card.x + props.bleed + x,
+ a4Height - (card.y + props.bleed + y)
+ ] as [number, number]);
+
+ const center = calculateCenter(pagePoints);
+ const pathD = pointsToSvgPath(pagePoints);
+
+ cardPaths.push({
+ pageIndex: page.pageIndex,
+ cardIndex: pathIndex++,
+ points: pagePoints,
+ centerX: center.x,
+ centerY: center.y,
+ pathD
+ });
+ }
+ }
+
+ // 生成 HPGL 代码用于下载
+ const allPaths = cardPaths.map(p => p.points);
+ const plotterCode = allPaths.length > 0 ? pts2plotter(allPaths, a4Width, a4Height, 1) : '';
+
+ // 进度控制 (0 到 cardPaths.length)
+ const [progress, setProgress] = createSignal(0);
+
+ // 计算当前正在切割的卡片索引
+ const currentPathIndex = createMemo(() => {
+ const p = progress();
+ if (p <= 0) return -1;
+ if (p >= cardPaths.length) return cardPaths.length - 1;
+ return Math.floor(p);
+ });
+
+ // 计算当前小球位置
+ const ballPosition = createMemo(() => {
+ const p = progress();
+ if (p <= 0 || cardPaths.length === 0) return null;
+
+ const cardIndex = Math.min(Math.floor(p), cardPaths.length - 1);
+ const cardProgress = p - cardIndex;
+ const cardPath = cardPaths[cardIndex];
+
+ return getPointOnPath(cardPath.points, cardProgress);
+ });
+
+ const handleDownload = () => {
+ if (!plotterCode) {
+ alert('没有可导出的卡片');
+ return;
+ }
+
+ const blob = new Blob([plotterCode], { type: 'application/vnd.hp-HPGL' });
+ const url = URL.createObjectURL(blob);
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = `deck-export-${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.plt`;
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ URL.revokeObjectURL(url);
+ };
+
+ const handleProgressChange = (e: Event) => {
+ const target = e.target as HTMLInputElement;
+ setProgress(Number(target.value));
+ };
+
+ return (
+
+
+ {/* 头部控制栏 */}
+
+
PLT 切割预览
+
+ 切割进度
+
+
+ {Math.floor(progress())} / {cardPaths.length}
+
+
+
+
+
+
+
+
+
+ {/* 预览区域 */}
+
+
+ {(page) => {
+ const pageCardPaths = cardPaths.filter(p => p.pageIndex === page.pageIndex);
+
+ return (
+
+ );
+ }}
+
+
+
+ {/* 图例说明 */}
+
+
+
+ );
+}
+
+/**
+ * 将路径点转换为 SVG path 命令
+ */
+function pointsToSvgPath(points: [number, number][], closed = true): string {
+ if (points.length === 0) return '';
+
+ const [startX, startY] = points[0];
+ let d = `M ${startX} ${startY}`;
+
+ for (let i = 1; i < points.length; i++) {
+ const [x, y] = points[i];
+ d += ` L ${x} ${y}`;
+ }
+
+ if (closed) {
+ d += ' Z';
+ }
+
+ return d;
+}
diff --git a/src/components/md-deck/PrintPreview.tsx b/src/components/md-deck/PrintPreview.tsx
index b2059e5..1eb7c50 100644
--- a/src/components/md-deck/PrintPreview.tsx
+++ b/src/components/md-deck/PrintPreview.tsx
@@ -1,4 +1,4 @@
-import { For, Show } from 'solid-js';
+import { createSignal, For, Show } from 'solid-js';
import type { DeckStore } from './hooks/deckStore';
import { usePageLayout } from './hooks/usePageLayout';
import { usePDFExport, type ExportOptions } from './hooks/usePDFExport';
@@ -7,6 +7,7 @@ import { getShapeSvgClipPath } from './hooks/shape-styles';
import { PrintPreviewHeader } from './PrintPreviewHeader';
import { PrintPreviewFooter } from './PrintPreviewFooter';
import { CardLayer } from './CardLayer';
+import { PltPreview } from './PltPreview';
export interface PrintPreviewProps {
store: DeckStore;
@@ -21,7 +22,9 @@ export function PrintPreview(props: PrintPreviewProps) {
const { store } = props;
const { getA4Size, pages, cropMarks } = usePageLayout(store);
const { exportToPDF } = usePDFExport(store, props.onClose);
- const { exportToPlt } = usePlotterExport(store);
+ const { generatePltData, downloadPltFile } = usePlotterExport(store);
+
+ const [showPltPreview, setShowPltPreview] = createSignal(false);
const frontVisibleLayers = () => store.state.frontLayerConfigs.filter((l) => l.visible);
const backVisibleLayers = () => store.state.backLayerConfigs.filter((l) => l.visible);
@@ -41,20 +44,25 @@ export function PrintPreview(props: PrintPreviewProps) {
await exportToPDF(pages(), cropMarks(), options);
};
- const handleExportPlt = () => {
- exportToPlt(pages());
+ const handleOpenPltPreview = () => {
+ setShowPltPreview(true);
+ };
+
+ const handleClosePltPreview = () => {
+ setShowPltPreview(false);
};
return (
-
-
-
+
}>
+
+
+
@@ -181,5 +189,6 @@ export function PrintPreview(props: PrintPreviewProps) {
+
);
}
diff --git a/src/components/md-deck/PrintPreviewHeader.tsx b/src/components/md-deck/PrintPreviewHeader.tsx
index f95ce98..196230c 100644
--- a/src/components/md-deck/PrintPreviewHeader.tsx
+++ b/src/components/md-deck/PrintPreviewHeader.tsx
@@ -4,7 +4,7 @@ export interface PrintPreviewHeaderProps {
store: DeckStore;
pageCount: number;
onExport: () => void;
- onExportPlt: () => void;
+ onOpenPltPreview: () => void;
onClose: () => void;
}
@@ -88,11 +88,11 @@ export function PrintPreviewHeader(props: PrintPreviewHeaderProps) {