This commit is contained in:
j3d1 2026-08-24 15:57:17 +02:00
parent 8d96bc97c4
commit ed04d98bf1
54 changed files with 661 additions and 1214 deletions

View file

@ -1,38 +1,16 @@
import {loadAnyDCode} from "../vendor/anyd-qr.js";
import {encodeHandleForUrl} from "@/router"
// anyd-qr.js's own loadAnyDCode() memoizes the wasm instantiation itself, so calling it more
// than once (each of Print.vue and LabelLayoutPreview.vue does, on mount) is free - `anyd` just
// mirrors its resolved value so buildRenderTree below can use it synchronously. Until it
// resolves, a QR-family leaf (any QR_LEAF_TYPES entry) throws (see encodeQr) the same way an
// oversized value already does - callers already have to handle layoutContent throwing, so this
// reuses that path rather than adding a second failure mode.
// Mirrors loadAnyDCode()'s memoized wasm instance for synchronous use in buildRenderTree. See docs/implementation.md#qr-encoder-loading.
let anyd = null;
export function preloadQrEncoder() {
return loadAnyDCode().then(instance => { anyd = instance; });
}
// Maps each of label-layouts.js's LABEL_TEMPLATES leaf types that draw a code to the anyd-qr.js
// symbology/error-correction level (and, for rMQR, size strategy) it renders as (see anyd's
// EncodeOptions - `ecc`/`size` - and its per-symbology EcLevel enums, `wasm.rs`'s
// qr_ec/micro_ec/rmqr_ec/rmqr_size) - which combination a given label uses is baked into its
// layout tree (see label-layouts.js's "qr"-prefixed templates), rather than a single choice
// applied to every code leaf alike, so there's no longer a global selector for any of them (see
// Print.vue). A plain symbology id (no suffix) always means anyd's own defaults - ecc "M", rMQR
// size "balanced" - every other value gets a "-<name>" suffix naming it:
// - ecc: the same letter anyd itself uses (qr_ec/micro_ec's L/M/Q/H), except micro-qr's
// "Detection" (`MicroEcLevel::Detection`, an M1-only error-*detection*-but-not-correction mode
// with no plain single-letter grade of its own). Coverage isn't uniform across symbologies
// (see qr_ec/micro_ec/rmqr_ec) - full QR takes all four grades, Micro QR swaps "L" (QR's
// actual lowest) for "Detection" (lower still, but M1-only) and has no "H" at all, and rMQR
// only ever supports "M" or "H".
// - size (rMQR only, see rmqr_size/SizeStrategy): "min"/"max" prefer the shortest (flattest,
// widest) or tallest (narrowest) symbol that fits the text, over the default "balanced"
// (smallest total module area) - which shape to prefer depends on which of the tape's two
// axes (across vs. along the feed) is more constrained.
// rMQR's matrix isn't square (see encodeQr's width/height below), unlike qr/micro-qr, which
// always are.
// Maps label-layouts.js leaf types to anyd-qr.js symbology/ecc/size options, via a naming
// convention (plain id = anyd defaults; "-<name>" suffix names an ecc letter or rMQR size
// strategy). See docs/implementation.md#qr-leaf-type-mapping.
const QR_LEAF_TYPES = {
"qr-l": {codeType: "qr", ecc: "L"},
qr: {codeType: "qr", ecc: "M"},
@ -58,12 +36,9 @@ function encodeQr(text, codeType, options) {
if (!anyd) {
throw new Error("The QR encoder is still loading — try again in a moment.");
}
// BitMatrix-alike view over anyd's row-major Uint8Array, matching the shape drawQrLeaf/
// snapQrToCrispSize below expect (they predate this and were written against the "qrcode"
// package's own modules.size/get()). anyd's matrix already excludes the quiet zone from
// width/height (see its ModuleMatrix type), same as the old library's BitMatrix. width/height
// are kept separate rather than a single `size` (the old library's own shape, always square)
// since rMQR symbols are rectangular.
// BitMatrix-alike shim over anyd's row-major matrix, matching the old "qrcode" package's
// modules.size/get() shape that drawQrLeaf/snapQrToCrispSize expect. See
// docs/implementation.md#qr-module-matrix-shim.
const {width, height, modules} = anyd.encode(codeType, new TextEncoder().encode(text), options).matrix;
return {width, height, get: (row, col) => modules[row * width + col] !== 0};
}
@ -89,51 +64,13 @@ export function tapeFromStatus(status) {
};
}
/*
A layout is a tree built from two shapes, alternating orientation by nesting depth:
- An array is a "split" node: its children sit side by side (a *row*) at even depth
(the root, depth 0, is always a row), or stacked (a *column*) at odd depth. To turn a
row into a column, wrap it in an extra one-element array - that array is one depth
deeper, so its lone child (the original row) is now read at odd depth.
- An object is a leaf: {type, content} where `type` is one of QR_LEAF_TYPES' keys draws a
QR/Micro QR/rMQR code at that id's symbology/error-correction level (see QR_LEAF_TYPES
above), {type: "text", content} draws a text block - either way `content` is a function
from the resolved field values to the string (or, for "text", an array of strings - one
per line) to render. {type: "empty",
"min-width": "2mm"} / {type: "empty", "min-height": "2mm"} is a spacer with no ink of
its own - the *only* way padding/gaps enter a layout, since nothing here draws a
border, margin or gap on its own. An "empty" leaf's dimension always names the axis its
enclosing split flows along: "min-width" inside a row, "min-height" inside a column.
See label-layouts.js's LABEL_TEMPLATES for concrete trees.
*/
// A layout tree alternates row/column split nodes by nesting depth, with QR/text/empty leaves.
// See docs/implementation.md#layout-tree-structure.
const TEXT_REFERENCE_PX = 100; /* font size text leaves measure their natural aspect ratio at */
// Below 10px, a general-purpose sans-serif gets blurry/illegible, so drawTextLeaf switches to one
// of these bitmap-style fonts instead (see ../scss/_pixel-fonts.scss) - Tom Thumb for the smallest
// sizes, Silkscreen once there's enough room for its more conventional letterforms.
//
// Both were chosen only after rendering single letters in a real browser *at raw canvas pixel
// sizes* and inspecting the actual pixels - checking that fillText was merely *called* doesn't
// confirm anything legible got drawn, and neither does a DPI-adjusted size that was never the
// number actually handed to ctx.font. Silkscreen confirmed clean at 8px+. A third candidate,
// PICO-8, also rendered cleanly across the whole range, but has no lowercase glyphs at all - it
// silently draws lowercase input as uppercase - which rules it out for real label content (item
// handles, URLs) that isn't reliably all-caps. Tom Thumb's declared ascent/descent (0 / ~fontPx,
// backwards from a normal font) turned out not to be a centering quirk: its actual visible ink is
// only ~1/3.2 of its own nominal font-size (confirmed both by measuring actualBoundingBox at
// several sizes and by a live-browser check - "16px" reads as roughly 5px of real glyph height),
// hence the `scale` below - whatever logical size is requested, the font is actually drawn that
// many times larger so its real ink comes out at the intended size. Silkscreen's declared size
// already matches its ink, so it has no `scale` (equivalent to 1).
//
// belowPx and MIN_READABLE_TEXT_PX below are both compared against the *logical* (unscaled)
// fontPx, deliberately not adjusted for the tape's dpi: a browser's font rasterizer only ever
// sees a raw pixel count, with no notion of "physical size" at all, so that's what determines
// whether a glyph's fine detail survives - confirmed by the same real-Chromium testing, where a
// raw 4.35px render was a solid blob regardless of what a dpi-scaled version of that number would
// have implied.
// 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"},
@ -143,11 +80,7 @@ function fontFamilyFor(fontPx) {
return PIXEL_FONT_TIERS.find(t => fontPx < t.belowPx) ?? {family: "sans-serif"};
}
// Tom Thumb (see PIXEL_FONT_TIERS above) held up down to 5px in the same real-Chromium pixel-level
// verification - a plain sans-serif this small would fail the old MIN_READABLE_TEXT_PX=8 floor
// that predates it. 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.
// Tom Thumb reads clearly down to 5px per the same real-Chromium testing as PIXEL_FONT_TIERS; below this, drawTextLeaf blanks the field instead of rejecting the whole label.
const MIN_READABLE_TEXT_PX = 5;
function isSplit(node) {
@ -162,20 +95,9 @@ function parseMm(value, key) {
return parseFloat(match[1]);
}
/* Every node's width/height relate to each other affinely - width = A*height + B for a node
read in row context, height = A*width + B in column context - because a leaf is either
scale-free (a text block, whose aspect ratio holds at any size: A = aspect or 1/aspect,
B = 0) or a fixed physical size (an "empty" spacer, or a QR code once its crisp pixel size is
known - see snapQrToCrispSize below: A = 0, B = the size in px). Splits combine their
children's relations by addition (a row's total width is the sum of each child's width for
the shared height, and symmetrically for a column), which stays affine, so the same two
numbers describe a whole subtree no matter how deeply it nests.
`ownAxis` is true if the split directly containing `node` is a row, false if a column - for
a leaf, that's what "empty" measures itself against; for a split, its own axis (and thus how
it combines its children) is always the opposite, per the alternating-depth rule. `wantWidth`
is true to ask for {A, B} such that width = A*height + B, false for height = A*width + B;
requesting the direction a split doesn't naturally combine in just inverts its own relation. */
// Every node's width/height relate affinely (width = A*height + B, or symmetrically); `ownAxis`
// and `wantWidth` pick which direction and against which split axis. See
// docs/implementation.md#affine-width-height-relations.
function relation(node, ownAxis, wantWidth, pxPerMm) {
if (!isSplit(node)) {
if (isQrLeaf(node) && node.crispWidth !== undefined) {
@ -198,23 +120,16 @@ function relation(node, ownAxis, wantWidth, pxPerMm) {
return {a, b};
}
if (a === 0) {
// Every child is a fixed size (a === 0) in the combining direction - e.g. a row that's
// just one crisp QR leaf, with no scale-free (text) sibling to invert against. Inverting
// "width = b" for an a of 0 would divide by zero: a constant width genuinely doesn't
// determine a height, since nothing here actually scales with it. Ask each child directly
// for its own size in the wanted direction instead (every one of them must be similarly
// fixed, since only a fixed leaf ever contributes a === 0), and take the largest - the
// shared dimension has to fit whichever child needs the most room, with any child that
// ends up with room to spare centered within it (see drawQrLeaf).
// Every child is fixed-size (a === 0) in the combining direction, so inverting would
// divide by zero. See docs/implementation.md#fixed-size-relation-edge-case.
const otherParts = node.map(child => relation(child, axis, wantWidth, pxPerMm));
return {a: 0, b: Math.max(...otherParts.map(p => p.b))};
}
return {a: 1 / a, b: -b / a}; // invert: solve the affine relation the other way
}
/* Top-down: given the fixed (width, height) box `node` must exactly fill, assigns that box to
it and, recursively, an appropriately-shaped box to every descendant. `ownAxis` carries the
same meaning as in relation() above. */
// Top-down: assigns the fixed (width, height) box `node` must exactly fill, recursively, to
// every descendant; `ownAxis` carries the same meaning as in relation() above.
function layoutTree(node, ownAxis, width, height, pxPerMm) {
node.box = {width, height};
if (!isSplit(node)) {
@ -232,9 +147,8 @@ function layoutTree(node, ownAxis, width, height, pxPerMm) {
}
}
/* Second top-down pass: turns each node's already-sized box into an absolute (x, y) position,
placing a row's children left to right and a column's top to bottom. Kept separate from
layoutTree since a node's size doesn't depend on its position, only on its box dimensions. */
// Second top-down pass: turns each already-sized box into an absolute (x, y) position; kept
// separate from layoutTree since a node's size doesn't depend on its position.
function positionTree(node, ownAxis, x, y) {
node.box.x = x;
node.box.y = y;
@ -254,17 +168,8 @@ function positionTree(node, ownAxis, x, y) {
}
}
/* 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 box its aspect ratio alone would suggest. Called once every QR-family leaf has a
provisional (scale-free) box from a first layoutTree pass, this pins each one's real
box.width/box.height as `crispWidth`/`crispHeight`, so relation() above starts treating it as a
fixed size, the same as an "empty" leaf, instead of one that scales with whatever height/width
it's offered. A second relation()/layoutTree() pass (see layoutContent) then resizes everything
else around that real footprint, so nothing downstream reserves - and leaves unfilled - room
for a squarer/differently-shaped code than what actually gets drawn. Kept as two independent
dimensions rather than one `crispSize` (as when every code here was a square QR) since an rMQR
symbol isn't square - see encodeQr. */
// Pins each QR-family leaf's real crisp-pixel box.width/box.height so relation() above starts
// treating it as fixed-size. See docs/implementation.md#crisp-qr-sizing.
function snapQrToCrispSize(node) {
if (isSplit(node)) {
node.forEach(snapQrToCrispSize);
@ -282,12 +187,9 @@ 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.
// 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 MIN_READABLE_TEXT_PX still catches real failures.
function measureTextBlock(ctx, lines, referencePx) {
ctx.font = `${referencePx}px sans-serif`;
const width = Math.max(...lines.map(line => ctx.measureText(line).width));
@ -295,15 +197,8 @@ function measureTextBlock(ctx, lines, referencePx) {
return {width, height};
}
/* Turns a resolved content tree (see templateContent below - leaf objects carry a `value`
rather than a `content` function) into one ready for layout: a QR-family leaf gets its
actual encoded modules (see encodeQr, keyed off the leaf's own type via QR_LEAF_TYPES) and an
aspect ratio taken from their real width/height - 1 (square) for qr/micro-qr, but not for rmqr,
whose symbols are rectangular - a text leaf gets its measured natural aspect ratio, and an
"empty" leaf passes through untouched. Multi-line text (`value` is an array) measures as one
leaf, not one per line - splitting it into a column of independently-sized leaves would let
each line grow to its own full width, ending up at a different font size than its neighbors,
which is legible but not what "one text field" should look like. */
// Converts a resolved content tree (see templateContent) into one ready for layout. See
// docs/implementation.md#render-tree-construction.
function buildRenderTree(ctx, node, referencePx) {
if (isSplit(node)) {
return node.map(child => buildRenderTree(ctx, child, referencePx));
@ -337,54 +232,40 @@ function drawQrLeaf(ctx, node) {
}
}
// Returns the effective (raw, un-normalized) 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.
// Returns the effective (un-normalized) font size drawn at, or that would have been if 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.
// Below the smallest legible size, leave this leaf blank rather than reject the whole label;
// its box was already accounted for, so nothing else in the layout shifts.
if (fontPx < MIN_READABLE_TEXT_PX) {
return fontPx;
}
const {family, scale = 1} = fontFamilyFor(fontPx);
const isPixelFont = family !== "sans-serif";
// A pixel font's glyphs are meant to land exactly on the pixel grid - node.box.x/y are
// ordinary layout math (sums/quotients of affine-solved sizes) and essentially never land on
// a whole pixel, so drawing at their exact fractional size/position would misalign a pixel
// font's 1px-wide strokes the same as it would any other font. Snapping size and position to
// the nearest whole pixel fixes that; sans-serif is left at its exact fractional fit, since
// ordinary anti-aliased text is expected to (and looks fine) regardless of position.
// Pixel-font glyphs need whole-pixel size/position to stay grid-aligned, since node.box.x/y
// are ordinary (fractional) layout math; sans-serif is left exact since anti-aliasing handles
// fractional positions fine.
const snap = isPixelFont ? Math.round : (v) => v;
const drawFontPx = snap(fontPx);
// `scale` (Tom Thumb only, see PIXEL_FONT_TIERS above) corrects for a font whose declared
// size doesn't match its real visible ink - the size actually handed to ctx.font, not
// drawFontPx itself, which stays the logical size everything else here (box centering, line
// stacking) is measured against.
// `scale` (Tom Thumb only, see PIXEL_FONT_TIERS above) corrects the size handed to ctx.font
// for its real ink; drawFontPx itself stays the logical size used for centering/stacking math.
ctx.font = `${drawFontPx * scale}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.
// 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);
}
ctx.textAlign = "center";
const centerX = snap(node.box.x + node.box.width / 2);
const lineHeight = node.box.height / node.lines.length;
// Lines stack as a block, each centered under the last - keeps a multi-line field reading as
// one unit rather than drifting apart.
// Lines stack as a block, each centered under the last, so a multi-line field reads as one unit.
let sliceTop = node.box.y;
for (const line of node.lines) {
if (isPixelFont) {
// textBaseline:"middle" centers on the font's *declared* ascent/descent (its line
// height) - Tom Thumb's are backwards (see PIXEL_FONT_TIERS above) and would center on
// nonsense. Centering on actualBoundingBox{Ascent,Descent} instead - this specific
// string's real rendered ink (its character height) - costs nothing and stays correct
// regardless of whether a pixel font's declared metrics can be trusted.
// textBaseline:"middle" centers on declared ascent/descent, which is backwards for
// Tom Thumb; centering on actualBoundingBox{Ascent,Descent} instead measures this
// string's real rendered ink and stays correct regardless.
ctx.textBaseline = "alphabetic";
const {actualBoundingBoxAscent: up, actualBoundingBoxDescent: down} = ctx.measureText(line);
ctx.fillText(line, centerX, snap(sliceTop + (lineHeight + up - down) / 2));
@ -397,26 +278,22 @@ function drawTextLeaf(ctx, node, referencePx) {
return fontPx;
}
// Flip this to true (in a debugger or a local edit) to outline every leaf's box - including
// "empty" ones, normally invisible - in a color that can't be mistaken for real label ink. Handy
// for checking a layout's actual padding/alignment; never wanted on a real printed label, so it's
// a manual toggle rather than something wired up to any UI.
// Manual debug toggle (flip in a debugger) to outline every leaf's box, including
// normally-invisible "empty" ones, in a color that can't be mistaken for real label ink; never
// wired up to any UI.
let DEBUG_LEAF_BORDERS = false;
function drawDebugBorder(ctx, node) {
ctx.save();
ctx.strokeStyle = "red";
ctx.lineWidth = 1;
// Inset by half a pixel so the 1px stroke lands crisply on-pixel instead of straddling the
// box edge and rendering as a blurry 2px line.
// Inset by half a pixel so the 1px stroke lands crisply on-pixel instead of straddling the edge.
ctx.strokeRect(node.box.x + 0.5, node.box.y + 0.5, node.box.width - 1, node.box.height - 1);
ctx.restore();
}
// `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.
// Collects each text leaf's effective font size so callers (Print.vue) can spot a blank-rendered
// field (see drawTextLeaf's MIN_READABLE_TEXT_PX check) as suspiciously small rather than silently missing.
function drawTree(ctx, node, referencePx, textSizesPx) {
if (isSplit(node)) {
node.forEach(child => drawTree(ctx, child, referencePx, textSizesPx));
@ -433,27 +310,9 @@ function drawTree(ctx, node, referencePx, textSizesPx) {
}
}
/* Builds, sizes and validates the tree for a fixed `fixedSize` (the tape's cross-web printAreaPx,
or the fallback preview's reference height) - the one dimension every layout scales from, plus
`pxPerMm` to turn "empty" leaves' physical sizes into pixels. `fixedSize` and the tree's content
fully determine its overall size along the other, growing axis (the one that runs along the
tape as it feeds); `maxLength`, when finite (a fixed-length/die-cut tape), rejects content that
doesn't fit rather than shrinking it.
`orientation` picks which axis `fixedSize` binds to: "along" (the default) fixes the tree's
height - the tape's cross-web width - and grows its width along the feed direction, same as a
plain read top-to-bottom design. "across" fixes the tree's width instead and grows its height,
so the design is built turned 90deg from how it'd read "along" - drawLabel/drawFallbackLabel
are what actually rotate the drawing back into the physical raster's fixed orientation; nothing
here needs to know about that rotation, since relation()/layoutTree() below already solve the
tree in either direction symmetrically.
Sizing runs twice: a first pass treats every QR-family leaf as the scale-free box its real
width/height ratio suggests, purely to find out how much room each one would actually be
offered; from that, snapQrToCrispSize pins each one's real (smaller, crisp-pixel) size. The
second pass then resolves the whole tree again with that real size fixed in, so every sibling
and the overall size reflect what's actually drawn rather than the idealized box no code ever
quite fills. */
// Builds, sizes and validates the tree for a fixed dimension plus pxPerMm; runs sizing twice so
// QR-family leaves' real crisp size is known before the tree is finally resolved. See
// docs/implementation.md#label-content-layout.
function layoutContent(ctx, content, fixedSize, maxLength, referencePx, pxPerMm, orientation) {
const tree = buildRenderTree(ctx, content, referencePx);
const alongTape = orientation !== "across";
@ -479,16 +338,10 @@ function layoutContent(ctx, content, fixedSize, maxLength, referencePx, pxPerMm,
return {tree, length};
}
/* The tape-fed layout - draws a fully resolved content tree (see templateContent) at the tape's
real pixel dimensions. `orientation` is "along" (the default) to lay the design out reading
along the tape's feed direction, or "across" to turn it 90deg so it reads across the tape
instead - either way the physical raster this returns is still exactly
printedLength x tape.printAreaPx (that's fixed by the tape/print head, not a choice this
makes); "across" just draws the (now width-fixed, see layoutContent) tree through a rotated
canvas transform so it lands correctly in that same raster, rather than transposing every box
the tree itself computed. 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. */
// The tape-fed layout: draws a fully resolved content tree (see templateContent) at the tape's
// real pixel dimensions. See docs/implementation.md#tape-fed-label-drawing. 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, orientation = "along") {
const maxLength = tape.printLengthPx
? tape.printLengthPx - tape.leadPx - TRAILING_PADDING_PX
@ -511,10 +364,8 @@ export function drawLabel(canvas, tape, content, orientation = "along") {
+ Math.floor((printedLength - tape.leadPx - TRAILING_PADDING_PX - contentLength) / 2);
const textSizesPx = [];
if (orientation === "across") {
// The tree was solved width-fixed (see layoutContent) - its width already exactly fills
// tape.printAreaPx, so only its (growing) height needs the same along-the-feed centering
// originX got above; translate+rotate then carries that tree-local (x, y) box straight
// into the physical (printedLength x printAreaPx) raster, a quarter turn at a time.
// Rotates/translates the width-fixed tree into the physical raster a quarter turn at a
// time. See docs/implementation.md#across-orientation-rotation.
positionTree(tree, false, 0, origin);
ctx.save();
ctx.translate(0, tape.printAreaPx);
@ -531,11 +382,9 @@ export function drawLabel(canvas, tape, content, orientation = "along") {
const FALLBACK_LABEL_HEIGHT_PX = 200; /* reference height the no-webusb preview/PNG scales from */
const FALLBACK_DPI = 203; /* reference resolution for turning "empty" leaves' mm sizes into px */
/* 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. `orientation`, see drawLabel. Returns {textSizesPx},
see drawLabel. */
// The no-webusb preview/PNG: same layout tree/renderer as drawLabel, scaled from a fixed
// reference height instead. See docs/implementation.md#fallback-label-preview. `orientation` and
// the {textSizesPx} return, see drawLabel.
export function drawFallbackLabel(canvas, content, orientation = "along") {
const measureCtx = canvas.getContext("2d");
const pxPerMm = FALLBACK_DPI / 25.4;
@ -566,21 +415,14 @@ export function drawFallbackLabel(canvas, content, orientation = "along") {
}
// Turns a {kind, components} prefill (see Print.vue's `prefill` prop) into the literal string a
// print label should show/encode. Keeping this keyed by `kind` rather than having each caller
// build its own string means the format for a given kind of label content only has to be gotten
// right in one place.
// 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) - 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. Nothing here
// needs anything beyond the prefill's own {userHandle, id} - the short link (see Print.vue's
// `shortUrl` computed) needs a store lookup no synchronous builder can do, so it's never baked
// into `text` this way; it's just another field/template a user can pick once the page is up.
// 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
// lookup, so it stays a separate field/template rather than being baked in here.
"item": ({userHandle, id}) => `${window.location.origin}/i/${encodeHandleForUrl(userHandle)}/${id}`,
// Storage locations have no long-form URL route of their own (see router.js - only items get
// an /i/:handle/:id) - so there's nothing to bake synchronously here. Its base vars (below)
// still populate normally, so the short link (Print.vue's `shortUrl`) and any future
// location template are still available; `text` just starts blank until one is picked.
// 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.
};
export function buildLabelContent(prefill) {
@ -591,10 +433,9 @@ export function buildLabelContent(prefill) {
return build ? build(prefill.components) : "";
}
// A prefill's {userHandle, id} is the same raw identity for either resource kind below - this
// just splits the handle into label-layouts.js's separate `user`/`domain` base vars the same way
// store.js's own lookupServer does, and tags on whichever id field the resource's own templates
// key their required_vars by.
// Splits a prefill's {userHandle, id} 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) {
if (!userHandle) {
return null;
@ -606,13 +447,9 @@ function splitUserHandle(userHandle) {
};
}
// Seeds for the *base* label-layouts.js vars (see BASE_VARS there) - keyed by `kind` for the same
// reason LABEL_CONTENT_BUILDERS is. Format-string vars derived from these (userHandle, itemUrl,
// itemHandle, …) aren't built here; they're calculated live from whatever the base vars currently
// are (see label-layouts.js's DERIVED_VARS and Print.vue's `shortUrl`), prefill or hand-typed
// alike. A field missing from the result (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.
// Seeds label-layouts.js's BASE_VARS, keyed by `kind`; derived vars (userHandle, itemUrl, …) are
// 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}) => {
const split = splitUserHandle(userHandle);