ttrpg-tools/src/components/md-deck/hooks/usePageLayout.ts

154 lines
5.2 KiB
TypeScript
Raw Normal View History

2026-02-27 21:12:23 +08:00
import { createMemo } from 'solid-js';
import type { DeckStore } from './deckStore';
import type { PageData, CropMarkData } from './usePDFExport';
export interface A4Size {
width: number;
height: number;
}
export interface UsePageLayoutReturn {
getA4Size: () => A4Size;
pages: ReturnType<typeof createMemo<PageData[]>>;
cropMarks: ReturnType<typeof createMemo<CropMarkData[]>>;
}
const A4_WIDTH_PORTRAIT = 210;
const A4_HEIGHT_PORTRAIT = 297;
const A4_WIDTH_LANDSCAPE = 297;
const A4_HEIGHT_LANDSCAPE = 210;
const PRINT_MARGIN = 5;
/**
* hook
*/
export function usePageLayout(store: DeckStore): UsePageLayoutReturn {
const orientation = () => store.state.printOrientation;
const oddPageOffsetX = () => store.state.printOddPageOffsetX;
const oddPageOffsetY = () => store.state.printOddPageOffsetY;
const getA4Size = () => {
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 cardWidth = store.state.dimensions?.cardWidth || 56;
const cardHeight = store.state.dimensions?.cardHeight || 88;
const { width: a4Width, height: a4Height } = getA4Size();
const usableWidth = a4Width - PRINT_MARGIN * 2;
const cardsPerRow = Math.floor(usableWidth / cardWidth);
const usableHeight = a4Height - PRINT_MARGIN * 2;
const rowsPerPage = Math.floor(usableHeight / cardHeight);
const cardsPerPage = cardsPerRow * rowsPerPage;
const maxGridWidth = cardsPerRow * cardWidth;
const maxGridHeight = rowsPerPage * cardHeight;
const baseOffsetX = (a4Width - maxGridWidth) / 2;
const baseOffsetY = (a4Height - maxGridHeight) / 2;
const result: PageData[] = [];
let currentPage: PageData = {
pageIndex: 0,
cards: [],
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++) {
const pageIndex = Math.floor(i / cardsPerPage);
const indexInPage = i % cardsPerPage;
const row = Math.floor(indexInPage / cardsPerRow);
const col = indexInPage % cardsPerRow;
if (pageIndex !== currentPage.pageIndex) {
result.push(currentPage);
currentPage = {
pageIndex,
cards: [],
bounds: { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity },
frameBounds: { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity }
};
}
const isOddPage = pageIndex % 2 === 0;
const pageOffsetX = isOddPage ? oddPageOffsetX() : 0;
const pageOffsetY = isOddPage ? oddPageOffsetY() : 0;
const cardX = baseOffsetX + col * cardWidth + pageOffsetX;
const cardY = baseOffsetY + row * cardHeight + pageOffsetY;
currentPage.cards.push({ data: cards[i], x: cardX, y: cardY });
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);
}
if (currentPage.cards.length > 0) {
result.push(currentPage);
}
return result.map(page => ({
...page,
frameBounds: {
minX: baseOffsetX + (page.pageIndex % 2 === 0 ? oddPageOffsetX() : 0),
minY: baseOffsetY + (page.pageIndex % 2 === 0 ? oddPageOffsetY() : 0),
maxX: baseOffsetX + maxGridWidth + (page.pageIndex % 2 === 0 ? oddPageOffsetX() : 0),
maxY: baseOffsetY + maxGridHeight + (page.pageIndex % 2 === 0 ? oddPageOffsetY() : 0)
}
}));
});
const cropMarks = createMemo<CropMarkData[]>(() => {
const pagesData = pages();
return pagesData.map(page => {
const { frameBounds, cards } = page;
const cardWidth = store.state.dimensions?.cardWidth || 56;
const cardHeight = store.state.dimensions?.cardHeight || 88;
const xPositions = new Set<number>();
const yPositions = new Set<number>();
cards.forEach(card => {
xPositions.add(card.x);
xPositions.add(card.x + cardWidth);
yPositions.add(card.y);
yPositions.add(card.y + cardHeight);
});
const sortedX = Array.from(xPositions).sort((a, b) => a - b);
const sortedY = Array.from(yPositions).sort((a, b) => a - b);
const OVERLAP = 3;
const horizontalLines = sortedY.map(y => ({
y,
xStart: frameBounds.minX - OVERLAP,
xEnd: frameBounds.maxX + OVERLAP
}));
const verticalLines = sortedX.map(x => ({
x,
yStart: frameBounds.minY - 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
};
return { horizontalLines, verticalLines, frameBounds, frameBoundsWithMargin };
});
});
return { getA4Size, pages, cropMarks };
}