This commit is contained in:
j3d1 2026-08-24 21:35:38 +02:00
parent f87b689e27
commit 0c025db799
16 changed files with 555 additions and 105 deletions

View file

@ -81,14 +81,18 @@ const TEXT_REFERENCE_PX = 100; /* font size text leaves measure their natural a
// Below 10px, a general-purpose sans-serif gets illegible, so drawTextLeaf switches to a bitmap
// font (Tom Thumb/Silkscreen) instead; see the empirical rationale (font choice, `scale`, and why
// sizes aren't dpi-adjusted) at docs/implementation.md#pixel-font-selection.
const PIXEL_FONT_TIERS = [
{belowPx: 8, family: "Tom Thumb", scale: 3.2},
{belowPx: 10, family: "Silkscreen"},
// sizes aren't dpi-adjusted) at docs/implementation.md#pixel-font-selection. The 10px+ tier names
// Inter explicitly (see ../scss/_label-fonts.scss) rather than falling back to the CSS generic
// "sans-serif" keyword, which resolves to a different real font per browser/OS and would make the
// same label print differently depending on where it was rendered from.
const FONT_TIERS = [
{belowPx: 8, family: "Tom Thumb", scale: 3.2, pixel: true},
{belowPx: 10, family: "Silkscreen", pixel: true},
{belowPx: Infinity, family: "Inter"},
];
function fontFamilyFor(fontPx) {
return PIXEL_FONT_TIERS.find(t => fontPx < t.belowPx) ?? {family: "sans-serif"};
return FONT_TIERS.find(t => fontPx < t.belowPx);
}
// Below this font size (px) or physical height (mm) - Tom Thumb's real-Chromium-tested legibility
@ -291,11 +295,12 @@ function checkTextSizes(node, referencePx, pxPerMm, warnings, textInfo) {
}
}
// Always measures in sans-serif at the reference size, since the eventual font (see
// PIXEL_FONT_TIERS) isn't known until layoutTree sizes the box this aspect ratio feeds into; the
// resulting mismatch is invisible in practice, and checkTextSizes still catches real failures.
// Always measures in Inter at the reference size, since the eventual font (see FONT_TIERS) isn't
// known until layoutTree sizes the box this aspect ratio feeds into; the resulting mismatch (when
// a pixel font tier ends up chosen instead) is invisible in practice, and checkTextSizes still
// catches real failures.
function measureTextBlock(ctx, lines, referencePx) {
ctx.font = `${referencePx}px sans-serif`;
ctx.font = `${referencePx}px "Inter"`;
const width = Math.max(...lines.map(line => ctx.measureText(line).width));
const height = referencePx * 1.15 * lines.length;
return {width, height};
@ -342,20 +347,18 @@ function drawQrLeaf(ctx, node) {
// there's no "too small to draw" case left to special-case here.
function drawTextLeaf(ctx, node, referencePx) {
const fontPx = effectiveFontPx(node, referencePx);
const {family, scale = 1} = fontFamilyFor(fontPx);
const isPixelFont = family !== "sans-serif";
const {family, scale = 1, pixel: isPixelFont = false} = fontFamilyFor(fontPx);
// Position (not size - fontPx is already a whole pixel) still needs snapping for pixel fonts
// to stay grid-aligned, since node.box.x/y are ordinary (fractional) layout math; sans-serif is
// to stay grid-aligned, since node.box.x/y are ordinary (fractional) layout math; Inter is
// left exact since anti-aliasing handles fractional positions fine.
const snap = isPixelFont ? Math.round : (v) => v;
// `scale` (Tom Thumb only, see PIXEL_FONT_TIERS above) corrects the size handed to ctx.font
// for its real ink; fontPx itself stays the logical size used for centering/stacking math.
// `scale` (Tom Thumb only, see FONT_TIERS above) corrects the size handed to ctx.font for its
// real ink; fontPx itself stays the logical size used for centering/stacking math.
ctx.font = `${fontPx * scale}px "${family}"`;
// Canvas text silently falls back if drawn before a not-yet-loaded font resolves, unlike DOM
// text. See docs/implementation.md#canvas-font-loading.
if (isPixelFont) {
document.fonts.load(ctx.font);
}
// text. See docs/implementation.md#canvas-font-loading. Every tier now names a real,
// self-hosted webfont (see FONT_TIERS above), so this always applies, not just to pixel fonts.
document.fonts.load(ctx.font);
ctx.textAlign = "center";
const centerX = snap(node.box.x + node.box.width / 2);
const lineHeight = node.box.height / node.lines.length;
@ -527,9 +530,9 @@ export function drawFallbackLabel(canvas, content, orientation = "along") {
// print label should show/encode; keyed by `kind` so each kind's format is defined in one place.
export const LABEL_CONTENT_BUILDERS = {
// The self-contained Item URL (see docs/design-in-progress/items-labels.md), built from just
// the prefill's {userHandle, id}; the short link (Print.vue's `shortUrl`) needs an async store
// the prefill's {userHandle, item}; the short link (Print.vue's `shortUrl`) needs an async store
// lookup, so it stays a separate field/template rather than being baked in here.
"item": ({userHandle, id}) => `${window.location.origin}/i/${encodeHandleForUrl(userHandle)}/${id}`,
"item": ({userHandle, item}) => `${window.location.origin}/i/${encodeHandleForUrl(userHandle)}/${item}`,
// Storage locations have no long-form URL route yet (see router.js), so `text` starts blank;
// the short link and any future location template still work via the base vars below.
};
@ -542,7 +545,7 @@ export function buildLabelContent(prefill) {
return build ? build(prefill.components) : "";
}
// Splits a prefill's {userHandle, id} into label-layouts.js's user/domain base vars (the same way
// Splits a prefill's {userHandle, item/location} into label-layouts.js's user/domain base vars (the same way
// store.js's lookupServer does), tagging on whichever id field the resource's templates key
// required_vars by.
function splitUserHandle(userHandle) {
@ -560,19 +563,19 @@ function splitUserHandle(userHandle) {
// computed live elsewhere (see DERIVED_VARS, Print.vue's `shortUrl`), and omitting a field
// (rather than leaving it present-but-empty) signals "not available" to templateIsAvailable.
const LABEL_FIELD_BUILDERS = {
"item": ({userHandle, id}) => {
"item": ({userHandle, item}) => {
const split = splitUserHandle(userHandle);
if (!split || !id) {
if (!split || !item) {
return {};
}
return {...split, itemId: String(id)};
return {...split, itemId: String(item)};
},
"storage-location": ({userHandle, id}) => {
"storage-location": ({userHandle, location}) => {
const split = splitUserHandle(userHandle);
if (!split || !id) {
if (!split || !location) {
return {};
}
return {...split, locationId: String(id)};
return {...split, locationId: String(location)};
},
};