This commit is contained in:
j3d1 2026-08-31 17:01:28 +02:00
parent 15dad8e3be
commit 9c9f0f2d9b
4 changed files with 117 additions and 179 deletions

View file

@ -79,7 +79,7 @@
} }
/* Corner badge flagging *why* a layout failed to render - a capacity error (label.js's "bigger /* Corner badge flagging *why* a layout failed to render - a capacity error (label.js's "bigger
code than the tape allows"/"doesn't fit on this tape") or label-layouts.js's templateContent code than the tape allows"/"doesn't fit on this tape") or label.js's templateContent
throwing "missing data" because a field it needs isn't filled in yet (see redraw()'s catch). throwing "missing data" because a field it needs isn't filled in yet (see redraw()'s catch).
Shows the short label directly (e.g. "too big", "missing data") rather than just an icon, so the Shows the short label directly (e.g. "too big", "missing data") rather than just an icon, so the
reason reads at a glance; the full sentence is still the title tooltip. Layered on top of the reason reads at a glance; the full sentence is still the title tooltip. Layered on top of the
@ -113,8 +113,15 @@
</style> </style>
<script> <script>
import {drawLabel, drawFallbackLabel, preloadQrEncoder, preloadPixelFontRenderer} from "@/label.js"; import {
import {LABEL_TEMPLATES, LABEL_TAGS, templateIsAvailable, templateContent} from "@/label-layouts.js"; drawLabel,
drawFallbackLabel,
preloadQrEncoder,
preloadPixelFontRenderer,
templateIsAvailable,
templateContent
} from "@/label.js";
import {LABEL_TEMPLATES, LABEL_TAGS} from "@/label-layouts.js";
export default { export default {
name: "LabelLayoutPreview", name: "LabelLayoutPreview",

View file

@ -4,148 +4,92 @@ import {encodeHandleForUrl} from "@/router";
// docs/implementation.md#template-layout-tree. // docs/implementation.md#template-layout-tree.
const GAP = {type: "empty", "min-width": "1mm", "min-height": "1mm"}; const GAP = {type: "empty", "min-width": "1mm", "min-height": "1mm"};
// Generated matrix of "just the code" templates covering every QR_LEAF_TYPES combo. See
// docs/implementation.md#generated-qr-only-template-matrix. // QR-types
const QR_ONLY_TEMPLATES = [ // qr-l, qr, qr-q, qr-h, mqr-d, mqr-l, mqr, mqr-q, rmqr, rmqr-min, rmqr-max, rmqr-h, rmqr-h-min, rmqr-h-max
{
id: "qr-l", name: "QR code only (low error correction)",
description: "Just the code, at QR's lowest error-correction level - fits more data (or a "
+ "smaller code) for the same text, but less tolerant of damage.",
},
{id: "qr", name: "QR code only", description: "Just the code - smallest label, prints fastest."},
{
id: "qr-q", name: "QR code only (quartile error correction)",
description: "Just the code, at QR's second-highest (quartile) error-correction level - a "
+ "middle ground between code size and damage tolerance.",
},
{
id: "qr-h", name: "QR code only (high error correction)",
description: "Just the code, at QR's highest error-correction level - still scans if scuffed "
+ "or partly obscured, at the cost of a bigger code for the same text.",
},
{
id: "mqr-d", name: "Micro QR code only (detection only)",
description: "Just the code, in the more compact Micro QR format at its lowest, M1-only level - "
+ "the smallest QR-family code there is, but can only tell a scan is corrupted, not "
+ "recover from it.",
},
{
id: "mqr-l", name: "Micro QR code only (low error correction)",
description: "Just the code, in the more compact Micro QR format at its low error-correction level.",
},
{
id: "mqr", name: "Micro QR code only",
description: "Just the code, in the more compact Micro QR format - smallest label, prints fastest.",
},
{
id: "mqr-q", name: "Micro QR code only (quartile error correction)",
description: "Just the code, in the more compact Micro QR format at its highest (quartile) "
+ "error-correction level.",
},
{
id: "rmqr", name: "rMQR code only",
description: "Just the code, in the rectangular rMQR format, sized for the smallest total area "
+ "that fits the text - smallest label, prints fastest.",
},
{
id: "rmqr-min", name: "rMQR code only (shortest, widest)",
description: "Just the code, in the rectangular rMQR format, preferring the flattest/widest "
+ "symbol that fits the text - shortest across the tape, longest along it.",
},
{
id: "rmqr-max", name: "rMQR code only (tallest, narrowest)",
description: "Just the code, in the rectangular rMQR format, preferring the tallest/narrowest "
+ "symbol that fits the text - tallest across the tape, shortest along it.",
},
{
id: "rmqr-h", name: "rMQR code only (high error correction)",
description: "Just the code, in the rectangular rMQR format at its high error-correction level, "
+ "sized for the smallest total area that fits the text - still scans if scuffed or partly "
+ "obscured, at the cost of a bigger code for the same text.",
},
{
id: "rmqr-h-min", name: "rMQR code only (high error correction, shortest/widest)",
description: "Just the code, in the rectangular rMQR format at its high error-correction level, "
+ "preferring the flattest/widest symbol that fits the text.",
},
{
id: "rmqr-h-max", name: "rMQR code only (high error correction, tallest/narrowest)",
description: "Just the code, in the rectangular rMQR format at its high error-correction level, "
+ "preferring the tallest/narrowest symbol that fits the text.",
},
].map(t => ({...t, required_vars: ["text"], layout: [{type: t.id, content: c => c.text}]}));
export const LABEL_TEMPLATES = [ export const LABEL_TEMPLATES = [
{ {
id: "mqr-token", name: "MQR Token", description: "The code with the encoded text printed next to it.", id: "mqr-token", name: "MQR Token",
description: "Just the code, encoding the compact internal short id - no other text on the label.",
required_vars: ["shortId"], tags: ["internal"], required_vars: ["shortId"], tags: ["internal"],
layout: [{type: "mqr", content: c => c.shortId}] layout: [{type: "mqr", content: c => c.shortId}]
}, },
{ {
id: "qr-url", name: "MQR Token", description: "The code with the encoded text printed next to it.", id: "qr-url", name: "QR Token",
description: "Just the code, encoding the short link - no other text on the label.",
required_vars: ["shortUrl"], tags: ["external"], required_vars: ["shortUrl"], tags: ["external"],
layout: [{type: "qr-h", content: c => c.shortUrl}] layout: [{type: "qr-h", content: c => c.shortUrl}]
}, },
{ {
id: "rmqr-url", name: "rMQR Token", description: "The code with the encoded text printed next to it.", id: "rmqr-url", name: "rMQR Token + Item ID",
description: "The code encoding the short link, with the item id printed alongside for a human to read.",
required_vars: ["shortUrl", "itemId"], tags: ["external"], required_vars: ["shortUrl", "itemId"], tags: ["external"],
layout: [{type: "rmqr", content: c => c.shortUrl}, GAP, { layout: [{type: "rmqr", content: c => c.shortUrl}, {
type: "text", type: "text",
content: c => c.itemId?.toString().padStart(4, "0") content: c => c.itemId?.toString().padStart(4, "0")
}] }]
}, },
{ {
id: "mqr-tokem-id", name: "rMQR Token", description: "The code with the encoded text printed next to it.", id: "rmqr-url-url", name: "rMQR Token + Item ID + URL",
description: "The code encoding the short link, with the item id printed alongside for a human to read.",
required_vars: ["shortUrl", "itemId"], tags: ["external"],
layout: [[[{type: "rmqr", content: c => c.shortUrl}, {
type: "text",
content: c => "I"+c.itemId?.toString().padStart(4, "0")
}],{type: "text", content: c => c.itemUrl}]]
},
{
id: "mqr-token-id", name: "MQR Token + Item ID",
description: "The code encoding the internal short id, with the item id printed alongside for a human to read.",
required_vars: ["shortId", "itemId"], tags: ["internal"], required_vars: ["shortId", "itemId"], tags: ["internal"],
layout: [{type: "mqr", content: c => c.shortId}, GAP, { layout: [{type: "mqr", content: c => c.shortId}, {
type: "text", type: "text",
content: c => c.itemId?.toString().padStart(4, "0") content: c => c.itemId?.toString().padStart(4, "0")
}] }]
}, },
{ {
id: "location-rmqr-url", name: "rMQR Token", description: "The code with the encoded text printed next to it.", id: "location-rmqr-url", name: "rMQR Token + Location ID",
description: "The code encoding the short link, with the storage location id printed alongside for a "
+ "human to read.",
required_vars: ["shortUrl", "locationId"], tags: ["external"], required_vars: ["shortUrl", "locationId"], tags: ["external"],
layout: [{type: "rmqr", content: c => c.shortUrl}, GAP, { layout: [{type: "rmqr", content: c => c.shortUrl}, {
type: "text", type: "text",
content: c => c.locationId?.toString().padStart(4, "0") content: c => c.locationId?.toString().padStart(4, "0")
}] }]
}, },
{ {
id: "location-mqr-tokem-id", id: "location-mqr-token-id",
name: "rMQR Token", name: "MQR Token + Location ID",
description: "The code with the encoded text printed next to it.", description: "The code encoding the internal short id, with the storage location id printed alongside "
+ "for a human to read.",
required_vars: ["shortId", "locationId"], required_vars: ["shortId", "locationId"],
tags: ["internal"], tags: ["internal"],
layout: [{type: "mqr", content: c => c.shortId}, GAP, { layout: [{type: "mqr", content: c => c.shortId}, {
type: "text", type: "text",
content: c => c.locationId?.toString().padStart(4, "0") content: c => c.locationId?.toString().padStart(4, "0")
}] }]
}, ...QR_ONLY_TEMPLATES, },
{ {
id: "qr-text", name: "QR code + text", description: "The code with the encoded text printed next to it.", id: "qr-text", name: "QR code + text", description: "The code with the encoded text printed next to it.",
required_vars: ["text"], required_vars: ["text"],
layout: [{type: "qr", content: c => c.text}, GAP, {type: "text", content: c => c.text?.split("\n")}] layout: [{type: "qr", content: c => c.text}, {type: "text", content: c => c.text?.split("\n")}]
}, },
{ {
id: "qr-text-below", name: "QR code + text below", id: "qr-text-below", name: "QR code + text below",
description: "The code with the encoded text printed below it.", description: "The code with the encoded text printed below it.",
required_vars: ["text"], required_vars: ["text"],
layout: [[{type: "qr", content: c => c.text}, GAP, {type: "text", content: c => c.text?.split("\n")}]] layout: [[{type: "qr", content: c => c.text}, {type: "text", content: c => c.text?.split("\n")}]]
}, },
{ {
id: "id-qr-text-vertical", name: "ID + QR code + text below", id: "id-qr-text-vertical", name: "ID + QR code + text below",
description: "The code with the encoded text printed below it.", description: "The code with the encoded text printed below it.",
required_vars: ["itemId", "text", "userHandle"], required_vars: ["itemId", "text", "userHandle"],
layout: [[{type: "text", content: c => "Item: " + c.itemId}, GAP, { layout: [[{type: "text", content: c => "Item: " + c.itemId}, {
type: "qr", type: "qr",
content: c => c.text content: c => c.text
}, GAP, {type: "text", content: c => c.userHandle}]] }, {type: "text", content: c => c.userHandle}]]
},
{
id: "text", name: "Text only", description: "No code, just the text itself, as large as it fits.",
required_vars: ["text"],
layout: [{type: "text", content: c => c.text?.split("\n")}]
}, },
{ {
id: "item-handle", name: "Item handle", id: "item-handle", name: "Item handle",
@ -159,12 +103,6 @@ export const LABEL_TEMPLATES = [
required_vars: ["locationHandle"], tags: ["internal"], required_vars: ["locationHandle"], tags: ["internal"],
layout: [{type: "text", content: c => c.locationHandle}] layout: [{type: "text", content: c => c.locationHandle}]
}, },
{
id: "item-url", name: "Item URL",
description: "The full item URL as text, with no code - for copying rather than scanning.",
required_vars: ["itemUrl"], tags: ["external"],
layout: [{type: "text", content: c => c.itemUrl}]
},
{ {
id: "owner-handle", name: "Owner handle", description: "Just the owning user's handle, as text only.", id: "owner-handle", name: "Owner handle", description: "Just the owning user's handle, as text only.",
required_vars: ["userHandle"], tags: ["internal"], required_vars: ["userHandle"], tags: ["internal"],
@ -176,11 +114,17 @@ export const LABEL_TEMPLATES = [
layout: [{type: "text", content: c => c.itemId}] layout: [{type: "text", content: c => c.itemId}]
}, },
{ {
id: "owner-id-text", name: "Owner + item ID", id: "owner-id-text-multiline", name: "Owner + item ID (same size)",
description: "The owner's handle and the item id, as two lines of text - no code.", description: "The owner's handle and the item id, as two lines of text - no code.",
required_vars: ["userHandle", "itemId"], tags: ["internal"], required_vars: ["userHandle", "itemId"], tags: ["internal"],
layout: [{type: "text", content: c => [c.userHandle, c.itemId]}] layout: [{type: "text", content: c => [c.userHandle, c.itemId]}]
}, },
{
id: "owner-id-text", name: "Owner + item ID",
description: "The owner's handle and the item id, as two lines of text - no code.",
required_vars: ["userHandle", "itemId"], tags: ["internal"],
layout: [[{type: "text", content: c => [c.userHandle]},{type: "text", content: c => [c.itemId]}]]
},
{ {
id: "location-id", name: "Location ID", description: "Just the bare storage location id, as text only.", id: "location-id", name: "Location ID", description: "Just the bare storage location id, as text only.",
required_vars: ["locationId"], tags: ["internal"], required_vars: ["locationId"], tags: ["internal"],
@ -196,25 +140,25 @@ export const LABEL_TEMPLATES = [
id: "item-url-qr-handle", name: "Item URL + handle", id: "item-url-qr-handle", name: "Item URL + handle",
description: "Scannable item URL, with the item's compact handle printed alongside.", description: "Scannable item URL, with the item's compact handle printed alongside.",
required_vars: ["itemUrl", "itemHandle"], tags: ["external"], required_vars: ["itemUrl", "itemHandle"], tags: ["external"],
layout: [{type: "qr", content: c => c.itemUrl}, GAP, {type: "text", content: c => c.itemHandle}] layout: [{type: "qr", content: c => c.itemUrl}, {type: "text", content: c => c.itemHandle}]
}, },
{ {
id: "item-url-qr-owner", name: "Item URL + owner", id: "item-url-qr-owner", name: "Item URL + owner",
description: "Scannable item URL, with the owner's handle printed alongside.", description: "Scannable item URL, with the owner's handle printed alongside.",
required_vars: ["itemUrl", "userHandle"], tags: ["external"], required_vars: ["itemUrl", "userHandle"], tags: ["external"],
layout: [{type: "qr", content: c => c.itemUrl}, GAP, {type: "text", content: c => c.userHandle}] layout: [{type: "qr", content: c => c.itemUrl}, {type: "text", content: c => c.userHandle}]
}, },
{ {
id: "item-url-qr-id", name: "Item URL + item ID", id: "item-url-qr-id", name: "Item URL + item ID",
description: "Scannable item URL, with the bare item id printed alongside.", description: "Scannable item URL, with the bare item id printed alongside.",
required_vars: ["itemUrl", "itemId"], tags: ["external"], required_vars: ["itemUrl", "itemId"], tags: ["external"],
layout: [{type: "qr", content: c => c.itemUrl}, GAP, {type: "text", content: c => c.itemId}] layout: [{type: "qr", content: c => c.itemUrl}, {type: "text", content: c => c.itemId}]
}, },
{ {
id: "item-url-qr-owner-id", name: "Item URL + owner + ID", id: "item-url-qr-owner-id", name: "Item URL + owner + ID",
description: "Scannable item URL, with the owner's handle and the item id on two lines alongside.", description: "Scannable item URL, with the owner's handle and the item id on two lines alongside.",
required_vars: ["itemUrl", "userHandle", "itemId"], tags: ["external"], required_vars: ["itemUrl", "userHandle", "itemId"], tags: ["external"],
layout: [{type: "qr", content: c => c.itemUrl}, GAP, {type: "text", content: c => [c.userHandle, c.itemId]}] layout: [{type: "qr", content: c => c.itemUrl}, {type: "text", content: c => [c.userHandle, c.itemId]}]
}, },
{ {
id: "short-url-qr", name: "Short link (QR code)", id: "short-url-qr", name: "Short link (QR code)",
@ -227,40 +171,26 @@ export const LABEL_TEMPLATES = [
id: "item-url-qr-owner-id2", name: "Item URL + owner + ID", id: "item-url-qr-owner-id2", name: "Item URL + owner + ID",
description: "Scannable item URL, with the owner's handle and the item id on two lines alongside.", description: "Scannable item URL, with the owner's handle and the item id on two lines alongside.",
required_vars: ["itemUrl", "userHandle", "itemId"], tags: ["external"], required_vars: ["itemUrl", "userHandle", "itemId"], tags: ["external"],
layout: [{type: "qr", content: c => c.itemUrl}, GAP, [{ layout: [{type: "qr", content: c => c.itemUrl}, [{
type: "text", type: "text",
content: c => c.userHandle content: c => c.userHandle
}, GAP, {type: "text", content: c => c.itemId}]] }, {type: "text", content: c => c.itemId}]]
}, },
]; ];
// Every distinct tag value used across LABEL_TEMPLATES' tags, in first-seen order - drives
// LabelLayoutPreview.vue's filter buttons without hand-listing "internal"/"external" there.
export const LABEL_TAGS = [...new Set(LABEL_TEMPLATES.flatMap(t => t.tags ?? []))]; export const LABEL_TAGS = [...new Set(LABEL_TEMPLATES.flatMap(t => t.tags ?? []))];
// Every field name any template's required_vars names, in first-seen order.
export const KNOWN_VARS = [...new Set(LABEL_TEMPLATES.flatMap(t => t.required_vars))]; export const KNOWN_VARS = [...new Set(LABEL_TEMPLATES.flatMap(t => t.required_vars))];
// DERIVED_VARS shape and declaration-order invariant. See
// docs/implementation.md#derived-vars-shape-and-ordering.
export const DERIVED_VARS = { export const DERIVED_VARS = {
// Full user@domain handle, kept as separate user/domain base vars since that's how the
// account is actually shaped (see federation.md's Unique Handles); this is just the
// display/URL form.
userHandle: { userHandle: {
inputs: ["user", "domain"], inputs: ["user", "domain"],
calc: (f) => `${f.user}@${f.domain}`, calc: (f) => `${f.user}@${f.domain}`,
}, },
// Self-contained Item URL (see docs/design-in-progress/items-labels.md) - what a printed
// label encodes, since scanning it must resolve everything with no other context. `webdomain`
// defaults to this origin but is editable, since any frontend can resolve any handle.
itemUrl: { itemUrl: {
inputs: ["webdomain", "userHandle", "itemId"], inputs: ["webdomain", "userHandle", "itemId"],
calc: (f) => `${f.webdomain}/i/${encodeHandleForUrl(f.userHandle)}/${f.itemId}`, calc: (f) => `${f.webdomain}/i/${encodeHandleForUrl(f.userHandle)}/${f.itemId}`,
}, },
// Compact User-Qualified ID (see docs/handles-and-shortids.md#user-qualified-id) - owner
// handle + a one-letter kind tag + local id, meaningful only where context already makes
// clear it's Toolshed data, unlike itemUrl. `i` = item, `s` = storage location.
itemHandle: { itemHandle: {
inputs: ["userHandle", "itemId"], inputs: ["userHandle", "itemId"],
calc: (f) => `${f.userHandle}:i${f.itemId}`, calc: (f) => `${f.userHandle}:i${f.itemId}`,
@ -271,60 +201,7 @@ export const DERIVED_VARS = {
}, },
}; };
// Text-input vars = template KNOWN_VARS minus derived ones, plus any var a DERIVED_VARS calc
// needs (e.g. "webdomain") even if no template names it directly.
export const BASE_VARS = [...new Set([ export const BASE_VARS = [...new Set([
...KNOWN_VARS.filter(v => !(v in DERIVED_VARS)), ...KNOWN_VARS.filter(v => !(v in DERIVED_VARS)),
...Object.values(DERIVED_VARS).flatMap(d => d.inputs).filter(v => !(v in DERIVED_VARS)), ...Object.values(DERIVED_VARS).flatMap(d => d.inputs).filter(v => !(v in DERIVED_VARS)),
])]; ])];
// Runs each DERIVED_VARS calc against `fields`, adding the result wherever its inputs are
// present, so callers never need to know DERIVED_VARS' {inputs, calc} shape.
export function withDerivedVars(fields) {
const result = {...fields};
for (const [name, {inputs, calc}] of Object.entries(DERIVED_VARS)) {
if (inputs.every(v => result[v])) {
result[name] = calc(result);
}
}
return result;
}
function walkLeaves(node, fn) {
if (Array.isArray(node)) {
node.forEach(child => walkLeaves(child, fn));
} else {
fn(node);
}
}
function mapTree(node, fn) {
return Array.isArray(node) ? node.map(child => mapTree(child, fn)) : fn(node);
}
// Resolved only if every part is - a single string for a QR-family/"text" leaf, every line for a
// multi-line "text" (see "owner-id-text" and "item-url-qr-owner-id" above).
function isResolved(value) {
return Array.isArray(value) ? value.every(isResolved) : value !== undefined && value !== null;
}
// Shared by LabelLayoutPreview.vue's grid and Print.vue's preview, so neither reimplements
// leaf-walking/field-resolving itself.
export function templateIsAvailable(t, fields) {
let available = true;
walkLeaves(t.layout, leaf => {
if (leaf.type !== "empty" && !isResolved(leaf.content(fields))) {
available = false;
}
});
return available;
}
export function templateContent(t, fields) {
if (!templateIsAvailable(t, fields)) {
const err = new Error("This layout needs more fields filled in before it can be drawn.");
err.short = "missing data";
throw err;
}
return mapTree(t.layout, leaf => leaf.type === "empty" ? leaf : {...leaf, value: leaf.content(fields)});
}

View file

@ -1,6 +1,7 @@
import {loadAnyDCode} from "../vendor/anyd-qr.js"; import {loadAnyDCode} from "../vendor/anyd-qr.js";
import {encodeHandleForUrl} from "@/router" import {encodeHandleForUrl} from "@/router"
import {drawPixelText, preloadPixelFontRenderer} from "@/pixel-font.js"; import {drawPixelText, preloadPixelFontRenderer} from "@/pixel-font.js";
import {DERIVED_VARS} from "@/label-layouts.js";
// Re-exported so every caller can preload both the QR encoder and the bitmap-font rasterizer // Re-exported so every caller can preload both the QR encoder and the bitmap-font rasterizer
// (see fontTierFor's `pixel` tiers below) the same way, alongside this file's own preloadQrEncoder. // (see fontTierFor's `pixel` tiers below) the same way, alongside this file's own preloadQrEncoder.
@ -349,7 +350,7 @@ function measureTextBlock(ctx, lines, referencePx) {
return {width, height}; return {width, height};
} }
// Converts a resolved content tree (see label-layouts.js's resolveContent) into one ready for layout. See // Converts a resolved content tree (see templateContent below) into one ready for layout. See
// docs/implementation.md#render-tree-construction. // docs/implementation.md#render-tree-construction.
function buildRenderTree(ctx, node, referencePx) { function buildRenderTree(ctx, node, referencePx) {
if (isSplit(node)) { if (isSplit(node)) {
@ -483,7 +484,7 @@ function layoutContent(ctx, content, fixedSize, maxLength, referencePx, pxPerMm,
return {tree, length, warnings, qrInfo, textInfo}; return {tree, length, warnings, qrInfo, textInfo};
} }
// The tape-fed layout: draws a fully resolved content tree (see label-layouts.js's resolveContent) at the tape's // The tape-fed layout: draws a fully resolved content tree (see templateContent below) at the tape's
// real pixel dimensions. See docs/implementation.md#tape-fed-label-drawing. Returns // 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, // {textSizesPx}: each "text" leaf's effective font size, in the tree's own left-to-right,
// top-to-bottom order; {warnings}: {short, message} scan-reliability entries from // top-to-bottom order; {warnings}: {short, message} scan-reliability entries from
@ -627,3 +628,54 @@ export function buildLabelFields(prefill) {
const build = LABEL_FIELD_BUILDERS[prefill.kind]; const build = LABEL_FIELD_BUILDERS[prefill.kind];
return build ? build(prefill.components) : {}; return build ? build(prefill.components) : {};
} }
// Runs each DERIVED_VARS calc against `fields`, adding the result wherever its inputs are
// present, so callers never need to know DERIVED_VARS' {inputs, calc} shape.
export function withDerivedVars(fields) {
const result = {...fields};
for (const [name, {inputs, calc}] of Object.entries(DERIVED_VARS)) {
if (inputs.every(v => result[v])) {
result[name] = calc(result);
}
}
return result;
}
function walkLeaves(node, fn) {
if (Array.isArray(node)) {
node.forEach(child => walkLeaves(child, fn));
} else {
fn(node);
}
}
function mapTree(node, fn) {
return Array.isArray(node) ? node.map(child => mapTree(child, fn)) : fn(node);
}
// Resolved only if every part is - a single string for a QR-family/"text" leaf, every line for a
// multi-line "text" (see label-layouts.js's "owner-id-text" and "item-url-qr-owner-id" templates).
function isResolved(value) {
return Array.isArray(value) ? value.every(isResolved) : value !== undefined && value !== null;
}
// Shared by LabelLayoutPreview.vue's grid and Print.vue's preview, so neither reimplements
// leaf-walking/field-resolving itself.
export function templateIsAvailable(t, fields) {
let available = true;
walkLeaves(t.layout, leaf => {
if (leaf.type !== "empty" && !isResolved(leaf.content(fields))) {
available = false;
}
});
return available;
}
export function templateContent(t, fields) {
if (!templateIsAvailable(t, fields)) {
const err = new Error("This layout needs more fields filled in before it can be drawn.");
err.short = "missing data";
throw err;
}
return mapTree(t.layout, leaf => leaf.type === "empty" ? leaf : {...leaf, value: leaf.content(fields)});
}

View file

@ -197,9 +197,11 @@ import {
buildLabelContent, buildLabelContent,
buildLabelFields, buildLabelFields,
preloadQrEncoder, preloadQrEncoder,
preloadPixelFontRenderer preloadPixelFontRenderer,
withDerivedVars,
templateContent
} from "@/label.js"; } from "@/label.js";
import {LABEL_TEMPLATES, BASE_VARS, DERIVED_VARS, withDerivedVars, templateContent} from "@/label-layouts.js"; import {LABEL_TEMPLATES, BASE_VARS, DERIVED_VARS} from "@/label-layouts.js";
import {shortenedRoute} from "@/router"; import {shortenedRoute} from "@/router";
// Print.vue-local calculated fields on top of label-layouts.js's DERIVED_VARS. See // Print.vue-local calculated fields on top of label-layouts.js's DERIVED_VARS. See