stash
This commit is contained in:
parent
ce1e5f1d62
commit
c345372382
9 changed files with 233 additions and 82 deletions
|
|
@ -1,4 +1,5 @@
|
|||
import QRCode from "qrcode";
|
||||
import {encodeHandleForUrl} from "@/handle-url";
|
||||
|
||||
const TRAILING_PADDING_PX = 3; /* blank columns after the cut, same idea as the leading margin */
|
||||
|
||||
|
|
@ -40,7 +41,26 @@ export function tapeFromStatus(status) {
|
|||
*/
|
||||
const QR_ASPECT = 1; /* a QR code is always square */
|
||||
const TEXT_REFERENCE_PX = 100; /* font size text leaves measure their natural aspect ratio at */
|
||||
const MIN_READABLE_TEXT_PX = 8; /* below this, a layout is rejected rather than rendered unreadably small */
|
||||
|
||||
// Below 10px, a general-purpose sans-serif gets blurry/illegible, so drawTextLeaf switches to
|
||||
// whichever of these bitmap-style fonts (see ../scss/_pixel-fonts.scss) is designed closest to -
|
||||
// and no smaller than - the box's actual effective size. Ordered smallest first; fontFamilyFor
|
||||
// below picks the first tier whose belowPx clears the requested size, sans-serif once none do.
|
||||
const PIXEL_FONT_TIERS = [
|
||||
{belowPx: 6, family: "Tom Thumb"}, /* designed for ~5px */
|
||||
{belowPx: 7, family: "PICO-8"}, /* designed for ~6px */
|
||||
{belowPx: 10, family: "Silkscreen"}, /* designed for ~7-8px */
|
||||
];
|
||||
|
||||
function fontFamilyFor(fontPx) {
|
||||
return PIXEL_FONT_TIERS.find(t => fontPx < t.belowPx)?.family ?? "sans-serif";
|
||||
}
|
||||
|
||||
// A pixel font (see PIXEL_FONT_TIERS above) is what makes a size down here still legible - a
|
||||
// plain sans-serif this small would fail the old MIN_READABLE_TEXT_PX=8 floor that predates them.
|
||||
// Below this, drawTextLeaf leaves that one field blank rather than drawing illegible ink - see
|
||||
// there for why that's a quieter failure than rejecting the whole label over it.
|
||||
const MIN_READABLE_TEXT_PX = 5;
|
||||
|
||||
function isSplit(node) {
|
||||
return Array.isArray(node);
|
||||
|
|
@ -134,26 +154,6 @@ function positionTree(node, ownAxis, x, y) {
|
|||
}
|
||||
}
|
||||
|
||||
/* Walks the tree once it's been sized (layoutTree) and given a hard minimum to check against -
|
||||
this is the text-too-small-to-read validation, kept as one pass over the already-final boxes
|
||||
rather than scattered through the drawing code, so nothing gets mutated on the canvas before
|
||||
every leaf is confirmed to fit. The equivalent QR-doesn't-fit check already happened earlier,
|
||||
in snapQrToCrispSize. */
|
||||
function assertLeavesFit(node) {
|
||||
if (isSplit(node)) {
|
||||
node.forEach(child => assertLeavesFit(child));
|
||||
return;
|
||||
}
|
||||
if (node.type === "text") {
|
||||
const fontPx = TEXT_REFERENCE_PX * (node.box.height / node.naturalHeight);
|
||||
if (fontPx < MIN_READABLE_TEXT_PX) {
|
||||
throw new Error("This text doesn't fit this layout even at the smallest readable size — "
|
||||
+ "try a shorter value, a wider tape, or a different layout.");
|
||||
}
|
||||
}
|
||||
// "qrcode"/"empty" leaves have nothing left to check by this point.
|
||||
}
|
||||
|
||||
/* A QR code needs an integer number of pixels per module to render crisply rather than blurring
|
||||
at a fractional scale, so its true size is whatever that rounds down to - almost never the
|
||||
scale-free square a bare aspect ratio of 1 would suggest. Called once every qrcode leaf has a
|
||||
|
|
@ -180,6 +180,12 @@ function snapQrToCrispSize(node) {
|
|||
}
|
||||
}
|
||||
|
||||
// Always measures in sans-serif at the fixed reference size, even though drawTextLeaf may end up
|
||||
// actually drawing in one of PIXEL_FONT_TIERS' fonts - the final effective size (and so which
|
||||
// font applies) isn't known until layoutTree has already sized the box this aspect ratio feeds
|
||||
// into. The pixel fonts are close enough in proportion for basic Latin/digits that the tiny
|
||||
// resulting mismatch is invisible in practice at these sizes, and the MIN_READABLE_TEXT_PX check
|
||||
// still catches anything that genuinely doesn't fit.
|
||||
function measureTextBlock(ctx, lines, referencePx) {
|
||||
ctx.font = `${referencePx}px sans-serif`;
|
||||
const width = Math.max(...lines.map(line => ctx.measureText(line).width));
|
||||
|
|
@ -224,10 +230,28 @@ function drawQrLeaf(ctx, node) {
|
|||
}
|
||||
}
|
||||
|
||||
// Returns the effective font size drawn at (or that would have been, if it's too small to draw -
|
||||
// see below) - drawTree collects these into drawLabel/drawFallbackLabel's textSizesPx.
|
||||
function drawTextLeaf(ctx, node, referencePx) {
|
||||
const fontPx = referencePx * (node.box.height / node.naturalHeight);
|
||||
// Even the smallest PIXEL_FONT_TIERS entry stops being legible below this - rather than
|
||||
// reject the whole label over one field that's too small (the old behavior), just leave this
|
||||
// leaf blank; its box was already accounted for, so nothing else in the layout shifts.
|
||||
if (fontPx < MIN_READABLE_TEXT_PX) {
|
||||
return fontPx;
|
||||
}
|
||||
const lineHeight = node.box.height / node.lines.length;
|
||||
ctx.font = `${fontPx}px sans-serif`;
|
||||
const family = fontFamilyFor(fontPx);
|
||||
ctx.font = `${fontPx}px "${family}"`;
|
||||
// A @font-face family already in use elsewhere on the page loads in time for this, but canvas
|
||||
// text silently falls back to the next font in the stack (there isn't one here, so the
|
||||
// browser default) if drawn before its first-ever load finishes - unlike DOM text, a canvas
|
||||
// fillText never waits or repaints on its own once the real font arrives. Kicking off the load
|
||||
// here means only that very first draw at a given size risks the fallback; every redraw after
|
||||
// it (Print.vue's live preview redraws on every keystroke) picks up the real font.
|
||||
if (family !== "sans-serif") {
|
||||
document.fonts.load(ctx.font);
|
||||
}
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.textAlign = "center";
|
||||
const centerX = node.box.x + node.box.width / 2;
|
||||
|
|
@ -238,6 +262,7 @@ function drawTextLeaf(ctx, node, referencePx) {
|
|||
ctx.fillText(line, centerX, y);
|
||||
y += lineHeight;
|
||||
}
|
||||
return fontPx;
|
||||
}
|
||||
|
||||
// Flip this to true (in a debugger or a local edit) to outline every leaf's box - including
|
||||
|
|
@ -256,15 +281,19 @@ function drawDebugBorder(ctx, node) {
|
|||
ctx.restore();
|
||||
}
|
||||
|
||||
function drawTree(ctx, node, referencePx) {
|
||||
// `textSizesPx` collects each "text" leaf's effective font size as drawTree walks the tree - see
|
||||
// drawLabel/drawFallbackLabel, which hand it back to the caller (Print.vue shows it alongside the
|
||||
// tape width) so a field rendering blank (see drawTextLeaf's MIN_READABLE_TEXT_PX check) shows up
|
||||
// as a suspiciously small size here rather than just silently not being there.
|
||||
function drawTree(ctx, node, referencePx, textSizesPx) {
|
||||
if (isSplit(node)) {
|
||||
node.forEach(child => drawTree(ctx, child, referencePx));
|
||||
node.forEach(child => drawTree(ctx, child, referencePx, textSizesPx));
|
||||
return;
|
||||
}
|
||||
if (node.type === "qrcode") {
|
||||
drawQrLeaf(ctx, node);
|
||||
} else if (node.type === "text") {
|
||||
drawTextLeaf(ctx, node, referencePx);
|
||||
textSizesPx.push(drawTextLeaf(ctx, node, referencePx));
|
||||
}
|
||||
// "empty" leaves carry no ink - their box just reserves the space.
|
||||
if (DEBUG_LEAF_BORDERS) {
|
||||
|
|
@ -298,12 +327,13 @@ function layoutContent(ctx, content, height, maxLength, referencePx, pxPerMm) {
|
|||
+ "try a shorter value, a different layout, or a bigger label.");
|
||||
}
|
||||
layoutTree(tree, false, width, height, pxPerMm);
|
||||
assertLeavesFit(tree);
|
||||
return {tree, width};
|
||||
}
|
||||
|
||||
/* The tape-fed layout - draws a fully resolved content tree (see templateContent) at the tape's
|
||||
real pixel dimensions. See DEBUG_LEAF_BORDERS above to outline every leaf's box. */
|
||||
real pixel dimensions. See DEBUG_LEAF_BORDERS above to outline every leaf's box. Returns
|
||||
{textSizesPx}: each "text" leaf's effective font size, in the tree's own left-to-right,
|
||||
top-to-bottom order. */
|
||||
export function drawLabel(canvas, tape, content) {
|
||||
const maxLength = tape.printLengthPx
|
||||
? tape.printLengthPx - tape.leadPx - TRAILING_PADDING_PX
|
||||
|
|
@ -324,7 +354,9 @@ export function drawLabel(canvas, tape, content) {
|
|||
|
||||
const originX = tape.leadPx + Math.floor((width - tape.leadPx - TRAILING_PADDING_PX - contentWidth) / 2);
|
||||
positionTree(tree, false, originX, 0);
|
||||
drawTree(ctx, tree, TEXT_REFERENCE_PX);
|
||||
const textSizesPx = [];
|
||||
drawTree(ctx, tree, TEXT_REFERENCE_PX, textSizesPx);
|
||||
return {textSizesPx};
|
||||
}
|
||||
|
||||
const FALLBACK_LABEL_HEIGHT_PX = 200; /* reference height the no-webusb preview/PNG scales from */
|
||||
|
|
@ -333,7 +365,7 @@ const FALLBACK_DPI = 203; /* reference resolution for turning "empty" leaves' m
|
|||
/* The no-webusb preview/PNG - same layout tree and renderer as drawLabel, just scaled from a
|
||||
fixed reference height instead of a real tape's, and with no maxLength (there's no physical
|
||||
tape to run out of, so the canvas just grows to fit) and no printer feed margin, since there's
|
||||
no real print head here to keep clear of. */
|
||||
no real print head here to keep clear of. Returns {textSizesPx}, see drawLabel. */
|
||||
export function drawFallbackLabel(canvas, content) {
|
||||
const measureCtx = canvas.getContext("2d");
|
||||
const pxPerMm = FALLBACK_DPI / 25.4;
|
||||
|
|
@ -349,7 +381,9 @@ export function drawFallbackLabel(canvas, content) {
|
|||
ctx.fillStyle = "#000";
|
||||
|
||||
positionTree(tree, false, 0, 0);
|
||||
drawTree(ctx, tree, TEXT_REFERENCE_PX);
|
||||
const textSizesPx = [];
|
||||
drawTree(ctx, tree, TEXT_REFERENCE_PX, textSizesPx);
|
||||
return {textSizesPx};
|
||||
}
|
||||
|
||||
// Turns a {kind, components} prefill (see Print.vue's `prefill` prop) into the literal string a
|
||||
|
|
@ -360,7 +394,7 @@ export const LABEL_CONTENT_BUILDERS = {
|
|||
// The self-contained Item URL (see docs/design-in-progress/items-labels.md) - what a
|
||||
// printed label actually encodes, since scanning it has to resolve the right
|
||||
// frontend/backend/item with no other context, not just this browser's history.
|
||||
"item-url": ({user, id}) => `${window.location.origin}/i/${user}/${id}`,
|
||||
"item-url": ({user, id}) => `${window.location.origin}/i/${encodeHandleForUrl(user)}/${id}`,
|
||||
};
|
||||
|
||||
export function buildLabelContent(prefill) {
|
||||
|
|
@ -378,11 +412,20 @@ export function buildLabelContent(prefill) {
|
|||
// (rather than present-but-empty) is what label-layouts.js's templateIsAvailable treats as "not
|
||||
// available", so builders should only include a field once its inputs actually check out.
|
||||
const LABEL_FIELD_BUILDERS = {
|
||||
// `user` here is already a full "user@domain" handle (that's the form login usernames take -
|
||||
// see Login.vue/store.js), so it's split into label-layouts.js's separate `user`/`domain`
|
||||
// base vars the same way store.js's own lookupServer does, rather than stuffing the whole
|
||||
// handle into one field the way userHandle (now derived from these two) used to be.
|
||||
"item-url": ({user, id}) => {
|
||||
if (!user || !id) {
|
||||
return {};
|
||||
}
|
||||
return {userHandle: user, itemId: String(id)};
|
||||
const at = user.indexOf("@");
|
||||
return {
|
||||
user: at === -1 ? user : user.slice(0, at),
|
||||
domain: at === -1 ? "" : user.slice(at + 1),
|
||||
itemId: String(id),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue