stash
This commit is contained in:
parent
8d96bc97c4
commit
ed04d98bf1
54 changed files with 661 additions and 1214 deletions
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue