feat: initial commit web-cad-neu with docker-compose, frontend and backend

This commit is contained in:
2026-06-26 10:50:24 +02:00
commit 4ec76fe406
102 changed files with 25722 additions and 0 deletions
+174
View File
@@ -0,0 +1,174 @@
/**
* 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);
}