Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
16 changes: 2 additions & 14 deletions src/components/Canvas/BarcodeObject.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@ 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";
import type { LabelObject } from "../../registry";
import { BARCODE_1D_TYPES, ObjectRegistry } from "../../registry";
import type { ObjectChanges } from "../../store/labelStore";
import { dotsToPx, pxToDots } from "../../lib/coordinates";
import type { KonvaObjectProps } from "./konvaObjectProps";
import {
buildBwipOptions,
getDisplaySize,
Expand All @@ -23,17 +22,6 @@ import {
EAN_UPC_TYPES,
} from "./bwipConstants";

interface Props {
obj: LabelObject;
scale: number;
dpmm: number;
offsetX: number;
offsetY: number;
isSelected: boolean;
onSelect: (addToSelection: boolean) => void;
onChange: (changes: ObjectChanges) => void;
snap: (dots: number) => number;
}
export function BarcodeObject({
obj,
scale,
Expand All @@ -44,7 +32,7 @@ export function BarcodeObject({
onSelect,
onChange,
snap,
}: Props) {
}: KonvaObjectProps) {
const groupRef = useRef<Konva.Group>(null);
const textRef = useRef<Konva.Text>(null);

Expand Down
136 changes: 136 additions & 0 deletions src/components/Canvas/ImageObject.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { useState, useEffect, useRef } from "react";
import { Group, Image as KImage, Rect, Text } from "react-konva";
import type Konva from "konva";
import type { LabelObject } from "../../registry";
import { dotsToPx, pxToDots } from "../../lib/coordinates";
import { getImage } from "../../lib/imageCache";
import type { KonvaObjectProps } from "./konvaObjectProps";

type ImageLabelObject = Extract<LabelObject, { type: "image" }>;
type Props = Omit<KonvaObjectProps, "obj"> & { obj: ImageLabelObject };

/** Image renderer. Hosted as its own component so hooks (useState/
* useEffect for async image loading) can run without violating
* rules-of-hooks. The dispatcher in KonvaObject narrows `obj`
* before passing — no runtime cast needed here. */
export function ImageObject({
obj,
scale,
dpmm,
offsetX,
offsetY,
isSelected,
onSelect,
onChange,
snap,
}: Props) {
Comment thread
u8array marked this conversation as resolved.
Comment thread
u8array marked this conversation as resolved.
const p = obj.props;
const cached = getImage(p.imageId);
const w = dotsToPx(p.widthDots, scale, dpmm);
// Guard against a 0-width cached image: the imageCache pipeline
// doesn't normally produce one, but a malformed file could leak
// through and div-by-zero would render NaN-sized canvas nodes.
const h = cached && cached.width > 0
? w * (cached.height / cached.width)
: w;
const x = offsetX + dotsToPx(obj.x, scale, dpmm);
const y = offsetY + dotsToPx(obj.y, scale, dpmm);

const [htmlImg, setHtmlImg] = useState<HTMLImageElement | null>(null);
// Reset the cached HTMLImageElement during render when the source changes,
// instead of inside an effect. The "set state during render on prop change"
// pattern is the official React workaround for what would otherwise be a
// setState-in-effect anti-pattern. The next render observes prevDataUrl
// already updated, so this does not loop.
const prevDataUrlRef = useRef<string | undefined>(cached?.dataUrl);
if (prevDataUrlRef.current !== cached?.dataUrl) {
prevDataUrlRef.current = cached?.dataUrl;
setHtmlImg(null);
}
useEffect(() => {
if (!cached) return;
let active = true;
const img = new window.Image();
img.src = cached.dataUrl;
img.onload = () => {
if (active) setHtmlImg(img);
};
return () => {
active = false;
};
}, [cached]);

// Snap during drag for visual feedback; commit only on dragEnd so
// the store doesn't update on every mouse pixel. Mirrors the
// pattern KonvaObjectInner uses for shape/text objects.
const handleDragMove = (e: Konva.KonvaEventObject<DragEvent>) => {
e.target.position({
x:
offsetX +
dotsToPx(snap(pxToDots(e.target.x() - offsetX, scale, dpmm)), scale, dpmm),
y:
offsetY +
dotsToPx(snap(pxToDots(e.target.y() - offsetY, scale, dpmm)), scale, dpmm),
});
};

const handleDragEnd = (e: Konva.KonvaEventObject<DragEvent>) => {
onChange({
x: pxToDots(e.target.x() - offsetX, scale, dpmm),
y: pxToDots(e.target.y() - offsetY, scale, dpmm),
});
};
Comment thread
u8array marked this conversation as resolved.
Comment thread
u8array marked this conversation as resolved.

if (htmlImg && cached) {
return (
<KImage
id={obj.id}
x={x}
y={y}
image={htmlImg}
width={w}
height={h}
stroke={isSelected ? "#6366f1" : undefined}
strokeWidth={isSelected ? 2 : 0}
draggable
onClick={(e) =>
onSelect(e.evt.shiftKey || e.evt.ctrlKey || e.evt.metaKey)
}
onTap={() => onSelect(false)}
onDragMove={handleDragMove}
onDragEnd={handleDragEnd}
/>
);
}

return (
<Group
id={obj.id}
x={x}
y={y}
draggable
onClick={(e) =>
onSelect(e.evt.shiftKey || e.evt.ctrlKey || e.evt.metaKey)
}
onTap={() => onSelect(false)}
onDragMove={handleDragMove}
onDragEnd={handleDragEnd}
>
<Rect
width={w}
height={h}
fill="#f9fafb"
stroke={isSelected ? "#6366f1" : "#9ca3af"}
strokeWidth={isSelected ? 2 : 1}
dash={[4, 2]}
/>
<Text
x={6}
y={6}
text="🖼"
fontSize={Math.max(w * 0.3, 12)}
fill="#374151"
/>
</Group>
);
}
Loading