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}