Skip to content

Commit a7111f9

Browse files
authored
Merge pull request #4 from u8array/fix/object-resize-jump
Fix QR code Y-offset drift and 1-dot resize drift
2 parents e924e1c + 49e3706 commit a7111f9

4 files changed

Lines changed: 107 additions & 5 deletions

File tree

‎src/components/Canvas/LabelCanvas.tsx‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -621,6 +621,11 @@ export function LabelCanvas({
621621
onTransformStart={onTransformStart}
622622
boundBoxFunc={boundBoxFunc}
623623
onTransformEnd={onTransformEnd}
624+
// Exclude selection stroke from the bbox; otherwise scale-aware
625+
// stroke padding leaks into the resize math and produces sub-dot
626+
// drift in node.x()/y() that surfaces as 1-dot ZPL coordinate
627+
// jumps under pxToDots rounding.
628+
ignoreStroke
624629
/>
625630
</Layer>
626631

‎src/components/Canvas/hooks/useKonvaTransformer.ts‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
positionDidMove,
1414
type BoundingBox,
1515
} from "../transformerGeometry";
16+
import { modelPositionFromRenderedTopLeft } from "../transformPosition";
1617

1718
interface Options {
1819
transformerRef: React.RefObject<Konva.Transformer | null>;
@@ -151,14 +152,17 @@ export function useKonvaTransformer({
151152
sy,
152153
isCenterAnchored,
153154
);
154-
const rawX = pxToDots(topLeft.x - objectsOffsetX, scale, dpmm);
155-
const rawY = pxToDots(topLeft.y - labelOffsetY, scale, dpmm);
156-
// Only apply snap to the position when the resize actually moved it
155+
const renderedXDots = pxToDots(topLeft.x - objectsOffsetX, scale, dpmm);
156+
const renderedYDots = pxToDots(topLeft.y - labelOffsetY, scale, dpmm);
157+
// Invert per-type render offsets (e.g. QR's hardcoded +10 dot Y) so the
158+
// stored model position matches what BarcodeObject.handleDragEnd produces.
159+
const modelPos = modelPositionFromRenderedTopLeft(obj, renderedXDots, renderedYDots);
160+
// Only apply snap when the resize actually moved the position
157161
// (e.g. dragging the top-left handle). Anchored-corner drags must keep
158162
// the original position so off-grid shapes don't snap as a side-effect.
159163
const pos = {
160-
x: positionDidMove(rawX, obj.x) ? snap(rawX) : obj.x,
161-
y: positionDidMove(rawY, obj.y) ? snap(rawY) : obj.y,
164+
x: positionDidMove(modelPos.x, obj.x) ? snap(modelPos.x) : obj.x,
165+
y: positionDidMove(modelPos.y, obj.y) ? snap(modelPos.y) : obj.y,
162166
};
163167
const commit = ObjectRegistry[obj.type]?.commitTransform;
164168
if (commit) {
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { describe, it, expect } from "vitest";
2+
import { modelPositionFromRenderedTopLeft } from "./transformPosition";
3+
import { QR_FO_Y_OFFSET_DOTS } from "./bwipConstants";
4+
import type { LabelObject } from "../../registry";
5+
6+
const qrFo: LabelObject = {
7+
id: "q1",
8+
type: "qrcode",
9+
x: 0,
10+
y: 0,
11+
rotation: 0,
12+
positionType: "FO",
13+
props: { content: "x", magnification: 4, errorCorrection: "Q" },
14+
};
15+
16+
const ellipse: LabelObject = {
17+
id: "e1",
18+
type: "ellipse",
19+
x: 0,
20+
y: 0,
21+
rotation: 0,
22+
props: { width: 150, height: 100, thickness: 3, filled: false, color: "B" },
23+
};
24+
25+
describe("modelPositionFromRenderedTopLeft", () => {
26+
it("subtracts QR FO Y offset for QR codes with positionType=FO", () => {
27+
expect(modelPositionFromRenderedTopLeft(qrFo, 100, 200)).toEqual({
28+
x: 100,
29+
y: 200 - QR_FO_Y_OFFSET_DOTS,
30+
});
31+
});
32+
33+
it("subtracts QR FO Y offset when positionType is undefined (defaults to FO)", () => {
34+
const obj = { ...qrFo, positionType: undefined };
35+
expect(modelPositionFromRenderedTopLeft(obj, 100, 200)).toEqual({
36+
x: 100,
37+
y: 200 - QR_FO_Y_OFFSET_DOTS,
38+
});
39+
});
40+
41+
it("does not subtract Y offset for QR codes with positionType=FT", () => {
42+
const obj: LabelObject = { ...qrFo, positionType: "FT" };
43+
expect(modelPositionFromRenderedTopLeft(obj, 100, 200)).toEqual({
44+
x: 100,
45+
y: 200,
46+
});
47+
});
48+
49+
it("returns rendered position unchanged for ellipse", () => {
50+
expect(modelPositionFromRenderedTopLeft(ellipse, 50, 80)).toEqual({
51+
x: 50,
52+
y: 80,
53+
});
54+
});
55+
56+
it("preserves x for QR codes (only Y is offset)", () => {
57+
expect(modelPositionFromRenderedTopLeft(qrFo, -7, 17).x).toBe(-7);
58+
});
59+
60+
it("is idempotent under repeated application for non-QR types", () => {
61+
const once = modelPositionFromRenderedTopLeft(ellipse, 10, 20);
62+
const twice = modelPositionFromRenderedTopLeft(ellipse, once.x, once.y);
63+
expect(twice).toEqual(once);
64+
});
65+
});
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import type { LabelObject } from "../../registry";
2+
import { QR_FO_Y_OFFSET_DOTS } from "./bwipConstants";
3+
4+
/**
5+
* Convert the rendered top-left of a Konva node back to the object's stored
6+
* model position, in dots. Inverts per-type render offsets that the renderer
7+
* adds at draw time.
8+
*
9+
* Currently handles:
10+
* - QR (FO): subtracts the hardcoded +10 dot Y-offset that BarcodeObject adds
11+
* to compensate for Zebra firmware artifact.
12+
*
13+
* Used by onTransformEnd to mirror the rendered→model conversion that
14+
* BarcodeObject.handleDragEnd performs for drag.
15+
*
16+
* Note: Field-Typeset (FT) corrections for barcode resize are not yet
17+
* implemented here; FT-mode barcode resize still has known position drift.
18+
*/
19+
export function modelPositionFromRenderedTopLeft(
20+
obj: LabelObject,
21+
renderedXDots: number,
22+
renderedYDots: number,
23+
): { x: number; y: number } {
24+
if (obj.type === "qrcode" && obj.positionType !== "FT") {
25+
return { x: renderedXDots, y: renderedYDots - QR_FO_Y_OFFSET_DOTS };
26+
}
27+
return { x: renderedXDots, y: renderedYDots };
28+
}

0 commit comments

Comments
 (0)