From 49e370695101ec132e0dbeed7892bc96c31fee2d Mon Sep 17 00:00:00 2001 From: u8array Date: Mon, 4 May 2026 22:42:57 +0200 Subject: [PATCH] Fix ellipse scaling and QR code Y offset Exclude selection stroke from the bbox to prevent sub-dot drift. Adjust Konva transformer logic to correctly calculate the rendered top-left for ellipses, which are center-anchored. Introduce a new utility function `modelPositionFromRenderedTopLeft` to handle per-object rendering offsets, specifically addressing the +10 dot Y-offset for QR codes when `positionType` is not 'FT'. This ensures accurate model position updates after transformations. --- src/components/Canvas/LabelCanvas.tsx | 5 ++ .../Canvas/hooks/useKonvaTransformer.ts | 14 ++-- .../Canvas/transformPosition.test.ts | 65 +++++++++++++++++++ src/components/Canvas/transformPosition.ts | 28 ++++++++ 4 files changed, 107 insertions(+), 5 deletions(-) create mode 100644 src/components/Canvas/transformPosition.test.ts create mode 100644 src/components/Canvas/transformPosition.ts diff --git a/src/components/Canvas/LabelCanvas.tsx b/src/components/Canvas/LabelCanvas.tsx index ce766ec7..ca49801a 100644 --- a/src/components/Canvas/LabelCanvas.tsx +++ b/src/components/Canvas/LabelCanvas.tsx @@ -621,6 +621,11 @@ export function LabelCanvas({ onTransformStart={onTransformStart} boundBoxFunc={boundBoxFunc} onTransformEnd={onTransformEnd} + // Exclude selection stroke from the bbox; otherwise scale-aware + // stroke padding leaks into the resize math and produces sub-dot + // drift in node.x()/y() that surfaces as 1-dot ZPL coordinate + // jumps under pxToDots rounding. + ignoreStroke /> diff --git a/src/components/Canvas/hooks/useKonvaTransformer.ts b/src/components/Canvas/hooks/useKonvaTransformer.ts index 1c8f1de6..1942a1d6 100644 --- a/src/components/Canvas/hooks/useKonvaTransformer.ts +++ b/src/components/Canvas/hooks/useKonvaTransformer.ts @@ -13,6 +13,7 @@ import { positionDidMove, type BoundingBox, } from "../transformerGeometry"; +import { modelPositionFromRenderedTopLeft } from "../transformPosition"; interface Options { transformerRef: React.RefObject; @@ -151,14 +152,17 @@ export function useKonvaTransformer({ sy, isCenterAnchored, ); - const rawX = pxToDots(topLeft.x - objectsOffsetX, scale, dpmm); - const rawY = pxToDots(topLeft.y - labelOffsetY, scale, dpmm); - // Only apply snap to the position when the resize actually moved it + const renderedXDots = pxToDots(topLeft.x - objectsOffsetX, scale, dpmm); + const renderedYDots = pxToDots(topLeft.y - labelOffsetY, scale, dpmm); + // Invert per-type render offsets (e.g. QR's hardcoded +10 dot Y) so the + // stored model position matches what BarcodeObject.handleDragEnd produces. + const modelPos = modelPositionFromRenderedTopLeft(obj, renderedXDots, renderedYDots); + // Only apply snap when the resize actually moved the position // (e.g. dragging the top-left handle). Anchored-corner drags must keep // the original position so off-grid shapes don't snap as a side-effect. const pos = { - x: positionDidMove(rawX, obj.x) ? snap(rawX) : obj.x, - y: positionDidMove(rawY, obj.y) ? snap(rawY) : obj.y, + x: positionDidMove(modelPos.x, obj.x) ? snap(modelPos.x) : obj.x, + y: positionDidMove(modelPos.y, obj.y) ? snap(modelPos.y) : obj.y, }; const commit = ObjectRegistry[obj.type]?.commitTransform; if (commit) { diff --git a/src/components/Canvas/transformPosition.test.ts b/src/components/Canvas/transformPosition.test.ts new file mode 100644 index 00000000..bae49176 --- /dev/null +++ b/src/components/Canvas/transformPosition.test.ts @@ -0,0 +1,65 @@ +import { describe, it, expect } from "vitest"; +import { modelPositionFromRenderedTopLeft } from "./transformPosition"; +import { QR_FO_Y_OFFSET_DOTS } from "./bwipConstants"; +import type { LabelObject } from "../../registry"; + +const qrFo: LabelObject = { + id: "q1", + type: "qrcode", + x: 0, + y: 0, + rotation: 0, + positionType: "FO", + props: { content: "x", magnification: 4, errorCorrection: "Q" }, +}; + +const ellipse: LabelObject = { + id: "e1", + type: "ellipse", + x: 0, + y: 0, + rotation: 0, + props: { width: 150, height: 100, thickness: 3, filled: false, color: "B" }, +}; + +describe("modelPositionFromRenderedTopLeft", () => { + it("subtracts QR FO Y offset for QR codes with positionType=FO", () => { + expect(modelPositionFromRenderedTopLeft(qrFo, 100, 200)).toEqual({ + x: 100, + y: 200 - QR_FO_Y_OFFSET_DOTS, + }); + }); + + it("subtracts QR FO Y offset when positionType is undefined (defaults to FO)", () => { + const obj = { ...qrFo, positionType: undefined }; + expect(modelPositionFromRenderedTopLeft(obj, 100, 200)).toEqual({ + x: 100, + y: 200 - QR_FO_Y_OFFSET_DOTS, + }); + }); + + it("does not subtract Y offset for QR codes with positionType=FT", () => { + const obj: LabelObject = { ...qrFo, positionType: "FT" }; + expect(modelPositionFromRenderedTopLeft(obj, 100, 200)).toEqual({ + x: 100, + y: 200, + }); + }); + + it("returns rendered position unchanged for ellipse", () => { + expect(modelPositionFromRenderedTopLeft(ellipse, 50, 80)).toEqual({ + x: 50, + y: 80, + }); + }); + + it("preserves x for QR codes (only Y is offset)", () => { + expect(modelPositionFromRenderedTopLeft(qrFo, -7, 17).x).toBe(-7); + }); + + it("is idempotent under repeated application for non-QR types", () => { + const once = modelPositionFromRenderedTopLeft(ellipse, 10, 20); + const twice = modelPositionFromRenderedTopLeft(ellipse, once.x, once.y); + expect(twice).toEqual(once); + }); +}); diff --git a/src/components/Canvas/transformPosition.ts b/src/components/Canvas/transformPosition.ts new file mode 100644 index 00000000..5855faba --- /dev/null +++ b/src/components/Canvas/transformPosition.ts @@ -0,0 +1,28 @@ +import type { LabelObject } from "../../registry"; +import { QR_FO_Y_OFFSET_DOTS } from "./bwipConstants"; + +/** + * Convert the rendered top-left of a Konva node back to the object's stored + * model position, in dots. Inverts per-type render offsets that the renderer + * adds at draw time. + * + * Currently handles: + * - QR (FO): subtracts the hardcoded +10 dot Y-offset that BarcodeObject adds + * to compensate for Zebra firmware artifact. + * + * Used by onTransformEnd to mirror the rendered→model conversion that + * BarcodeObject.handleDragEnd performs for drag. + * + * Note: Field-Typeset (FT) corrections for barcode resize are not yet + * implemented here; FT-mode barcode resize still has known position drift. + */ +export function modelPositionFromRenderedTopLeft( + obj: LabelObject, + renderedXDots: number, + renderedYDots: number, +): { x: number; y: number } { + if (obj.type === "qrcode" && obj.positionType !== "FT") { + return { x: renderedXDots, y: renderedYDots - QR_FO_Y_OFFSET_DOTS }; + } + return { x: renderedXDots, y: renderedYDots }; +}