diff --git a/src/registry/aztec.tsx b/src/registry/aztec.tsx index 70e59a80..262fd882 100644 --- a/src/registry/aztec.tsx +++ b/src/registry/aztec.tsx @@ -2,13 +2,21 @@ import type { ObjectTypeDefinition } from "../types/ObjectType"; import { useT } from "../lib/useT"; import { inputCls, labelCls } from "../components/Properties/styles"; import { fieldPos, fdField } from "./zplHelpers"; +import { commitUniformScaleTransform } from "./transformHelpers"; import { type ZplRotation } from "./rotation"; import { RotationSelect } from "../components/Properties/RotationSelect"; import { NumberInput } from "../components/Properties/NumberInput"; +const MAGNIFICATION_MIN = 1; +const MAGNIFICATION_MAX = 10; +const EC_LEVEL_MIN = 0; +// NumberInput can't express the discontinuous AztecProps domain — use the +// highest valid value (Rune = 300) as the upper bound. +const EC_LEVEL_MAX = 300; + export interface AztecProps { content: string; - magnification: number; // 1–10, module size in dots + magnification: number; // module size in dots ecLevel: number; // 0=default, 1-99=error correction %, 101-104=compact, 201-232=full, 300=rune rotation: ZplRotation; } @@ -25,6 +33,8 @@ export const aztec: ObjectTypeDefinition = { }, defaultSize: { width: 200, height: 200 }, + commitTransform: commitUniformScaleTransform('magnification', MAGNIFICATION_MIN, MAGNIFICATION_MAX), + toZPL: (obj) => { const p = obj.props; // ^B0 a,b,c,d,e,f,g = orientation, magnification, ecic, errorControl, @@ -55,16 +65,16 @@ export const aztec: ObjectTypeDefinition = { onChange({ magnification })} /> onChange({ ecLevel })} /> diff --git a/src/registry/datamatrix.tsx b/src/registry/datamatrix.tsx index 183c539c..b72f4d04 100644 --- a/src/registry/datamatrix.tsx +++ b/src/registry/datamatrix.tsx @@ -2,14 +2,17 @@ import type { ObjectTypeDefinition } from '../types/ObjectType'; import { useT } from '../lib/useT'; import { inputCls, labelCls } from '../components/Properties/styles'; import { fieldPos, fdField } from './zplHelpers'; -import { clamp } from './transformHelpers'; +import { commitUniformScaleTransform } from './transformHelpers'; import { type ZplRotation } from './rotation'; import { RotationSelect } from '../components/Properties/RotationSelect'; import { NumberInput } from '../components/Properties/NumberInput'; +const DIMENSION_MIN = 1; +const DIMENSION_MAX = 12; + export interface DataMatrixProps { content: string; - dimension: number; // module size in dots (1–12) + dimension: number; // module size in dots quality: 0 | 50 | 80 | 140 | 200; // 0 = auto rotation: ZplRotation; } @@ -26,9 +29,7 @@ export const datamatrix: ObjectTypeDefinition = { }, defaultSize: { width: 150, height: 150 }, - commitTransform: (obj, { sx, sy }) => ({ - dimension: clamp(1, 12, Math.round(obj.props.dimension * Math.min(sx, sy))), - }), + commitTransform: commitUniformScaleTransform('dimension', DIMENSION_MIN, DIMENSION_MAX), toZPL: (obj) => { const p = obj.props; @@ -56,8 +57,8 @@ export const datamatrix: ObjectTypeDefinition = { onChange({ dimension })} /> diff --git a/src/registry/qrcode.tsx b/src/registry/qrcode.tsx index 8979c807..e9519430 100644 --- a/src/registry/qrcode.tsx +++ b/src/registry/qrcode.tsx @@ -2,14 +2,17 @@ import type { ObjectTypeDefinition } from '../types/ObjectType'; import { useT } from '../lib/useT'; import { inputCls, labelCls } from '../components/Properties/styles'; import { fieldPos, fdField } from './zplHelpers'; -import { clamp } from './transformHelpers'; +import { commitUniformScaleTransform } from './transformHelpers'; import { type ZplRotation } from './rotation'; import { RotationSelect } from '../components/Properties/RotationSelect'; import { NumberInput } from '../components/Properties/NumberInput'; +const MAGNIFICATION_MIN = 1; +const MAGNIFICATION_MAX = 10; + export interface QrCodeProps { content: string; - magnification: number; // 1–10, dot size per module + magnification: number; // dot size per module errorCorrection: 'H' | 'Q' | 'M' | 'L'; rotation: ZplRotation; } @@ -26,9 +29,7 @@ export const qrcode: ObjectTypeDefinition = { }, defaultSize: { width: 200, height: 200 }, - commitTransform: (obj, { sx, sy }) => ({ - magnification: clamp(1, 10, Math.round(obj.props.magnification * Math.min(sx, sy))), - }), + commitTransform: commitUniformScaleTransform('magnification', MAGNIFICATION_MIN, MAGNIFICATION_MAX), toZPL: (obj) => { const p = obj.props; @@ -67,8 +68,8 @@ export const qrcode: ObjectTypeDefinition = { onChange({ magnification })} /> diff --git a/src/registry/registry.test.ts b/src/registry/registry.test.ts index bef3af2b..74c878d8 100644 --- a/src/registry/registry.test.ts +++ b/src/registry/registry.test.ts @@ -414,4 +414,14 @@ describe('ObjectRegistry', () => { expect(validGroups.has(def.group)).toBe(true); } }); + + // 2D codes are always resizable on the canvas — without commitTransform a + // drag-resize silently has no effect (this was the aztec regression). + // Every code-2d entry must declare a commit handler. + it('every code-2d type has a commitTransform handler', () => { + for (const [key, def] of Object.entries(ObjectRegistry)) { + if (def.group !== 'code-2d') continue; + expect(def.commitTransform, `${key} is missing commitTransform`).toBeDefined(); + } + }); }); diff --git a/src/registry/transformHelpers.test.ts b/src/registry/transformHelpers.test.ts new file mode 100644 index 00000000..db1d5098 --- /dev/null +++ b/src/registry/transformHelpers.test.ts @@ -0,0 +1,33 @@ +import { describe, it, expect } from 'vitest'; +import { commitUniformScaleTransform } from './transformHelpers'; +import type { LabelObjectBase, TransformContext } from '../types/ObjectType'; + +const ctx = (sx: number, sy: number): TransformContext => ({ + sx, sy, snap: (n) => n, nodeHeight: 0, anchor: null, +}); + +interface Sample { magnification: number } +const obj = (mag: number): LabelObjectBase & { props: Sample } => ({ + id: 'id', type: 'sample', x: 0, y: 0, rotation: 0, props: { magnification: mag }, +}); + +describe('commitUniformScaleTransform', () => { + const handler = commitUniformScaleTransform('magnification', 1, 10); + + it('scales by min(sx, sy) so non-uniform drags stay inside the box', () => { + expect(handler(obj(4), ctx(2, 1.5))).toEqual({ magnification: 6 }); + expect(handler(obj(4), ctx(1.5, 2))).toEqual({ magnification: 6 }); + }); + + it('rounds to integer module sizes', () => { + expect(handler(obj(3), ctx(1.4, 1.4))).toEqual({ magnification: 4 }); + }); + + it('clamps to the configured maximum', () => { + expect(handler(obj(8), ctx(3, 3))).toEqual({ magnification: 10 }); + }); + + it('clamps to the configured minimum (collapsing drags)', () => { + expect(handler(obj(4), ctx(0, 0))).toEqual({ magnification: 1 }); + }); +}); diff --git a/src/registry/transformHelpers.ts b/src/registry/transformHelpers.ts index 2b786c13..f35a3527 100644 --- a/src/registry/transformHelpers.ts +++ b/src/registry/transformHelpers.ts @@ -5,6 +5,22 @@ export function clamp(min: number, max: number, value: number): number { return Math.max(min, Math.min(max, value)); } +/** + * Factory for commitTransform on uniformly-scaling 2D codes (QR, Aztec, + * DataMatrix): a single integer module-size prop scales by min(sx, sy) + * and clamps to [min, max]. The prop name and range vary per code, so they + * are closed over at registry-definition time. + */ +export function commitUniformScaleTransform< + K extends string, + P extends Record = Record, +>(propName: K, min: number, max: number) { + return (obj: LabelObjectBase & { props: P }, ctx: TransformContext): Partial

=> { + const next = clamp(min, max, Math.round(obj.props[propName] * Math.min(ctx.sx, ctx.sy))); + return { [propName]: next } as Partial

; + }; +} + interface WidthHeightProps { width: number; height: number;