stash
This commit is contained in:
parent
5c3b7fc252
commit
bbe52e4a78
9 changed files with 817 additions and 287 deletions
|
|
@ -4,21 +4,57 @@ 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 "qrcode" leaf 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.
|
||||
// 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.
|
||||
let anyd = null;
|
||||
|
||||
export function preloadQrEncoder() {
|
||||
return loadAnyDCode().then(instance => { anyd = instance; });
|
||||
}
|
||||
|
||||
// The three symbologies anyd-qr.js exposes (see its CodeType) - Print.vue's code-type selector
|
||||
// offers exactly these. rMQR's matrix isn't square (see encodeQr's width/height below), unlike
|
||||
// qr/micro-qr, which always are.
|
||||
export const QR_CODE_TYPES = ["qr", "micro-qr", "rmqr"];
|
||||
// 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.
|
||||
const QR_LEAF_TYPES = {
|
||||
"qr-l": {codeType: "qr", ecc: "L"},
|
||||
qr: {codeType: "qr", ecc: "M"},
|
||||
"qr-q": {codeType: "qr", ecc: "Q"},
|
||||
"qr-h": {codeType: "qr", ecc: "H"},
|
||||
"mqr-d": {codeType: "micro-qr", ecc: "Detection"},
|
||||
"mqr-l": {codeType: "micro-qr", ecc: "L"},
|
||||
mqr: {codeType: "micro-qr", ecc: "M"},
|
||||
"mqr-q": {codeType: "micro-qr", ecc: "Q"},
|
||||
rmqr: {codeType: "rmqr", ecc: "M"},
|
||||
"rmqr-min": {codeType: "rmqr", ecc: "M", size: "min"},
|
||||
"rmqr-max": {codeType: "rmqr", ecc: "M", size: "max"},
|
||||
"rmqr-h": {codeType: "rmqr", ecc: "H"},
|
||||
"rmqr-h-min": {codeType: "rmqr", ecc: "H", size: "min"},
|
||||
"rmqr-h-max": {codeType: "rmqr", ecc: "H", size: "max"},
|
||||
};
|
||||
|
||||
function encodeQr(text, codeType) {
|
||||
function isQrLeaf(node) {
|
||||
return node.type in QR_LEAF_TYPES;
|
||||
}
|
||||
|
||||
function encodeQr(text, codeType, options) {
|
||||
if (!anyd) {
|
||||
throw new Error("The QR encoder is still loading — try again in a moment.");
|
||||
}
|
||||
|
|
@ -28,7 +64,7 @@ function encodeQr(text, codeType) {
|
|||
// 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.
|
||||
const {width, height, modules} = anyd.encode(codeType, new TextEncoder().encode(text), {ecc: "M"}).matrix;
|
||||
const {width, height, modules} = anyd.encode(codeType, new TextEncoder().encode(text), options).matrix;
|
||||
return {width, height, get: (row, col) => modules[row * width + col] !== 0};
|
||||
}
|
||||
|
||||
|
|
@ -60,9 +96,11 @@ export function tapeFromStatus(status) {
|
|||
(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: "qrcode", content} / {type: "text", content} draw a QR code
|
||||
or text block, where `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",
|
||||
- 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
|
||||
|
|
@ -140,10 +178,10 @@ function parseMm(value, key) {
|
|||
requesting the direction a split doesn't naturally combine in just inverts its own relation. */
|
||||
function relation(node, ownAxis, wantWidth, pxPerMm) {
|
||||
if (!isSplit(node)) {
|
||||
if (node.type === "qrcode" && node.crispWidth !== undefined) {
|
||||
if (isQrLeaf(node) && node.crispWidth !== undefined) {
|
||||
return {a: 0, b: wantWidth ? node.crispWidth : node.crispHeight};
|
||||
}
|
||||
if (node.type === "qrcode" || node.type === "text") {
|
||||
if (isQrLeaf(node) || node.type === "text") {
|
||||
const aspect = node.aspect;
|
||||
return wantWidth ? {a: aspect, b: 0} : {a: 1 / aspect, b: 0};
|
||||
}
|
||||
|
|
@ -206,7 +244,7 @@ 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 qrcode leaf has a
|
||||
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
|
||||
|
|
@ -220,7 +258,7 @@ function snapQrToCrispSize(node) {
|
|||
node.forEach(snapQrToCrispSize);
|
||||
return;
|
||||
}
|
||||
if (node.type === "qrcode") {
|
||||
if (isQrLeaf(node)) {
|
||||
const {width: modulesW, height: modulesH} = node.qr;
|
||||
const scale = Math.floor(Math.min(node.box.width / modulesW, node.box.height / modulesH));
|
||||
if (!(scale >= 1)) {
|
||||
|
|
@ -246,22 +284,22 @@ function measureTextBlock(ctx, lines, referencePx) {
|
|||
}
|
||||
|
||||
/* 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 leaf gets its actual encoded
|
||||
modules (see encodeQr) 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. `codeType`
|
||||
is Print.vue's global qr/micro-qr/rmqr choice - see QR_CODE_TYPES - applied to every qrcode leaf
|
||||
in the tree alike, the same way `orientation` applies to the whole tree in layoutContent. */
|
||||
function buildRenderTree(ctx, node, referencePx, codeType) {
|
||||
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. */
|
||||
function buildRenderTree(ctx, node, referencePx) {
|
||||
if (isSplit(node)) {
|
||||
return node.map(child => buildRenderTree(ctx, child, referencePx, codeType));
|
||||
return node.map(child => buildRenderTree(ctx, child, referencePx));
|
||||
}
|
||||
if (node.type === "qrcode") {
|
||||
const qr = encodeQr(node.value, codeType);
|
||||
return {type: "qrcode", aspect: qr.width / qr.height, qr};
|
||||
if (isQrLeaf(node)) {
|
||||
const {codeType, ...options} = QR_LEAF_TYPES[node.type];
|
||||
const qr = encodeQr(node.value, codeType, options);
|
||||
return {type: node.type, aspect: qr.width / qr.height, qr};
|
||||
}
|
||||
if (node.type === "text") {
|
||||
const lines = Array.isArray(node.value) ? node.value : [node.value];
|
||||
|
|
@ -372,7 +410,7 @@ function drawTree(ctx, node, referencePx, textSizesPx) {
|
|||
node.forEach(child => drawTree(ctx, child, referencePx, textSizesPx));
|
||||
return;
|
||||
}
|
||||
if (node.type === "qrcode") {
|
||||
if (isQrLeaf(node)) {
|
||||
drawQrLeaf(ctx, node);
|
||||
} else if (node.type === "text") {
|
||||
textSizesPx.push(drawTextLeaf(ctx, node, referencePx));
|
||||
|
|
@ -398,14 +436,14 @@ function drawTree(ctx, node, referencePx, textSizesPx) {
|
|||
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 qrcode leaf as the scale-free box its real
|
||||
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. `codeType`, see buildRenderTree. */
|
||||
function layoutContent(ctx, content, fixedSize, maxLength, referencePx, pxPerMm, orientation, codeType) {
|
||||
const tree = buildRenderTree(ctx, content, referencePx, codeType);
|
||||
quite fills. */
|
||||
function layoutContent(ctx, content, fixedSize, maxLength, referencePx, pxPerMm, orientation) {
|
||||
const tree = buildRenderTree(ctx, content, referencePx);
|
||||
const alongTape = orientation !== "across";
|
||||
|
||||
const solve = () => {
|
||||
|
|
@ -438,15 +476,15 @@ function layoutContent(ctx, content, fixedSize, maxLength, referencePx, pxPerMm,
|
|||
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. `codeType` (default "qr"), see buildRenderTree/QR_CODE_TYPES. */
|
||||
export function drawLabel(canvas, tape, content, orientation = "along", codeType = "qr") {
|
||||
top-to-bottom order. */
|
||||
export function drawLabel(canvas, tape, content, orientation = "along") {
|
||||
const maxLength = tape.printLengthPx
|
||||
? tape.printLengthPx - tape.leadPx - TRAILING_PADDING_PX
|
||||
: Infinity;
|
||||
const measureCtx = canvas.getContext("2d");
|
||||
const pxPerMm = tape.dpi / 25.4;
|
||||
const {tree, length: contentLength} = layoutContent(
|
||||
measureCtx, content, tape.printAreaPx, maxLength, TEXT_REFERENCE_PX, pxPerMm, orientation, codeType);
|
||||
measureCtx, content, tape.printAreaPx, maxLength, TEXT_REFERENCE_PX, pxPerMm, orientation);
|
||||
|
||||
const printedLength = tape.printLengthPx || Math.ceil(contentLength + tape.leadPx + TRAILING_PADDING_PX);
|
||||
canvas.width = printedLength;
|
||||
|
|
@ -484,13 +522,13 @@ 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. `orientation`/`codeType`, see drawLabel. Returns
|
||||
{textSizesPx}, see drawLabel. */
|
||||
export function drawFallbackLabel(canvas, content, orientation = "along", codeType = "qr") {
|
||||
no real print head here to keep clear of. `orientation`, see drawLabel. Returns {textSizesPx},
|
||||
see drawLabel. */
|
||||
export function drawFallbackLabel(canvas, content, orientation = "along") {
|
||||
const measureCtx = canvas.getContext("2d");
|
||||
const pxPerMm = FALLBACK_DPI / 25.4;
|
||||
const {tree, length: contentLength} = layoutContent(
|
||||
measureCtx, content, FALLBACK_LABEL_HEIGHT_PX, Infinity, TEXT_REFERENCE_PX, pxPerMm, orientation, codeType);
|
||||
measureCtx, content, FALLBACK_LABEL_HEIGHT_PX, Infinity, TEXT_REFERENCE_PX, pxPerMm, orientation);
|
||||
|
||||
canvas.width = Math.ceil(contentLength);
|
||||
canvas.height = FALLBACK_LABEL_HEIGHT_PX;
|
||||
|
|
@ -522,8 +560,15 @@ export function drawFallbackLabel(canvas, content, orientation = "along", codeTy
|
|||
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/${encodeHandleForUrl(user)}/${id}`,
|
||||
// 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.
|
||||
"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.
|
||||
};
|
||||
|
||||
export function buildLabelContent(prefill) {
|
||||
|
|
@ -534,27 +579,42 @@ 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.
|
||||
function splitUserHandle(userHandle) {
|
||||
if (!userHandle) {
|
||||
return null;
|
||||
}
|
||||
const at = userHandle.indexOf("@");
|
||||
return {
|
||||
user: at === -1 ? userHandle : userHandle.slice(0, at),
|
||||
domain: at === -1 ? "" : userHandle.slice(at + 1),
|
||||
};
|
||||
}
|
||||
|
||||
// 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 (itemUrl, itemHandle)
|
||||
// aren't built here; they're calculated live from whatever the base vars currently are (see
|
||||
// label-layouts.js's DERIVED_VARS), 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.
|
||||
// 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.
|
||||
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) {
|
||||
"item": ({userHandle, id}) => {
|
||||
const split = splitUserHandle(userHandle);
|
||||
if (!split || !id) {
|
||||
return {};
|
||||
}
|
||||
const at = user.indexOf("@");
|
||||
return {
|
||||
user: at === -1 ? user : user.slice(0, at),
|
||||
domain: at === -1 ? "" : user.slice(at + 1),
|
||||
itemId: String(id),
|
||||
};
|
||||
return {...split, itemId: String(id)};
|
||||
},
|
||||
"storage-location": ({userHandle, id}) => {
|
||||
const split = splitUserHandle(userHandle);
|
||||
if (!split || !id) {
|
||||
return {};
|
||||
}
|
||||
return {...split, locationId: String(id)};
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue