diff --git a/src/components/Canvas/BarcodeObject.tsx b/src/components/Canvas/BarcodeObject.tsx index c0a5efbb..470ff060 100644 --- a/src/components/Canvas/BarcodeObject.tsx +++ b/src/components/Canvas/BarcodeObject.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useCallback, useRef } from "react"; import bwipjs from "bwip-js/browser"; import { Image as KImage, Group, Rect, Text } from "react-konva"; import type Konva from "konva"; @@ -43,6 +43,20 @@ export function BarcodeObject({ onChange, snap, }: Props) { + const groupRef = useRef(null); + const textRef = useRef(null); + + // Exclude the HRI text from the parent Group's getClientRect. This anchors + // the resize at the bar top (logmars: was anchoring at text top above bars) + // and keeps the Transformer's bbox tight around the bars, eliminating the + // (h + textArea)*sy vs h*sy + textArea discrepancy during drag. + const setTextRef = useCallback((node: Konva.Text | null) => { + textRef.current = node; + if (node) { + node.getSelfRect = () => ({ x: 0, y: 0, width: 0, height: 0 }); + } + }, []); + const opts = buildBwipOptions(obj, scale, dpmm); let barcodeCanvas: HTMLCanvasElement | null = null; let errorMsg: string | null = null; @@ -413,6 +427,7 @@ export function BarcodeObject({ imageSmoothingEnabled={false} stroke={isSelected ? "#6366f1" : undefined} strokeWidth={isSelected ? 2 : 0} + strokeScaleEnabled={false} /> {textNodes} @@ -445,19 +460,43 @@ export function BarcodeObject({ const aboveGap = isTextAbove ? Math.max(dotsToPx(LOGMARS_TEXT_ABOVE_GAP_DOTS, scale, dpmm), 3) : textGap; - const txtY = isTextAbove ? -(textFontSize + aboveGap) : Math.max(h, 1) + textGap; - const clipY = isTextAbove ? -(textFontSize + aboveGap) : 0; - const clipHeight = Math.max(h, 1) + textFontSize + aboveGap; + // Local y for the HRI text. The /sy form keeps a constant *visual* offset + // when the group is being scaled (sy = 1 at rest, ≠ 1 during a drag). + const textLocalY = (sy: number) => + isTextAbove + ? -(textFontSize + aboveGap) / sy + : Math.max(h, 1) + textGap / sy; + const txtY = textLocalY(1); + + // Counter-scale the text so it stays at constant pixel size while the + // bars stretch with the parent group's scaleY during a resize drag. + const handleTransform = () => { + const grp = groupRef.current; + const txt = textRef.current; + if (!grp || !txt) return; + const sy = grp.scaleY(); + if (sy <= 0) return; + txt.scaleY(1 / sy); + txt.y(textLocalY(sy)); + }; + + // react-konva does not track imperatively-set scaleY/y. Reset both here + // so the next drag starts clean. For logmars the JSX y is constant, so + // without an explicit reset react-konva would not re-apply it on the + // post-commit render and the text would stay at its last drag-time y. + const handleTransformEnd = () => { + const txt = textRef.current; + if (!txt) return; + txt.scaleY(1); + txt.y(txtY); + }; return ( onSelect(e.evt.shiftKey || e.evt.ctrlKey || e.evt.metaKey) @@ -467,6 +506,8 @@ export function BarcodeObject({ e.target.position(snapPos(e.target.x(), e.target.y())) } onDragEnd={handleDragEnd} + onTransform={handleTransform} + onTransformEnd={handleTransformEnd} > onSelect(e.evt.shiftKey || e.evt.ctrlKey || e.evt.metaKey) diff --git a/src/components/Canvas/hooks/useKonvaTransformer.ts b/src/components/Canvas/hooks/useKonvaTransformer.ts index b60fcc25..e5b9ed91 100644 --- a/src/components/Canvas/hooks/useKonvaTransformer.ts +++ b/src/components/Canvas/hooks/useKonvaTransformer.ts @@ -147,6 +147,18 @@ export function useKonvaTransformer({ .map((id) => objects.find((o) => o.id === id)?.type ?? "") .join(","); + // Signature of the selected objects' size-relevant props. Changes after + // commitTransform → forces the transformer to re-measure the attached node + // so its bounding box matches the new rendered size. Position is excluded: + // moves don't change bbox dimensions, and Konva tracks the node's position + // automatically. + const selectedSignature = selectedIds + .map((id) => { + const o = objects.find((obj) => obj.id === id); + return o ? `${id}:${JSON.stringify(o.props)}` : id; + }) + .join("|"); + useEffect(() => { if (!transformerRef.current || !stageRef.current) return; if (selectedIds.length === 0) { @@ -167,10 +179,15 @@ export function useKonvaTransformer({ .filter((n): n is Konva.Node => n != null); transformerRef.current.nodes(nodes); } + // Force a re-measure: after commitTransform the node's getClientRect has + // changed but the transformer caches its bounds from the last interaction. + transformerRef.current.forceUpdate(); // selectedTypesKey encodes the type of every selected object — sufficient to // detect the line/non-line distinction that governs transformer attachment. + // selectedSignature triggers a re-measure when an object's size or position + // changes (e.g. after commitTransform finishes a resize). // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedIds, selectedTypesKey, stageRef, transformerRef]); + }, [selectedIds, selectedTypesKey, selectedSignature, stageRef, transformerRef]); const resizeEnabled = selectedIds.length <= 1; const enabledAnchors: string[] | undefined =