Skip to content

Commit 8016196

Browse files
authored
Merge pull request #103 from u8array/fix/maxicode-defaultsize-and-approx-severity
fix(maxicode): dpmm-aware defaultSize + correct approx severity
2 parents 6f00b33 + 592120d commit 8016196

6 files changed

Lines changed: 88 additions & 19 deletions

File tree

‎src/components/Canvas/bwipConstants.ts‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,6 @@ export const BWIP_APPROX_SEVERITY: ReadonlyMap<string, BwipApproxSeverity> = new
7878
["gs1databar", "approx"],
7979
["code49", "unverified"],
8080
["codablock", "unverified"],
81-
["maxicode", "unverified"],
8281
]);
8382

8483
// Per-symbology spec module heights for GS1 DataBar. bwip-js renders most

‎src/components/Palette/ObjectPalette.tsx‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { useDraggable } from '@dnd-kit/core';
22
import { ObjectRegistry } from '../../registry';
33
import { PALETTE_GROUPS } from './paletteGroups';
4-
import type { ObjectGroup } from '../../types/ObjectType';
54
import { useT } from '../../lib/useT';
65
import { useLabelStore } from '../../store/labelStore';
76
import { mmToDots } from '../../lib/coordinates';
7+
import type { ObjectGroup, ObjectTypeDefinition } from '../../types/ObjectType';
8+
import { resolveDefaultSizeDots } from './resolveDefaultSize';
89
import { DragHandleIcon } from '../ui/DragHandleIcon';
910
import { CollapsibleSection } from '../ui/CollapsibleSection';
1011
import type { PaletteDragData } from '../../dnd/types';
@@ -16,7 +17,7 @@ interface PaletteEntryProps {
1617
type: string;
1718
icon: string;
1819
label: string;
19-
defaultSize: { width: number; height: number };
20+
defaultSize: ObjectTypeDefinition["defaultSize"];
2021
propsOverride?: object;
2122
}
2223

