wip
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -80,19 +80,76 @@ function drawKeyValueGrid(args: {
|
|||||||
navy: ReturnType<typeof rgb>;
|
navy: ReturnType<typeof rgb>;
|
||||||
darkGray: ReturnType<typeof rgb>;
|
darkGray: ReturnType<typeof rgb>;
|
||||||
mediumGray: ReturnType<typeof rgb>;
|
mediumGray: ReturnType<typeof rgb>;
|
||||||
|
lightGray?: ReturnType<typeof rgb>;
|
||||||
|
almostWhite?: ReturnType<typeof rgb>;
|
||||||
allowNewPage?: boolean;
|
allowNewPage?: boolean;
|
||||||
|
boxed?: boolean;
|
||||||
}): number {
|
}): number {
|
||||||
let { title, items, newPage, getPage, page, y, margin, contentWidth, contentMinY, font, fontBold, navy, darkGray, mediumGray } = args;
|
let { title, items, newPage, getPage, page, y, margin, contentWidth, contentMinY, font, fontBold, navy, darkGray, mediumGray } = args;
|
||||||
const allowNewPage = args.allowNewPage ?? true;
|
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 colGap = 14;
|
||||||
const colW = (contentWidth - colGap) / 2;
|
const colW = (innerWidth - colGap) / 2;
|
||||||
const rowH = 20;
|
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 = () => {
|
const drawTitle = () => {
|
||||||
page = getPage();
|
page = getPage();
|
||||||
page.drawText(title, { x: margin, y, size: 10, font: fontBold, color: navy });
|
if (boxed) {
|
||||||
y -= 16;
|
// 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) {
|
if (y - 22 < contentMinY) {
|
||||||
@@ -105,7 +162,7 @@ function drawKeyValueGrid(args: {
|
|||||||
let rowY = y;
|
let rowY = y;
|
||||||
for (let i = 0; i < items.length; i++) {
|
for (let i = 0; i < items.length; i++) {
|
||||||
const col = i % 2;
|
const col = i % 2;
|
||||||
const x = margin + col * (colW + colGap);
|
const x = xBase + col * (colW + colGap);
|
||||||
const { label, value } = items[i];
|
const { label, value } = items[i];
|
||||||
|
|
||||||
if (col === 0 && rowY - rowH < contentMinY) {
|
if (col === 0 && rowY - rowH < contentMinY) {
|
||||||
@@ -116,13 +173,13 @@ function drawKeyValueGrid(args: {
|
|||||||
rowY = y;
|
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 });
|
page.drawText(value, { x, y: rowY - 10, size: 8, font, color: darkGray, maxWidth: colW });
|
||||||
|
|
||||||
if (col === 1) rowY -= rowH;
|
if (col === 1) rowY -= rowH;
|
||||||
}
|
}
|
||||||
|
|
||||||
return rowY - rowH;
|
return boxed ? rowY - rowH - padY : rowY - rowH;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureOutputDir(): void {
|
function ensureOutputDir(): void {
|
||||||
@@ -711,15 +768,14 @@ function drawCrossSectionChipsRow(args: {
|
|||||||
page.drawText(summaryParts.join(' · '), { x: margin, y, size: 8, font, color: mediumGray, maxWidth: contentWidth });
|
page.drawText(summaryParts.join(' · '), { x: margin, y, size: 8, font, color: mediumGray, maxWidth: contentWidth });
|
||||||
y -= summaryH;
|
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 padX = 7;
|
||||||
const chipFontSize = 7.5;
|
const chipFontSize = 7.5;
|
||||||
const chipGap = 6;
|
const chipGap = 6;
|
||||||
const chipPadTop = 4;
|
const chipPadTop = 4;
|
||||||
|
|
||||||
const startY = y - chipH; // baseline for first chip row
|
const startY = y - chipH; // baseline for first chip row
|
||||||
const maxLinesBySpace = Math.max(1, Math.floor((startY - contentMinY + lineGap) / (chipH + lineGap)));
|
const maxLines = Math.max(1, Math.floor((startY - contentMinY + lineGap) / (chipH + lineGap)));
|
||||||
const maxLines = Math.min(6, maxLinesBySpace); // visual cap
|
|
||||||
|
|
||||||
const chipWidth = (text: string) => font.widthOfTextAtSize(text, chipFontSize) + padX * 2;
|
const chipWidth = (text: string) => font.widthOfTextAtSize(text, chipFontSize) + padX * 2;
|
||||||
|
|
||||||
@@ -762,32 +818,120 @@ function drawCrossSectionChipsRow(args: {
|
|||||||
return { placements, shown };
|
return { placements, shown };
|
||||||
};
|
};
|
||||||
|
|
||||||
// First pass: lay out as many as possible.
|
// Group by cores: label on the left, mm² tags to the right.
|
||||||
const moreTextBase = locale === 'de' ? 'weitere' : 'more';
|
const byCores = new Map<number, number[]>();
|
||||||
let shown = 0;
|
const other: string[] = [];
|
||||||
let placements: Placement[] = [];
|
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.
|
const coreKeys = Array.from(byCores.keys()).sort((a, b) => a - b);
|
||||||
// This produces stable output without needing to "erase" drawings.
|
for (const k of coreKeys) {
|
||||||
const allTexts = items;
|
const uniq = Array.from(new Set(byCores.get(k) ?? [])).sort((a, b) => a - b);
|
||||||
let tmp = layout(allTexts, false, '');
|
byCores.set(k, uniq);
|
||||||
shown = tmp.shown;
|
}
|
||||||
const remaining0 = total - shown;
|
|
||||||
if (remaining0 <= 0) {
|
const fmtMm2 = (v: number) => {
|
||||||
placements = tmp.placements;
|
const s = Number.isInteger(v) ? String(v) : String(v).replace(/\.0+$/, '');
|
||||||
} else {
|
return s;
|
||||||
// Reduce shown until the "+N" chip fits.
|
};
|
||||||
for (let cut = shown; cut >= 0; cut--) {
|
|
||||||
const remaining = total - cut;
|
// Layout engine with group labels.
|
||||||
const moreText = `+${remaining} ${moreTextBase}`;
|
const labelW = 34;
|
||||||
const res = layout(allTexts.slice(0, cut), true, moreText);
|
const placements: Placement[] = [];
|
||||||
// Ensure the more-chip actually placed (variant 'more' exists)
|
let line = 0;
|
||||||
const hasMore = res.placements.some(p => p.variant === 'more');
|
let cy = startY;
|
||||||
if (hasMore) {
|
let x = margin + labelW;
|
||||||
shown = cut;
|
|
||||||
placements = res.placements;
|
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;
|
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,
|
height: chipH,
|
||||||
borderColor: lightGray,
|
borderColor: lightGray,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
color: p.variant === 'more' ? almostWhite : rgb(1, 1, 1),
|
color: almostWhite,
|
||||||
});
|
});
|
||||||
page.drawText(p.text, {
|
page.drawText(p.text, {
|
||||||
x: p.x + padX,
|
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;
|
const hasSpace = (needed: number) => y - needed >= contentMinY;
|
||||||
|
|
||||||
// Page 1
|
// Page 1
|
||||||
|
// Page background (STYLEGUIDE.md)
|
||||||
|
page.drawRectangle({
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
color: almostWhite,
|
||||||
|
});
|
||||||
|
|
||||||
drawFooter(ctx);
|
drawFooter(ctx);
|
||||||
let y = drawHeader(ctx, height - margin);
|
let y = drawHeader(ctx, height - margin);
|
||||||
|
|
||||||
@@ -1114,7 +1267,10 @@ async function generatePDF(product: ProductData, locale: 'en' | 'de'): Promise<B
|
|||||||
navy,
|
navy,
|
||||||
darkGray,
|
darkGray,
|
||||||
mediumGray,
|
mediumGray,
|
||||||
|
lightGray,
|
||||||
|
almostWhite,
|
||||||
allowNewPage: false,
|
allowNewPage: false,
|
||||||
|
boxed: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// === CROSS-SECTION TABLE (row-specific data) ===
|
// === CROSS-SECTION TABLE (row-specific data) ===
|
||||||
|
|||||||
Reference in New Issue
Block a user