ttrpg-tools/src/components/md-deck/PrintPreview.tsx

186 lines
7.5 KiB
TypeScript
Raw Normal View History

2026-03-14 15:48:55 +08:00
import { For, Show } from 'solid-js';
2026-02-27 16:02:53 +08:00
import type { DeckStore } from './hooks/deckStore';
2026-02-27 21:12:23 +08:00
import { usePageLayout } from './hooks/usePageLayout';
import { usePDFExport, type ExportOptions } from './hooks/usePDFExport';
2026-03-14 16:20:55 +08:00
import { usePlotterExport } from './hooks/usePlotterExport';
2026-03-14 15:48:55 +08:00
import { getShapeSvgClipPath } from './hooks/shape-styles';
2026-02-27 21:12:23 +08:00
import { PrintPreviewHeader } from './PrintPreviewHeader';
import { PrintPreviewFooter } from './PrintPreviewFooter';
import { CardLayer } from './CardLayer';
2026-02-27 16:02:53 +08:00
export interface PrintPreviewProps {
store: DeckStore;
onClose: () => void;
2026-02-27 20:27:26 +08:00
onExport: () => void;
2026-02-27 16:02:53 +08:00
}
/**
* A4
*/
export function PrintPreview(props: PrintPreviewProps) {
const { store } = props;
2026-02-27 21:12:23 +08:00
const { getA4Size, pages, cropMarks } = usePageLayout(store);
const { exportToPDF } = usePDFExport(store, props.onClose);
2026-03-14 16:20:55 +08:00
const { exportToPlt } = usePlotterExport(store);
2026-02-27 21:12:23 +08:00
2026-03-13 17:26:00 +08:00
const frontVisibleLayers = () => store.state.frontLayerConfigs.filter((l) => l.visible);
const backVisibleLayers = () => store.state.backLayerConfigs.filter((l) => l.visible);
2026-02-27 21:12:23 +08:00
const handleExport = async () => {
const options: ExportOptions = {
orientation: store.state.printOrientation,
cardWidth: store.state.dimensions?.cardWidth || 56,
cardHeight: store.state.dimensions?.cardHeight || 88,
gridOriginX: store.state.dimensions?.gridOriginX || 0,
gridOriginY: store.state.dimensions?.gridOriginY || 0,
gridAreaWidth: store.state.dimensions?.gridAreaWidth || 56,
gridAreaHeight: store.state.dimensions?.gridAreaHeight || 88,
2026-03-13 17:26:00 +08:00
visibleLayers: frontVisibleLayers(),
2026-02-27 21:12:23 +08:00
dimensions: store.state.dimensions!
};
await exportToPDF(pages(), cropMarks(), options);
2026-02-27 18:19:37 +08:00
};
2026-03-14 16:20:55 +08:00
const handleExportPlt = () => {
exportToPlt(pages());
};
2026-02-27 16:02:53 +08:00
return (
2026-02-27 20:27:26 +08:00
<div class="fixed inset-0 bg-black/50 z-50 overflow-auto">
<div class="min-h-screen py-20 px-4">
2026-02-27 21:12:23 +08:00
<PrintPreviewHeader
store={store}
pageCount={pages().length}
onExport={handleExport}
2026-03-14 16:20:55 +08:00
onExportPlt={handleExportPlt}
2026-02-27 21:12:23 +08:00
onClose={props.onClose}
/>
2026-02-27 16:02:53 +08:00
2026-02-27 21:12:23 +08:00
<PrintPreviewFooter store={store} />
2026-02-27 21:02:33 +08:00
2026-02-27 20:27:26 +08:00
<div class="flex flex-col items-center gap-8">
2026-02-27 16:02:53 +08:00
<For each={pages()}>
2026-03-13 17:26:00 +08:00
{(page) => {
// 根据页面类型(正面/背面)决定使用哪个图层配置
const isFrontPage = page.cards[0]?.side !== 'back';
const visibleLayersForPage = isFrontPage ? frontVisibleLayers() : backVisibleLayers();
return (
<svg
class="bg-white shadow-xl"
viewBox={`0 0 ${getA4Size().width}mm ${getA4Size().height}mm`}
style={{
width: `${getA4Size().width}mm`,
height: `${getA4Size().height}mm`
}}
data-page={page.pageIndex + 1}
xmlns="http://www.w3.org/2000/svg"
>
<rect
x={`${cropMarks()[page.pageIndex]?.frameBoundsWithMargin.x}mm`}
y={`${cropMarks()[page.pageIndex]?.frameBoundsWithMargin.y}mm`}
width={`${cropMarks()[page.pageIndex]?.frameBoundsWithMargin.width}mm`}
height={`${cropMarks()[page.pageIndex]?.frameBoundsWithMargin.height}mm`}
fill="none"
stroke="black"
stroke-width="0.2"
/>
2026-02-27 18:19:37 +08:00
2026-03-13 17:26:00 +08:00
<For each={cropMarks()[page.pageIndex]?.horizontalLines}>
{(line) => (
<>
<line
x1={`${line.xStart}mm`}
y1={`${line.y}mm`}
x2={`${page.frameBounds.minX}mm`}
y2={`${line.y}mm`}
stroke="#888"
stroke-width="0.1"
/>
<line
x1={`${page.frameBounds.maxX}mm`}
y1={`${line.y}mm`}
x2={`${line.xEnd}mm`}
y2={`${line.y}mm`}
stroke="#888"
stroke-width="0.1"
/>
</>
)}
</For>
2026-02-27 21:12:23 +08:00
2026-03-13 17:26:00 +08:00
<For each={cropMarks()[page.pageIndex]?.verticalLines}>
{(line) => (
<>
<line
x1={`${line.x}mm`}
y1={`${line.yStart}mm`}
x2={`${line.x}mm`}
y2={`${page.frameBounds.minY}mm`}
stroke="#888"
stroke-width="0.1"
/>
<line
x1={`${line.x}mm`}
y1={`${page.frameBounds.maxY}mm`}
x2={`${line.x}mm`}
y2={`${line.yEnd}mm`}
stroke="#888"
stroke-width="0.1"
/>
</>
)}
</For>
2026-02-27 18:19:37 +08:00
2026-03-13 17:26:00 +08:00
<For each={page.cards}>
2026-03-14 15:48:55 +08:00
{(card) => {
const cardWidth = store.state.dimensions?.cardWidth || 56;
const cardHeight = store.state.dimensions?.cardHeight || 88;
const clipPathId = `clip-${page.pageIndex}-${card.data.id || card.x}-${card.y}`;
const shapeClipPath = getShapeSvgClipPath(clipPathId, cardWidth, cardHeight, store.state.shape);
return (
<g class="card-group">
<Show when={shapeClipPath}>
<defs>{shapeClipPath}</defs>
</Show>
<foreignObject
x={`${card.x}mm`}
y={`${card.y}mm`}
width={`${cardWidth}mm`}
height={`${cardHeight}mm`}
clip-path={shapeClipPath ? `url(#${clipPathId})` : undefined}
>
<div class="w-full h-full bg-white" {...({ xmlns: 'http://www.w3.org/1999/xhtml' } as any)}>
<div
class="absolute"
style={{
position: 'absolute',
left: `${store.state.dimensions?.gridOriginX}mm`,
top: `${store.state.dimensions?.gridOriginY}mm`,
width: `${store.state.dimensions?.gridAreaWidth}mm`,
height: `${store.state.dimensions?.gridAreaHeight}mm`
}}
>
<CardLayer
store={store}
cardData={card.data}
side={card.side || 'front'}
/>
</div>
2026-03-13 17:26:00 +08:00
</div>
2026-03-14 15:48:55 +08:00
</foreignObject>
</g>
);
}}
2026-03-13 17:26:00 +08:00
</For>
</svg>
);
}}
2026-02-27 16:02:53 +08:00
</For>
</div>
</div>
</div>
);
}