This commit is contained in:
j3d1 2026-08-20 01:54:37 +02:00
parent 67c7415c3b
commit ce1e5f1d62

View file

@ -95,8 +95,33 @@
Connect a printer to preview and print a label. Connect a printer to preview and print a label.
</p> </p>
<template v-else> <template v-else>
<div class="label-preview mb-3" v-show="selectedContent"> <div class="preview-row mb-3" v-show="selectedContent">
<canvas ref="labelCanvas"></canvas> <div class="ruler-v">
<div class="ruler-v-corner"></div>
<div class="ruler ruler-v-ticks"
:style="{height: (tape.printAreaPx * zoom) + 'px'}">
<span v-for="t in verticalRulerTicks" :key="'tick-' + t.mm"
class="tick" :class="{'tick-major': t.major}"
:style="{top: t.pos + 'px'}"></span>
<span v-for="t in verticalRulerTicks.filter(t => t.major)"
:key="'label-' + t.mm" class="tick-label"
:style="{top: t.pos + 'px'}">{{ t.mm }}</span>
</div>
</div>
<div class="preview-scroll">
<div class="ruler ruler-h"
:style="{width: (printedWidthPx * zoom) + 'px'}">
<span v-for="t in horizontalRulerTicks" :key="'tick-' + t.mm"
class="tick" :class="{'tick-major': t.major}"
:style="{left: t.pos + 'px'}"></span>
<span v-for="t in horizontalRulerTicks.filter(t => t.major)"
:key="'label-' + t.mm" class="tick-label"
:style="{left: t.pos + 'px'}">{{ t.mm }}</span>
</div>
<div class="label-preview">
<canvas ref="labelCanvas"></canvas>
</div>
</div>
</div> </div>
<div class="row g-2 align-items-end"> <div class="row g-2 align-items-end">
@ -181,6 +206,9 @@ import {LABEL_TEMPLATES, BASE_VARS, DERIVED_VARS, withDerivedVars, templateConte
const BLOB_URL = "/vendor/libweblabel.js"; const BLOB_URL = "/vendor/libweblabel.js";
const MAX_ZOOM = 4; /* never magnify the preview more than this */ const MAX_ZOOM = 4; /* never magnify the preview more than this */
const MAX_PREVIEW_HEIGHT_PX = 300; /* never let the on-screen preview grow taller than this */
const RULER_TICK_MM = 1; /* finest tick spacing the mm ruler draws */
const RULER_MAJOR_EVERY_MM = 5; /* every Nth tick is taller and labeled with its mm value */
export default { export default {
name: "Print", name: "Print",
@ -210,6 +238,12 @@ export default {
connected: null, connected: null,
tape: null, tape: null,
labelBitmap: null, labelBitmap: null,
// The tape-fed preview's current on-screen scale and printed pixel width (see
// fitZoom/redraw) - tracked reactively, rather than read straight off the canvas
// element, purely so the mm ruler below can recompute its tick positions whenever
// either one changes.
zoom: 1,
printedWidthPx: 0,
// One input per *base* template variable (see label-layouts.js's BASE_VARS) - the // One input per *base* template variable (see label-layouts.js's BASE_VARS) - the
// derived ones (itemUrl, itemHandle) are format strings calculated from these, not // derived ones (itemUrl, itemHandle) are format strings calculated from these, not
@ -286,6 +320,25 @@ export default {
canPrint() { canPrint() {
return Boolean(this.tape && this.labelBitmap && !this.busy); return Boolean(this.tape && this.labelBitmap && !this.busy);
}, },
// On-screen pixels per real millimeter of tape, at the preview's current zoom - what
// turns a physical mm into a tick position the ruler can actually draw. Only meaningful
// for the tape-fed preview (see redraw's printedWidthPx) - the no-webusb fallback preview
// isn't fed from any particular real tape/dpi, so it gets no ruler (see the template).
tapePxPerMm() {
return this.tape ? (this.tape.dpi / 25.4) * this.zoom : 0;
},
// Ticks along the tape's length (the printed bitmap's actual width, lead/trailing feed
// margin included, since that's real physical tape too).
horizontalRulerTicks() {
if (!this.tape || !this.printedWidthPx) {
return [];
}
return this.rulerTicks(this.printedWidthPx / (this.tape.dpi / 25.4));
},
// Ticks across the tape's fixed physical width.
verticalRulerTicks() {
return this.tape ? this.rulerTicks(this.tape.mediaWidthMm) : [];
},
}, },
watch: { watch: {
varValues: { varValues: {
@ -324,6 +377,16 @@ export default {
return v.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/^./, c => c.toUpperCase()); return v.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/^./, c => c.toUpperCase());
}, },
// One tick per RULER_TICK_MM from 0 up to totalMm, each positioned in on-screen pixels
// via tapePxPerMm - shared by the horizontal/vertical ruler computeds above.
rulerTicks(totalMm) {
const ticks = [];
for (let mm = 0; mm <= totalMm; mm += RULER_TICK_MM) {
ticks.push({mm, pos: mm * this.tapePxPerMm, major: mm % RULER_MAJOR_EVERY_MM === 0});
}
return ticks;
},
async guard(fn) { async guard(fn) {
this.error = null; this.error = null;
this.busy = true; this.busy = true;
@ -410,7 +473,8 @@ export default {
const bitmap = canvasToBitmap(canvas); const bitmap = canvasToBitmap(canvas);
bitmapToCanvas(canvas, bitmap); bitmapToCanvas(canvas, bitmap);
this.labelBitmap = bitmap; this.labelBitmap = bitmap;
this.fitZoom(canvas); this.printedWidthPx = canvas.width;
this.zoom = this.fitZoom(canvas);
}, },
redrawFallback() { redrawFallback() {
@ -448,16 +512,20 @@ export default {
/* Fit the preview to its card without ever needing a horizontal /* Fit the preview to its card without ever needing a horizontal
scrollbar for a label this small, magnifying short labels up to scrollbar for a label this small, magnifying short labels up to
MAX_ZOOM rather than showing them at native (tiny) size. */ MAX_ZOOM rather than showing them at native (tiny) size - and never
past MAX_PREVIEW_HEIGHT_PX tall, however long/wide the label itself
runs. Returns the zoom actually used, so callers that care (see
redraw's ruler bookkeeping) don't have to re-derive it. */
fitZoom(canvas) { fitZoom(canvas) {
const available = canvas.parentElement.clientWidth; const available = canvas.parentElement.clientWidth;
if (!(available > 0)) { if (!(available > 0)) {
return; return 1;
} }
const zoom = Math.min(MAX_ZOOM, available / canvas.width); const zoom = Math.min(MAX_ZOOM, available / canvas.width, MAX_PREVIEW_HEIGHT_PX / canvas.height);
canvas.style.width = `${canvas.width * zoom}px`; canvas.style.width = `${canvas.width * zoom}px`;
canvas.style.height = `${canvas.height * zoom}px`; canvas.style.height = `${canvas.height * zoom}px`;
canvas.style.imageRendering = zoom >= 1 ? "pixelated" : "auto"; canvas.style.imageRendering = zoom >= 1 ? "pixelated" : "auto";
return zoom;
}, },
print() { print() {
@ -476,7 +544,7 @@ export default {
for (const entry of entries) { for (const entry of entries) {
const canvas = entry.target.querySelector("canvas"); const canvas = entry.target.querySelector("canvas");
if (canvas) { if (canvas) {
this.fitZoom(canvas); this.zoom = this.fitZoom(canvas);
} }
} }
}, 100); }, 100);
@ -528,6 +596,7 @@ export default {
background: rgba(127, 127, 127, .08); background: rgba(127, 127, 127, .08);
border-radius: .35rem; border-radius: .35rem;
text-align: center; text-align: center;
max-height: 300px;
} }
.label-preview canvas { .label-preview canvas {
@ -536,6 +605,103 @@ export default {
box-shadow: 0 0 0 1px rgba(127, 127, 127, .5); box-shadow: 0 0 0 1px rgba(127, 127, 127, .5);
} }
/* The tape-fed preview's mm ruler (see Print.vue's template/script) - a horizontal track above
the canvas and a vertical one to its left, both ticked in real physical millimeters rather than
preview pixels, since what they're measuring is the actual label. */
.preview-row {
display: flex;
align-items: flex-start;
}
/* The single horizontal scroll region for a label wider than its card - the horizontal ruler and
the canvas are both direct children of this, so they scroll in lockstep with no JS needed to
keep them in sync. */
.preview-scroll {
flex: 1 1 auto;
min-width: 0;
overflow-x: auto;
}
/* Overrides the standalone rule above: nested here, .label-preview must neither clip nor center
its canvas - overflow-x:visible lets the canvas's true width "bubble up" to .preview-scroll's
own scrollbar instead of scrolling twice, and text-align:left keeps the canvas flush with the
ruler's zero tick instead of drifting to the middle of whatever spare width this card has. */
.preview-scroll .label-preview {
overflow-x: visible;
text-align: left;
}
.ruler-v {
flex: none;
width: 2.2rem;
color: rgba(127, 127, 127, .9);
}
/* Matches .ruler-h's own height below - the vertical ruler's ticks start only after this, so tick
0 lines up with the canvas's top edge rather than the horizontal ruler sitting above it. */
.ruler-v-corner {
height: 1.6rem;
}
.ruler-v-ticks {
position: relative;
margin-top: .75rem; /* .label-preview's own padding, so tick 0 meets the canvas, not its box */
}
.ruler-h {
position: relative;
height: 1.6rem;
margin-left: .75rem; /* .label-preview's own padding, so tick 0 meets the canvas, not its box */
color: rgba(127, 127, 127, .9);
}
.ruler .tick {
position: absolute;
background: currentColor;
}
/* Ticks anchor to the edge nearest the canvas (right for the vertical ruler, bottom for the
horizontal one) and grow outward from it, so they read as pointing at the label; the mm labels
sit on the opposite, outer edge, out of the ticks' way. */
.ruler-v-ticks .tick {
right: 0;
width: .4rem;
height: 1px;
}
.ruler-v-ticks .tick-major {
width: .7rem;
}
.ruler-h .tick {
bottom: 0;
width: 1px;
height: .4rem;
}
.ruler-h .tick-major {
height: .7rem;
}
.tick-label {
position: absolute;
font-size: .6rem;
line-height: 1;
white-space: nowrap;
}
/* transform, not a fixed em nudge, so the label's actual center - not its edge - lands on the
tick's mm position (t.pos, set inline), whatever the text's width/height happens to be. */
.ruler-v-ticks .tick-label {
left: 0;
transform: translateY(-50%);
}
.ruler-h .tick-label {
top: 0;
transform: translateX(-50%);
}
.copies-input { .copies-input {
width: 5.5rem; width: 5.5rem;
} }