// Vektor-PDF-Export des Grundrisses. Baut aus einem Plan (generatePlan → // Primitive) ein massstäbliches Druck-SVG (planToPrintSvg) und rendert dieses // per jsPDF + svg2pdf.js als ECHTES Vektor-PDF (Pfad-/Text-Operatoren, KEINE // Rasterung). Papierformat (A4/A3) + Ausrichtung (Hoch/Quer) wählbar; der Plan // wird zentriert aufs Blatt gesetzt, ein schlichtes Schriftfeld (Titel/Massstab) // optional unten rechts. // // Bezeichner englisch, Kommentare deutsch (CONVENTIONS.md). import { jsPDF } from "jspdf"; import "svg2pdf.js"; import type { Plan } from "../plan/generatePlan"; import { planToPrintSvg } from "./planToPrintSvg"; /** Unterstützte Papierformate (Blattmasse in mm, Hochformat-Basis). */ export type PaperFormat = "a4" | "a3"; /** Ausrichtung des Blatts. */ export type Orientation = "portrait" | "landscape"; /** Blattmasse je Format im Hochformat (mm). */ const PAGE_MM: Record = { a4: { w: 210, h: 297 }, a3: { w: 297, h: 420 }, }; export interface ExportPdfOptions { /** Massstab-Nenner N (1:N). */ scaleDenominator: number; paper: PaperFormat; orientation: Orientation; /** Plantitel/Geschossname fürs Schriftfeld. */ title: string; /** Optionaler Untertitel (z. B. Projektname). */ subtitle?: string; /** Schriftfeld zeichnen (Titel/Massstab). Default true. */ titleBlock?: boolean; /** Dateiname des Downloads (ohne/inkl. .pdf). */ fileName?: string; } /** Blattmasse (mm) nach Format + Ausrichtung. */ export function pageSizeMm( paper: PaperFormat, orientation: Orientation, ): { w: number; h: number } { const base = PAGE_MM[paper]; return orientation === "landscape" ? { w: base.h, h: base.w } : base; } /** * Erzeugt das Vektor-PDF des Plans und liefert das jsPDF-Dokument zurück (der * Aufrufer speichert via {@link savePlanPdf} oder nutzt `doc.output(...)` zum * Testen). Das Schriftfeld nutzt die eingebaute Helvetica (Vektor-Text). */ export async function buildPlanPdf( plan: Plan, opts: ExportPdfOptions, ): Promise { const { w: pageW, h: pageH } = pageSizeMm(opts.paper, opts.orientation); // Druck-SVG (mm-Koordinaten) aufbauen. Etwas mehr unterer Rand, falls ein // Schriftfeld gezeichnet wird (damit es nicht in den Plan ragt). const margin = 10; const print = planToPrintSvg(plan, { scaleDenominator: opts.scaleDenominator, pageWidthMm: pageW, pageHeightMm: pageH, marginMm: margin, }); const doc = new jsPDF({ unit: "mm", format: [pageW, pageH], orientation: opts.orientation, compress: true, }); // SVG → PDF (Vektor). Die viewBox des SVG ist in mm (Papier-Benutzereinheiten) // und deckungsgleich mit der Blattgröße; svg2pdf bildet sie 1:1 auf das mm-Ziel // (Blattbreite/-höhe) ab. await doc.svg(print.svg, { x: 0, y: 0, width: pageW, height: pageH }); if (opts.titleBlock ?? true) { drawTitleBlock(doc, pageW, pageH, opts); } return doc; } /** Schlichtes Schriftfeld unten rechts: Titel, Untertitel, Massstab. */ function drawTitleBlock( doc: jsPDF, pageW: number, pageH: number, opts: ExportPdfOptions, ): void { const boxW = 70; const boxH = 22; const pad = 6; const x = pageW - boxW - pad; const y = pageH - boxH - pad; doc.setDrawColor(17, 17, 17); doc.setLineWidth(0.25); doc.rect(x, y, boxW, boxH); // Trennlinie zwischen Titel-Block und Massstab-Zeile. doc.line(x, y + boxH - 7, x + boxW, y + boxH - 7); doc.setTextColor(17, 17, 17); doc.setFont("helvetica", "bold"); doc.setFontSize(10); doc.text(clip(opts.title, 30), x + 3, y + 6); if (opts.subtitle) { doc.setFont("helvetica", "normal"); doc.setFontSize(7); doc.text(clip(opts.subtitle, 40), x + 3, y + 11); } doc.setFont("helvetica", "normal"); doc.setFontSize(9); doc.text(`1:${opts.scaleDenominator}`, x + 3, y + boxH - 2.2); const fmt = `${opts.paper.toUpperCase()} ${ opts.orientation === "landscape" ? "quer" : "hoch" }`; doc.text(fmt, x + boxW - 3, y + boxH - 2.2, { align: "right" }); } /** Kürzt einen String auf maximal `max` Zeichen (mit Auslassung). */ function clip(s: string, max: number): string { return s.length > max ? s.slice(0, max - 1) + "…" : s; } /** Baut das PDF und löst den Download aus (Browser). */ export async function savePlanPdf( plan: Plan, opts: ExportPdfOptions, ): Promise { const doc = await buildPlanPdf(plan, opts); const name = (opts.fileName ?? "grundriss").replace(/\.pdf$/i, ""); doc.save(`${name}.pdf`); }