Skip to content
60 changes: 52 additions & 8 deletions src/components/Canvas/BarcodeObject.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -43,6 +43,20 @@ export function BarcodeObject({
onChange,
snap,
}: Props) {
const groupRef = useRef<Konva.Group>(null);
const textRef = useRef<Konva.Text>(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;
Expand Down Expand Up @@ -413,6 +427,7 @@ export function BarcodeObject({
imageSmoothingEnabled={false}
stroke={isSelected ? "#6366f1" : undefined}
strokeWidth={isSelected ? 2 : 0}
strokeScaleEnabled={false}
/>
{textNodes}
</Group>
Expand Down Expand Up @@ -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 (
<Group
ref={groupRef}
id={obj.id}
x={x}
y={y}
clipX={0}
clipY={clipY}
clipWidth={Math.max(w, 1)}
clipHeight={clipHeight}
draggable
onClick={(e) =>
onSelect(e.evt.shiftKey || e.evt.ctrlKey || e.evt.metaKey)
Expand All @@ -467,6 +506,8 @@ export function BarcodeObject({
e.target.position(snapPos(e.target.x(), e.target.y()))
}
onDragEnd={handleDragEnd}
onTransform={handleTransform}
onTransformEnd={handleTransformEnd}
>
<KImage
x={0}
Expand All @@ -477,8 +518,10 @@ export function BarcodeObject({
imageSmoothingEnabled={false}
stroke={isSelected ? "#6366f1" : undefined}
strokeWidth={isSelected ? 2 : 0}
strokeScaleEnabled={false}
/>
<Text
ref={setTextRef}
x={0}
y={txtY}
width={Math.max(w, 1)}
Expand All @@ -505,6 +548,7 @@ export function BarcodeObject({
imageSmoothingEnabled={false}
stroke={isSelected ? "#6366f1" : undefined}
strokeWidth={isSelected ? 2 : 0}
strokeScaleEnabled={false}
draggable
onClick={(e) =>
onSelect(e.evt.shiftKey || e.evt.ctrlKey || e.evt.metaKey)
Expand Down
19 changes: 18 additions & 1 deletion src/components/Canvas/hooks/useKonvaTransformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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("|");
Comment thread
u8array marked this conversation as resolved.

useEffect(() => {
if (!transformerRef.current || !stageRef.current) return;
if (selectedIds.length === 0) {
Expand All @@ -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 =
Expand Down