Files
web-cad/frontend/src/services/pdfExport.ts
T

175 lines
5.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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<Uint8Array> {
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<typeof rgb> | 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);
}