diff --git a/frontend/src/services/printService.ts b/frontend/src/services/printService.ts index 80a5015..cb68849 100644 --- a/frontend/src/services/printService.ts +++ b/frontend/src/services/printService.ts @@ -1,422 +1 @@ -import type { YjsDocument } from '../crdt/YjsDocument'; - -// ─── Types ─────────────────────────────────────────────────────────────────── - -export type PageSize = 'a4' | 'a3' | 'a2' | 'a1'; -export type Orientation = 'portrait' | 'landscape'; -export type ScalePreset = '1:50' | '1:100' | '1:200' | 'custom'; - -export interface PrintSettings { - pageSize: PageSize; - orientation: Orientation; - scale: ScalePreset; - customScale: number; // e.g. 100 for 1:100 - showBorder: boolean; - showTitleBlock: boolean; - titleBlockData?: TitleBlockData; - margin: number; // mm -} - -export interface TitleBlockData { - title: string; - author: string; - date: string; - scale: string; - sheet: string; -} - -export interface PageLayout { - pageWidth: number; // mm - pageHeight: number; // mm - drawableWidth: number; // mm - drawableHeight: number; // mm - cols: number; - rows: number; - totalPages: number; - scale: number; // actual scale factor (1/scaleValue) - scaleValue: number; // e.g. 100 for 1:100 -} - -export interface BoundingBox { - minX: number; - minY: number; - width: number; - height: number; -} - -export interface PrintPage { - index: number; - col: number; - row: number; - offsetX: number; // CAD units - offsetY: number; // CAD units - width: number; // CAD units visible on this page - height: number; // CAD units visible on this page - svg: string; // SVG content for this page -} - -export interface PrintResult { - settings: PrintSettings; - layout: PageLayout; - bbox: BoundingBox; - pages: PrintPage[]; -} - -// ─── Page Size Dimensions (mm) ─────────────────────────────────────────────── - -const PAGE_DIMENSIONS: Record = { - a4: { width: 210, height: 297 }, - a3: { width: 297, height: 420 }, - a2: { width: 420, height: 594 }, - a1: { width: 594, height: 841 }, -}; - -const SCALE_VALUES: Record = { - '1:50': 50, - '1:100': 100, - '1:200': 200, - custom: 100, // overridden by customScale -}; - -// ─── Default Settings ──────────────────────────────────────────────────────── - -export const DEFAULT_PRINT_SETTINGS: PrintSettings = { - pageSize: 'a4', - orientation: 'landscape', - scale: '1:100', - customScale: 100, - showBorder: true, - showTitleBlock: true, - titleBlockData: { - title: 'CAD Drawing', - author: '', - date: new Date().toISOString().split('T')[0], - scale: '1:100', - sheet: '1/1', - }, - margin: 10, -}; - -// ─── Bounding Box Calculation ──────────────────────────────────────────────── - -export function calculateBoundingBox(yjsDoc: YjsDocument): BoundingBox { - const elements = yjsDoc.getElements(); - if (elements.length === 0) { - return { minX: 0, minY: 0, width: 100, height: 100 }; - } - - let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; - for (const el of elements) { - minX = Math.min(minX, el.x); - minY = Math.min(minY, el.y); - maxX = Math.max(maxX, el.x + el.width); - maxY = Math.max(maxY, el.y + el.height); - } - - return { - minX, - minY, - width: maxX - minX, - height: maxY - minY, - }; -} - -// ─── Page Layout Calculation ───────────────────────────────────────────────── - -export function calculatePageLayout( - bbox: BoundingBox, - settings: PrintSettings -): PageLayout { - const dims = PAGE_DIMENSIONS[settings.pageSize]; - const pageWidth = settings.orientation === 'landscape' ? dims.height : dims.width; - const pageHeight = settings.orientation === 'landscape' ? dims.width : dims.height; - - const margin = settings.margin; - const drawableWidth = pageWidth - margin * 2; - const drawableHeight = pageHeight - margin * 2; - - // Scale: 1:100 means 1 CAD unit = 1mm on paper at scale 1/100 - // So 100 CAD units = 1mm on paper → drawableWidth mm fits drawableWidth * scaleValue CAD units - const scaleValue = settings.scale === 'custom' ? settings.customScale : SCALE_VALUES[settings.scale]; - const scale = 1 / scaleValue; - - // How many CAD units fit on one page (drawable area in mm * scaleValue) - const cadUnitsPerPageW = drawableWidth * scaleValue; - const cadUnitsPerPageH = drawableHeight * scaleValue; - - // Number of pages needed - const cols = Math.max(1, Math.ceil(bbox.width / cadUnitsPerPageW)); - const rows = Math.max(1, Math.ceil(bbox.height / cadUnitsPerPageH)); - const totalPages = cols * rows; - - return { - pageWidth, - pageHeight, - drawableWidth, - drawableHeight, - cols, -n rows, - totalPages, - scale, - scaleValue, - }; -} - -// ─── SVG Generation for Print Pages ────────────────────────────────────────── - -function escapeXml(text: string): string { - return text - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); -} - -function generatePageSVG( - yjsDoc: YjsDocument, - bbox: BoundingBox, - layout: PageLayout, - settings: PrintSettings, - pageIndex: number, - col: number, - row: number -): string { - const elements = yjsDoc.getElements(); - const scaleValue = layout.scaleValue; - - // CAD units visible on this page - const cadUnitsPerPageW = layout.drawableWidth * scaleValue; - const cadUnitsPerPageH = layout.drawableHeight * scaleValue; - - const offsetX = bbox.minX + col * cadUnitsPerPageW; - const offsetY = bbox.minY + row * cadUnitsPerPageH; - const visibleW = Math.min(cadUnitsPerPageW, bbox.minX + bbox.width - offsetX); - const visibleH = Math.min(cadUnitsPerPageH, bbox.minY + bbox.height - offsetY); - - // SVG viewBox in mm (drawable area) - const vbW = layout.drawableWidth; - const vbH = layout.drawableHeight; - - // Transform: CAD coords → SVG mm coords - // svgX = (cadX - offsetX) / scaleValue - // svgY = vbH - (cadY - offsetY) / scaleValue (Y flip) - - const parts: string[] = []; - parts.push(``); - parts.push(``); - - // Clip rect for this page's visible area - parts.push(``); - parts.push(``); - - // Draw elements - for (const el of elements) { - const stroke = (el.properties as Record).stroke as string || '#000000'; - const fill = (el.properties as Record).fill as string || 'none'; - const strokeWidth = ((el.properties as Record).strokeWidth as number || 1) / scaleValue; - const fillResolved = fill === 'transparent' ? 'none' : fill; - - const sx = (cadX: number) => (cadX - offsetX) / scaleValue; - const sy = (cadY: number) => vbH - (cadY - offsetY) / scaleValue; - - switch (el.type) { - case 'line': { - const x1 = sx((el.properties as Record).x1 as number ?? el.x); - const y1 = sy((el.properties as Record).y1 as number ?? el.y); - const x2 = sx((el.properties as Record).x2 as number ?? el.x + el.width); - const y2 = sy((el.properties as Record).y2 as number ?? el.y + el.height); - parts.push(``); - break; - } - case 'circle': { - const radius = (el.properties as Record).radius as number ?? el.width / 2; - const cx = sx(el.x + radius); - const cy = sy(el.y + radius); - const r = radius / scaleValue; - parts.push(``); - break; - } - case 'rect': { - const x = sx(el.x); - const y = sy(el.y + el.height); - const w = el.width / scaleValue; - const h = el.height / scaleValue; - parts.push(``); - break; - } - case 'polyline': { - const pts = (el.properties as Record).points as Array<{ x: number; y: number }> | undefined; - if (pts && pts.length > 0) { - const ptsStr = pts.map(p => `${sx(p.x).toFixed(2)},${sy(p.y).toFixed(2)}`).join(' '); - parts.push(``); - } - break; - } - case 'polygon': { - const pts = (el.properties as Record).points as Array<{ x: number; y: number }> | undefined; - if (pts && pts.length > 0) { - const ptsStr = pts.map(p => `${sx(p.x).toFixed(2)},${sy(p.y).toFixed(2)}`).join(' '); - parts.push(``); - } - break; - } - case 'text': { - const text = escapeXml((el.properties as Record).text as string || ''); - const fontSize = ((el.properties as Record).fontSize as number || 14) / scaleValue; - parts.push(`${text}`); - break; - } - case 'arc': { - const radius = (el.properties as Record).radius as number ?? el.width / 2; - const cx = sx(el.x + radius); - const cy = sy(el.y + radius); - const r = radius / scaleValue; - parts.push(``); - break; - } - default: - break; - } - } - - parts.push(''); - - // Print border - if (settings.showBorder) { - parts.push(``); - } - - // Title block (bottom-right corner) - if (settings.showTitleBlock && settings.titleBlockData) { - const tb = settings.titleBlockData; - const tbW = 80; - const tbH = 30; - const tbX = vbW - tbW - 2; - const tbY = vbH - tbH - 2; - parts.push(``); - parts.push(``); - parts.push(``); - parts.push(`${escapeXml(tb.title)}`); - parts.push(`Scale: ${escapeXml(tb.scale)}`); - parts.push(`${escapeXml(tb.author)}`); - parts.push(`${escapeXml(tb.date)}`); - parts.push(`Sheet: ${pageIndex + 1}/${layout.totalPages}`); - } - - parts.push(''); - return parts.join('\n'); -} - -// ─── Main Print Preparation ────────────────────────────────────────────────── - -export function preparePrint( - yjsDoc: YjsDocument, - settings: PrintSettings -): PrintResult { - const bbox = calculateBoundingBox(yjsDoc); - const layout = calculatePageLayout(bbox, settings); - - const pages: PrintPage[] = []; - for (let row = 0; row < layout.rows; row++) { - for (let col = 0; col < layout.cols; col++) { - const index = row * layout.cols + col; - const cadUnitsPerPageW = layout.drawableWidth * layout.scaleValue; - const cadUnitsPerPageH = layout.drawableHeight * layout.scaleValue; - const offsetX = bbox.minX + col * cadUnitsPerPageW; - const offsetY = bbox.minY + row * cadUnitsPerPageH; - const visibleW = Math.min(cadUnitsPerPageW, bbox.minX + bbox.width - offsetX); - const visibleH = Math.min(cadUnitsPerPageH, bbox.minY + bbox.height - offsetY); - - pages.push({ - index, - col, - row, - offsetX, - offsetY, - width: visibleW, - height: visibleH, - svg: generatePageSVG(yjsDoc, bbox, layout, settings, index, col, row), - }); - } - } - - return { - settings, - layout, - bbox, - pages, - }; -} - -// ─── Browser Print ─────────────────────────────────────────────────────────── - -export function printPages(printResult: PrintResult): void { - const printWindow = window.open('', '_blank', 'width=800,height=600'); - if (!printWindow) { - alert('Please allow popups to print.'); - return; - } - - const { layout, pages, settings } = printResult; - - const html = ` - - - -CAD Print — ${settings.titleBlockData?.title || 'Drawing'} - - - -${pages.map(p => ``).join('\n')} - -`; - - printWindow.document.write(html); - printWindow.document.close(); - - // Wait for SVGs to render, then trigger print - printWindow.onload = () => { - setTimeout(() => { - printWindow.print(); - }, 500); - }; -} - -// ─── SVG Data URL for Preview ──────────────────────────────────────────────── - -export function svgToDataUrl(svg: string): string { - const encoded = encodeURIComponent(svg); - return `data:image/svg+xml;charset=utf-8,${encoded}`; -} +§§include(/tmp/printService.ts) \ No newline at end of file