Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 20 additions & 21 deletions src/components/Canvas/LabelCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -394,21 +394,7 @@ export const LabelCanvas = forwardRef<LabelCanvasHandle, Props>(function LabelCa
: null;
const stepRotation = singleSelected ? getStepRotation(singleSelected) : null;
const [rotationBtnPos, setRotationBtnPos] = useState<{ x: number; y: number } | null>(null);
// Hide the rotate affordance during an active drag / transform — the
// button's position is React-state-driven and would otherwise lag behind
// the live Konva node until the interaction ends.
const [isInteracting, setIsInteracting] = useState(false);
useEffect(() => {
const stage = stageRef.current;
if (!stage) return;
const start = () => setIsInteracting(true);
const end = () => setIsInteracting(false);
stage.on("dragstart.rotbtn transformstart.rotbtn", start);
stage.on("dragend.rotbtn transformend.rotbtn", end);
return () => {
stage.off("dragstart.rotbtn transformstart.rotbtn dragend.rotbtn transformend.rotbtn");
};
}, []);
const rotationBtnRef = useRef<Konva.Group>(null);
useLayoutEffect(() => {
if (!singleSelected || !stepRotation) {
setRotationBtnPos(null);
Expand All @@ -421,11 +407,23 @@ export const LabelCanvas = forwardRef<LabelCanvasHandle, Props>(function LabelCa
setRotationBtnPos(null);
return;
}
const rect = node.getClientRect({ relativeTo: stage, skipStroke: true });
setRotationBtnPos({
x: rect.x + rect.width + ROTATE_BUTTON_GAP_PX,
y: rect.y + ROTATE_BUTTON_TOP_OFFSET_PX,
});
// Recompute the button anchor whenever the selected node moves.
// The handler updates the Konva node directly so the button tracks
// the drag at full frame rate; React state is kept in sync so a
// subsequent re-render (selection change, scale, etc.) starts from
// the latest position instead of snapping back.
const update = () => {
const rect = node.getClientRect({ relativeTo: stage, skipStroke: true });
const x = rect.x + rect.width + ROTATE_BUTTON_GAP_PX;
const y = rect.y + ROTATE_BUTTON_TOP_OFFSET_PX;
rotationBtnRef.current?.position({ x, y });
setRotationBtnPos({ x, y });
};
update();
stage.on("dragmove.rotbtn transform.rotbtn", update);
return () => {
stage.off("dragmove.rotbtn transform.rotbtn");
};
}, [singleSelected, stepRotation, scale, viewRotation]);

const handleRotateStep = () => {
Expand Down Expand Up @@ -819,8 +817,9 @@ export const LabelCanvas = forwardRef<LabelCanvasHandle, Props>(function LabelCa
ignoreStroke
/>

{rotationBtnPos && !isInteracting && (
{rotationBtnPos && (
<RotationButton
ref={rotationBtnRef}
x={rotationBtnPos.x}
y={rotationBtnPos.y}
color={colors.selection}
Expand Down
10 changes: 7 additions & 3 deletions src/components/Canvas/RotationButton.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from "react";
import { forwardRef, useEffect, useRef, useState } from "react";
import { Group, Circle, Path } from "react-konva";
import type Konva from "konva";

Expand Down Expand Up @@ -30,7 +30,10 @@ const ARROW_PATH_ICON =
const ICON_SCALE = 0.55;
const ICON_OFFSET = 12; // 24x24 viewBox → centre at (12, 12)

export function RotationButton({ x, y, color, onClick }: Props) {
export const RotationButton = forwardRef<Konva.Group, Props>(function RotationButton(
{ x, y, color, onClick },
ref,
) {
const [hover, setHover] = useState(false);
// Track the stage we set a cursor on so unmount-while-hovering can still
// clean up (onMouseLeave never fires in that case — e.g. user hits Delete
Expand Down Expand Up @@ -61,6 +64,7 @@ export function RotationButton({ x, y, color, onClick }: Props) {

return (
<Group
ref={ref}
x={x}
y={y}
onMouseEnter={cursorIn}
Expand Down Expand Up @@ -95,4 +99,4 @@ export function RotationButton({ x, y, color, onClick }: Props) {
</Group>
</Group>
);
}
});
6 changes: 6 additions & 0 deletions src/components/Canvas/bwipConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,12 @@ export const GS1_DATABAR_SPEC_HEIGHT_MODULES: Partial<
6: 34,
};

/** Rows of whitespace bwip adds top and bottom of the GS1 DataBar bar
* pattern when buildBwipOptions sets `paddingheight: N`. Re-used by
* the bitmap-crop logic so the bar-extraction stays in lockstep with
* the bwip option above. */
export const GS1_DATABAR_PADDING_ROWS = 2;

export const EAN_UPC_TYPES = new Set<string>([
"ean13",
"ean8",
Expand Down
34 changes: 24 additions & 10 deletions src/components/Canvas/bwipHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
CODE11_QUIET_ZONE_DELTA_MODULES,
CODE93_QUIET_ZONE_DELTA_MODULES,
EAN_TEXT_ZONE_DOTS,
GS1_DATABAR_PADDING_ROWS,
GS1_DATABAR_SPEC_HEIGHT_MODULES,
LOGMARS_TEXT_ZONE_DOTS,
MICROPDF417_QUIET_ZONE_ROWS,
Expand Down Expand Up @@ -407,7 +408,7 @@ export function buildBwipOptions(
text,
scale,
height: 10,
paddingheight: 2,
paddingheight: GS1_DATABAR_PADDING_ROWS,
...(sym === 7 ? { segments: p.segments ?? GS1_DATABAR_DEFAULT_SEGMENTS } : {}),
};
break;
Expand Down Expand Up @@ -599,23 +600,36 @@ export function getDisplaySize(
}
}

// GS1 DataBar opts include `paddingheight: 2`, which adds whitespace
// GS1 DataBar opts include `paddingheight: N`, which adds whitespace
// rows on top and bottom of the bwip canvas. Without cropping them out,
// the bitmap drawn at displayH leaves the bars proportionally shorter
// than the spec-correct height. Zebra firmware fills the full reserved
// height with bars; mirror that by cropping the source bitmap to the
// bar-only rows.
//
// Rotation flips which axis the padding sits on. For N / I the padding
// is on top/bottom of the bitmap as bwip produced it; for R / B (bwip
// rotated 90° CW / CCW respectively) the same rows end up on the
// left/right edges, so the crop must run along the x-axis instead.
let bitmapCrop: BarcodeDisplaySize["bitmapCrop"];
if (obj.type === "gs1databar") {
const bwipSc = get1DBwipScale(obj.props.moduleWidth, scale, dpmm);
const padPx = 2 * bwipSc; // paddingheight=2 × bwip scale per side
if (canvas.height > 2 * padPx) {
bitmapCrop = {
x: 0,
y: padPx,
width: canvas.width,
height: canvas.height - 2 * padPx,
};
const padPx = GS1_DATABAR_PADDING_ROWS * bwipSc;
const axisDim = isQuarter ? canvas.width : canvas.height;
if (axisDim > 2 * padPx) {
bitmapCrop = isQuarter
? {
x: padPx,
y: 0,
width: canvas.width - 2 * padPx,
height: canvas.height,
}
: {
x: 0,
y: padPx,
width: canvas.width,
height: canvas.height - 2 * padPx,
};
}
}

Expand Down