@@ -29,11 +30,12 @@ function PaletteEntry({ id, type, icon, label, defaultSize, propsOverride }: Pal
2930

3031
const handleDoubleClick = () => {
3132
const { label: labelConfig } = useLabelStore.getState();
33+
const size = resolveDefaultSizeDots(defaultSize, labelConfig);
3234
addObject(
3335
type,
3436
{
35-
x: Math.round(mmToDots(labelConfig.widthMm, labelConfig.dpmm) / 2 - defaultSize.width / 2),
36-
y: Math.round(mmToDots(labelConfig.heightMm, labelConfig.dpmm) / 2 - defaultSize.height / 2),
37+
x: Math.round(mmToDots(labelConfig.widthMm, labelConfig.dpmm) / 2 - size.width / 2),
38+
y: Math.round(mmToDots(labelConfig.heightMm, labelConfig.dpmm) / 2 - size.height / 2),
3739
},
3840
propsOverride,
3941
);
@@ -67,7 +69,7 @@ interface ResolvedEntry {
6769
type: string;
6870
icon: string;
6971
label: string;
70-
defaultSize: { width: number; height: number };
72+
defaultSize: ObjectTypeDefinition["defaultSize"];
7173
propsOverride?: object;
7274
}
7375

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { describe, it, expect } from "vitest";
2+
import { resolveDefaultSizeDots } from "./resolveDefaultSize";
3+
import type { LabelConfig } from "../../types/ObjectType";
4+
5+
const labelAt = (dpmm: number): LabelConfig =>
6+
({ widthMm: 100, heightMm: 50, dpmm }) as LabelConfig;
7+
8+
describe("resolveDefaultSizeDots", () => {
9+
it("passes a dots-shape through unchanged regardless of dpmm", () => {
10+
const dots = { width: 200, height: 40 };
11+
expect(resolveDefaultSizeDots(dots, labelAt(8))).toEqual({ width: 200, height: 40 });
12+
expect(resolveDefaultSizeDots(dots, labelAt(12))).toEqual({ width: 200, height: 40 });
13+
});
14+
15+
it("converts mm-shape to dots at the label's dpmm", () => {
16+
const mm = { widthMm: 28.14, heightMm: 26.91 };
17+
// mmToDots rounds, so spell out the expected integer dot values.
18+
expect(resolveDefaultSizeDots(mm, labelAt(8))).toEqual({ width: 225, height: 215 });
19+
expect(resolveDefaultSizeDots(mm, labelAt(12))).toEqual({ width: 338, height: 323 });
20+
});
21+
22+
it("keeps the physical footprint constant across dpmm (no drift)", () => {
23+
const mm = { widthMm: 28.14, heightMm: 26.91 };
24+
const at8 = resolveDefaultSizeDots(mm, labelAt(8));
25+
const at12 = resolveDefaultSizeDots(mm, labelAt(12));
26+
// dots / dpmm == mm: rounded-trip stays within 0.1 mm of the input
27+
expect(at8.width / 8).toBeCloseTo(28.14, 1);
28+
expect(at12.width / 12).toBeCloseTo(28.14, 1);
29+
});
30+
});
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { mmToDots } from "../../lib/coordinates";
2+
import type { LabelConfig, ObjectTypeDefinition } from "../../types/ObjectType";
3+
4+
/** Resolve a registry `defaultSize` declaration to dot units against
5+
* the active label config. Spec-fixed-physical-size symbols (e.g.
6+
* Maxicode) declare `widthMm`/`heightMm` so their footprint stays
7+
* correct across dpmm; all other types declare `width`/`height`
8+
* directly in dots. Lives next to the palette since that is the
9+
* only current drop site, but is its own module so the resolver
10+
* stays testable in isolation and any future drop path can reuse
11+
* it without depending on the palette component. */
12+
export function resolveDefaultSizeDots(
13+
defaultSize: ObjectTypeDefinition["defaultSize"],
14+
label: LabelConfig,
15+
): { width: number; height: number } {
16+
if ("widthMm" in defaultSize) {
17+
return {
18+
width: mmToDots(defaultSize.widthMm, label.dpmm),
19+
height: mmToDots(defaultSize.heightMm, label.dpmm),
20+
};
21+
}
22+
// Shallow-copy the dots-branch so callers can't accidentally
23+
// mutate the shared registry object via the returned reference.
24+
return { ...defaultSize };
25+
}

‎src/registry/maxicode.tsx‎

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@ import { fieldPos, fdFieldFor } from "./zplHelpers";
66
import { type ZplRotation } from "./rotation";
77
import { RotationSelect } from "../components/Properties/RotationSelect";
88

9-
/** Maxicode is a fixed-physical-size 2D symbology (1.11" × 1.054"
10-
* at standard scale, ~28 × 26 mm). Unlike QR or DataMatrix it has
11-
* no per-module magnification — the printer always renders a
12-
* single fixed size. Therefore no `commitTransform` and no
13-
* magnification prop; only the data, mode, and rotation vary. */
9+
/** Maxicode is a fixed-physical-size 2D symbology. Per ISO/IEC
10+
* 16023 §4.11 the printed footprint is 28.14 × 26.91 mm at
11+
* standard module size (1.11 × 1.054 inch). Unlike QR or
12+
* DataMatrix it has no per-module magnification: the printer
13+
* always renders the same physical dimensions regardless of
14+
* dpmm. Therefore no `commitTransform` and no magnification
15+
* prop; only the data, mode, and rotation vary. */
1416

1517
/** All modes the spec defines. Editor exposes every one and lets
1618
* bwip-js validate the (content, mode) pair on encode:
@@ -27,9 +29,6 @@ import { RotationSelect } from "../components/Properties/RotationSelect";
2729
* shown instead of an error. */
2830
const ALL_MODES = [2, 3, 4, 5, 6] as const;
2931

30-
/** Default physical footprint of a Maxicode at 8 dpmm: spec is
31-
* ~28 × 26.9 mm; converted to dots that's 224 × 216. */
32-
const MAXICODE_DEFAULT_FOOTPRINT = { width: 224, height: 216 } as const;
3332

3433
/** Mode 4 = standard symbol with arbitrary payload, the only mode
3534
* most users without UPS-domain knowledge can reach for without
@@ -52,10 +51,11 @@ export const maxicode: ObjectTypeDefinition<MaxicodeProps> = {
5251
mode: MAXICODE_DEFAULT_MODE,
5352
rotation: "N",
5453
},
55-
// Fixed-size symbol — the printer always renders at standard
56-
// Maxicode dimensions, so the resize transformer is disabled
57-
// (heightLocked) and no commitTransform is needed.
58-
defaultSize: MAXICODE_DEFAULT_FOOTPRINT,
54+
// Spec-fixed physical footprint per ISO/IEC 16023. Declared in
55+
// mm so the palette resolves to the active label's dpmm at drop
56+
// time; `heightLocked` then disables both transformer axes since
57+
// the symbology has no resize semantics.
58+
defaultSize: { widthMm: 28.14, heightMm: 26.91 },
5959
heightLocked: true,
6060

6161
toZPL: (obj, ctx) => {

‎src/types/ObjectType.ts‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,20 @@ export interface ObjectTypeDefinition<P extends object = object> {
198198
icon: string;
199199
group: ObjectGroup;
200200
defaultProps: P;
201-
defaultSize: { width: number; height: number };
201+
/** Default footprint at palette-drop time. Two shapes:
202+
* `{ width, height }` in dots (most types: an editor
203+
* default chosen for visual size,
204+
* not a spec quantity)
205+
* `{ widthMm, heightMm }` in millimetres (spec-fixed
206+
* physical-size types like Maxicode,
207+
* where the printed footprint is set
208+
* by the symbology spec regardless
209+
* of dpmm)
210+
* The palette resolves the active variant against the current
211+
* label's dpmm so centring works at any resolution. */
212+
defaultSize:
213+
| { width: number; height: number }
214+
| { widthMm: number; heightMm: number };
202215
/**
203216
* True if the rendered height is fixed by the symbology spec rather than the
204217
* `height` prop (e.g. GS1 Databar Omnidirectional). The transformer disables

0 commit comments

Comments
 (0)