From a7cf59ae20485cb5c310667524205a6d9f7bd020 Mon Sep 17 00:00:00 2001 From: Thomas Reitz Date: Sat, 14 Mar 2026 15:39:10 +0100 Subject: [PATCH] =?UTF-8?q?fix:=20Timeline-Linie=20am=20Anfang=20der=20n?= =?UTF-8?q?=C3=A4chsten=20Iteration=20zeichnen=20(Deferred-Draw)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Vorherige Ansätze berechneten die Ziel-Header-Höhe am Ende des Eintrags neu (fehleranfällig durch doppelte Font-State-Operationen). Neuer Ansatz: Linie für Entry i wird am ANFANG von Entry i+1 gezeichnet, BEVOR der Seitenumbruch-Check läuft — mit demselben headerH der bereits berechnet wurde. Eine einzige Bedingung entscheidet konsistent ob Linie gezeichnet wird UND ob ein Seitenumbruch folgt, ohne Redundanz oder State-Probleme. Co-Authored-By: Claude Sonnet 4.6 --- .../expert-profile/profile-export.service.ts | 41 ++++++++++--------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/packages/core-service/src/core/expert-profile/profile-export.service.ts b/packages/core-service/src/core/expert-profile/profile-export.service.ts index 88ce1ab..97f807d 100644 --- a/packages/core-service/src/core/expert-profile/profile-export.service.ts +++ b/packages/core-service/src/core/expert-profile/profile-export.service.ts @@ -491,6 +491,11 @@ export class ProfileExportService { const contentX = rightColX + 18; const contentWidth = rightColWidth - 18; + // Vorgemerkter Linien-Start vom vorherigen Eintrag + // (wird am Anfang der nächsten Iteration gezeichnet, wenn kein Seitenumbruch nötig ist) + let pendingLineStartY: number | null = null; + let pendingLineEndY: number | null = null; + for (let i = 0; i < profile.projects.length; i++) { const proj = profile.projects[i]; @@ -507,6 +512,19 @@ export class ProfileExportService { const companyH = companyLine ? doc.heightOfString(companyLine, { width: contentWidth }) + 2 : 0; const headerH = 14 + roleH + 2 + companyH + 8; // Datum + Rolle + Firma + Puffer + // Linie des vorherigen Eintrags zeichnen — JETZT, bevor der Seitenumbruch-Check läuft. + // Nur wenn dieser Eintrag auf derselben Seite bleibt (kein Seitenumbruch nötig), + // sind Quell-Dot und Ziel-Dot auf derselben Seite → Linie korrekt. + if (pendingLineStartY !== null && pendingLineEndY !== null) { + if (yRight + headerH <= pageBottom) { + doc.moveTo(timelineX, pendingLineStartY + 8) + .lineTo(timelineX, pendingLineEndY) + .strokeColor(accentColor).lineWidth(1).stroke(); + } + pendingLineStartY = null; + pendingLineEndY = null; + } + // Seitenumbruch wenn Header nicht mehr passt if (yRight + headerH > pageBottom) { doc.addPage(); @@ -567,25 +585,10 @@ export class ProfileExportService { yRight += 12; - // Timeline-Linie: nur zeichnen wenn kein Seitenumbruch im Eintrag - // UND der nächste Eintrag noch auf dieser Seite passt (exakte Header-Höhe prüfen) - if (i < profile.projects.length - 1 && !pageBreakOccurred) { - const nextProj = profile.projects[i + 1]; - const nextRoleText = this.sanitizePdfText(nextProj.role); - const nextCompanyLine = nextProj.company - ? this.sanitizePdfText([nextProj.company, nextProj.industry].filter(Boolean).join(' \u00b7 ')) - : ''; - doc.font('Helvetica-Bold').fontSize(10); - const nextRoleH = doc.heightOfString(nextRoleText, { width: contentWidth }); - doc.fontSize(9); - const nextCompanyH = nextCompanyLine - ? doc.heightOfString(nextCompanyLine, { width: contentWidth }) + 2 - : 0; - const nextHeaderH = 14 + nextRoleH + 2 + nextCompanyH + 8; - if (yRight + nextHeaderH <= pageBottom) { - doc.moveTo(timelineX, entryStartY + 8).lineTo(timelineX, yRight - 4) - .strokeColor(accentColor).lineWidth(1).stroke(); - } + // Linie für nächste Iteration vormerken — nur wenn kein Seitenumbruch innerhalb des Eintrags + if (!pageBreakOccurred) { + pendingLineStartY = entryStartY; + pendingLineEndY = yRight - 4; } } }