stash
This commit is contained in:
parent
ed04d98bf1
commit
aa94c92000
10 changed files with 873 additions and 296 deletions
|
|
@ -4,19 +4,51 @@
|
|||
<h5 class="card-title mb-0">Label layout</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3 btn-group" role="group">
|
||||
<input type="radio" class="btn-check" id="layout-filter-all"
|
||||
autocomplete="off" value="all" v-model="activeTag">
|
||||
<label class="btn btn-outline-secondary" for="layout-filter-all">All</label>
|
||||
<template v-for="tag in labelTags" :key="tag">
|
||||
<input type="radio" class="btn-check" :id="'layout-filter-' + tag"
|
||||
autocomplete="off" :value="tag" v-model="activeTag">
|
||||
<label class="btn btn-outline-secondary" :for="'layout-filter-' + tag">
|
||||
{{ tagLabel(tag) }}
|
||||
</label>
|
||||
</template>
|
||||
</div>
|
||||
<div class="template-grid d-flex flex-wrap align-items-start">
|
||||
<div v-for="t in labelTemplates" :key="t.id" class="template-option d-flex flex-column text-center"
|
||||
:class="{'template-option-disabled': !isSelectable(t)}"
|
||||
:title="unavailableReason(t)"
|
||||
role="button" @click="isSelectable(t) && $emit('input', t.id)">
|
||||
<canvas :ref="el => setTemplateCanvasRef(t.id, el)"
|
||||
class="img-thumbnail template-thumb-canvas"
|
||||
:class="{'border-primary': value === t.id}"></canvas>
|
||||
<div class="template-thumb-wrap">
|
||||
<canvas :ref="el => setTemplateCanvasRef(t.id, el)"
|
||||
class="img-thumbnail template-thumb-canvas"
|
||||
:class="{'border-primary': value === t.id}"></canvas>
|
||||
<span v-if="t.id in failed" class="template-thumb-warning" :title="failed[t.id].message">
|
||||
{{ failed[t.id].short }}
|
||||
</span>
|
||||
<span v-else-if="t.id in warned" class="template-thumb-warning template-thumb-warning-soft"
|
||||
:title="warned[t.id].map(w => w.message).join(' ')">
|
||||
{{ warned[t.id].map(w => w.short).join(", ") }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="small"
|
||||
:class="{'fw-bold text-primary': value === t.id}">
|
||||
{{ t.name }}
|
||||
<span v-for="tag in t.tags" :key="tag" class="badge bg-secondary">{{ tagLabel(tag) }}</span>
|
||||
</div>
|
||||
<div class="small text-muted">{{ t.description }}</div>
|
||||
<!-- Debugging aid: every QR-family/text leaf's raw size numbers, shown regardless
|
||||
of whether either tripped a warning above - see label.js's qrInfo/textInfo. -->
|
||||
<div v-if="t.id in qrInfo || t.id in textInfo" class="small text-secondary">
|
||||
<div v-for="(info, i) in qrInfo[t.id]" :key="'qr' + i">
|
||||
mm-per-mod: {{ info.moduleMm.toFixed(2) }}, px-per-mod: {{ info.scale }}
|
||||
</div>
|
||||
<div v-for="(info, i) in textInfo[t.id]" :key="'text' + i">
|
||||
text-mm: {{ info.fontMm.toFixed(2) }}, text-px: {{ info.fontPx.toFixed(1) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -38,6 +70,16 @@
|
|||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.template-option .badge {
|
||||
font-size: .65rem;
|
||||
vertical-align: middle;
|
||||
margin-left: .35rem;
|
||||
}
|
||||
|
||||
.template-thumb-wrap {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.template-thumb-canvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
|
|
@ -47,11 +89,44 @@
|
|||
object-fit: contain;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* Corner badge flagging *why* a layout that's otherwise available (fields filled in) still
|
||||
failed to render - e.g. label.js's "bigger code than the tape allows" or "doesn't fit on this
|
||||
tape" errors (see redraw()'s catch). Shows the short label directly (e.g. "too big") rather than
|
||||
just an icon, so the reason reads at a glance; the full sentence is still the title tooltip. Not
|
||||
shown for the more common "fields not filled in yet" case (see isSelectable/unavailableReason)
|
||||
since that's already conveyed by the greyed-out thumbnail. */
|
||||
.template-thumb-warning {
|
||||
position: absolute;
|
||||
top: .35rem;
|
||||
right: .35rem;
|
||||
max-width: calc(100% - .7rem);
|
||||
padding: .15rem .45rem;
|
||||
border-radius: 1rem;
|
||||
background: rgba(220, 53, 69, .9);
|
||||
color: #fff;
|
||||
font-size: .7rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, .15);
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
/* Soft variant for a template that rendered fine but tripped one of label.js's scan-reliability
|
||||
thresholds (MIN_RECOMMENDED_QR_PX_PER_MODULE/MIN_RECOMMENDED_QR_MODULE_MM) - still selectable,
|
||||
just flagged, so amber rather than the hard-failure badge's red. */
|
||||
.template-thumb-warning-soft {
|
||||
background: rgba(255, 193, 7, .9);
|
||||
color: #000;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import {drawFallbackLabel, preloadQrEncoder} from "@/label.js";
|
||||
import {LABEL_TEMPLATES, templateIsAvailable, templateContent} from "@/label-layouts.js";
|
||||
import {drawLabel, drawFallbackLabel, preloadQrEncoder} from "@/label.js";
|
||||
import {LABEL_TEMPLATES, LABEL_TAGS, templateIsAvailable, templateContent} from "@/label-layouts.js";
|
||||
|
||||
export default {
|
||||
name: "LabelLayoutPreview",
|
||||
|
|
@ -73,6 +148,15 @@ export default {
|
|||
recentTemplateIds: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
// The connected printer's tape/resolution (see label.js's tapeFromStatus), or null when
|
||||
// no printer is connected. When present, redraw() renders every thumbnail with drawLabel
|
||||
// at this tape's real dpi/printArea/length instead of drawFallbackLabel's fixed reference
|
||||
// - so the grid's px/mm-per-module numbers and any length-limit failures reflect what
|
||||
// would actually come out of the connected printer.
|
||||
tape: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
model: {
|
||||
|
|
@ -82,30 +166,67 @@ export default {
|
|||
emits: ["input"],
|
||||
data() {
|
||||
return {
|
||||
// Keyed by template id, holding the error message from its most recent redraw()
|
||||
// failure (e.g. text too long, or too many QR modules for the thumbnail's fixed
|
||||
// reference size - see label.js's snapQrToCrispSize). Absent, not just falsy, for a
|
||||
// template that last drew fine, so `t.id in failed` matches "has a message".
|
||||
// Keyed by template id, holding the {short, message} error from its most recent
|
||||
// redraw() failure (e.g. text too long, or too many QR modules for the thumbnail's
|
||||
// fixed reference size - see label.js's snapQrToCrispSize/encodeQr). Absent, not just
|
||||
// falsy, for a template that last drew fine, so `t.id in failed` matches "has one".
|
||||
failed: {},
|
||||
// Keyed by template id, holding the array of {short, message} scan-reliability
|
||||
// warnings (see label.js's drawFallbackLabel) from its most recent successful
|
||||
// redraw() - present only when that render tripped
|
||||
// MIN_RECOMMENDED_QR_PX_PER_MODULE/MIN_RECOMMENDED_QR_MODULE_MM.
|
||||
warned: {},
|
||||
// Keyed by template id, holding every QR-family leaf's raw {scale, moduleMm} from its
|
||||
// most recent successful redraw(), regardless of whether it warned - a debugging aid
|
||||
// shown unconditionally below each thumbnail.
|
||||
qrInfo: {},
|
||||
// Same idea as qrInfo, but every text leaf's raw {fontPx, fontMm} (see label.js's
|
||||
// checkTextSizes).
|
||||
textInfo: {},
|
||||
// Which LABEL_TAGS entry (or "all") the grid below is narrowed to.
|
||||
activeTag: "all",
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
labelTags() {
|
||||
return LABEL_TAGS;
|
||||
},
|
||||
// LABEL_TEMPLATES with recentTemplateIds' entries pulled to the front (most recent
|
||||
// first), everything else following in its original order.
|
||||
// first, everything else following in its original order), narrowed to activeTag, then
|
||||
// with anything currently in `failed` (not printable - see redraw()'s catch) stably
|
||||
// sorted to the back regardless of recency/tag order, so real errors don't crowd out
|
||||
// layouts that actually work.
|
||||
labelTemplates() {
|
||||
const recent = this.recentTemplateIds
|
||||
.map(id => LABEL_TEMPLATES.find(t => t.id === id))
|
||||
.filter(Boolean);
|
||||
const recentIds = new Set(recent.map(t => t.id));
|
||||
return [...recent, ...LABEL_TEMPLATES.filter(t => !recentIds.has(t.id))];
|
||||
const ordered = [...recent, ...LABEL_TEMPLATES.filter(t => !recentIds.has(t.id))];
|
||||
const filtered = this.activeTag === "all" ? ordered : ordered.filter(t => t.tags?.includes(this.activeTag));
|
||||
return [...filtered].sort((a, b) => (a.id in this.failed ? 1 : 0) - (b.id in this.failed ? 1 : 0));
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
fields() {
|
||||
this.redraw();
|
||||
},
|
||||
// Switching filters mounts fresh canvas elements for thumbnails hidden until now (see
|
||||
// labelTemplates); nextTick waits for those refs to land before drawing into them.
|
||||
activeTag() {
|
||||
this.$nextTick(() => this.redraw());
|
||||
},
|
||||
// Printer connect/disconnect/switch - every thumbnail's canvas already exists regardless
|
||||
// of tape (unlike Print.vue's own tape-fed preview), so no flush:'post' is needed here.
|
||||
tape() {
|
||||
this.redraw();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// "internal" -> "Internal", for both the filter buttons and each thumbnail's badge.
|
||||
tagLabel(tag) {
|
||||
return tag.charAt(0).toUpperCase() + tag.slice(1);
|
||||
},
|
||||
|
||||
setTemplateCanvasRef(id, el) {
|
||||
if (el) {
|
||||
this.templateCanvases[id] = el;
|
||||
|
|
@ -130,12 +251,25 @@ export default {
|
|||
if (!this.isAvailable(t)) {
|
||||
return "Not available - fill in the fields this layout needs above.";
|
||||
}
|
||||
return this.failed[t.id] ?? "";
|
||||
return this.failed[t.id]?.message ?? "";
|
||||
},
|
||||
|
||||
// Live per-template thumbnails. Always uses the content-fit fallback renderer (not the
|
||||
// tape-fed one) regardless of printer connection - illustrative previews via CSS
|
||||
// object-fit, not the to-be-printed-accurate canvas the main preview is.
|
||||
// .template-thumb-canvas's object-fit:contain scales the just-drawn bitmap into its fixed
|
||||
// CSS box (getBoundingClientRect, unlike canvas.width/height, reflects that box - object-fit
|
||||
// doesn't change it); when that's a magnification, switch to nearest-neighbor so the
|
||||
// print-accurate pixel edges stay crisp instead of blurring, same call Print.vue's own
|
||||
// fitZoom makes for its explicitly-sized preview.
|
||||
applyImageRendering(canvas) {
|
||||
const {width: boxWidth, height: boxHeight} = canvas.getBoundingClientRect();
|
||||
const scale = Math.min(boxWidth / canvas.width, boxHeight / canvas.height);
|
||||
canvas.style.imageRendering = scale >= 1 ? "pixelated" : "auto";
|
||||
},
|
||||
|
||||
// Live per-template thumbnails. Renders each with drawLabel at the connected printer's
|
||||
// real tape/resolution when one's connected (see the `tape` prop), so the numbers/errors
|
||||
// shown match what would actually print; falls back to drawFallbackLabel's fixed
|
||||
// reference size otherwise. Either way, still illustrative previews via CSS object-fit,
|
||||
// not the to-be-printed-accurate canvas Print.vue's own main preview is.
|
||||
redraw() {
|
||||
for (const t of LABEL_TEMPLATES) {
|
||||
const canvas = this.templateCanvases[t.id];
|
||||
|
|
@ -147,18 +281,42 @@ export default {
|
|||
canvas.width = 1;
|
||||
canvas.height = 1;
|
||||
delete this.failed[t.id];
|
||||
delete this.warned[t.id];
|
||||
delete this.qrInfo[t.id];
|
||||
delete this.textInfo[t.id];
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
drawFallbackLabel(canvas, content, "along");
|
||||
const {warnings, qrInfo, textInfo} = this.tape
|
||||
? drawLabel(canvas, this.tape, content, "along")
|
||||
: drawFallbackLabel(canvas, content, "along");
|
||||
this.applyImageRendering(canvas);
|
||||
delete this.failed[t.id];
|
||||
if (warnings.length) {
|
||||
this.warned[t.id] = warnings;
|
||||
} else {
|
||||
delete this.warned[t.id];
|
||||
}
|
||||
if (qrInfo.length) {
|
||||
this.qrInfo[t.id] = qrInfo;
|
||||
} else {
|
||||
delete this.qrInfo[t.id];
|
||||
}
|
||||
if (textInfo.length) {
|
||||
this.textInfo[t.id] = textInfo;
|
||||
} else {
|
||||
delete this.textInfo[t.id];
|
||||
}
|
||||
} catch (e) {
|
||||
// Grey the thumbnail out (see isSelectable/unavailableReason) rather than
|
||||
// leaving it blank - selecting a template that can't render here would only
|
||||
// hand Print.vue's own redraw the exact same failure.
|
||||
canvas.width = 1;
|
||||
canvas.height = 1;
|
||||
this.failed[t.id] = e.message;
|
||||
this.failed[t.id] = {short: e.short ?? "error", message: e.message};
|
||||
delete this.warned[t.id];
|
||||
delete this.qrInfo[t.id];
|
||||
delete this.textInfo[t.id];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue