From d6c28b49ac4f6f92741dd70fa8cb18bc55ad97ca Mon Sep 17 00:00:00 2001 From: u8array Date: Fri, 31 Jul 2026 20:58:03 +0200 Subject: [PATCH 1/3] fix(maxicode): keep the red canvas alarm for bound content The suppression predicate now shares one owner check with barcodePreflight; BarcodeObject receives the pre-binding content. --- packages/core/src/registry/maxicode.test.ts | 18 +++++++++++++++++- packages/core/src/registry/maxicode.ts | 7 +++++++ src/components/Canvas/BarcodeObject.tsx | 21 ++++++++++++++------- src/components/Canvas/KonvaObject.tsx | 4 ++-- src/components/Canvas/barcodePreflight.ts | 6 ++---- 5 files changed, 42 insertions(+), 14 deletions(-) diff --git a/packages/core/src/registry/maxicode.test.ts b/packages/core/src/registry/maxicode.test.ts index 5433c50e..640644c7 100644 --- a/packages/core/src/registry/maxicode.test.ts +++ b/packages/core/src/registry/maxicode.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; import bwipjs from "bwip-js/generic"; -import { maxicodeMissingScm } from "./maxicode"; +import { maxicodeMissingScm, maxicodeScmOwnedByPreflight } from "./maxicode"; import { getEntry, type LeafObject } from "."; import { measureBarcodeFootprintDotsWith, type BwipEngine } from "../lib/barcodeDims"; import type { LabelObject } from "../types/Group"; @@ -31,6 +31,22 @@ describe("maxicodeMissingScm", () => { }); }); +describe("maxicodeScmOwnedByPreflight", () => { + it("owns only literal marker-free content", () => { + expect(maxicodeScmOwnedByPreflight("1234567890", { mode: 2, content: "1234567890" })).toBe(true); + }); + + it("leaves bound content to renderFailed: markers in the raw string keep the alarm", () => { + // The producer never fires for «scm», so suppressing here too would hide + // the broken symbol behind a neutral placeholder on every surface. + expect(maxicodeScmOwnedByPreflight("«scm»", { mode: 2, content: "1234567890" })).toBe(false); + }); + + it("never owns a resolved value that carries a separator", () => { + expect(maxicodeScmOwnedByPreflight("12345", { mode: 2, content: `12345${GS}840${GS}001` })).toBe(false); + }); +}); + describe("maxicode preflight producer", () => { it("flags a configured mode 2 payload without a carrier message", () => { expect(getEntry("maxicode")!.preflight!(mc(2, "1234567890"), pctx)).toEqual([{ kind: "maxicodeModeMissingScm" }]); diff --git a/packages/core/src/registry/maxicode.ts b/packages/core/src/registry/maxicode.ts index 62ab1684..3e747e74 100644 --- a/packages/core/src/registry/maxicode.ts +++ b/packages/core/src/registry/maxicode.ts @@ -33,6 +33,13 @@ export function maxicodeMissingScm(props: MaxicodeProps): boolean { return (props.mode === 2 || props.mode === 3) && !props.content.includes(SCM_FIELD_SEPARATOR); } +/** True when the missing-SCM report is owned by the preflight producer, so + * renderFailed/red-box surfaces must stand down. Only literal content: the + * producer never sees a bound value, so marker fields keep the alarm. */ +export function maxicodeScmOwnedByPreflight(rawContent: string, resolved: MaxicodeProps): boolean { + return !hasTemplateMarkers(rawContent) && maxicodeMissingScm(resolved); +} + export const maxicode: ObjectTypeCore = { label: "Maxicode", icon: "⬡", diff --git a/src/components/Canvas/BarcodeObject.tsx b/src/components/Canvas/BarcodeObject.tsx index d4949525..4f45f26c 100644 --- a/src/components/Canvas/BarcodeObject.tsx +++ b/src/components/Canvas/BarcodeObject.tsx @@ -24,7 +24,7 @@ import { getObjectStringContent } from "@zplab/core/lib/variableBinding"; import { hasTemplateMarkers } from "@zplab/core/lib/fnTemplate"; import { hasControlMarkers } from "@zplab/core/types/controlKey"; import { objectRotation } from "@zplab/core/registry/rotation"; -import { maxicodeMissingScm, type MaxicodeProps } from "@zplab/core/registry/maxicode"; +import { maxicodeScmOwnedByPreflight, type MaxicodeProps } from "@zplab/core/registry/maxicode"; import { rotatedGroupTransform } from "./rotatedGroupTransform"; import { buildEanUpcDigitOverlay } from "./eanUpcDigitNodes"; import { buildCode1dStartStopGlyphs } from "./code1dHriOverlay"; @@ -110,7 +110,12 @@ export function BarcodeObject({ isSelected, onSelect, dragHandlers, -}: KonvaObjectProps) { + preBindingContent, +}: KonvaObjectProps & { + /** Content before marker resolution: `obj` arrives resolved, so the SCM + * alarm suppression needs the raw string to tell literal from bound. */ + preBindingContent?: string; +}) { const colors = useColorScheme(); // Vera Mono (HRI) loads async; Konva won't repaint on an unchanged // fontFamily once the FontFace resolves. Keying the overlay on this @@ -510,11 +515,13 @@ export function BarcodeObject({ ); } - // Fallback placeholder (error or not yet rendered); mode 2/3 MaxiCode - // missing its carrier message already reported via preflight, so suppress - // the red alarm box here and show the neutral placeholder instead. - const scmMissing = obj.type === "maxicode" && maxicodeMissingScm(obj.props as MaxicodeProps); - const fallbackError = scmMissing ? null : errorMsg; + // Fallback placeholder (error or not yet rendered); a literal mode 2/3 + // MaxiCode missing its carrier message already reports via preflight, so + // suppress the red alarm box. Bound fields keep it (renderFailed case). + const scmOwned = + obj.type === "maxicode" && + maxicodeScmOwnedByPreflight(preBindingContent ?? getObjectStringContent(obj) ?? "", obj.props as MaxicodeProps); + const fallbackError = scmOwned ? null : errorMsg; const fbW = dotsToPx(200, scale, dpmm); const fbH = dotsToPx(80, scale, dpmm); return ( diff --git a/src/components/Canvas/KonvaObject.tsx b/src/components/Canvas/KonvaObject.tsx index 17ee93ea..01508d82 100644 --- a/src/components/Canvas/KonvaObject.tsx +++ b/src/components/Canvas/KonvaObject.tsx @@ -1,7 +1,7 @@ import { useEffect } from "react"; import { useFontCacheVersion } from "../../hooks/useFontCacheVersion"; import { Ellipse, Group, Rect, Shape, Text } from "react-konva"; -import { shouldShowFallbackTint } from "@zplab/core/lib/variableBinding"; +import { getObjectStringContent, shouldShowFallbackTint } from "@zplab/core/lib/variableBinding"; import { lookupBoundVariable } from "@zplab/core/lib/variableField"; import { BarcodeObject } from "./BarcodeObject"; import { LineObject } from "./LineObject"; @@ -465,7 +465,7 @@ export function KonvaObject(props_: Props) { const shape = obj.type === "line" ? : obj.type === "image" ? : - BARCODE_TYPES.has(obj.type) ? : + BARCODE_TYPES.has(obj.type) ? : ; if (!showFallbackTint) return shape; diff --git a/src/components/Canvas/barcodePreflight.ts b/src/components/Canvas/barcodePreflight.ts index 5ad106a9..03f36962 100644 --- a/src/components/Canvas/barcodePreflight.ts +++ b/src/components/Canvas/barcodePreflight.ts @@ -1,6 +1,5 @@ import { objectResolvesCtrl, type LeafObject } from "@zplab/core/registry"; -import { maxicodeMissingScm, type MaxicodeProps } from "@zplab/core/registry/maxicode"; -import { hasTemplateMarkers } from "@zplab/core/lib/fnTemplate"; +import { maxicodeScmOwnedByPreflight, type MaxicodeProps } from "@zplab/core/registry/maxicode"; import { isBarcode } from "@zplab/core/lib/objectBounds"; import { PREFLIGHT_SEVERITY, type PreflightFinding } from "@zplab/core/lib/preflight"; import type { Variable } from "@zplab/core/types/Variable"; @@ -87,8 +86,7 @@ export function barcodeEncodeFindings( // double report. Marker content isn't skipped: the producer guards it out. if ( resolved.type === "maxicode" && - !hasTemplateMarkers(getObjectStringContent(leaf) ?? "") && - maxicodeMissingScm(resolved.props as MaxicodeProps) + maxicodeScmOwnedByPreflight(getObjectStringContent(leaf) ?? "", resolved.props as MaxicodeProps) ) { continue; } From aaf3be28fe80a3b7916c5517111b4bd741da2c04 Mon Sep 17 00:00:00 2001 From: u8array Date: Fri, 31 Jul 2026 21:21:21 +0200 Subject: [PATCH 2/3] fix(maxicode): require the pre-binding content prop and pin the alarm wiring --- .../Canvas/BarcodeObject.scmAlarm.test.tsx | 72 +++++++++++++++++++ .../BarcodeObject.symbologySwitch.test.tsx | 1 + src/components/Canvas/BarcodeObject.tsx | 8 ++- src/components/Canvas/KonvaObject.tsx | 2 +- 4 files changed, 79 insertions(+), 4 deletions(-) create mode 100644 src/components/Canvas/BarcodeObject.scmAlarm.test.tsx diff --git a/src/components/Canvas/BarcodeObject.scmAlarm.test.tsx b/src/components/Canvas/BarcodeObject.scmAlarm.test.tsx new file mode 100644 index 00000000..712c6f6b --- /dev/null +++ b/src/components/Canvas/BarcodeObject.scmAlarm.test.tsx @@ -0,0 +1,72 @@ +// @vitest-environment jsdom +import { describe, it, expect, beforeAll, afterEach } from "vitest"; +import { render, cleanup } from "@testing-library/react"; +import { Stage, Layer } from "react-konva"; +import type Konva from "konva"; +import { BarcodeObject } from "./BarcodeObject"; +import type { LeafObject } from "@zplab/core/registry"; +import type { LabelObject } from "@zplab/core/types/Group"; + +beforeAll(() => { + // jsdom has no 2d context; Konva needs one for Stage/Layer plumbing and + // sceneFunc draws. Geometry never reads pixels back. + const noop = () => undefined; + HTMLCanvasElement.prototype.getContext = (() => + new Proxy({ getImageData: () => ({ data: new Uint8ClampedArray(4) }), measureText: () => ({ width: 0 }) }, { + get: (target, prop) => (prop in target ? target[prop as keyof typeof target] : noop), + })) as unknown as typeof HTMLCanvasElement.prototype.getContext; +}); + +afterEach(cleanup); + +// A mode 2/3 MaxiCode without a separator never encodes, so BarcodeObject +// lands in the fallback branch; the ⚠ red text is the alarm surface under test. +const mc = (content: string): LeafObject => + ({ + id: "m", + type: "maxicode", + x: 0, + y: 0, + rotation: 0, + positionType: "FO", + props: { mode: 2, content }, + }) as LabelObject as LeafObject; + +function fallbackText(obj: LeafObject, preBindingContent: string) { + let stage: Konva.Stage | null = null; + render( + { stage = n; }}> + + undefined} + onChange={() => undefined} + snap={(d) => d} + preBindingContent={preBindingContent} + /> + + , + ); + const t = stage!.findOne("Text") as Konva.Text; + return { text: t.text(), fill: t.fill() }; +} + +describe("MaxiCode SCM alarm suppression wiring", () => { + it("suppresses the red alarm for literal no-SCM content (preflight owns it)", () => { + const out = fallbackText(mc("1234567890"), "1234567890"); + expect(out.fill).toBe("#374151"); + expect(out.text).toBe("maxicode"); + }); + + it("keeps the red alarm for bound content resolving without a separator", () => { + // obj arrives marker-resolved; the raw «scm» marker rides in the prop. + const out = fallbackText(mc("1234567890"), "«scm»"); + expect(out.fill).toBe("#b91c1c"); + expect(out.text.startsWith("⚠")).toBe(true); + }); +}); diff --git a/src/components/Canvas/BarcodeObject.symbologySwitch.test.tsx b/src/components/Canvas/BarcodeObject.symbologySwitch.test.tsx index 068bfbf7..3089ae5b 100644 --- a/src/components/Canvas/BarcodeObject.symbologySwitch.test.tsx +++ b/src/components/Canvas/BarcodeObject.symbologySwitch.test.tsx @@ -56,6 +56,7 @@ describe("symbology switch re-measures the node footprint", () => { dpmm={8} offsetX={0} offsetY={0} + preBindingContent={(o.props as { content?: string }).content ?? ""} {...noopHandlers} /> diff --git a/src/components/Canvas/BarcodeObject.tsx b/src/components/Canvas/BarcodeObject.tsx index 4f45f26c..d9036533 100644 --- a/src/components/Canvas/BarcodeObject.tsx +++ b/src/components/Canvas/BarcodeObject.tsx @@ -113,8 +113,10 @@ export function BarcodeObject({ preBindingContent, }: KonvaObjectProps & { /** Content before marker resolution: `obj` arrives resolved, so the SCM - * alarm suppression needs the raw string to tell literal from bound. */ - preBindingContent?: string; + * alarm suppression needs the raw string to tell literal from bound. + * Required: a resolved-content fallback would silently re-suppress the + * alarm for bound fields. */ + preBindingContent: string; }) { const colors = useColorScheme(); // Vera Mono (HRI) loads async; Konva won't repaint on an unchanged @@ -520,7 +522,7 @@ export function BarcodeObject({ // suppress the red alarm box. Bound fields keep it (renderFailed case). const scmOwned = obj.type === "maxicode" && - maxicodeScmOwnedByPreflight(preBindingContent ?? getObjectStringContent(obj) ?? "", obj.props as MaxicodeProps); + maxicodeScmOwnedByPreflight(preBindingContent, obj.props as MaxicodeProps); const fallbackError = scmOwned ? null : errorMsg; const fbW = dotsToPx(200, scale, dpmm); const fbH = dotsToPx(80, scale, dpmm); diff --git a/src/components/Canvas/KonvaObject.tsx b/src/components/Canvas/KonvaObject.tsx index 01508d82..27d1ee9f 100644 --- a/src/components/Canvas/KonvaObject.tsx +++ b/src/components/Canvas/KonvaObject.tsx @@ -465,7 +465,7 @@ export function KonvaObject(props_: Props) { const shape = obj.type === "line" ? : obj.type === "image" ? : - BARCODE_TYPES.has(obj.type) ? : + BARCODE_TYPES.has(obj.type) ? : ; if (!showFallbackTint) return shape; From eeb13ff9e9b2dc65ce53823d44c08a1e12080197 Mon Sep 17 00:00:00 2001 From: u8array Date: Fri, 31 Jul 2026 21:35:34 +0200 Subject: [PATCH 3/3] fix(aztec): warn on ecLevel values outside every defined domain --- packages/core/src/registry/aztec.test.ts | 10 ++++++++++ packages/core/src/registry/aztec.ts | 19 +++++++++++++------ packages/core/src/types/preflight.ts | 4 ++-- src/components/Canvas/BarcodeObject.tsx | 5 +++-- 4 files changed, 28 insertions(+), 10 deletions(-) diff --git a/packages/core/src/registry/aztec.test.ts b/packages/core/src/registry/aztec.test.ts index eb2b9715..d06dd7fe 100644 --- a/packages/core/src/registry/aztec.test.ts +++ b/packages/core/src/registry/aztec.test.ts @@ -31,6 +31,16 @@ describe("aztec ecLevel preflight", () => { expect(getEntry("aztec")!.preflight!(az(ec), pctx)).toEqual([]); } }); + + it("flags the undefined zones between the domains (100, 105-200, 233-299)", () => { + // The UI range is a continuous 0-300, so these are enterable; the preview + // silently auto-sizes and printer behavior is undefined, hence the warning. + for (const ec of [100, 105, 150, 200, 233, 299]) { + expect(getEntry("aztec")!.preflight!(az(ec), pctx)).toEqual([ + { kind: "aztecEcLevelOutOfRange", detail: `${ec} (valid 5-95)` }, + ]); + } + }); }); describe("aztecBwipOptions maps to the encoder's real 5-95 band", () => { diff --git a/packages/core/src/registry/aztec.ts b/packages/core/src/registry/aztec.ts index 85bebef0..ff66dd96 100644 --- a/packages/core/src/registry/aztec.ts +++ b/packages/core/src/registry/aztec.ts @@ -15,13 +15,18 @@ export const EC_LEVEL_MAX = 300; export const EC_PERCENT_MIN = 5; export const EC_PERCENT_MAX = 95; -/** Flags an ecLevel that reads as a percent (1-99) but sits outside the - * encoder's 5-95 band. The special layer/rune domains (0, 101-104, 201-232, - * 300) are valid and never flagged. */ +/** Flags an ecLevel outside every defined domain (0, 5-95 percent, 101-104 + * compact, 201-232 full, 300 rune): the preview falls back to auto sizing + * and the printer behavior is undefined, so the value deserves a warning. */ function ecLevelFindings(ecLevel: number): PreflightProducerResult[] { const ec = Math.round(ecLevel); - const inPercentBand = ec >= 1 && ec <= 99; - if (inPercentBand && (ec < EC_PERCENT_MIN || ec > EC_PERCENT_MAX)) { + const defined = + ec === 0 || + (ec >= EC_PERCENT_MIN && ec <= EC_PERCENT_MAX) || + (ec >= 101 && ec <= 104) || + (ec >= 201 && ec <= 232) || + ec === 300; + if (!defined) { return [{ kind: "aztecEcLevelOutOfRange", detail: `${ec} (valid ${EC_PERCENT_MIN}-${EC_PERCENT_MAX})` }]; } return []; @@ -37,6 +42,8 @@ export interface AztecProps { // One switch for the capability flag and the emitter's chip resolution. const CONTROL_CHARS = true; +const magnificationTooSmall = moduleTooSmallPreflight('magnification'); + export const aztec: ObjectTypeCore = { label: "Aztec", icon: "◇", @@ -57,7 +64,7 @@ export const aztec: ObjectTypeCore = { uniformScaleProp: { name: 'magnification', min: MAGNIFICATION_MIN, max: MAGNIFICATION_MAX }, preflight: (obj, ctx) => [ - ...moduleTooSmallPreflight('magnification')(obj, ctx), + ...magnificationTooSmall(obj, ctx), ...ecLevelFindings(obj.props.ecLevel), ], diff --git a/packages/core/src/types/preflight.ts b/packages/core/src/types/preflight.ts index 73a12c34..a0d2d72b 100644 --- a/packages/core/src/types/preflight.ts +++ b/packages/core/src/types/preflight.ts @@ -71,8 +71,8 @@ export const PREFLIGHT_SEVERITY: Record = { // Modes 2/3 need a structured carrier message (GS-separated postcode, // country, service); without it the symbol cannot encode. maxicodeModeMissingScm: 'error', - // Percent band outside the preview encoder's 5-95 range: the printer accepts - // 1-99 (spec p64) and the value round-trips, only the canvas cannot show it. + // Outside every defined ^B0 d-domain, or a percent the preview encoder + // cannot show (printer accepts 1-99 per spec p64); export keeps the value. aztecEcLevelOutOfRange: 'warning', // A leaf whose type isn't in the registry: it has no emitter, so it prints // nothing. Only reachable via an imported/foreign design file. diff --git a/src/components/Canvas/BarcodeObject.tsx b/src/components/Canvas/BarcodeObject.tsx index d9036533..912c5a66 100644 --- a/src/components/Canvas/BarcodeObject.tsx +++ b/src/components/Canvas/BarcodeObject.tsx @@ -518,8 +518,9 @@ export function BarcodeObject({ } // Fallback placeholder (error or not yet rendered); a literal mode 2/3 - // MaxiCode missing its carrier message already reports via preflight, so - // suppress the red alarm box. Bound fields keep it (renderFailed case). + // MaxiCode missing its carrier message already reports via preflight + // (missing-SCM, or emptyContent when blank), so suppress the red alarm + // box. Bound fields keep it (renderFailed case). const scmOwned = obj.type === "maxicode" && maxicodeScmOwnedByPreflight(preBindingContent, obj.props as MaxicodeProps);