/** * Task G4 – PDF-Export je Layout im Ausgabemaßstab. * * exportLayoutPDF rendert den Layout-Viewport (center/scale) auf eine A4- * Seite (landscape), skaliert Weltkoordinaten in den Ausgabemaßstab, * berücksichtigt Linienbreiten aus Layern (lineType) und unterstützt * eine monochrome Option. Titelblock (frameBlockRef) wird mitgezeichnet. * Rückgabe: PDF-Bytes, per PDFDocument.load verifizierbar. */ import { describe, it, expect } from 'vitest'; import { PDFDocument } from 'pdf-lib'; import { CADDocument } from '../src/kernel/document/CADDocument'; import { createInMemoryDoc } from './helpers/inMemoryDoc'; import { exportLayoutPDF } from '../src/services/pdfExport'; import type { CADElement, LayoutDefinition } from '../src/types/cad.types'; function line(id: string, x1: number, y1: number, x2: number, y2: number, layerId = 'layer-0'): CADElement { return { id, type: 'line', layerId, x: Math.min(x1, x2), y: Math.min(y1, y2), width: Math.abs(x2 - x1), height: Math.abs(y2 - y1), properties: { x1, y1, x2, y2, stroke: '#ff0000' }, } as CADElement; } function mkDoc(els: CADElement[], layouts: LayoutDefinition[]): CADDocument { const { ydoc } = createInMemoryDoc(); const doc = new CADDocument(ydoc); for (const e of els) doc.addElement(e); for (const l of layouts) doc.addLayout(l); return doc; } const LAYOUT: LayoutDefinition = { id: 'layout-1', name: 'A4 Plan', viewport: { center: { x: 50, y: 50 }, scale: 1 }, }; describe('G4: exportLayoutPDF', () => { it('liefert valides PDF mit genau einer A4-landscape-Seite', async () => { const doc = mkDoc([line('l1', 0, 0, 100, 100)], [LAYOUT]); const bytes = await exportLayoutPDF(doc, 'layout-1'); expect(bytes.byteLength).toBeGreaterThan(500); const pdf = await PDFDocument.load(bytes); expect(pdf.getPageCount()).toBe(1); const { width, height } = pdf.getPage(0).getSize(); expect(width).toBe(842); // A4 landscape pt expect(height).toBe(595); }); it('Titelblock-Elemente (frameBlockRef) werden mitgezeichnet', async () => { const doc = mkDoc([line('l1', 0, 0, 100, 0)], [LAYOUT]); // Titelblock-Definition anlegen und referenzieren const blockId = 'title-1'; doc.addBlock({ id: blockId, name: 'Titelblock', description: '', category: 'frame', elements: [line('tb1', 0, 0, 50, 0)], }); doc.updateLayout('layout-1', { frameBlockRef: blockId }); const bytes = await exportLayoutPDF(doc, 'layout-1'); const pdf = await PDFDocument.load(bytes); expect(pdf.getPageCount()).toBe(1); // Strukturelle Prüfung: Export läuft fehlerfrei mit Frame-Block expect(bytes.byteLength).toBeGreaterThan(500); }); it('monochrome Option exportiert ohne Farbinformationen zu verlieren', async () => { const doc = mkDoc([line('l1', 0, 0, 100, 100)], [LAYOUT]); const bytes = await exportLayoutPDF(doc, 'layout-1', { monochrome: true }); const pdf = await PDFDocument.load(bytes); expect(pdf.getPageCount()).toBe(1); }); it('leeres Layout (keine Elemente im Viewport) exportiert trotzdem eine Seite', async () => { const doc = mkDoc([], [LAYOUT]); const bytes = await exportLayoutPDF(doc, 'layout-1'); const pdf = await PDFDocument.load(bytes); expect(pdf.getPageCount()).toBe(1); }); it('Layout ohne existierende ID → throw', async () => { const doc = mkDoc([], []); await expect(exportLayoutPDF(doc, 'gibts-nicht')).rejects.toThrow(); }); it('unsichtbare Layer werden übersprungen (Export läuft fehlerfrei)', async () => { const { ydoc } = createInMemoryDoc(); const doc = new CADDocument(ydoc); ydoc.data.layers.set('layer-0', { id: 'layer-0', name: '0', visible: false, locked: false, color: '#ffffff', lineType: 'solid', transparency: 0, sortOrder: 0, parentId: null, }); doc.addElement(line('l1', 0, 0, 100, 100)); doc.addLayout(LAYOUT); const bytes = await exportLayoutPDF(doc, 'layout-1'); const pdf = await PDFDocument.load(bytes); expect(pdf.getPageCount()).toBe(1); }); });