From 29654b39f211d0c4df910cb49ca2875444265a07 Mon Sep 17 00:00:00 2001 From: u8array Date: Sun, 24 May 2026 10:24:17 +0200 Subject: [PATCH] feat(barcode): ^B4 Code 49 (stacked 1D) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the ^B4 Code 49 stacked-1D symbology — registry leaf with custom `mode` prop ('A' | '0'..'5'), parser handler, bwip-js mapping, getDisplaySize case, command-support flag, 32 locale entries. bwip-js `mode` accepts numbers 0-5 only; ZPL m='A' (auto) is expressed by omitting the option so bwip's internal auto-subset picker runs, matching firmware behaviour. Garbage values in `mode` (parser-rejected on entry, possible via hand-edited JSON) fall back to auto-mode rather than throwing. bwip-js rejects code49 `rowheight` outside 8..50 modules, so the valid ZPL h range is [8·moduleWidth, 50·moduleWidth] dots. Enforced at four layers, each catching a distinct caller: - UI: NumberInput min/max blocks invalid typing (dynamic on moduleWidth) - normalize: moduleWidth change re-clamps height across fields (guards non-finite / non-positive moduleWidth so programmatic paths don't anchor on garbage) - commit: Konva transformer drag past the range pins height to the limit before it lands in props - render: bwipHelpers clamps as a defensive net for hand-edited JSON loads Without the cross-layer enforcement, canvas silently falls back to a valid rowheight while props (and emitted ZPL) carry the invalid value, producing design-vs-print drift on resize or moduleWidth change. Labelary caveat: Labelary's docs list ^B4 as supported, but the emulator renders the HRI text only and omits the bars (verified May 2026, same situation as ^BB CODABLOCK F). bwip-js is the authoritative render; no visualRegression / labelarySync fixtures. Tests: 10 added — 3 round-trip (parser/generator/mode handling), 7 clamp-contract (normalize × 5, commit × 2). --- docs/zpl-roadmap.md | 2 +- src/components/Canvas/KonvaObject.tsx | 1 + src/components/Canvas/bwipHelpers.ts | 45 ++++++++ src/lib/zplCommandSupport.ts | 2 +- src/lib/zplGenerator.test.ts | 43 ++++++++ src/lib/zplParser.ts | 32 ++++++ src/locales/ar.ts | 9 ++ src/locales/bg.ts | 9 ++ src/locales/cs.ts | 9 ++ src/locales/da.ts | 9 ++ src/locales/de.ts | 9 ++ src/locales/el.ts | 9 ++ src/locales/en.ts | 9 ++ src/locales/es.ts | 9 ++ src/locales/et.ts | 9 ++ src/locales/fa.ts | 9 ++ src/locales/fi.ts | 9 ++ src/locales/fr.ts | 9 ++ src/locales/he.ts | 9 ++ src/locales/hr.ts | 9 ++ src/locales/hu.ts | 9 ++ src/locales/it.ts | 9 ++ src/locales/ja.ts | 9 ++ src/locales/ko.ts | 9 ++ src/locales/lt.ts | 9 ++ src/locales/lv.ts | 9 ++ src/locales/nl.ts | 9 ++ src/locales/no.ts | 9 ++ src/locales/pl.ts | 9 ++ src/locales/pt.ts | 9 ++ src/locales/ro.ts | 9 ++ src/locales/sk.ts | 9 ++ src/locales/sl.ts | 9 ++ src/locales/sr.ts | 9 ++ src/locales/sv.ts | 9 ++ src/locales/tr.ts | 9 ++ src/locales/zh-hans.ts | 9 ++ src/locales/zh-hant.ts | 9 ++ src/registry/code49.test.ts | 102 ++++++++++++++++++ src/registry/code49.tsx | 149 ++++++++++++++++++++++++++ src/registry/index.ts | 8 +- 41 files changed, 668 insertions(+), 4 deletions(-) create mode 100644 src/registry/code49.test.ts create mode 100644 src/registry/code49.tsx diff --git a/docs/zpl-roadmap.md b/docs/zpl-roadmap.md index 7213a9d..8d7dae8 100644 --- a/docs/zpl-roadmap.md +++ b/docs/zpl-roadmap.md @@ -70,6 +70,7 @@ What's supported, what's next, what's planned. - [x] `^B5` — Planet Code - [x] `^BZ` — POSTNET - [x] `^BS` — UPC/EAN 2- or 5-digit supplement +- [x] `^B4` — Code 49 - [x] `^BQ` — QR Code - [x] `^BX` — DataMatrix - [x] `^B7` — PDF417 @@ -112,7 +113,6 @@ What's supported, what's next, what's planned. ### Barcodes -- [ ] `^B4` — Code 49 - [ ] `^BD` — UPS MaxiCode - [ ] `^BT` — TLC39 diff --git a/src/components/Canvas/KonvaObject.tsx b/src/components/Canvas/KonvaObject.tsx index 2f726be..f7ff624 100644 --- a/src/components/Canvas/KonvaObject.tsx +++ b/src/components/Canvas/KonvaObject.tsx @@ -106,6 +106,7 @@ const BARCODE_TYPES = new Set([ "micropdf417", "codablock", "upcEanExtension", + "code49", ]); export function KonvaObject(props_: Props) { diff --git a/src/components/Canvas/bwipHelpers.ts b/src/components/Canvas/bwipHelpers.ts index 847800b..48e5e95 100644 --- a/src/components/Canvas/bwipHelpers.ts +++ b/src/components/Canvas/bwipHelpers.ts @@ -97,6 +97,7 @@ const BCID: Partial> = { // Placeholder — actual bcid (ean2 vs ean5) is resolved from the // content length in the per-type switch in buildBwipOptions. upcEanExtension: "ean5", + code49: "code49", }; export const BWIP_SCALE = 2; @@ -317,6 +318,29 @@ export function buildBwipOptions( opts = { bcid, text, scale, height: 10 }; break; } + case "code49": { + const p = obj.props; + const scale = bwipScale1D(p.moduleWidth, renderScale, renderDpmm); + // Stacked 1D, bwip auto-picks row count. Clamp rowheight to + // bwip's 8..50 range — defensive net for JSON loads that + // bypass the registry's commitTransform/normalize clamps. + const rawRow = Math.round(p.height / Math.max(p.moduleWidth, 1)); + const rowheight = Math.min(50, Math.max(8, rawRow)); + opts = { + bcid, + text: p.content || "0", + scale, + rowheight, + }; + // bwip's mode is numeric 0-5; 'A' (auto) is the no-option case. + if (p.mode !== "A") { + const m = parseInt(p.mode, 10); + if (Number.isInteger(m) && m >= 0 && m <= 5) { + (opts as Record).mode = m; + } + } + break; + } case "upcEanExtension": { const p = obj.props; const scale = bwipScale1D(p.moduleWidth, renderScale, renderDpmm); @@ -858,6 +882,27 @@ function getUprightDisplaySize( const h = dotsToPx(obj.props.height + LOGMARS_TEXT_ZONE_DOTS, scale, dpmm); return { w, h }; } + case "code49": { + // Stacked 1D. Labelary's emulator only renders the HRI line + // for ^B4 (not the bars), so bwip-js is the ground truth; + // bbox math is not Labelary-cross-validated. Same as ^BB. + const p = obj.props; + // Mirror buildBwipOptions's 8..50 clamp so numRows recovery + // matches what bwip actually drew. + const rawRow = Math.round(p.height / Math.max(p.moduleWidth, 1)); + const rowheightUnits = Math.min(50, Math.max(8, rawRow)); + const modulePx = dotsToPx(p.moduleWidth, scale, dpmm); + const bwipSc = get1DBwipScale(p.moduleWidth, scale, dpmm); + // numRows recovery uses bwipSc (matches the scale bwip was + // called with), not BWIP_SCALE — otherwise the row count is + // wrong whenever rendering at a non-default scale/dpmm. + const numRows = Math.max(1, Math.round(ch / (rowheightUnits * bwipSc))); + const w = (cw / bwipSc) * modulePx; + // Bbox uses the clamped row height so the preview matches the + // rendered bars when raw h is outside bwip's range. + const h = numRows * dotsToPx(rowheightUnits * p.moduleWidth, scale, dpmm); + return { w, h }; + } case "code39": case "interleaved2of5": case "industrial2of5": diff --git a/src/lib/zplCommandSupport.ts b/src/lib/zplCommandSupport.ts index 30f7c81..0e7c5e1 100644 --- a/src/lib/zplCommandSupport.ts +++ b/src/lib/zplCommandSupport.ts @@ -96,7 +96,7 @@ export const ZPL_COMMANDS: readonly ZplCommandInfo[] = [ { cmd: 'B1', status: 'supported', description: 'Code 11 barcode' }, { cmd: 'B2', status: 'supported', description: 'Interleaved 2 of 5 barcode' }, { cmd: 'B3', status: 'supported', description: 'Code 39 barcode' }, - { cmd: 'B4', status: 'unsupported', description: 'Code 49 barcode' }, + { cmd: 'B4', status: 'supported', description: 'Code 49 barcode' }, { cmd: 'B5', status: 'supported', description: 'Planet Code barcode' }, { cmd: 'B7', status: 'supported', description: 'PDF417 barcode' }, { cmd: 'B8', status: 'supported', description: 'EAN-8 barcode' }, diff --git a/src/lib/zplGenerator.test.ts b/src/lib/zplGenerator.test.ts index 27c743e..63a1211 100644 --- a/src/lib/zplGenerator.test.ts +++ b/src/lib/zplGenerator.test.ts @@ -701,6 +701,49 @@ describe('generateZPL — parse/generate roundtrip', () => { expect(props(ext).moduleWidth).toBe(3); }); + it('round-trips a ^B4 Code 49 with default mode A', () => { + const original = parseZPL('^XA^FO10,10^B4N,20,Y,A^FDCODE49^FS^XZ', 8); + const regenerated = generateZPL(BASE_LABEL, original.objects); + const reparsed = parseZPL(regenerated, 8); + const bc = defined(reparsed.objects.find((o) => o.type === 'code49')); + expect(props(bc).content).toBe('CODE49'); + expect(props(bc).height).toBe(20); + expect(props(bc).printInterpretation).toBe(true); + expect(props(bc).mode).toBe('A'); + }); + + it('round-trips ^B4 explicit mode + rotation + moduleWidth', () => { + const original = parseZPL('^XA^BY3^FO10,10^B4R,30,N,2^FD12345^FS^XZ', 8); + const regenerated = generateZPL(BASE_LABEL, original.objects); + const reparsed = parseZPL(regenerated, 8); + const bc = defined(reparsed.objects.find((o) => o.type === 'code49')); + expect(props(bc).rotation).toBe('R'); + expect(props(bc).moduleWidth).toBe(3); + expect(props(bc).mode).toBe('2'); + expect(props(bc).printInterpretation).toBe(false); + }); + + it('falls back to mode A when ^B4 receives an unknown mode', () => { + const r = parseZPL('^XA^FO10,10^B4N,20,Y,X^FDCODE49^FS^XZ', 8); + const bc = defined(r.objects.find((o) => o.type === 'code49')); + expect(props(bc).mode).toBe('A'); + }); + + it('does not leak ^B4 mode from one symbol to the next', () => { + // Two B4 fields back-to-back: first explicit mode=3, second omits + // the mode parameter. The second must default to 'A' even though + // the parser variable still holds '3' from the previous handler + // run — the handler resets it on each B4 via the `?? "A"` fallback. + const r = parseZPL( + '^XA^FO10,10^B4N,20,Y,3^FDONE^FS^FO10,200^B4N,20,Y^FDTWO^FS^XZ', + 8, + ); + const codes = r.objects.filter((o) => o.type === 'code49'); + expect(codes).toHaveLength(2); + expect(props(codes[0]!).mode).toBe('3'); + expect(props(codes[1]!).mode).toBe('A'); + }); + it('preserves printer params through generate -> parse', () => { const label: LabelConfig = { ...BASE_LABEL, diff --git a/src/lib/zplParser.ts b/src/lib/zplParser.ts index 1bce600..4d94351 100644 --- a/src/lib/zplParser.ts +++ b/src/lib/zplParser.ts @@ -21,6 +21,7 @@ import type { ImageProps } from "../registry/image"; import type { Barcode1DProps } from "../registry/barcode1d"; import type { Gs1DatabarProps } from "../registry/gs1databar"; import type { Pdf417Props } from "../registry/pdf417"; +import type { Code49Props } from "../registry/code49"; import type { SerialProps } from "../registry/serial"; import { isZplRotation, type ZplRotation } from "../registry/rotation"; import type { AztecProps } from "../registry/aztec"; @@ -556,6 +557,7 @@ export function parseZPL(zpl: string, dpmm = 8): ParsedZPL { let bcInterp = true; let bcCheck = false; let bcRotation: ZplRotation = "N"; + let bcCode49Mode: Code49Props["mode"] = "A"; let gsSymbology: Gs1DatabarProps["symbology"] = 1; let gsSegments: number | undefined = undefined; // ^BY barcode defaults @@ -901,6 +903,25 @@ export function parseZPL(zpl: string, dpmm = 8): ParsedZPL { ), ); break; + case "code49": + objects.push( + makeObj( + "code49", + x, + y, + { + content, + height: bcHeight, + moduleWidth: byModuleWidth, + printInterpretation: bcInterp, + mode: bcCode49Mode, + rotation: bcRotation, + } satisfies Code49Props, + posType, + comment, + ), + ); + break; case "aztec": objects.push( makeObj( @@ -1152,6 +1173,17 @@ export function parseZPL(zpl: string, dpmm = 8): ParsedZPL { B5: mkBarcode("planet", 1, 2), // ^B5N,h,i,N BZ: mkBarcode("postal", 1, 2), // ^BZN,h,i,N BS: mkBarcode("upcEanExtension", 1, 2), // ^BSo,h,f (UPC/EAN 2- or 5-digit supplement) + B4: (p) => { + // ^B4o,h,f,m — Code 49. Custom handler for the extra `m`. + fieldType = "code49"; + bcRotation = readRotation(p[0]); + bcHeight = int(p[1], byHeight || 20); + bcInterp = (p[2] ?? "N") === "Y"; + const m = (p[3] ?? "A").toUpperCase(); + bcCode49Mode = /^[A0-5]$/.test(m) + ? (m as Code49Props["mode"]) + : "A"; + }, // MSI: check logic is "any letter except N" (not simple "Y") — keep inline // ^BMN,{checkType},{height},{interp},N (checkType: A/B/C/D=enabled, N=none) diff --git a/src/locales/ar.ts b/src/locales/ar.ts index bbec5b2..ad946c5 100644 --- a/src/locales/ar.ts +++ b/src/locales/ar.ts @@ -26,6 +26,7 @@ const ar = { ean8: 'EAN-8', upce: 'UPC-E', upcEanExtension: 'امتداد UPC/EAN', + code49: 'Code 49', interleaved2of5: 'Interleaved 2 من 5', code93: 'Code 93', pdf417: 'PDF417', @@ -341,6 +342,14 @@ const ar = { moduleWidth: 'عرض الوحدة', placeholder: '2 أو 5 أرقام', }, + code49: { + content: 'المحتوى', + height: 'الارتفاع (نقاط)', + moduleWidth: 'عرض الوحدة', + printInterpretation: 'قابل للقراءة', + mode: 'الوضع', + modeAuto: 'تلقائي', + }, interleaved2of5: { content: 'المحتوى', height: 'الارتفاع (نقاط)', diff --git a/src/locales/bg.ts b/src/locales/bg.ts index c37444e..a4ca5f7 100644 --- a/src/locales/bg.ts +++ b/src/locales/bg.ts @@ -26,6 +26,7 @@ const bg = { ean8: 'EAN-8', upce: 'UPC-E', upcEanExtension: 'Разширение UPC/EAN', + code49: 'Code 49', interleaved2of5: 'Interleaved 2 от 5', code93: 'Code 93', pdf417: 'PDF417', @@ -341,6 +342,14 @@ const bg = { moduleWidth: 'Ширина на модула', placeholder: '2 или 5 цифри', }, + code49: { + content: 'Съдържание', + height: 'Височина (точки)', + moduleWidth: 'Ширина на модула', + printInterpretation: 'Четим', + mode: 'Режим', + modeAuto: 'авто', + }, interleaved2of5: { content: 'Съдържание', height: 'Височина (точки)', diff --git a/src/locales/cs.ts b/src/locales/cs.ts index 8b0c80f..0aa1712 100644 --- a/src/locales/cs.ts +++ b/src/locales/cs.ts @@ -26,6 +26,7 @@ const cs = { ean8: 'EAN-8', upce: 'UPC-E', upcEanExtension: 'Rozšíření UPC/EAN', + code49: 'Code 49', interleaved2of5: 'Interleaved 2 z 5', code93: 'Code 93', pdf417: 'PDF417', @@ -341,6 +342,14 @@ const cs = { moduleWidth: 'Šířka modulu', placeholder: '2 nebo 5 číslic', }, + code49: { + content: 'Obsah', + height: 'Výška (body)', + moduleWidth: 'Šířka modulu', + printInterpretation: 'Čitelné', + mode: 'Režim', + modeAuto: 'auto', + }, interleaved2of5: { content: 'Obsah', height: 'Výška (body)', diff --git a/src/locales/da.ts b/src/locales/da.ts index a3c3af4..c67c3f0 100644 --- a/src/locales/da.ts +++ b/src/locales/da.ts @@ -26,6 +26,7 @@ const da = { ean8: 'EAN-8', upce: 'UPC-E', upcEanExtension: 'UPC/EAN-tillæg', + code49: 'Code 49', interleaved2of5: 'Interleaved 2 af 5', code93: 'Code 93', pdf417: 'PDF417', @@ -341,6 +342,14 @@ const da = { moduleWidth: 'Modulbredde', placeholder: '2 eller 5 cifre', }, + code49: { + content: 'Indhold', + height: 'Højde (punkter)', + moduleWidth: 'Modulbredde', + printInterpretation: 'Læsbar', + mode: 'Tilstand', + modeAuto: 'auto', + }, interleaved2of5: { content: 'Indhold', height: 'Højde (punkter)', diff --git a/src/locales/de.ts b/src/locales/de.ts index 067833f..8ab17e2 100644 --- a/src/locales/de.ts +++ b/src/locales/de.ts @@ -26,6 +26,7 @@ const de = { ean8: 'EAN-8', upce: 'UPC-E', upcEanExtension: 'UPC/EAN-Erweiterung', + code49: 'Code 49', interleaved2of5: 'Interleaved 2 of 5', code93: 'Code 93', pdf417: 'PDF417', @@ -362,6 +363,14 @@ const de = { moduleWidth: 'Modulbreite', placeholder: '2 oder 5 Ziffern', }, + code49: { + content: 'Inhalt', + height: 'Höhe (Punkte)', + moduleWidth: 'Modulbreite', + printInterpretation: 'Klartext', + mode: 'Modus', + modeAuto: 'auto', + }, interleaved2of5: { content: 'Inhalt', height: 'Höhe (Punkte)', diff --git a/src/locales/el.ts b/src/locales/el.ts index 4dab100..48458d5 100644 --- a/src/locales/el.ts +++ b/src/locales/el.ts @@ -26,6 +26,7 @@ const el = { ean8: 'EAN-8', upce: 'UPC-E', upcEanExtension: 'Επέκταση UPC/EAN', + code49: 'Κωδικός 49', interleaved2of5: 'Interleaved 2 από 5', code93: 'Code 93', pdf417: 'PDF417', @@ -341,6 +342,14 @@ const el = { moduleWidth: 'Πλάτος μονάδας', placeholder: '2 ή 5 ψηφία', }, + code49: { + content: 'Περιεχόμενο', + height: 'Ύψος (κουκκίδες)', + moduleWidth: 'Πλάτος μονάδας', + printInterpretation: 'Αναγνώσιμο', + mode: 'Λειτουργία', + modeAuto: 'αυτόματο', + }, interleaved2of5: { content: 'Περιεχόμενο', height: 'Ύψος (κουκκίδες)', diff --git a/src/locales/en.ts b/src/locales/en.ts index e5a78e3..d0f4547 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -26,6 +26,7 @@ const en = { ean8: 'EAN-8', upce: 'UPC-E', upcEanExtension: 'UPC/EAN extension', + code49: 'Code 49', interleaved2of5: 'Interleaved 2 of 5', code93: 'Code 93', pdf417: 'PDF417', @@ -362,6 +363,14 @@ const en = { moduleWidth: 'Module width', placeholder: '2 or 5 digits', }, + code49: { + content: 'Content', + height: 'Height (dots)', + moduleWidth: 'Module width', + printInterpretation: 'Human readable', + mode: 'Mode', + modeAuto: 'auto', + }, interleaved2of5: { content: 'Content', height: 'Height (dots)', diff --git a/src/locales/es.ts b/src/locales/es.ts index 1935149..4b541ea 100644 --- a/src/locales/es.ts +++ b/src/locales/es.ts @@ -26,6 +26,7 @@ const es = { ean8: 'EAN-8', upce: 'UPC-E', upcEanExtension: 'Extensión UPC/EAN', + code49: 'Code 49', interleaved2of5: 'Intercalado 2 de 5', code93: 'Code 93', pdf417: 'PDF417', @@ -341,6 +342,14 @@ const es = { moduleWidth: 'Ancho de módulo', placeholder: '2 o 5 dígitos', }, + code49: { + content: 'Contenido', + height: 'Altura (puntos)', + moduleWidth: 'Ancho de módulo', + printInterpretation: 'Legible', + mode: 'Modo', + modeAuto: 'auto', + }, interleaved2of5: { content: 'Contenido', height: 'Altura (puntos)', diff --git a/src/locales/et.ts b/src/locales/et.ts index 3d8cba7..af4a63d 100644 --- a/src/locales/et.ts +++ b/src/locales/et.ts @@ -26,6 +26,7 @@ const et = { ean8: 'EAN-8', upce: 'UPC-E', upcEanExtension: 'UPC/EAN laiendus', + code49: 'Code 49', interleaved2of5: 'Interleaved 2/5', code93: 'Code 93', pdf417: 'PDF417', @@ -341,6 +342,14 @@ const et = { moduleWidth: 'Mooduli laius', placeholder: '2 või 5 numbrit', }, + code49: { + content: 'Sisu', + height: 'Kõrgus (punktid)', + moduleWidth: 'Mooduli laius', + printInterpretation: 'Loetav', + mode: 'Režiim', + modeAuto: 'auto', + }, interleaved2of5: { content: 'Sisu', height: 'Kõrgus (punkti)', diff --git a/src/locales/fa.ts b/src/locales/fa.ts index 16d3c89..cc49e2c 100644 --- a/src/locales/fa.ts +++ b/src/locales/fa.ts @@ -26,6 +26,7 @@ const fa = { ean8: 'EAN-8', upce: 'UPC-E', upcEanExtension: 'افزونه UPC/EAN', + code49: 'Code 49', interleaved2of5: 'Interleaved 2 از 5', code93: 'Code 93', pdf417: 'PDF417', @@ -341,6 +342,14 @@ const fa = { moduleWidth: 'عرض ماژول', placeholder: '۲ یا ۵ رقم', }, + code49: { + content: 'محتوا', + height: 'ارتفاع (نقطه)', + moduleWidth: 'عرض ماژول', + printInterpretation: 'خوانا', + mode: 'حالت', + modeAuto: 'خودکار', + }, interleaved2of5: { content: 'محتوا', height: 'ارتفاع (نقطه)', diff --git a/src/locales/fi.ts b/src/locales/fi.ts index e11524c..81787b7 100644 --- a/src/locales/fi.ts +++ b/src/locales/fi.ts @@ -26,6 +26,7 @@ const fi = { ean8: 'EAN-8', upce: 'UPC-E', upcEanExtension: 'UPC/EAN-lisäys', + code49: 'Code 49', interleaved2of5: 'Interleaved 2/5', code93: 'Code 93', pdf417: 'PDF417', @@ -341,6 +342,14 @@ const fi = { moduleWidth: 'Moduulin leveys', placeholder: '2 tai 5 numeroa', }, + code49: { + content: 'Sisältö', + height: 'Korkeus (pisteet)', + moduleWidth: 'Moduulin leveys', + printInterpretation: 'Luettava', + mode: 'Tila', + modeAuto: 'auto', + }, interleaved2of5: { content: 'Sisältö', height: 'Korkeus (pistettä)', diff --git a/src/locales/fr.ts b/src/locales/fr.ts index 3d0abb3..898e567 100644 --- a/src/locales/fr.ts +++ b/src/locales/fr.ts @@ -26,6 +26,7 @@ const fr = { ean8: 'EAN-8', upce: 'UPC-E', upcEanExtension: 'Extension UPC/EAN', + code49: 'Code 49', interleaved2of5: 'Entrelacé 2 parmi 5', code93: 'Code 93', pdf417: 'PDF417', @@ -341,6 +342,14 @@ const fr = { moduleWidth: 'Largeur de module', placeholder: '2 ou 5 chiffres', }, + code49: { + content: 'Contenu', + height: 'Hauteur (points)', + moduleWidth: 'Largeur de module', + printInterpretation: 'Lisible', + mode: 'Mode', + modeAuto: 'auto', + }, interleaved2of5: { content: 'Contenu', height: 'Hauteur (points)', diff --git a/src/locales/he.ts b/src/locales/he.ts index 5f8005a..71afdde 100644 --- a/src/locales/he.ts +++ b/src/locales/he.ts @@ -26,6 +26,7 @@ const he = { ean8: 'EAN-8', upce: 'UPC-E', upcEanExtension: 'הרחבת UPC/EAN', + code49: 'Code 49', interleaved2of5: 'Interleaved 2 מ-5', code93: 'Code 93', pdf417: 'PDF417', @@ -341,6 +342,14 @@ const he = { moduleWidth: 'רוחב מודול', placeholder: '2 או 5 ספרות', }, + code49: { + content: 'תוכן', + height: 'גובה (נקודות)', + moduleWidth: 'רוחב מודול', + printInterpretation: 'קריא', + mode: 'מצב', + modeAuto: 'אוטומטי', + }, interleaved2of5: { content: 'תוכן', height: 'גובה (נקודות)', diff --git a/src/locales/hr.ts b/src/locales/hr.ts index 60a0978..7297e0e 100644 --- a/src/locales/hr.ts +++ b/src/locales/hr.ts @@ -26,6 +26,7 @@ const hr = { ean8: 'EAN-8', upce: 'UPC-E', upcEanExtension: 'UPC/EAN proširenje', + code49: 'Code 49', interleaved2of5: 'Interleaved 2 od 5', code93: 'Code 93', pdf417: 'PDF417', @@ -341,6 +342,14 @@ const hr = { moduleWidth: 'Širina modula', placeholder: '2 ili 5 znamenki', }, + code49: { + content: 'Sadržaj', + height: 'Visina (točke)', + moduleWidth: 'Širina modula', + printInterpretation: 'Čitljivo', + mode: 'Način', + modeAuto: 'auto', + }, interleaved2of5: { content: 'Sadržaj', height: 'Visina (točke)', diff --git a/src/locales/hu.ts b/src/locales/hu.ts index 1fda5dd..2e39b79 100644 --- a/src/locales/hu.ts +++ b/src/locales/hu.ts @@ -26,6 +26,7 @@ const hu = { ean8: 'EAN-8', upce: 'UPC-E', upcEanExtension: 'UPC/EAN kiegészítés', + code49: 'Code 49', interleaved2of5: 'Interleaved 2/5', code93: 'Code 93', pdf417: 'PDF417', @@ -341,6 +342,14 @@ const hu = { moduleWidth: 'Modulszélesség', placeholder: '2 vagy 5 számjegy', }, + code49: { + content: 'Tartalom', + height: 'Magasság (pont)', + moduleWidth: 'Modulszélesség', + printInterpretation: 'Olvasható', + mode: 'Mód', + modeAuto: 'auto', + }, interleaved2of5: { content: 'Tartalom', height: 'Magasság (pont)', diff --git a/src/locales/it.ts b/src/locales/it.ts index eaf35f5..2dc0b46 100644 --- a/src/locales/it.ts +++ b/src/locales/it.ts @@ -26,6 +26,7 @@ const it = { ean8: 'EAN-8', upce: 'UPC-E', upcEanExtension: 'Estensione UPC/EAN', + code49: 'Code 49', interleaved2of5: 'Interleaved 2 di 5', code93: 'Code 93', pdf417: 'PDF417', @@ -341,6 +342,14 @@ const it = { moduleWidth: 'Larghezza modulo', placeholder: '2 o 5 cifre', }, + code49: { + content: 'Contenuto', + height: 'Altezza (punti)', + moduleWidth: 'Larghezza modulo', + printInterpretation: 'Leggibile', + mode: 'Modalità', + modeAuto: 'auto', + }, interleaved2of5: { content: 'Contenuto', height: 'Altezza (punti)', diff --git a/src/locales/ja.ts b/src/locales/ja.ts index d03729d..550e868 100644 --- a/src/locales/ja.ts +++ b/src/locales/ja.ts @@ -26,6 +26,7 @@ const ja = { ean8: 'EAN-8', upce: 'UPC-E', upcEanExtension: 'UPC/EAN 拡張', + code49: 'Code 49', interleaved2of5: 'インターリーブド 2/5', code93: 'Code 93', pdf417: 'PDF417', @@ -341,6 +342,14 @@ const ja = { moduleWidth: 'モジュール幅', placeholder: '2 桁または 5 桁', }, + code49: { + content: 'コンテンツ', + height: '高さ (ドット)', + moduleWidth: 'モジュール幅', + printInterpretation: '人間可読', + mode: 'モード', + modeAuto: '自動', + }, interleaved2of5: { content: '内容', height: '高さ(ドット)', diff --git a/src/locales/ko.ts b/src/locales/ko.ts index a908b82..aaf61e5 100644 --- a/src/locales/ko.ts +++ b/src/locales/ko.ts @@ -26,6 +26,7 @@ const ko = { ean8: 'EAN-8', upce: 'UPC-E', upcEanExtension: 'UPC/EAN 확장', + code49: 'Code 49', interleaved2of5: '인터리브드 2/5', code93: 'Code 93', pdf417: 'PDF417', @@ -341,6 +342,14 @@ const ko = { moduleWidth: '모듈 너비', placeholder: '2자리 또는 5자리', }, + code49: { + content: '내용', + height: '높이 (도트)', + moduleWidth: '모듈 너비', + printInterpretation: '판독 가능', + mode: '모드', + modeAuto: '자동', + }, interleaved2of5: { content: '내용', height: '높이 (도트)', diff --git a/src/locales/lt.ts b/src/locales/lt.ts index 399f34a..75f4a37 100644 --- a/src/locales/lt.ts +++ b/src/locales/lt.ts @@ -26,6 +26,7 @@ const lt = { ean8: 'EAN-8', upce: 'UPC-E', upcEanExtension: 'UPC/EAN papildymas', + code49: 'Code 49', interleaved2of5: 'Interleaved 2 iš 5', code93: 'Code 93', pdf417: 'PDF417', @@ -341,6 +342,14 @@ const lt = { moduleWidth: 'Modulio plotis', placeholder: '2 arba 5 skaitmenys', }, + code49: { + content: 'Turinys', + height: 'Aukštis (taškai)', + moduleWidth: 'Modulio plotis', + printInterpretation: 'Skaitomas', + mode: 'Režimas', + modeAuto: 'auto', + }, interleaved2of5: { content: 'Turinys', height: 'Aukštis (taškai)', diff --git a/src/locales/lv.ts b/src/locales/lv.ts index 120668e..d5d4100 100644 --- a/src/locales/lv.ts +++ b/src/locales/lv.ts @@ -26,6 +26,7 @@ const lv = { ean8: 'EAN-8', upce: 'UPC-E', upcEanExtension: 'UPC/EAN paplašinājums', + code49: 'Code 49', interleaved2of5: 'Interleaved 2 no 5', code93: 'Code 93', pdf417: 'PDF417', @@ -341,6 +342,14 @@ const lv = { moduleWidth: 'Moduļa platums', placeholder: '2 vai 5 cipari', }, + code49: { + content: 'Saturs', + height: 'Augstums (punkti)', + moduleWidth: 'Moduļa platums', + printInterpretation: 'Lasāms', + mode: 'Režīms', + modeAuto: 'auto', + }, interleaved2of5: { content: 'Saturs', height: 'Augstums (punkti)', diff --git a/src/locales/nl.ts b/src/locales/nl.ts index 418bd71..b8db90d 100644 --- a/src/locales/nl.ts +++ b/src/locales/nl.ts @@ -26,6 +26,7 @@ const nl = { ean8: 'EAN-8', upce: 'UPC-E', upcEanExtension: 'UPC/EAN-uitbreiding', + code49: 'Code 49', interleaved2of5: 'Interleaved 2 van 5', code93: 'Code 93', pdf417: 'PDF417', @@ -341,6 +342,14 @@ const nl = { moduleWidth: 'Modulebreedte', placeholder: '2 of 5 cijfers', }, + code49: { + content: 'Inhoud', + height: 'Hoogte (punten)', + moduleWidth: 'Modulebreedte', + printInterpretation: 'Leesbaar', + mode: 'Modus', + modeAuto: 'auto', + }, interleaved2of5: { content: 'Inhoud', height: 'Hoogte (punten)', diff --git a/src/locales/no.ts b/src/locales/no.ts index cc62978..d644ef2 100644 --- a/src/locales/no.ts +++ b/src/locales/no.ts @@ -26,6 +26,7 @@ const no = { ean8: 'EAN-8', upce: 'UPC-E', upcEanExtension: 'UPC/EAN-tillegg', + code49: 'Code 49', interleaved2of5: 'Interleaved 2 av 5', code93: 'Code 93', pdf417: 'PDF417', @@ -341,6 +342,14 @@ const no = { moduleWidth: 'Modulbredde', placeholder: '2 eller 5 sifre', }, + code49: { + content: 'Innhold', + height: 'Høyde (punkter)', + moduleWidth: 'Modulbredde', + printInterpretation: 'Lesbar', + mode: 'Modus', + modeAuto: 'auto', + }, interleaved2of5: { content: 'Innhold', height: 'Høyde (punkter)', diff --git a/src/locales/pl.ts b/src/locales/pl.ts index a6bbc3d..9b00202 100644 --- a/src/locales/pl.ts +++ b/src/locales/pl.ts @@ -26,6 +26,7 @@ const pl = { ean8: 'EAN-8', upce: 'UPC-E', upcEanExtension: 'Rozszerzenie UPC/EAN', + code49: 'Code 49', interleaved2of5: 'Interleaved 2 z 5', code93: 'Code 93', pdf417: 'PDF417', @@ -341,6 +342,14 @@ const pl = { moduleWidth: 'Szerokość modułu', placeholder: '2 lub 5 cyfr', }, + code49: { + content: 'Treść', + height: 'Wysokość (punkty)', + moduleWidth: 'Szerokość modułu', + printInterpretation: 'Czytelne', + mode: 'Tryb', + modeAuto: 'auto', + }, interleaved2of5: { content: 'Zawartość', height: 'Wysokość (punkty)', diff --git a/src/locales/pt.ts b/src/locales/pt.ts index 989485a..ba4ea3c 100644 --- a/src/locales/pt.ts +++ b/src/locales/pt.ts @@ -26,6 +26,7 @@ const pt = { ean8: 'EAN-8', upce: 'UPC-E', upcEanExtension: 'Extensão UPC/EAN', + code49: 'Code 49', interleaved2of5: 'Intercalado 2 de 5', code93: 'Code 93', pdf417: 'PDF417', @@ -341,6 +342,14 @@ const pt = { moduleWidth: 'Largura do módulo', placeholder: '2 ou 5 dígitos', }, + code49: { + content: 'Conteúdo', + height: 'Altura (pontos)', + moduleWidth: 'Largura do módulo', + printInterpretation: 'Legível', + mode: 'Modo', + modeAuto: 'auto', + }, interleaved2of5: { content: 'Conteúdo', height: 'Altura (pontos)', diff --git a/src/locales/ro.ts b/src/locales/ro.ts index 8f5c877..0eb7c4a 100644 --- a/src/locales/ro.ts +++ b/src/locales/ro.ts @@ -26,6 +26,7 @@ const ro = { ean8: 'EAN-8', upce: 'UPC-E', upcEanExtension: 'Extensie UPC/EAN', + code49: 'Code 49', interleaved2of5: 'Interleaved 2 din 5', code93: 'Code 93', pdf417: 'PDF417', @@ -341,6 +342,14 @@ const ro = { moduleWidth: 'Lățime modul', placeholder: '2 sau 5 cifre', }, + code49: { + content: 'Conținut', + height: 'Înălțime (puncte)', + moduleWidth: 'Lățime modul', + printInterpretation: 'Lizibil', + mode: 'Mod', + modeAuto: 'auto', + }, interleaved2of5: { content: 'Conținut', height: 'Înălțime (puncte)', diff --git a/src/locales/sk.ts b/src/locales/sk.ts index 3999aa4..2ea1d77 100644 --- a/src/locales/sk.ts +++ b/src/locales/sk.ts @@ -26,6 +26,7 @@ const sk = { ean8: 'EAN-8', upce: 'UPC-E', upcEanExtension: 'Rozšírenie UPC/EAN', + code49: 'Code 49', interleaved2of5: 'Interleaved 2 z 5', code93: 'Code 93', pdf417: 'PDF417', @@ -341,6 +342,14 @@ const sk = { moduleWidth: 'Šírka modulu', placeholder: '2 alebo 5 číslic', }, + code49: { + content: 'Obsah', + height: 'Výška (body)', + moduleWidth: 'Šírka modulu', + printInterpretation: 'Čitateľné', + mode: 'Režim', + modeAuto: 'auto', + }, interleaved2of5: { content: 'Obsah', height: 'Výška (body)', diff --git a/src/locales/sl.ts b/src/locales/sl.ts index 07e2eb6..da1777f 100644 --- a/src/locales/sl.ts +++ b/src/locales/sl.ts @@ -26,6 +26,7 @@ const sl = { ean8: 'EAN-8', upce: 'UPC-E', upcEanExtension: 'UPC/EAN razširitev', + code49: 'Code 49', interleaved2of5: 'Interleaved 2 od 5', code93: 'Code 93', pdf417: 'PDF417', @@ -341,6 +342,14 @@ const sl = { moduleWidth: 'Širina modula', placeholder: '2 ali 5 števk', }, + code49: { + content: 'Vsebina', + height: 'Višina (točke)', + moduleWidth: 'Širina modula', + printInterpretation: 'Berljivo', + mode: 'Način', + modeAuto: 'samodejno', + }, interleaved2of5: { content: 'Vsebina', height: 'Višina (pike)', diff --git a/src/locales/sr.ts b/src/locales/sr.ts index ba2830f..d3324ef 100644 --- a/src/locales/sr.ts +++ b/src/locales/sr.ts @@ -26,6 +26,7 @@ const sr = { ean8: 'EAN-8', upce: 'UPC-E', upcEanExtension: 'UPC/EAN proširenje', + code49: 'Code 49', interleaved2of5: 'Interleaved 2 од 5', code93: 'Code 93', pdf417: 'PDF417', @@ -341,6 +342,14 @@ const sr = { moduleWidth: 'Širina modula', placeholder: '2 ili 5 cifara', }, + code49: { + content: 'Sadržaj', + height: 'Visina (tačke)', + moduleWidth: 'Širina modula', + printInterpretation: 'Čitljivo', + mode: 'Režim', + modeAuto: 'auto', + }, interleaved2of5: { content: 'Садржај', height: 'Висина (тачке)', diff --git a/src/locales/sv.ts b/src/locales/sv.ts index ed8cff4..2290fb8 100644 --- a/src/locales/sv.ts +++ b/src/locales/sv.ts @@ -26,6 +26,7 @@ const sv = { ean8: 'EAN-8', upce: 'UPC-E', upcEanExtension: 'UPC/EAN-tillägg', + code49: 'Code 49', interleaved2of5: 'Interleaved 2 av 5', code93: 'Code 93', pdf417: 'PDF417', @@ -341,6 +342,14 @@ const sv = { moduleWidth: 'Modulbredd', placeholder: '2 eller 5 siffror', }, + code49: { + content: 'Innehåll', + height: 'Höjd (punkter)', + moduleWidth: 'Modulbredd', + printInterpretation: 'Läsbar', + mode: 'Läge', + modeAuto: 'auto', + }, interleaved2of5: { content: 'Innehåll', height: 'Höjd (punkter)', diff --git a/src/locales/tr.ts b/src/locales/tr.ts index 300a46e..e5c6c38 100644 --- a/src/locales/tr.ts +++ b/src/locales/tr.ts @@ -26,6 +26,7 @@ const tr = { ean8: 'EAN-8', upce: 'UPC-E', upcEanExtension: 'UPC/EAN eki', + code49: 'Code 49', interleaved2of5: 'Interleaved 2/5', code93: 'Code 93', pdf417: 'PDF417', @@ -341,6 +342,14 @@ const tr = { moduleWidth: 'Modül genişliği', placeholder: '2 veya 5 hane', }, + code49: { + content: 'İçerik', + height: 'Yükseklik (nokta)', + moduleWidth: 'Modül genişliği', + printInterpretation: 'Okunabilir', + mode: 'Mod', + modeAuto: 'auto', + }, interleaved2of5: { content: 'İçerik', height: 'Yükseklik (nokta)', diff --git a/src/locales/zh-hans.ts b/src/locales/zh-hans.ts index 1908653..4f620e0 100644 --- a/src/locales/zh-hans.ts +++ b/src/locales/zh-hans.ts @@ -26,6 +26,7 @@ const zhHans = { ean8: 'EAN-8', upce: 'UPC-E', upcEanExtension: 'UPC/EAN 扩展', + code49: '49 码', interleaved2of5: '交叉 2/5', code93: 'Code 93', pdf417: 'PDF417', @@ -341,6 +342,14 @@ const zhHans = { moduleWidth: '模块宽度', placeholder: '2 位或 5 位数字', }, + code49: { + content: '内容', + height: '高度 (点)', + moduleWidth: '模块宽度', + printInterpretation: '可读', + mode: '模式', + modeAuto: '自动', + }, interleaved2of5: { content: '内容', height: '高度(点)', diff --git a/src/locales/zh-hant.ts b/src/locales/zh-hant.ts index 736e9e6..cdf1c99 100644 --- a/src/locales/zh-hant.ts +++ b/src/locales/zh-hant.ts @@ -26,6 +26,7 @@ const zhHant = { ean8: 'EAN-8', upce: 'UPC-E', upcEanExtension: 'UPC/EAN 擴充', + code49: '49 碼', interleaved2of5: '交叉 2/5', code93: 'Code 93', pdf417: 'PDF417', @@ -341,6 +342,14 @@ const zhHant = { moduleWidth: '模組寬度', placeholder: '2 位或 5 位數字', }, + code49: { + content: '內容', + height: '高度 (點)', + moduleWidth: '模組寬度', + printInterpretation: '可讀', + mode: '模式', + modeAuto: '自動', + }, interleaved2of5: { content: '內容', height: '高度(點)', diff --git a/src/registry/code49.test.ts b/src/registry/code49.test.ts new file mode 100644 index 0000000..314363a --- /dev/null +++ b/src/registry/code49.test.ts @@ -0,0 +1,102 @@ +import { describe, it, expect } from 'vitest'; +import { code49 } from './code49'; +import type { Code49Props } from './code49'; +import type { LabelObjectBase } from '../types/ObjectType'; + +/** + * The four-layer height-clamp contract for ^B4 Code 49: + * - UI: NumberInput min/max blocks invalid typing + * - normalize: moduleWidth change re-clamps height across fields + * - commit: Konva transformer drag past bwip's range pins to limit + * - render: bwipHelpers clamps as a last-line defense for JSON loads + * + * The UI layer is enforced by HTML form behavior and not unit-tested + * here; the other three layers live in pure code and get coverage below. + */ + +const baseObj = ( + overrides: Partial = {}, +): LabelObjectBase & { props: Code49Props } => ({ + id: 'test', + type: 'code49', + x: 0, + y: 0, + rotation: 0, + props: { + content: 'CODE49', + height: 20, + moduleWidth: 2, + printInterpretation: true, + mode: 'A', + rotation: 'N', + ...overrides, + }, +}); + +describe('code49 — normalizeChanges height clamp on moduleWidth change', () => { + it('clamps height up when new moduleWidth pushes minimum above current h', () => { + // mw 2 → range [16, 100], h=20 valid. Bump to mw=5 → range [40, 250]. + // h=20 falls below the new floor, must round up to 40. + const obj = baseObj({ height: 20, moduleWidth: 2 }); + const result = code49.normalizeChanges?.(obj, { props: { moduleWidth: 5 } }); + expect((result?.props as Partial).height).toBe(40); + }); + + it('clamps height down when new moduleWidth pushes maximum below current h', () => { + // mw 8 → range [64, 400], h=300 valid. Drop to mw=2 → range [16, 100]. + // h=300 exceeds new ceiling, must round down to 100. + const obj = baseObj({ height: 300, moduleWidth: 8 }); + const result = code49.normalizeChanges?.(obj, { props: { moduleWidth: 2 } }); + expect((result?.props as Partial).height).toBe(100); + }); + + it('leaves height untouched when moduleWidth is not in the change set', () => { + const obj = baseObj({ height: 20, moduleWidth: 2 }); + const changes = { props: { content: 'NEW' } }; + expect(code49.normalizeChanges?.(obj, changes)).toBe(changes); + }); + + it('skips clamping when incoming moduleWidth is not a positive number', () => { + // Defends against JSON imports / undo with garbage in moduleWidth. + // Render-edge guard handles the genuinely-broken case; normalize + // shouldn't anchor the clamp range to nonsense input. + const obj = baseObj({ height: 20, moduleWidth: 2 }); + const changes = { props: { moduleWidth: 0 } }; + expect(code49.normalizeChanges?.(obj, changes)).toBe(changes); + }); + + it('respects an incoming height that is already valid for the new moduleWidth', () => { + const obj = baseObj({ height: 20, moduleWidth: 2 }); + const changes = { props: { moduleWidth: 4, height: 64 } }; + const result = code49.normalizeChanges?.(obj, changes); + // 64 is at the floor for mw=4 (8*4) — keep as-is. + expect((result?.props as Partial).height).toBe(64); + }); +}); + +describe('code49 — commitTransform height clamp on resize drag', () => { + it('clamps height up when a drag pulls it below the bwip minimum', () => { + const obj = baseObj({ height: 20, moduleWidth: 2 }); + // Mimic a resize that halves the height and keeps moduleWidth roughly + // intact. esy = 0.4 → new height ≈ 8 (below mw=2 floor of 16). + const result = code49.commitTransform?.(obj, { + sx: 1, sy: 0.4, + snap: (v) => v, + nodeHeight: 0, + anchor: null, + }); + expect(result?.height).toBeGreaterThanOrEqual(16); + }); + + it('clamps height down when a drag pushes it above the bwip maximum', () => { + const obj = baseObj({ height: 20, moduleWidth: 2 }); + // esy = 10 → new height ≈ 200 (above mw=2 ceiling of 100). + const result = code49.commitTransform?.(obj, { + sx: 1, sy: 10, + snap: (v) => v, + nodeHeight: 0, + anchor: null, + }); + expect(result?.height).toBeLessThanOrEqual(100); + }); +}); diff --git a/src/registry/code49.tsx b/src/registry/code49.tsx new file mode 100644 index 0000000..ee0546b --- /dev/null +++ b/src/registry/code49.tsx @@ -0,0 +1,149 @@ +import type { ObjectTypeDefinition } from '../types/ObjectType'; +import { useT } from '../lib/useT'; +import { inputCls, labelCls } from '../components/Properties/styles'; +import { fieldPos, fdFieldFor } from './zplHelpers'; +import { commitBarcodeWidthHeightTransform } from './transformHelpers'; +import { type ZplRotation } from './rotation'; +import { RotationSelect } from '../components/Properties/RotationSelect'; +import { NumberInput } from '../components/Properties/NumberInput'; + +/** ZPL ^B4 m: 'A' = auto subset. 0-5 force a specific subset. */ +export type Code49Mode = 'A' | '0' | '1' | '2' | '3' | '4' | '5'; + +const CODE49_MODES: readonly Code49Mode[] = ['A', '0', '1', '2', '3', '4', '5']; + +// bwip-js rejects code49 rowheight outside 8..50 modules. +const code49MinHeight = (moduleWidth: number) => 8 * Math.max(moduleWidth, 1); +const code49MaxHeight = (moduleWidth: number) => 50 * Math.max(moduleWidth, 1); + +export interface Code49Props { + content: string; + height: number; + moduleWidth: number; + printInterpretation: boolean; + mode: Code49Mode; + rotation: ZplRotation; +} + +export const code49: ObjectTypeDefinition = { + label: 'Code 49', + icon: 'C49', + group: 'code-1d', + bindable: true, + defaultProps: { + content: 'CODE49', + height: 20, + moduleWidth: 2, + printInterpretation: true, + mode: 'A', + rotation: 'N', + }, + defaultSize: { width: 300, height: 120 }, + + // Clamp height into bwip's range so drag past the limit lands in + // props (not just in the render) — otherwise ZPL h drifts from + // the visible bars. + commitTransform: (obj, ctx) => { + const next = commitBarcodeWidthHeightTransform(obj, ctx); + const mw = next.moduleWidth ?? obj.props.moduleWidth; + const rawH = next.height ?? obj.props.height; + return { + ...next, + height: Math.min(code49MaxHeight(mw), Math.max(code49MinHeight(mw), rawH)), + }; + }, + + // Re-clamp height when moduleWidth shifts the valid range (the + // height input's own min/max only guards its own field). Skip on + // non-positive moduleWidth so JSON-import / undo with garbage + // doesn't anchor the clamp. + normalizeChanges: (obj, changes) => { + const nextProps = changes.props as Partial | undefined; + if (!nextProps || nextProps.moduleWidth === undefined) return changes; + const newMw = nextProps.moduleWidth; + if (!Number.isFinite(newMw) || newMw < 1) return changes; + const curH = nextProps.height ?? obj.props.height; + const clampedH = Math.min( + code49MaxHeight(newMw), + Math.max(code49MinHeight(newMw), curH), + ); + return clampedH === curH + ? changes + : { ...changes, props: { ...nextProps, height: clampedH } }; + }, + + toZPL: (obj, ctx) => { + const p = obj.props; + const interp = p.printInterpretation ? 'Y' : 'N'; + return [ + `^BY${p.moduleWidth}`, + fieldPos(obj), + `^B4${p.rotation},${p.height},${interp},${p.mode}`, + fdFieldFor(obj, p.content, ctx), + ] + .filter(Boolean) + .join(''); + }, + + PropertiesPanel: ({ obj, onChange }) => { + const t = useT(); + const p = obj.props; + const loc = t.registry.code49; + return ( +
+
+ + onChange({ content: e.target.value })} + /> +
+ +
+ onChange({ height })} + /> + onChange({ moduleWidth })} + /> +
+ +
+ + +
+ + + + onChange({ rotation })} /> +
+ ); + }, +}; diff --git a/src/registry/index.ts b/src/registry/index.ts index ffb5dbd..ecc719c 100644 --- a/src/registry/index.ts +++ b/src/registry/index.ts @@ -61,6 +61,8 @@ import { codablock } from './codablock.tsx'; import type { CodablockProps } from './codablock.tsx'; import { upcEanExtension } from './upcEanExtension.tsx'; import type { UpcEanExtensionProps } from './upcEanExtension.tsx'; +import { code49 } from './code49.tsx'; +import type { Code49Props } from './code49.tsx'; /** Single-branch shape for one registry type: the common base plus a * literal `type` discriminator and that type's props. Used to compose @@ -102,12 +104,13 @@ export type LeafObject = | Leaf<'aztec', AztecProps> | Leaf<'micropdf417', MicroPdf417Props> | Leaf<'codablock', CodablockProps> - | Leaf<'upcEanExtension', UpcEanExtensionProps>; + | Leaf<'upcEanExtension', UpcEanExtensionProps> + | Leaf<'code49', Code49Props>; export const BARCODE_1D_TYPES = new Set([ 'code128', 'code39', 'ean13', 'ean8', 'upca', 'upce', 'interleaved2of5', 'code93', 'code11', 'industrial2of5', 'standard2of5', 'codabar', 'logmars', 'msi', 'plessey', - 'gs1databar', 'planet', 'postal', 'upcEanExtension', + 'gs1databar', 'planet', 'postal', 'upcEanExtension', 'code49', ]); export const STACKED_2D_TYPES = new Set(['pdf417', 'micropdf417', 'codablock']); @@ -130,6 +133,7 @@ export const ObjectRegistry: Record> = { ean8, upce, upcEanExtension, + code49, logmars, code93, codabar,