Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions packages/core/src/registry/aztec.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
19 changes: 13 additions & 6 deletions packages/core/src/registry/aztec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 [];
Expand All @@ -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<AztecProps>('magnification');

export const aztec: ObjectTypeCore<AztecProps> = {
label: "Aztec",
icon: "◇",
Expand All @@ -57,7 +64,7 @@ export const aztec: ObjectTypeCore<AztecProps> = {
uniformScaleProp: { name: 'magnification', min: MAGNIFICATION_MIN, max: MAGNIFICATION_MAX },

preflight: (obj, ctx) => [
...moduleTooSmallPreflight<AztecProps>('magnification')(obj, ctx),
...magnificationTooSmall(obj, ctx),
...ecLevelFindings(obj.props.ecLevel),
],

Expand Down
18 changes: 17 additions & 1 deletion packages/core/src/registry/maxicode.test.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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" }]);
Expand Down
7 changes: 7 additions & 0 deletions packages/core/src/registry/maxicode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<MaxicodeProps> = {
label: "Maxicode",
icon: "⬡",
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/types/preflight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ export const PREFLIGHT_SEVERITY: Record<PreflightKind, PreflightSeverity> = {
// 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.
Expand Down
72 changes: 72 additions & 0 deletions src/components/Canvas/BarcodeObject.scmAlarm.test.tsx
Original file line number Diff line number Diff line change
@@ -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 width={300} height={200} ref={(n) => { stage = n; }}>
<Layer>
<BarcodeObject
obj={obj}
scale={1}
dpmm={8}
offsetX={0}
offsetY={0}
isSelected={false}
onSelect={() => undefined}
onChange={() => undefined}
snap={(d) => d}
preBindingContent={preBindingContent}
/>
</Layer>
</Stage>,
);
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);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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}
/>
</Layer>
Expand Down
24 changes: 17 additions & 7 deletions src/components/Canvas/BarcodeObject.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -110,7 +110,14 @@ 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.
* 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
// fontFamily once the FontFace resolves. Keying the overlay on this
Expand Down Expand Up @@ -510,11 +517,14 @@ 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
// (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);
const fallbackError = scmOwned ? null : errorMsg;
const fbW = dotsToPx(200, scale, dpmm);
const fbH = dotsToPx(80, scale, dpmm);
return (
Expand Down
4 changes: 2 additions & 2 deletions src/components/Canvas/KonvaObject.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -465,7 +465,7 @@ export function KonvaObject(props_: Props) {
const shape =
obj.type === "line" ? <LineObject {...renderProps} obj={obj} /> :
obj.type === "image" ? <ImageObject {...renderProps} obj={obj} /> :
BARCODE_TYPES.has(obj.type) ? <BarcodeObject {...renderProps} /> :
BARCODE_TYPES.has(obj.type) ? <BarcodeObject {...renderProps} preBindingContent={getObjectStringContent(props_.obj) ?? ""} /> :
<KonvaObjectInner {...renderProps} />;

if (!showFallbackTint) return shape;
Expand Down
6 changes: 2 additions & 4 deletions src/components/Canvas/barcodePreflight.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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;
}
Expand Down
Loading