/** * PDF Export – renders CAD elements to PDF using pdf-lib */ import { PDFDocument, rgb, StandardFonts } from 'pdf-lib'; import type { CADElement, CADLayer, ProjectData } from '../types/cad.types'; /** * Export project data to a PDF byte array. */ export async function exportPDF(data: ProjectData): Promise { const pdfDoc = await PDFDocument.create(); const font = await pdfDoc.embedFont(StandardFonts.Helvetica); // Calculate bounding box of all elements const bbox = calculateBoundingBox(data.elements); const padding = 20; const contentW = bbox.maxX - bbox.minX + padding * 2; const contentH = bbox.maxY - bbox.minY + padding * 2; // Use A4 landscape or fit to content (whichever is larger) const a4W = 842; // A4 landscape in points const a4H = 595; const pageW = Math.max(a4W, contentW); const pageH = Math.max(a4H, contentH); const page = pdfDoc.addPage([pageW, pageH]); const { width: pw, height: ph } = page.getSize(); // Flip Y axis: PDF origin is bottom-left, CAD origin is top-left const flipY = (y: number) => ph - y + bbox.minY - padding; const offsetX = -bbox.minX + padding; const offsetY = bbox.minY - padding; // Draw elements for (const el of data.elements) { const layer = data.layers.find(l => l.id === el.layerId); if (layer && !layer.visible) continue; drawElement(page, el, font, offsetX, offsetY, flipY, layer?.color); } return pdfDoc.save(); } function drawElement( page: any, el: CADElement, font: any, offX: number, offY: number, flipY: (y: number) => number, layerColor?: string, ): void { const p = el.properties; const color = hexToRgb(p.stroke as string) ?? hexToRgb(layerColor ?? '#000000') ?? rgb(0, 0, 0); const lineWidth = p.strokeWidth ?? 1; switch (el.type) { case 'line': { const x1 = (p.x1 ?? el.x) + offX; const y1 = flipY((p.y1 ?? el.y) - offY); const x2 = (p.x2 ?? el.x + el.width) + offX; const y2 = flipY((p.y2 ?? el.y + el.height) - offY); page.drawLine({ start: { x: x1, y: y1 }, end: { x: x2, y: y2 }, thickness: lineWidth, color }); break; } case 'circle': { const cx = el.x + el.width / 2 + offX; const cy = flipY(el.y + el.height / 2 - offY); const r = p.radius ?? el.width / 2; page.drawCircle({ x: cx, y: cy, radius: r, borderColor: color, borderWidth: lineWidth }); break; } case 'arc': { // Approximate arc with line segments const cx = el.x + el.width / 2 + offX; const cy = flipY(el.y + el.height / 2 - offY); const r = p.radius ?? el.width / 2; const start = p.startAngle ?? 0; const end = p.endAngle ?? Math.PI * 2; const segments = 32; for (let i = 0; i < segments; i++) { const a1 = start + (end - start) * (i / segments); const a2 = start + (end - start) * ((i + 1) / segments); page.drawLine({ start: { x: cx + Math.cos(a1) * r, y: cy + Math.sin(a1) * r }, end: { x: cx + Math.cos(a2) * r, y: cy + Math.sin(a2) * r }, thickness: lineWidth, color, }); } break; } case 'rect': { page.drawRectangle({ x: el.x + offX, y: flipY(el.y + el.height - offY), width: el.width, height: el.height, borderColor: color, borderWidth: lineWidth, }); break; } case 'polygon': case 'polyline': { const pts = p.points ?? []; for (let i = 0; i < pts.length - 1; i++) { page.drawLine({ start: { x: pts[i].x + offX, y: flipY(pts[i].y - offY) }, end: { x: pts[i + 1].x + offX, y: flipY(pts[i + 1].y - offY) }, thickness: lineWidth, color, }); } if (el.type === 'polygon' && pts.length > 2) { page.drawLine({ start: { x: pts[pts.length - 1].x + offX, y: flipY(pts[pts.length - 1].y - offY) }, end: { x: pts[0].x + offX, y: flipY(pts[0].y - offY) }, thickness: lineWidth, color, }); } break; } case 'text': { const size = p.fontSize ?? 12; page.drawText(p.text ?? '', { x: el.x + offX, y: flipY(el.y - offY) - size, size, font, color, }); break; } case 'dimension': { const pts = p.points ?? []; if (pts.length >= 2) { page.drawLine({ start: { x: pts[0].x + offX, y: flipY(pts[0].y - offY) }, end: { x: pts[1].x + offX, y: flipY(pts[1].y - offY) }, thickness: lineWidth, color, }); const midX = (pts[0].x + pts[1].x) / 2 + offX; const midY = flipY((pts[0].y + pts[1].y) / 2 - offY); const dist = Math.hypot(pts[1].x - pts[0].x, pts[1].y - pts[0].y); page.drawText(dist.toFixed(2), { x: midX, y: midY, size: 10, font, color }); } break; } default: { // Bounding box for other element types page.drawRectangle({ x: el.x + offX, y: flipY(el.y + el.height - offY), width: el.width, height: el.height, borderColor: color, borderWidth: lineWidth, }); break; } } } function calculateBoundingBox(elements: CADElement[]) { if (elements.length === 0) return { minX: 0, minY: 0, maxX: 1000, maxY: 1000 }; 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, maxX, maxY }; } function hexToRgb(hex: string | undefined): ReturnType | null { if (!hex) return null; const h = hex.replace('#', ''); if (h.length < 6) return null; const r = parseInt(h.substring(0, 2), 16) / 255; const g = parseInt(h.substring(2, 4), 16) / 255; const b = parseInt(h.substring(4, 6), 16) / 255; return rgb(r, g, b); }