diff --git a/src/components/Canvas/BarcodeObject.symbologySwitch.test.tsx b/src/components/Canvas/BarcodeObject.symbologySwitch.test.tsx new file mode 100644 index 00000000..068bfbf7 --- /dev/null +++ b/src/components/Canvas/BarcodeObject.symbologySwitch.test.tsx @@ -0,0 +1,94 @@ +// @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 { naturalRect } from "./nodeRect"; +import { convertSymbologyMapper } from "../../lib/symbologySwitch"; +import { ObjectRegistry, type LeafType } from "@zplab/core/registry"; +import type { LeafObject } from "@zplab/core/registry"; + +beforeAll(() => { + // jsdom has no 2d context; Konva needs one for Stage/Layer plumbing. + // Geometry (getClientRect) never reads pixels back. + const noop = () => undefined; + HTMLCanvasElement.prototype.getContext = (() => + new Proxy({ + getImageData: () => ({ data: new Uint8ClampedArray(4) }), + measureText: () => ({ width: 10 }), + }, { + get: (target, prop) => (prop in target ? target[prop as keyof typeof target] : noop), + })) as unknown as typeof HTMLCanvasElement.prototype.getContext; +}); + +afterEach(cleanup); + +function makeBarcode(type: LeafType): LeafObject { + return { + id: "sym-test", + type, + x: 100, + y: 100, + props: { ...(ObjectRegistry[type].defaultProps as object) }, + } as LeafObject; +} + +const noopHandlers = { + isSelected: false, + onSelect: () => undefined, + onChange: () => undefined, + snap: (n: number) => n, +}; + +// Regression: the EAN/UPC getSelfRect patch survived Konva's Image reuse +// across symbology switches, freezing every later type's measured rect. +describe("symbology switch re-measures the node footprint", () => { + it("footprints track the type through a chain crossing EAN/UPC", () => { + let obj = makeBarcode("code128"); + const stageRef = { current: null as Konva.Stage | null }; + const ui = (o: LeafObject) => ( + { stageRef.current = n; }}> + + + + + ); + const { rerender } = render(ui(obj)); + const rectOf = () => { + const node = stageRef.current?.findOne("#sym-test"); + expect(node).toBeTruthy(); + return naturalRect(node!); + }; + const switchTo = (t: LeafType) => { + obj = convertSymbologyMapper(t)(obj) as LeafObject; + rerender(ui(obj)); + return rectOf(); + }; + // EAN first: this is the branch that arms the stale getSelfRect patch. + const ean = switchTo("ean13"); + // Direct EAN -> 2D (skips the HRI branch): must not inherit the EAN rect. + const qr = switchTo("qrcode"); + expect(qr.width).toBeCloseTo(qr.height, 3); + expect({ w: qr.width, h: qr.height }).not.toEqual({ w: ean.width, h: ean.height }); + // Chain on: each stop must produce its own footprint, not the previous one. + const dm = switchTo("datamatrix"); + expect(dm.width).toBeCloseTo(dm.height, 3); + expect(dm.width).not.toBeCloseTo(qr.width, 1); + const pdf = switchTo("pdf417"); + expect(pdf.width).toBeGreaterThan(pdf.height); + // Back to EAN and out through the 1D HRI branch (KImage reused, else-path + // restore): the next type must measure its own footprint, not the EAN rect. + const upc = switchTo("upca"); + const c128 = switchTo("code128"); + expect(c128.width).not.toBeCloseTo(upc.width, 1); + expect(c128.width).not.toBeCloseTo(pdf.width, 1); + }); +}); diff --git a/src/components/Canvas/BarcodeObject.tsx b/src/components/Canvas/BarcodeObject.tsx index fa6296e2..b51b1f20 100644 --- a/src/components/Canvas/BarcodeObject.tsx +++ b/src/components/Canvas/BarcodeObject.tsx @@ -42,6 +42,13 @@ import { QR_FT_MODULE_OFFSET, } from "@zplab/core/lib/bwipConstants"; +/** Drops the EAN/UPC getSelfRect patch so the prototype method resurfaces. + * Konva reuses the Image instance across symbology switches; without this + * every type after an EAN/UPC visit keeps measuring the stale bar rect. */ +function restoreSelfRect(node: Konva.Image | null): void { + if (node) delete (node as Partial).getSelfRect; +} + /** Resolve a registry value that may be a constant or a function of * moduleWidth. Mirrors the pattern formatHri uses for content. */ function resolveMwValue( @@ -425,15 +432,19 @@ export function BarcodeObject({ height={isEanUpc ? ub.barH + dotsToPx(EAN_TEXT_ZONE_DOTS, scale, dpmm) : ub.barH} - // Clip Transformer bbox to the bar area so resize handles - // ignore the guard tails. - ref={isEanUpc ? (node) => { - if (node) { + // Clip Transformer bbox to the bar area so resize handles ignore + // the guard tails; the non-EAN case must undo the patch on the + // reused instance (see restoreSelfRect). + ref={(node) => { + if (!node) return; + if (isEanUpc) { const w = ub.barW; const h = ub.barH; node.getSelfRect = () => ({ x: 0, y: 0, width: w, height: h }); + } else { + restoreSelfRect(node); } - } : undefined} + }} imageSmoothingEnabled={false} // EAN/UPC draws its selection stroke as the Rect below so // the highlight skips the guard tails. @@ -484,6 +495,7 @@ export function BarcodeObject({ crop={bitmapCrop} width={ub.barW} height={ub.barH} + ref={restoreSelfRect} imageSmoothingEnabled={false} stroke={isSelected ? colors.selection : undefined} strokeWidth={isSelected ? 2 : 0}