@7365admin1/layer-common 4.97.1-staging.500 → 4.97.1-staging.502

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,58 +9,118 @@
9
9
  */
10
10
 
11
11
  import {
12
+ CUT_LINE_GAP,
12
13
  DEFAULT_STICKER_DESIGN,
13
14
  QR_MM,
14
- cardSize,
15
15
  isSilhouette,
16
- qrTop,
17
- silhouetteLayout,
16
+ stickerGeometry,
18
17
  stickerLabel,
19
18
  stickerTheme,
20
- type FrameShape,
21
19
  type StickerDesign,
22
20
  } from "./vehicle-qr-design.ts";
23
21
 
24
22
  /** The server's most vehicles per request (POST /api/vehicles/qr/stickers). */
25
23
  export const STICKER_MAX_PER_REQUEST = 500;
26
24
 
27
- /** A4 sheet with the classic cards: 4 columns x 4 rows of 47 mm cells. */
28
- export const STICKER_COLUMNS = 4;
29
- export const STICKER_ROWS = 4;
30
- export const STICKERS_PER_PAGE = STICKER_COLUMNS * STICKER_ROWS;
25
+ /** Modules across the sticker QR: "S365V:" + a 22-character token at ECC Q is always version 3. */
26
+ export const STICKER_QR_MODULES = 29;
27
+
28
+ /** The classic card sheet at Standard size: 4 columns x 4 rows. */
29
+ export const STICKERS_PER_PAGE = 16;
31
30
 
32
31
  /** Printer margin on every side of the A4 sheet, mm. */
33
32
  export const SHEET_MARGIN = 5;
34
33
  const PAGE_W = 210 - SHEET_MARGIN * 2;
35
34
  const PAGE_H = 297 - SHEET_MARGIN * 2;
35
+ /** Strip at the foot of every sheet: the sizes, the print-at-100% reminder and the 10 mm size check. */
36
+ export const INFO_H = 11;
37
+ /** The size-check bar printed on every sheet, mm. */
38
+ export const SIZE_CHECK_MM = 10;
36
39
 
37
40
  /**
38
41
  * How stickers sit on the A4 sheet. A cell = the sticker + the installer strip
39
42
  * under it; `gap` is the empty cutting gap between cells (no ink in it except
40
- * the small + cut marks in its middle).
43
+ * the small + cut marks in its middle). As many as fit above the info strip.
41
44
  */
42
45
  export type SheetLayout = { cols: number; rows: number; cellW: number; cardH: number; stripH: number; gap: number };
43
46
 
