From 9832dbf3d919137e24ce06801b71196134d37623 Mon Sep 17 00:00:00 2001 From: u8array Date: Tue, 12 May 2026 00:02:19 +0200 Subject: [PATCH 1/3] feat(barcode): resize 1D barcodes by module width via side anchors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enables middle-left / middle-right transformer anchors for 1D barcodes (whose width is content × moduleWidth × byRatio and has no free-form width property). The bbox snaps to integer moduleWidth multiples during the drag so the bitmap only jumps when the cursor crosses a module boundary, and commitBarcodeWidthHeightTransform records the new moduleWidth (clamped to the ZPL ^BY range 1..10) alongside any sy-driven height change. Rotation is handled by Konva natively: the anchors are defined in node-local coords, so the transformer renders them at the visually correct end of the rotated bbox and sx still applies to the local bar-axis. FT-positioned barcodes have a pre-existing position drift on resize (documented in transformPosition.ts) that this commit does not yet fix — follow-up. --- .../Canvas/hooks/useKonvaTransformer.ts | 42 ++++++++++++++++++- src/registry/barcode1d.tsx | 10 ++++- src/registry/transformHelpers.ts | 20 +++++++++ 3 files changed, 69 insertions(+), 3 deletions(-) diff --git a/src/components/Canvas/hooks/useKonvaTransformer.ts b/src/components/Canvas/hooks/useKonvaTransformer.ts index 0007fc30..784ddb35 100644 --- a/src/components/Canvas/hooks/useKonvaTransformer.ts +++ b/src/components/Canvas/hooks/useKonvaTransformer.ts @@ -14,6 +14,7 @@ import { type BoundingBox, } from "../transformerGeometry"; import { modelPositionFromRenderedTopLeft } from "../transformPosition"; +import { clamp } from "../../../registry/transformHelpers"; import { computeResizeSnap, deriveActiveEdges, @@ -122,6 +123,11 @@ export function useKonvaTransformer({ // Captures node height and rowHeight at drag start so boundBoxFunc uses a // fixed step size throughout the entire drag session. const transformAnchorRef = useRef<{ nodeHeight: number; rowHeight: number } | null>(null); + // 1D-barcode width anchor: pixels per one unit of moduleWidth at drag + // start, so boundBoxFunc can snap newBox.width to integer-moduleWidth + // multiples (the resulting visual: the bbox jumps by one module-pixel + // step whenever the cursor crosses a module boundary). + const barcodeAnchorRef = useRef<{ widthPerModule: number } | null>(null); // Captures the bbox at transform start so deriveActiveEdges can detect which // edges are moving relative to the start state (oldBox in boundBoxFunc is the // previous frame, which would always look "everything moved"). @@ -190,7 +196,16 @@ export function useKonvaTransformer({ : ObjectRegistry[singleType]?.heightLocked ? [] : BARCODE_1D_TYPES.has(singleType) - ? ["top-center", "bottom-center"] + ? [ + "top-center", + "bottom-center", + // middle-left / middle-right drag the module-width axis. + // The bar count is fixed by content, so the resulting width + // snaps to integer-moduleWidth multiples in boundBoxFunc and + // is committed via commitBarcodeWidthHeightTransform. + "middle-left", + "middle-right", + ] : isUniformScale ? ["top-left", "top-right", "bottom-left", "bottom-right"] : undefined; @@ -199,6 +214,7 @@ export function useKonvaTransformer({ /** Reset all transform-time state. Idempotent; safe to call from any exit path. */ function cleanupTransformState() { transformAnchorRef.current = null; + barcodeAnchorRef.current = null; transformStartBboxRef.current = null; othersSnapshotRef.current = []; setGuides([]); @@ -213,6 +229,19 @@ export function useKonvaTransformer({ transformAnchorRef.current = obj && STACKED_2D_TYPES.has(obj.type) ? { nodeHeight: node.height(), rowHeight: (obj.props as { rowHeight: number }).rowHeight } : null; + // 1D barcodes derive widthPerModule from the rendered node width and + // the stored moduleWidth: the bar count is content-determined and + // stays constant for the duration of the drag, so a single division + // gives us the px-per-step we need in boundBoxFunc. + if (obj && BARCODE_1D_TYPES.has(obj.type) && !ObjectRegistry[obj.type]?.heightLocked) { + const moduleWidth = (obj.props as { moduleWidth: number }).moduleWidth; + const nodeWidth = node.width(); + barcodeAnchorRef.current = moduleWidth > 0 && nodeWidth > 0 + ? { widthPerModule: nodeWidth / moduleWidth } + : null; + } else { + barcodeAnchorRef.current = null; + } // startBbox is captured lazily on the first boundBoxFunc call — Konva // passes those bboxes in the transformer's frame, which on rotated // parents differs from getClientRect's stage frame. @@ -250,6 +279,17 @@ export function useKonvaTransformer({ }; } const startBbox = transformStartBboxRef.current; + // 1D-barcode width snap: replace the cursor-following width with the + // closest integer-moduleWidth-multiple width. The active edge logic + // downstream (pinInactiveEdges) shifts x so the held edge stays put. + if (barcodeAnchorRef.current) { + const step = barcodeAnchorRef.current.widthPerModule; + if (step > 0) { + const rawModules = newBox.width / step; + const snappedModules = clamp(1, 10, Math.round(rawModules)); + newBox = { ...newBox, width: snappedModules * step }; + } + } let bbox = applyHeightSnap(oldBox, newBox, dotPx, transformAnchorRef.current); if (objectSnapEnabled && isFreeResize && startBbox) { const snapped = applyResizeObjectSnap(bbox, startBbox, othersSnapshotRef.current, labelRect); diff --git a/src/registry/barcode1d.tsx b/src/registry/barcode1d.tsx index 1f9fcdd1..a04f960c 100644 --- a/src/registry/barcode1d.tsx +++ b/src/registry/barcode1d.tsx @@ -3,7 +3,7 @@ import { useT } from '../lib/useT'; import type { Translations } from '../locales'; import { inputCls, labelCls } from '../components/Properties/styles'; import { fieldPos, fdField } from './zplHelpers'; -import { commitHeightTransform } from './transformHelpers'; +import { commitBarcodeWidthHeightTransform } from './transformHelpers'; import { filterContent, type ContentSpec } from './contentSpec'; import { type ZplRotation } from './rotation'; import { RotationSelect } from '../components/Properties/RotationSelect'; @@ -71,7 +71,13 @@ export function createBarcode1D(config: Barcode1DConfig): ObjectTypeDefinition { // Normalize printInterpretation for symbologies that have no HRI in ZPL diff --git a/src/registry/transformHelpers.ts b/src/registry/transformHelpers.ts index f35a3527..bb695528 100644 --- a/src/registry/transformHelpers.ts +++ b/src/registry/transformHelpers.ts @@ -50,6 +50,26 @@ export function commitHeightTransform

( } as Partial

; } +/** + * commitTransform for 1D barcodes that also expose `moduleWidth`. Vertical + * drag scales bar height (sy), horizontal drag scales the module width (sx); + * the latter is clamped to the ZPL `^BY` range [1, 10]. The boundBoxFunc + * snaps the bbox to integer-moduleWidth widths during the drag, so this + * commit just records the final integer step. + */ +export function commitBarcodeWidthHeightTransform< + P extends { height: number; moduleWidth: number }, +>( + obj: LabelObjectBase & { props: P }, + ctx: TransformContext, +): Partial

{ + const { sx, sy, snap } = ctx; + return { + height: Math.max(1, snap(Math.round(obj.props.height * sy))), + moduleWidth: clamp(1, 10, Math.round(obj.props.moduleWidth * sx)), + } as Partial

; +} + interface Stacked2DProps { rowHeight: number; moduleWidth: number; From 4dada68dd7d3b62451a91773408aa95c3f61bb30 Mon Sep 17 00:00:00 2001 From: u8array Date: Tue, 12 May 2026 00:10:32 +0200 Subject: [PATCH 2/3] fix(transform): FT-mode barcode resize, drop dead step-snap code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two changes from the post-feature audit: 1. FT-positioned 1D barcodes used to drift upward on every resize: the transformer-end logic treated the rendered bbox top as the new obj.y, but ^FT anchors at the bar baseline (= bbox bottom). The stored y was therefore the *previous* top, and the next render subtracted bar-height again, shifting the symbol upward by a bar height per resize. Pass the post-resize bar height through to modelPositionFromRenderedTopLeft and add it back when the object is FT-positioned. 2. The barcodeAnchorRef + boundBoxFunc width snap that was meant to make the bbox jump in integer-moduleWidth steps during the drag never actually fired — node.width() returns 0 for a Konva.Group so the guard always fell through. Free-form stretch during the drag with commit-time rounding turns out to be the more natural UX anyway (matches how every other shape resize behaves), so the dead code path is removed rather than fixed. Drops the now-unused commitHeightTransform helper at the same time. --- .../Canvas/hooks/useKonvaTransformer.ts | 51 +++++++------------ src/components/Canvas/transformPosition.ts | 19 +++++-- src/registry/transformHelpers.ts | 22 ++------ 3 files changed, 38 insertions(+), 54 deletions(-) diff --git a/src/components/Canvas/hooks/useKonvaTransformer.ts b/src/components/Canvas/hooks/useKonvaTransformer.ts index 784ddb35..826592fd 100644 --- a/src/components/Canvas/hooks/useKonvaTransformer.ts +++ b/src/components/Canvas/hooks/useKonvaTransformer.ts @@ -14,7 +14,6 @@ import { type BoundingBox, } from "../transformerGeometry"; import { modelPositionFromRenderedTopLeft } from "../transformPosition"; -import { clamp } from "../../../registry/transformHelpers"; import { computeResizeSnap, deriveActiveEdges, @@ -123,11 +122,6 @@ export function useKonvaTransformer({ // Captures node height and rowHeight at drag start so boundBoxFunc uses a // fixed step size throughout the entire drag session. const transformAnchorRef = useRef<{ nodeHeight: number; rowHeight: number } | null>(null); - // 1D-barcode width anchor: pixels per one unit of moduleWidth at drag - // start, so boundBoxFunc can snap newBox.width to integer-moduleWidth - // multiples (the resulting visual: the bbox jumps by one module-pixel - // step whenever the cursor crosses a module boundary). - const barcodeAnchorRef = useRef<{ widthPerModule: number } | null>(null); // Captures the bbox at transform start so deriveActiveEdges can detect which // edges are moving relative to the start state (oldBox in boundBoxFunc is the // previous frame, which would always look "everything moved"). @@ -201,8 +195,9 @@ export function useKonvaTransformer({ "bottom-center", // middle-left / middle-right drag the module-width axis. // The bar count is fixed by content, so the resulting width - // snaps to integer-moduleWidth multiples in boundBoxFunc and - // is committed via commitBarcodeWidthHeightTransform. + // is rounded to a valid ZPL ^BY moduleWidth (1..10) in + // commitBarcodeWidthHeightTransform on release; during the + // drag the bitmap stretches free-form for visual feedback. "middle-left", "middle-right", ] @@ -214,7 +209,6 @@ export function useKonvaTransformer({ /** Reset all transform-time state. Idempotent; safe to call from any exit path. */ function cleanupTransformState() { transformAnchorRef.current = null; - barcodeAnchorRef.current = null; transformStartBboxRef.current = null; othersSnapshotRef.current = []; setGuides([]); @@ -229,19 +223,6 @@ export function useKonvaTransformer({ transformAnchorRef.current = obj && STACKED_2D_TYPES.has(obj.type) ? { nodeHeight: node.height(), rowHeight: (obj.props as { rowHeight: number }).rowHeight } : null; - // 1D barcodes derive widthPerModule from the rendered node width and - // the stored moduleWidth: the bar count is content-determined and - // stays constant for the duration of the drag, so a single division - // gives us the px-per-step we need in boundBoxFunc. - if (obj && BARCODE_1D_TYPES.has(obj.type) && !ObjectRegistry[obj.type]?.heightLocked) { - const moduleWidth = (obj.props as { moduleWidth: number }).moduleWidth; - const nodeWidth = node.width(); - barcodeAnchorRef.current = moduleWidth > 0 && nodeWidth > 0 - ? { widthPerModule: nodeWidth / moduleWidth } - : null; - } else { - barcodeAnchorRef.current = null; - } // startBbox is captured lazily on the first boundBoxFunc call — Konva // passes those bboxes in the transformer's frame, which on rotated // parents differs from getClientRect's stage frame. @@ -279,17 +260,6 @@ export function useKonvaTransformer({ }; } const startBbox = transformStartBboxRef.current; - // 1D-barcode width snap: replace the cursor-following width with the - // closest integer-moduleWidth-multiple width. The active edge logic - // downstream (pinInactiveEdges) shifts x so the held edge stays put. - if (barcodeAnchorRef.current) { - const step = barcodeAnchorRef.current.widthPerModule; - if (step > 0) { - const rawModules = newBox.width / step; - const snappedModules = clamp(1, 10, Math.round(rawModules)); - newBox = { ...newBox, width: snappedModules * step }; - } - } let bbox = applyHeightSnap(oldBox, newBox, dotPx, transformAnchorRef.current); if (objectSnapEnabled && isFreeResize && startBbox) { const snapped = applyResizeObjectSnap(bbox, startBbox, othersSnapshotRef.current, labelRect); @@ -347,9 +317,22 @@ export function useKonvaTransformer({ ); const renderedXDots = pxToDots(topLeft.x - objectsOffsetX, scale, dpmm); const renderedYDots = pxToDots(topLeft.y - labelOffsetY, scale, dpmm); + // For FT-anchored 1D barcodes, model.y is the bar baseline — needs the + // post-resize bar height to convert from the bbox top back. height is + // in ZPL dots directly, so scaling it by sy gives the new bar height + // before commit rounding (close enough for the position math). + const newBarHeightDots = + obj.positionType === "FT" && BARCODE_1D_TYPES.has(obj.type) + ? Math.max(1, Math.round((obj.props as { height: number }).height * sy)) + : undefined; // 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); + const modelPos = modelPositionFromRenderedTopLeft( + obj, + renderedXDots, + renderedYDots, + newBarHeightDots, + ); // 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. diff --git a/src/components/Canvas/transformPosition.ts b/src/components/Canvas/transformPosition.ts index 5855faba..08b30ffd 100644 --- a/src/components/Canvas/transformPosition.ts +++ b/src/components/Canvas/transformPosition.ts @@ -1,4 +1,4 @@ -import type { LabelObject } from "../../registry"; +import { BARCODE_1D_TYPES, type LabelObject } from "../../registry"; import { QR_FO_Y_OFFSET_DOTS } from "./bwipConstants"; /** @@ -9,20 +9,33 @@ import { QR_FO_Y_OFFSET_DOTS } from "./bwipConstants"; * Currently handles: * - QR (FO): subtracts the hardcoded +10 dot Y-offset that BarcodeObject adds * to compensate for Zebra firmware artifact. + * - 1D barcodes (FT): adds the new bar height back so obj.y stays on the + * bar baseline that ^FT anchors at. Without the correction every resize + * commits the bar TOP as the new obj.y and the next render shifts the + * barcode up by another bar-height. * * 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. + * Note: QR-FT correction is not yet implemented (the additional firmware + * 3-module offset would need a separate code path); FT resize on QR still + * drifts. */ export function modelPositionFromRenderedTopLeft( obj: LabelObject, renderedXDots: number, renderedYDots: number, + newBarHeightDots?: number, ): { x: number; y: number } { if (obj.type === "qrcode" && obj.positionType !== "FT") { return { x: renderedXDots, y: renderedYDots - QR_FO_Y_OFFSET_DOTS }; } + if ( + obj.positionType === "FT" && + BARCODE_1D_TYPES.has(obj.type) && + newBarHeightDots !== undefined + ) { + return { x: renderedXDots, y: renderedYDots + newBarHeightDots }; + } return { x: renderedXDots, y: renderedYDots }; } diff --git a/src/registry/transformHelpers.ts b/src/registry/transformHelpers.ts index bb695528..1702e156 100644 --- a/src/registry/transformHelpers.ts +++ b/src/registry/transformHelpers.ts @@ -38,24 +38,12 @@ export function commitWidthHeightTransform

( } as Partial

; } -/** Shared commitTransform for 1D barcodes — only the bar height scales (width - * is determined by content + module width, not by the resize anchor). */ -export function commitHeightTransform

( - obj: LabelObjectBase & { props: P }, - ctx: TransformContext, -): Partial

{ - const { sy, snap } = ctx; - return { - height: Math.max(1, snap(Math.round(obj.props.height * sy))), - } as Partial

; -} - /** - * commitTransform for 1D barcodes that also expose `moduleWidth`. Vertical - * drag scales bar height (sy), horizontal drag scales the module width (sx); - * the latter is clamped to the ZPL `^BY` range [1, 10]. The boundBoxFunc - * snaps the bbox to integer-moduleWidth widths during the drag, so this - * commit just records the final integer step. + * commitTransform for 1D barcodes. Vertical drag scales bar height (sy), + * horizontal drag scales the module width (sx) — the latter is clamped + * to the ZPL `^BY` range [1, 10]. During the drag the bitmap stretches + * free-form for visual feedback; this commit rounds the result to a + * valid integer moduleWidth on release. */ export function commitBarcodeWidthHeightTransform< P extends { height: number; moduleWidth: number }, From 7f5e7bbdd7f152aac34698942bc15a6ecd613644 Mon Sep 17 00:00:00 2001 From: u8array Date: Tue, 12 May 2026 00:13:16 +0200 Subject: [PATCH 3/3] fix(transform): snap FT bar-height estimate to match committed value MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gemini noted that the newBarHeightDots used for the FT baseline math read the raw scaled prop while commitBarcodeWidthHeightTransform stores it snapped to grid — a 1-dot drift between the two paths. Pipe the value through the same snap() so the position math sees exactly what gets committed. --- src/components/Canvas/hooks/useKonvaTransformer.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/components/Canvas/hooks/useKonvaTransformer.ts b/src/components/Canvas/hooks/useKonvaTransformer.ts index 826592fd..2f565157 100644 --- a/src/components/Canvas/hooks/useKonvaTransformer.ts +++ b/src/components/Canvas/hooks/useKonvaTransformer.ts @@ -318,12 +318,13 @@ export function useKonvaTransformer({ const renderedXDots = pxToDots(topLeft.x - objectsOffsetX, scale, dpmm); const renderedYDots = pxToDots(topLeft.y - labelOffsetY, scale, dpmm); // For FT-anchored 1D barcodes, model.y is the bar baseline — needs the - // post-resize bar height to convert from the bbox top back. height is - // in ZPL dots directly, so scaling it by sy gives the new bar height - // before commit rounding (close enough for the position math). + // post-resize bar height to convert from the bbox top back. Pipe the + // scaled height through the same snap() commitBarcodeWidthHeight- + // Transform uses so the baseline math sees the *committed* value and + // there's no 1-dot drift between the two pathways. const newBarHeightDots = obj.positionType === "FT" && BARCODE_1D_TYPES.has(obj.type) - ? Math.max(1, Math.round((obj.props as { height: number }).height * sy)) + ? Math.max(1, snap(Math.round((obj.props as { height: number }).height * sy))) : undefined; // Invert per-type render offsets (e.g. QR's hardcoded +10 dot Y) so the // stored model position matches what BarcodeObject.handleDragEnd produces.