feat: implement multi-deck merging for print preview

Introduce a deck registry to track all active deck instances. This
allows users to select and merge multiple decks with matching
dimensions into a single print preview and export session.
This commit is contained in:
2026-05-05 16:30:49 +08:00
parent d099cf5758
commit 4953d33f0f
5 changed files with 699 additions and 316 deletions
+121 -42
View File
@@ -1,6 +1,7 @@
import { createMemo } from 'solid-js';
import type { DeckStore } from './deckStore';
import type { PageData, CropMarkData } from './usePDFExport';
import { createMemo } from "solid-js";
import type { DeckStore } from "./deckStore";
import type { PageData, CropMarkData } from "./usePDFExport";
import type { CardData } from "../types";
export interface A4Size {
width: number;
@@ -21,22 +22,29 @@ const PRINT_MARGIN = 5;
/**
* 页面布局计算 hook
* @param store 主牌组的 store
* @param extraCards 可选的额外卡牌列表(用于合并打印)
*/
export function usePageLayout(store: DeckStore): UsePageLayoutReturn {
export function usePageLayout(
store: DeckStore,
extraCards?: () => CardData[],
): UsePageLayoutReturn {
const orientation = () => store.state.printOrientation;
const doubleSided = () => store.state.printDoubleSided;
const frontOddPageOffsetX = () => store.state.printFrontOddPageOffsetX;
const frontOddPageOffsetY = () => store.state.printFrontOddPageOffsetY;
const getA4Size = () => {
if (orientation() === 'landscape') {
if (orientation() === "landscape") {
return { width: A4_WIDTH_LANDSCAPE, height: A4_HEIGHT_LANDSCAPE };
}
return { width: A4_WIDTH_PORTRAIT, height: A4_HEIGHT_PORTRAIT };
};
const pages = createMemo<PageData[]>(() => {
const cards = store.state.cards;
const baseCards = store.state.cards as CardData[];
const extra = extraCards?.() ?? [];
const cards = [...baseCards, ...extra];
const cardWidth = store.state.dimensions?.cardWidth || 56;
const cardHeight = store.state.dimensions?.cardHeight || 88;
const { width: a4Width, height: a4Height } = getA4Size();
@@ -68,8 +76,18 @@ export function usePageLayout(store: DeckStore): UsePageLayoutReturn {
result.push({
pageIndex: result.length,
cards: [],
bounds: { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity },
frameBounds: { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity }
bounds: {
minX: Infinity,
minY: Infinity,
maxX: -Infinity,
maxY: -Infinity,
},
frameBounds: {
minX: Infinity,
minY: Infinity,
maxX: -Infinity,
maxY: -Infinity,
},
});
}
@@ -78,7 +96,10 @@ export function usePageLayout(store: DeckStore): UsePageLayoutReturn {
// 计算当前正面页的卡牌范围
const startCardIndex = pageIndex * cardsPerPage;
const endCardIndex = Math.min(startCardIndex + cardsPerPage, totalCards);
const endCardIndex = Math.min(
startCardIndex + cardsPerPage,
totalCards,
);
for (let i = startCardIndex; i < endCardIndex; i++) {
// 正面:正常顺序排列
@@ -92,30 +113,50 @@ export function usePageLayout(store: DeckStore): UsePageLayoutReturn {
const frontX = baseOffsetX + col * cardWidth + pageOffsetX;
const frontY = baseOffsetY + row * cardHeight + pageOffsetY;
frontPage.cards.push({ data: cards[i], x: frontX, y: frontY, side: 'front' as const });
frontPage.cards.push({
data: cards[i],
x: frontX,
y: frontY,
side: "front" as const,
});
frontPage.bounds.minX = Math.min(frontPage.bounds.minX, frontX);
frontPage.bounds.minY = Math.min(frontPage.bounds.minY, frontY);
frontPage.bounds.maxX = Math.max(frontPage.bounds.maxX, frontX + cardWidth);
frontPage.bounds.maxY = Math.max(frontPage.bounds.maxY, frontY + cardHeight);
frontPage.bounds.maxX = Math.max(
frontPage.bounds.maxX,
frontX + cardWidth,
);
frontPage.bounds.maxY = Math.max(
frontPage.bounds.maxY,
frontY + cardHeight,
);
// 背面:逆转顺序排列(长边方向)
// 对于竖向打印,长边是垂直方向,所以逆转行
// 对于横向打印,长边是水平方向,所以逆转列
const backRow = orientation() === 'portrait'
? (rowsPerPage - 1 - row)
: row;
const backCol = orientation() === 'portrait'
? col
: (cardsPerRow - 1 - col);
const backRow =
orientation() === "portrait" ? rowsPerPage - 1 - row : row;
const backCol =
orientation() === "portrait" ? col : cardsPerRow - 1 - col;
const backX = baseOffsetX + backCol * cardWidth;
const backY = baseOffsetY + backRow * cardHeight;
backPage.cards.push({ data: cards[i], x: backX, y: backY, side: 'back' as const });
backPage.cards.push({
data: cards[i],
x: backX,
y: backY,
side: "back" as const,
});
backPage.bounds.minX = Math.min(backPage.bounds.minX, backX);
backPage.bounds.minY = Math.min(backPage.bounds.minY, backY);
backPage.bounds.maxX = Math.max(backPage.bounds.maxX, backX + cardWidth);
backPage.bounds.maxY = Math.max(backPage.bounds.maxY, backY + cardHeight);
backPage.bounds.maxX = Math.max(
backPage.bounds.maxX,
backX + cardWidth,
);
backPage.bounds.maxY = Math.max(
backPage.bounds.maxY,
backY + cardHeight,
);
}
}
} else {
@@ -123,8 +164,18 @@ export function usePageLayout(store: DeckStore): UsePageLayoutReturn {
let currentPage: PageData = {
pageIndex: 0,
cards: [],
bounds: { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity },
frameBounds: { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity }
bounds: {
minX: Infinity,
minY: Infinity,
maxX: -Infinity,
maxY: -Infinity,
},
frameBounds: {
minX: Infinity,
minY: Infinity,
maxX: -Infinity,
maxY: -Infinity,
},
};
for (let i = 0; i < cards.length; i++) {
@@ -138,8 +189,18 @@ export function usePageLayout(store: DeckStore): UsePageLayoutReturn {
currentPage = {
pageIndex,
cards: [],
bounds: { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity },
frameBounds: { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity }
bounds: {
minX: Infinity,
minY: Infinity,
maxX: -Infinity,
maxY: -Infinity,
},
frameBounds: {
minX: Infinity,
minY: Infinity,
maxX: -Infinity,
maxY: -Infinity,
},
};
}
@@ -150,11 +211,22 @@ export function usePageLayout(store: DeckStore): UsePageLayoutReturn {
const cardX = baseOffsetX + col * cardWidth + pageOffsetX;
const cardY = baseOffsetY + row * cardHeight + pageOffsetY;
currentPage.cards.push({ data: cards[i], x: cardX, y: cardY, side: 'front' as const });
currentPage.cards.push({
data: cards[i],
x: cardX,
y: cardY,
side: "front" as const,
});
currentPage.bounds.minX = Math.min(currentPage.bounds.minX, cardX);
currentPage.bounds.minY = Math.min(currentPage.bounds.minY, cardY);
currentPage.bounds.maxX = Math.max(currentPage.bounds.maxX, cardX + cardWidth);
currentPage.bounds.maxY = Math.max(currentPage.bounds.maxY, cardY + cardHeight);
currentPage.bounds.maxX = Math.max(
currentPage.bounds.maxX,
cardX + cardWidth,
);
currentPage.bounds.maxY = Math.max(
currentPage.bounds.maxY,
cardY + cardHeight,
);
}
if (currentPage.cards.length > 0) {
@@ -162,25 +234,27 @@ export function usePageLayout(store: DeckStore): UsePageLayoutReturn {
}
}
return result.map(page => {
const offsetX = doubleSided() && page.pageIndex % 2 === 0 ? frontOddPageOffsetX() : 0;
const offsetY = doubleSided() && page.pageIndex % 2 === 0 ? frontOddPageOffsetY() : 0;
return result.map((page) => {
const offsetX =
doubleSided() && page.pageIndex % 2 === 0 ? frontOddPageOffsetX() : 0;
const offsetY =
doubleSided() && page.pageIndex % 2 === 0 ? frontOddPageOffsetY() : 0;
return {
...page,
frameBounds: {
minX: baseOffsetX + offsetX,
minY: baseOffsetY + offsetY,
maxX: baseOffsetX + maxGridWidth + offsetX,
maxY: baseOffsetY + maxGridHeight + offsetY
}
maxY: baseOffsetY + maxGridHeight + offsetY,
},
};
});
});
const cropMarks = createMemo<CropMarkData[]>(() => {
const pagesData = pages();
return pagesData.map(page => {
return pagesData.map((page) => {
const { frameBounds, cards } = page;
const cardWidth = store.state.dimensions?.cardWidth || 56;
const cardHeight = store.state.dimensions?.cardHeight || 88;
@@ -188,7 +262,7 @@ export function usePageLayout(store: DeckStore): UsePageLayoutReturn {
const xPositions = new Set<number>();
const yPositions = new Set<number>();
cards.forEach(card => {
cards.forEach((card) => {
xPositions.add(card.x);
xPositions.add(card.x + cardWidth);
yPositions.add(card.y);
@@ -200,26 +274,31 @@ export function usePageLayout(store: DeckStore): UsePageLayoutReturn {
const OVERLAP = 3;
const horizontalLines = sortedY.map(y => ({
const horizontalLines = sortedY.map((y) => ({
y,
xStart: frameBounds.minX - OVERLAP,
xEnd: frameBounds.maxX + OVERLAP
xEnd: frameBounds.maxX + OVERLAP,
}));
const verticalLines = sortedX.map(x => ({
const verticalLines = sortedX.map((x) => ({
x,
yStart: frameBounds.minY - OVERLAP,
yEnd: frameBounds.maxY + OVERLAP
yEnd: frameBounds.maxY + OVERLAP,
}));
const frameBoundsWithMargin = {
x: frameBounds.minX - 1,
y: frameBounds.minY - 1,
width: frameBounds.maxX - frameBounds.minX + 2,
height: frameBounds.maxY - frameBounds.minY + 2
height: frameBounds.maxY - frameBounds.minY + 2,
};
return { horizontalLines, verticalLines, frameBounds, frameBoundsWithMargin };
return {
horizontalLines,
verticalLines,
frameBounds,
frameBoundsWithMargin,
};
});
});