44
- export function sheetLayout(frame: FrameShape): SheetLayout {
45
- // Cut-out shapes are 62 mm wide: 3 across. Cards stay 4 x 4 = 16.
46
- return isSilhouette(frame)
47
- ? { cols: 3, rows: 4, cellW: cardSize(frame).w, cardH: cardSize(frame).h, stripH: 10, gap: 4 }
48
- : { cols: STICKER_COLUMNS, rows: STICKER_ROWS, cellW: cardSize(frame).w, cardH: cardSize(frame).h, stripH: 11, gap: 3 };
47
+ const mm = (v: number) => Math.round(v * 100) / 100;
48
+
49
+ export function sheetLayout(design: StickerDesign): SheetLayout {
50
+ const g = stickerGeometry(design, STICKER_QR_MODULES);
51
+ const cellW = mm(g.w * g.scale);
52
+ const cardH = mm(g.h * g.scale);
53
+ const stripH = 8;
54
+ const gap = isSilhouette(design.shape) ? 4 : 3;
55
+ return {
56
+ cols: Math.max(1, Math.floor((PAGE_W + gap) / (cellW + gap))),
57
+ rows: Math.max(1, Math.floor((PAGE_H - INFO_H + gap) / (cardH + stripH + gap))),
58
+ cellW,
59
+ cardH,
60
+ stripH,
61
+ gap,
62
+ };
49
63
  }
50
64
 
51
65
  export const perSheet = (l: SheetLayout) => l.cols * l.rows;
52
66
 
53
67
  /** "12 stickers per A4 sheet with 4 mm cutting gaps" */
54
- export function sheetSummary(frame: FrameShape): string {
55
- const l = sheetLayout(frame);
68
+ export function sheetSummary(design: StickerDesign): string {
69
+ const l = sheetLayout(design);
56
70
  return `${perSheet(l)} stickers per A4 sheet with ${l.gap} mm cutting gaps`;
57
71
  }
58
72
 
73
+ export type StickerSizeInfo = {
74
+ /** The QR code's data area (what a scanner reads), mm square. */
75
+ qrMm: number;
76
+ /** The shape (shaped code) or the card, mm. */
77
+ shapeW: number;
78
+ shapeH: number;
79
+ /** The piece in your hand after cutting (on the grey line when it is shown), mm. */
80
+ stickerW: number;
81
+ stickerH: number;
82
+ perSheet: number;
83
+ gap: number;
84
+ };
85
+
86
+ /** Every size the person printing needs to know, before printing. */
87
+ export function stickerSizeInfo(design: StickerDesign, n = STICKER_QR_MODULES): StickerSizeInfo {
88
+ const g = stickerGeometry(design, n);
89
+ const l = sheetLayout(design);
90
+ const cut = design.cutLines !== false ? 2 * CUT_LINE_GAP : 0;
91
+ const bare = isSilhouette(design.shape) && design.frame === "none";
92
+ return {
93
+ qrMm: mm(g.dataMm),
94
+ shapeW: mm(g.shapeW),
95
+ shapeH: mm(g.shapeH),
96
+ // a bare shape is cut round the shape and its plate tag; anything else round the card
97
+ stickerW: mm((bare ? g.shapeW : g.w * g.scale) + cut),
98
+ stickerH: mm(g.h * g.scale + cut),
99
+ perSheet: perSheet(l),
100
+ gap: l.gap,
101
+ };
102
+ }
103
+
104
+ const n1 = (v: number) => String(Math.round(v * 10) / 10);
105
+
106
+ /** The same size lines in the print preview and at the foot of every sheet. */
107
+ export function stickerSizeLines(design: StickerDesign, n = STICKER_QR_MODULES): string[] {
108
+ const i = stickerSizeInfo(design, n);
109
+ const shaped = isSilhouette(design.shape);
110
+ return [
111
+ `QR code: ${n1(i.qrMm)} x ${n1(i.qrMm)} mm`,
112
+ shaped ? `Shape: ${n1(i.shapeW)} x ${n1(i.shapeH)} mm` : `Card: ${n1(i.shapeW)} x ${n1(i.shapeH)} mm`,
113
+ `Sticker after cutting: ${n1(i.stickerW)} x ${n1(i.stickerH)} mm`,
114
+ `${i.perSheet} per A4 sheet, ${i.gap} mm gaps`,
115
+ `Print at 100% ("Actual size") - do not use "Fit to page"`,
116
+ ];
117
+ }
118
+
59
119
  /** Top-left of the sticker grid inside the printable area, so it sits in the middle of the sheet. */
60
120
  function gridOrigin(l: SheetLayout): { x: number; y: number; w: number; h: number } {
61
121
  const w = l.cols * l.cellW + (l.cols - 1) * l.gap;
62
122
  const h = l.rows * (l.cardH + l.stripH) + (l.rows - 1) * l.gap;
63
- return { x: (PAGE_W - w) / 2, y: (PAGE_H - h) / 2, w, h };
123
+ return { x: mm((PAGE_W - w) / 2), y: mm((PAGE_H - INFO_H - h) / 2), w, h };
64
124
  }
65
125
 
66
126
  /**
@@ -241,18 +301,39 @@ html, body { margin: 0; padding: 0; background: #fff; color: #111; font-family:
241
301
  .hint { font-size: 5.8pt; line-height: 1.2; opacity: 0.85; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; }
242
302
  .brand { display: flex; align-items: center; justify-content: center; gap: 1mm; font-size: 6pt; border-top: 0.2mm solid #ccc; width: 100%; padding-top: 1mm; }
243
303
  .mark { width: 3.2mm; height: 3.2mm; border-radius: 0.8mm; color: #fff; font-weight: 800; font-size: 5pt; line-height: 3.2mm; text-align: center; }
244
- .cut { margin-top: 1.2mm; border-top: 0.3mm dashed #666; padding-top: 1mm; font-size: 6.5pt; line-height: 1.25; color: #111; height: ${l.stripH - 1.2}mm; overflow: hidden; }
304
+ .cut { margin-top: 1.2mm; border-top: 0.3mm dashed #666; padding-top: 0.8mm; font-size: 6.5pt; line-height: 1.2; color: #111; height: ${l.stripH - 1.2}mm; overflow: hidden; }
245
305
  .cut.free { margin-top: 1.6mm; border-top: 0; padding-top: 0; height: ${l.stripH - 1.6}mm; }
246
306
  .cut .owner { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
247
- .cut .note { color: #666; font-size: 5.5pt; }
307
+ .info { position: absolute; left: 0; right: 0; bottom: 0; height: ${INFO_H}mm; border-top: 0.2mm solid #bbb; padding-top: 1mm; font-size: 6.5pt; line-height: 1.3; color: #333; display: flex; gap: 3mm; align-items: flex-start; }
308
+ .info .sizes { flex: 1; }
309
+ .info b { color: #000; }
310
+ .info .check { display: flex; flex-direction: column; align-items: flex-start; white-space: nowrap; }
311
+ .info svg { display: block; margin-top: 0.6mm; }
248
312
  @media screen { body { background: #e9e9e9; zoom: 0.75; padding: 4mm 0; } .page { background: #fff; margin-bottom: 4mm; box-shadow: 0 0 2mm rgba(0,0,0,.25); } }
249
313
  `;
250
314
  }
251
315
 
316
+ /**
317
+ * The foot of every sheet: the sizes, the print-at-100% reminder and a black
318
+ * bar that must measure exactly 10 mm - if it does not, the printer scaled the page.
319
+ */
320
+ function sheetInfoHtml(design: StickerDesign, n: number): string {
321
+ const lines = stickerSizeLines(design, n);
322
+ const bar = SIZE_CHECK_MM;
323
+ const ticks = Array.from({ length: bar + 1 }, (_, i) => `<line x1="${i}" y1="${i % 5 ? 1.2 : 0}" x2="${i}" y2="2.4"/>`).join("");
324
+ return (
325
+ `<div class="info"><div class="sizes"><b>Sizes:</b> ${lines.slice(0, 4).map(escapeHtml).join(" &middot; ")}` +
326
+ `<br><b>${escapeHtml(lines[4])}.</b> Installer: cut out each sticker; the strip under it says which car.</div>` +
327
+ `<div class="check"><b>Size check: this bar must measure ${bar} mm</b>` +
328
+ `<svg xmlns="http://www.w3.org/2000/svg" viewBox="-0.2 0 ${bar + 0.4} 3.4" width="${bar + 0.4}mm" height="3.4mm">` +
329
+ `<rect x="0" y="2.4" width="${bar}" height="1" fill="#000"/><g stroke="#000" stroke-width="0.15">${ticks}</g></svg></div></div>`
330
+ );
331
+ }
332
+
252
333
  /** Plate font size (mm) that fits the cut-out shape's plate box. */
253
- function plateSizeMm(plate: string, boxW: number): number {
334
+ function plateSizeMm(plate: string, boxW: number, max = 4.6): number {
254
335
  // bold capitals are about 0.74 em wide, plus 0.3 mm letter spacing each
255
- return Math.round(Math.min(4.6, (boxW / Math.max(plate.length, 1) - 0.3) / 0.74) * 100) / 100;
336
+ return Math.round(Math.min(max, (boxW / Math.max(plate.length, 1) - 0.3) / 0.74) * 100) / 100;
256
337
  }
257
338
 
258
339
  /**
@@ -263,21 +344,25 @@ function plateSizeMm(plate: string, boxW: number): number {
263
344
  export function buildStickerDocument(
264
345
  items: StickerItem[],
265
346
  faceSvgs: string[],
266
- opts: { design?: StickerDesign; siteName?: string } = {},
347
+ opts: { design?: StickerDesign; siteName?: string; modules?: number } = {},
267
348
  ): string {
268
349
  const design = opts.design ?? DEFAULT_STICKER_DESIGN;
269
- const { ink } = stickerTheme(design.theme);
350
+ const n = opts.modules ?? STICKER_QR_MODULES;
351
+ const { ink, paper } = stickerTheme(design.theme);
270
352
  const label = stickerLabel(design, opts.siteName);
271
- const layout = sheetLayout(design.frame);
272
- const shape = isSilhouette(design.frame) ? silhouetteLayout(design.frame) : null;
273
- const wordsTop = qrTop(design.frame) + QR_MM + 0.8;
353
+ const layout = sheetLayout(design);
354
+ const g = stickerGeometry(design, n);
355
+ const k = g.scale;
356
+ // Shaped codes: plate (and words) in paper colour on the ink tag at the bottom.
357
+ const tag = g.band ? { x: mm(g.band.x * k), y: mm(g.band.y * k), w: mm(g.band.w * k), h: mm(g.band.h * k) } : null;
358
+ const bare = !!tag && design.frame === "none";
359
+ const wordsTop = mm(((g.qrTop ?? 0) + QR_MM + 0.8) * k);
274
360
  const cells = items.map((item, i) => {
275
361
  const { place, owner } = installerLine(item);
276
362
  const plate = escapeHtml(item.plateNumber);
277
- // Cut-out shapes: plate (and words) in the box under the QR; no brand line - there is no room.
278
- const words = shape
279
- ? `<div class="words band" style="left:${shape.band.x}mm;right:auto;top:${shape.band.y}mm;width:${shape.band.w}mm;height:${shape.band.h}mm">` +
280
- `<div class="plate" style="font-size:${plateSizeMm(text(item.plateNumber), shape.band.w)}mm">${plate}</div>` +
363
+ const words = tag
364
+ ? `<div class="words band" style="left:${tag.x}mm;right:auto;top:${tag.y}mm;width:${tag.w}mm;height:${tag.h}mm;color:${paper}">` +
365
+ `<div class="plate" style="font-size:${plateSizeMm(text(item.plateNumber), tag.w, 4.6 * k)}mm">${plate}</div>` +
281
366
  (label ? `<div class="hint">${escapeHtml(label)}</div>` : "") +
282
367
  `</div>`
283
368
  : `<div class="words" style="top:${wordsTop}mm">` +
@@ -290,12 +375,12 @@ export function buildStickerDocument(
290
375
  `<div class="face">${faceSvgs[i] ?? ""}</div>` +
291
376
  words +
292
377
  `</div>` +
293
- `<div class="cut${shape ? " free" : ""}"><div><b>${escapeHtml(place || "-")}</b></div>` +
294
- `<div class="owner">Owner: ${escapeHtml(owner || "-")}</div>` +
295
- `<div class="note">${shape ? "Installer: cut out the sticker, then paste on this car" : "Installer: cut here, then paste on this car"}</div></div></div>`
378
+ `<div class="cut${bare ? " free" : ""}"><div><b>${escapeHtml(place || "-")}</b></div>` +
379
+ `<div class="owner">Owner: ${escapeHtml(owner || "-")}</div></div></div>`
296
380
  );
297
381
  });
298
382
  const marks = cropMarksSvg(layout);
299
- const pages = stickerPages(cells, perSheet(layout)).map((p) => `<div class="page">${marks}${p.join("")}</div>`);
383
+ const info = sheetInfoHtml(design, n);
384
+ const pages = stickerPages(cells, perSheet(layout)).map((p) => `<div class="page">${marks}${p.join("")}${info}</div>`);
300
385
  return `<!doctype html><html><head><meta charset="utf-8"><title>Vehicle QR stickers</title><style>${printCss(layout)}</style></head><body>${pages.join("")}</body></html>`;
301
386
  }