fix: Timeline-Linie am Anfang der nächsten Iteration zeichnen (Deferred-Draw)

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 <noreply@anthropic.com>
This commit is contained in:
Thomas Reitz 2026-03-14 15:39:10 +01:00
parent fb57f5a4dc
commit a7cf59ae20

View file

@ -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;
}
}
}