This commit is contained in:
j3d1 2026-08-20 03:41:09 +02:00
parent ce1e5f1d62
commit c345372382
9 changed files with 233 additions and 82 deletions

View file

@ -20,9 +20,9 @@
</div>
<div class="card-body">
<div class="row g-3">
<div :class="v === 'value' ? 'col-12' : 'col-md-6'" v-for="v in baseVars" :key="v">
<div :class="v === 'text' ? 'col-12' : 'col-md-6'" v-for="v in baseVars" :key="v">
<label class="form-label">{{ varLabel(v) }}</label>
<textarea v-if="v === 'value'" class="form-control" rows="3"
<textarea v-if="v === 'text'" class="form-control" rows="3"
v-model="varValues[v]"
placeholder="https://example.com/…"></textarea>
<input v-else type="text" class="form-control" v-model="varValues[v]">
@ -88,7 +88,7 @@
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">Label</h5>
<small v-if="tape" class="text-muted">{{ tape.mediaWidthMm }} mm tape</small>
<small v-if="tape" class="text-muted">{{ tape.mediaWidthMm }} mm tape{{ textSizesSummary }}</small>
</div>
<div class="card-body">
<p v-if="!tape" class="text-muted">
@ -99,7 +99,7 @@
<div class="ruler-v">
<div class="ruler-v-corner"></div>
<div class="ruler ruler-v-ticks"
:style="{height: (tape.printAreaPx * zoom) + 'px'}">
:style="{height: (tape.mediaWidthMm * tapePxPerMm) + 'px'}">
<span v-for="t in verticalRulerTicks" :key="'tick-' + t.mm"
class="tick" :class="{'tick-major': t.major}"
:style="{top: t.pos + 'px'}"></span>
@ -108,7 +108,7 @@
:style="{top: t.pos + 'px'}">{{ t.mm }}</span>
</div>
</div>
<div class="preview-scroll">
<div class="preview-track">
<div class="ruler ruler-h"
:style="{width: (printedWidthPx * zoom) + 'px'}">
<span v-for="t in horizontalRulerTicks" :key="'tick-' + t.mm"
@ -118,8 +118,15 @@
:key="'label-' + t.mm" class="tick-label"
:style="{left: t.pos + 'px'}">{{ t.mm }}</span>
</div>
<div class="label-preview">
<canvas ref="labelCanvas"></canvas>
<!-- The tape's full physical width, printable area included - the
print head can't mark all the way to the tape's outer edges, so
the canvas (printAreaPx tall) is narrower than this and centered
within it; the rest is real, if unprintable, tape margin. -->
<div class="tape-full"
:style="{height: (tape.mediaWidthMm * tapePxPerMm) + 'px'}">
<div class="label-preview">
<canvas ref="labelCanvas"></canvas>
</div>
</div>
</div>
</div>
@ -207,8 +214,17 @@ const BLOB_URL = "/vendor/libweblabel.js";
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 */
// How far apart plain and labeled/major ticks sit, both coarser the longer the ruler itself runs
// - tightly spaced ticks (and their labels) get too cramped to read/render once there are enough
// of them. Ordered smallest threshold first; rulerTicks below uses the last entry whose `aboveMm`
// the ruler's own length clears, so add a finer/coarser tier here rather than growing a pile of
// separate constants. Every tier's majorEveryMm is a multiple of its own tickMm, so major ticks
// always land on a tick that's actually drawn.
const RULER_TIERS = [
{aboveMm: 0, tickMm: 1, majorEveryMm: 5},
{aboveMm: 100, tickMm: 1, majorEveryMm: 10},
{aboveMm: 500, tickMm: 5, majorEveryMm: 25},
];
export default {
name: "Print",
@ -244,17 +260,21 @@ export default {
// either one changes.
zoom: 1,
printedWidthPx: 0,
// Each "text" leaf's effective font size in the current render (see label.js's
// drawLabel) - shown alongside the tape width so a field rendering blank (too small
// even for the smallest pixel font) shows up as a suspiciously tiny number here rather
// than just silently not being there.
textSizesPx: [],
// 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
// typed directly, so they're only ever shown (see the `fields` computed below), never
// stored here. Prefilled from the ?kind=& query params where
// derived ones (userHandle, itemUrl, itemHandle) are format strings calculated from
// these, not typed directly, so they're only ever shown (see the `fields` computed
// below), never stored here. Prefilled from the ?kind=& query params where
// buildLabelContent/buildLabelFields have a value for them, editable from there so a
// template needing e.g. userHandle isn't stuck depending on a prefill that never
// arrives.
// template needing e.g. domain isn't stuck depending on a prefill that never arrives.
varValues: {
...Object.fromEntries(BASE_VARS.map(v => [v, ""])),
value: buildLabelContent(this.prefill),
text: buildLabelContent(this.prefill),
// Defaults to wherever this page itself is being served from - editable since any
// frontend can resolve any handle (see label-layouts.js's DERIVED_VARS.itemUrl),
// so a label doesn't have to point back at this particular one.
@ -327,17 +347,46 @@ export default {
tapePxPerMm() {
return this.tape ? (this.tape.dpi / 25.4) * this.zoom : 0;
},
// The physical length, in mm, each ruler axis actually needs to cover - see
// horizontalRulerTicks/verticalRulerTicks below for what each one measures and why.
horizontalTotalMm() {
return (this.tape && this.printedWidthPx) ? this.printedWidthPx / (this.tape.dpi / 25.4) : 0;
},
verticalTotalMm() {
return this.tape ? this.tape.mediaWidthMm : 0;
},
// The single RULER_TIERS entry both rulers draw from, keyed off whichever axis is
// physically longer - so a long label's ruler doesn't end up coarser (or finer) than the
// tape-width ruler right next to it just because the other axis happens to be shorter.
rulerTier() {
return RULER_TIERS.filter(t => Math.max(this.horizontalTotalMm, this.verticalTotalMm) >= t.aboveMm)
.at(-1);
},
// 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));
return this.rulerTicks(this.horizontalTotalMm);
},
// Ticks across the tape's fixed physical width.
// Ticks across the tape's full physical width, mediaWidthMm - not printAreaPx/dpi: a
// print head can't reach the tape's outer edges, so the printable area (see .tape-full in
// the template) is genuinely narrower than the tape itself, by an amount that isn't a
// fixed/predictable fraction of it. The ruler still has to show the *whole* tape - its
// container is sized from mediaWidthMm too (see the template's inline height) precisely so
// these ticks can't run past it, the way they did when both were sized from printAreaPx.
verticalRulerTicks() {
return this.tape ? this.rulerTicks(this.tape.mediaWidthMm) : [];
return this.tape ? this.rulerTicks(this.verticalTotalMm) : [];
},
// "(5px, 23px)" for the current render's text leaves (see data's textSizesPx), or "" once
// there's nothing to show - appended straight onto the tape-width <small>, so the blank
// string here just means that text is left with no trailing space.
textSizesSummary() {
if (!this.textSizesPx.length) {
return "";
}
return ` (${this.textSizesPx.map(px => Math.round(px) + "px").join(", ")})`;
},
},
watch: {
@ -377,12 +426,15 @@ export default {
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.
// Ticks from 0 up to totalMm, each positioned in on-screen pixels via tapePxPerMm - shared
// by the horizontal/vertical ruler computeds above. Both the plain tick spacing and the
// labeled/major one come from the shared rulerTier (see above), not from this totalMm, so
// both rulers always coarsen together once *either* axis is long enough to need it.
rulerTicks(totalMm) {
const {tickMm, majorEveryMm} = this.rulerTier;
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});
for (let mm = 0; mm <= totalMm; mm += tickMm) {
ticks.push({mm, pos: mm * this.tapePxPerMm, major: mm % majorEveryMm === 0});
}
return ticks;
},
@ -463,13 +515,15 @@ export default {
return;
}
this.resizeObserver.observe(canvas.parentElement);
let textSizesPx;
try {
drawLabel(canvas, this.tape, content);
({textSizesPx} = drawLabel(canvas, this.tape, content));
} catch (e) {
this.error = e.message;
return;
}
this.error = null;
this.textSizesPx = textSizesPx;
const bitmap = canvasToBitmap(canvas);
bitmapToCanvas(canvas, bitmap);
this.labelBitmap = bitmap;
@ -593,7 +647,7 @@ export default {
.label-preview {
overflow-x: auto;
padding: .75rem;
background: rgba(127, 127, 127, .08);
//background: rgba(127, 127, 127, .08);
border-radius: .35rem;
text-align: center;
max-height: 300px;
@ -602,7 +656,7 @@ export default {
.label-preview canvas {
display: inline-block;
background: #fff;
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
@ -613,22 +667,38 @@ export default {
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 {
/* Holds the horizontal ruler and the canvas - deliberately never scrollable (no overflow-x:auto):
fitZoom's zoom always satisfies `canvas.width * zoom <= available`, so the canvas can never
actually be wider than this has room for, and a scrollbar here would let the ruler and canvas
drift apart (or just look broken) for no reason. min-width:0 only lets this flex item shrink
to the card's real available width - it doesn't enable scrolling. */
.preview-track {
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 {
/* Overrides the standalone rule above: nested here, .label-preview must neither scroll nor center
its canvas - overflow-x:visible (never auto) rules out a second, inner scrollbar, 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. padding:0 so the canvas's own edges are exactly
this box's edges too - the ruler's ticks (see .ruler-h/.ruler-v-ticks below) line up with those
same edges, so any padding here would leave the ticks and the actual canvas misaligned. */
.preview-track .label-preview {
overflow-x: visible;
text-align: left;
padding: 0;
}
/* The tape's full physical width (see the template) - a print head can't mark all the way to a
tape's outer edges, so .label-preview/the canvas is narrower than this and centered within it
(the print area sits centered on the tape, with equal margin on both sides); a faint tint
distinguishes the margin as real (if blank, unprintable) tape rather than empty space. */
.tape-full {
display: flex;
flex-direction: column;
justify-content: center;
background: rgba(127, 127, 127, .08);
border-radius: .35rem;
}
.ruler-v {
@ -645,13 +715,11 @@ export default {
.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);
}