wip
This commit is contained in:
@@ -80,19 +80,76 @@ function drawKeyValueGrid(args: {
|
||||
navy: ReturnType<typeof rgb>;
|
||||
darkGray: ReturnType<typeof rgb>;
|
||||
mediumGray: ReturnType<typeof rgb>;
|
||||
lightGray?: ReturnType<typeof rgb>;
|
||||
almostWhite?: ReturnType<typeof rgb>;
|
||||
allowNewPage?: boolean;
|
||||
boxed?: boolean;
|
||||
}): number {
|
||||
let { title, items, newPage, getPage, page, y, margin, contentWidth, contentMinY, font, fontBold, navy, darkGray, mediumGray } = args;
|
||||
const allowNewPage = args.allowNewPage ?? true;
|
||||
const boxed = args.boxed ?? false;
|
||||
|
||||
const lightGray = args.lightGray ?? rgb(0.9020, 0.9137, 0.9294);
|
||||
const almostWhite = args.almostWhite ?? rgb(0.9725, 0.9765, 0.9804);
|
||||
|
||||
// Inner layout (boxed vs. plain)
|
||||
const padX = boxed ? 14 : 0;
|
||||
const padY = boxed ? 12 : 0;
|
||||
const xBase = margin + padX;
|
||||
const innerWidth = contentWidth - padX * 2;
|
||||
const colGap = 14;
|
||||
const colW = (contentWidth - colGap) / 2;
|
||||
const rowH = 20;
|
||||
const colW = (innerWidth - colGap) / 2;
|
||||
const rowH = 18;
|
||||
const headerH = boxed ? 18 : 0;
|
||||
|
||||
// Draw a strict rectangular section container (no rounding)
|
||||
if (boxed && items.length) {
|
||||
const rows = Math.ceil(items.length / 2);
|
||||
const boxH = padY + headerH + rows * rowH + padY;
|
||||
const bottomY = y - boxH;
|
||||
if (bottomY < contentMinY) {
|
||||
if (!allowNewPage) return contentMinY - 1;
|
||||
y = newPage();
|
||||
}
|
||||
|
||||
page = getPage();
|
||||
page.drawRectangle({
|
||||
x: margin,
|
||||
y: y - boxH,
|
||||
width: contentWidth,
|
||||
height: boxH,
|
||||
borderColor: lightGray,
|
||||
borderWidth: 1,
|
||||
color: rgb(1, 1, 1),
|
||||
});
|
||||
|
||||
// Header band for the title
|
||||
page.drawRectangle({
|
||||
x: margin,
|
||||
y: y - headerH,
|
||||
width: contentWidth,
|
||||
height: headerH,
|
||||
color: almostWhite,
|
||||
});
|
||||
}
|
||||
|
||||
const drawTitle = () => {
|
||||
page = getPage();
|
||||
page.drawText(title, { x: margin, y, size: 10, font: fontBold, color: navy });
|
||||
y -= 16;
|
||||
if (boxed) {
|
||||
// Align title inside the header band.
|
||||
page.drawText(title, { x: xBase, y: y - 13, size: 9.5, font: fontBold, color: navy });
|
||||
// Divider line below header band
|
||||
page.drawLine({
|
||||
start: { x: margin, y: y - headerH },
|
||||
end: { x: margin + contentWidth, y: y - headerH },
|
||||
thickness: 1,
|
||||
color: lightGray,
|
||||
});
|
||||
y -= headerH + padY;
|
||||
} else {
|
||||
page.drawText(title, { x: margin, y, size: 10, font: fontBold, color: navy });
|
||||
y -= 16;
|
||||
}
|
||||
};
|
||||
|
||||
if (y - 22 < contentMinY) {
|
||||
@@ -105,7 +162,7 @@ function drawKeyValueGrid(args: {
|
||||
let rowY = y;
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const col = i % 2;
|
||||
const x = margin + col * (colW + colGap);
|
||||
const x = xBase + col * (colW + colGap);
|
||||
const { label, value } = items[i];
|
||||
|
||||
if (col === 0 && rowY - rowH < contentMinY) {
|
||||
@@ -116,13 +173,13 @@ function drawKeyValueGrid(args: {
|
||||
rowY = y;
|
||||
}
|
||||
|
||||
page.drawText(label, { x, y: rowY, size: 8, font: fontBold, color: mediumGray, maxWidth: colW });
|
||||
page.drawText(label, { x, y: rowY, size: 7.5, font: fontBold, color: mediumGray, maxWidth: colW });
|
||||
page.drawText(value, { x, y: rowY - 10, size: 8, font, color: darkGray, maxWidth: colW });
|
||||
|
||||
if (col === 1) rowY -= rowH;
|
||||
}
|
||||
|
||||
return rowY - rowH;
|
||||
return boxed ? rowY - rowH - padY : rowY - rowH;
|
||||
}
|
||||
|
||||
function ensureOutputDir(): void {
|
||||
@@ -711,15 +768,14 @@ function drawCrossSectionChipsRow(args: {
|
||||
page.drawText(summaryParts.join(' · '), { x: margin, y, size: 8, font, color: mediumGray, maxWidth: contentWidth });
|
||||
y -= summaryH;
|
||||
|
||||
// Chips (wrapping). We draw small bordered rectangles and wrap to next line.
|
||||
// Tags (wrapping). Rectangular, engineered (no playful rounding).
|
||||
const padX = 7;
|
||||
const chipFontSize = 7.5;
|
||||
const chipGap = 6;
|
||||
const chipPadTop = 4;
|
||||
|
||||
const startY = y - chipH; // baseline for first chip row
|
||||
const maxLinesBySpace = Math.max(1, Math.floor((startY - contentMinY + lineGap) / (chipH + lineGap)));
|
||||
const maxLines = Math.min(6, maxLinesBySpace); // visual cap
|
||||
const maxLines = Math.max(1, Math.floor((startY - contentMinY + lineGap) / (chipH + lineGap)));
|
||||
|
||||
const chipWidth = (text: string) => font.widthOfTextAtSize(text, chipFontSize) + padX * 2;
|
||||
|
||||
@@ -762,32 +818,120 @@ function drawCrossSectionChipsRow(args: {
|
||||
return { placements, shown };
|
||||
};
|
||||
|
||||
// First pass: lay out as many as possible.
|
||||
const moreTextBase = locale === 'de' ? 'weitere' : 'more';
|
||||
let shown = 0;
|
||||
let placements: Placement[] = [];
|
||||
// Group by cores: label on the left, mm² tags to the right.
|
||||
const byCores = new Map<number, number[]>();
|
||||
const other: string[] = [];
|
||||
for (const cs of items) {
|
||||
const p = parseCoresAndMm2(cs);
|
||||
if (p.cores !== null && p.mm2 !== null) {
|
||||
const arr = byCores.get(p.cores) ?? [];
|
||||
arr.push(p.mm2);
|
||||
byCores.set(p.cores, arr);
|
||||
} else {
|
||||
other.push(cs);
|
||||
}
|
||||
}
|
||||
|
||||
// We reserve space for the "+N" chip when there is truncation by iteratively reducing shown.
|
||||
// This produces stable output without needing to "erase" drawings.
|
||||
const allTexts = items;
|
||||
let tmp = layout(allTexts, false, '');
|
||||
shown = tmp.shown;
|
||||
const remaining0 = total - shown;
|
||||
if (remaining0 <= 0) {
|
||||
placements = tmp.placements;
|
||||
} else {
|
||||
// Reduce shown until the "+N" chip fits.
|
||||
for (let cut = shown; cut >= 0; cut--) {
|
||||
const remaining = total - cut;
|
||||
const moreText = `+${remaining} ${moreTextBase}`;
|
||||
const res = layout(allTexts.slice(0, cut), true, moreText);
|
||||
// Ensure the more-chip actually placed (variant 'more' exists)
|
||||
const hasMore = res.placements.some(p => p.variant === 'more');
|
||||
if (hasMore) {
|
||||
shown = cut;
|
||||
placements = res.placements;
|
||||
const coreKeys = Array.from(byCores.keys()).sort((a, b) => a - b);
|
||||
for (const k of coreKeys) {
|
||||
const uniq = Array.from(new Set(byCores.get(k) ?? [])).sort((a, b) => a - b);
|
||||
byCores.set(k, uniq);
|
||||
}
|
||||
|
||||
const fmtMm2 = (v: number) => {
|
||||
const s = Number.isInteger(v) ? String(v) : String(v).replace(/\.0+$/, '');
|
||||
return s;
|
||||
};
|
||||
|
||||
// Layout engine with group labels.
|
||||
const labelW = 34;
|
||||
const placements: Placement[] = [];
|
||||
let line = 0;
|
||||
let cy = startY;
|
||||
let x = margin + labelW;
|
||||
|
||||
const canAdvanceLine = () => line + 1 < maxLines;
|
||||
const advanceLine = () => {
|
||||
if (!canAdvanceLine()) return false;
|
||||
line += 1;
|
||||
cy -= chipH + lineGap;
|
||||
x = margin + labelW;
|
||||
return true;
|
||||
};
|
||||
|
||||
const drawGroupLabel = (label: string) => {
|
||||
// Draw label on each new line for the group (keeps readability when wrapping).
|
||||
page.drawText(label, {
|
||||
x: margin,
|
||||
y: cy + 4,
|
||||
size: 8,
|
||||
font: fontBold,
|
||||
color: mediumGray,
|
||||
maxWidth: labelW - 4,
|
||||
});
|
||||
};
|
||||
|
||||
const placeChip = (text: string, variant: 'normal' | 'more') => {
|
||||
const w = chipWidth(text);
|
||||
if (w > contentWidth - labelW) return false;
|
||||
if (x + w > margin + contentWidth) {
|
||||
if (!advanceLine()) return false;
|
||||
}
|
||||
placements.push({ text, x, y: cy, w, variant });
|
||||
x += w + chipGap;
|
||||
return true;
|
||||
};
|
||||
|
||||
let truncated = false;
|
||||
let renderedCount = 0;
|
||||
const totalChips = coreKeys.reduce((sum, k) => sum + (byCores.get(k)?.length ?? 0), 0) + other.length;
|
||||
|
||||
for (const cores of coreKeys) {
|
||||
const values = byCores.get(cores) ?? [];
|
||||
const label = `${cores}×`;
|
||||
// Ensure label is shown at least once per line block.
|
||||
drawGroupLabel(label);
|
||||
for (const v of values) {
|
||||
const ok = placeChip(fmtMm2(v), 'normal');
|
||||
if (!ok) {
|
||||
truncated = true;
|
||||
break;
|
||||
}
|
||||
renderedCount++;
|
||||
}
|
||||
if (truncated) break;
|
||||
// Add a tiny gap between core groups (only if we have room on the current line)
|
||||
x += 4;
|
||||
if (x > margin + contentWidth - 20) {
|
||||
if (!advanceLine()) {
|
||||
// out of vertical space; stop
|
||||
truncated = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!truncated && other.length) {
|
||||
const label = locale === 'de' ? 'Sonst.' : 'Other';
|
||||
drawGroupLabel(label);
|
||||
for (const t of other) {
|
||||
const ok = placeChip(t, 'normal');
|
||||
if (!ok) {
|
||||
truncated = true;
|
||||
break;
|
||||
}
|
||||
renderedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (truncated) {
|
||||
const remaining = Math.max(0, totalChips - renderedCount);
|
||||
const moreText = locale === 'de' ? `+${remaining} weitere` : `+${remaining} more`;
|
||||
// Try to place on current line; if not possible, try next line.
|
||||
if (!placeChip(moreText, 'more')) {
|
||||
if (advanceLine()) {
|
||||
placeChip(moreText, 'more');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -800,7 +944,7 @@ function drawCrossSectionChipsRow(args: {
|
||||
height: chipH,
|
||||
borderColor: lightGray,
|
||||
borderWidth: 1,
|
||||
color: p.variant === 'more' ? almostWhite : rgb(1, 1, 1),
|
||||
color: almostWhite,
|
||||
});
|
||||
page.drawText(p.text, {
|
||||
x: p.x + padX,
|
||||
@@ -950,6 +1094,15 @@ async function generatePDF(product: ProductData, locale: 'en' | 'de'): Promise<B
|
||||
const hasSpace = (needed: number) => y - needed >= contentMinY;
|
||||
|
||||
// Page 1
|
||||
// Page background (STYLEGUIDE.md)
|
||||
page.drawRectangle({
|
||||
x: 0,
|
||||
y: 0,
|
||||
width,
|
||||
height,
|
||||
color: almostWhite,
|
||||
});
|
||||
|
||||
drawFooter(ctx);
|
||||
let y = drawHeader(ctx, height - margin);
|
||||
|
||||
@@ -1114,7 +1267,10 @@ async function generatePDF(product: ProductData, locale: 'en' | 'de'): Promise<B
|
||||
navy,
|
||||
darkGray,
|
||||
mediumGray,
|
||||
lightGray,
|
||||
almostWhite,
|
||||
allowNewPage: false,
|
||||
boxed: true,
|
||||
});
|
||||
|
||||
// === CROSS-SECTION TABLE (row-specific data) ===
|
||||
|
||||
Reference in New Issue
Block a user