Skip to content
4 changes: 2 additions & 2 deletions src/components/Canvas/LabelCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { useDroppable, useDndMonitor } from "@dnd-kit/core";
import type { PaletteDragData } from "../../dnd/types";
import { Stage, Layer, Group, Rect, Transformer } from "react-konva";
import type Konva from "konva";
import { useLabelStore, useCurrentObjects, currentObjects } from "../../store/labelStore";
import { useLabelStore, useCurrentObjects, currentObjects, getCurrentObjects } from "../../store/labelStore";
import { pxToDots, SCREEN_PX_PER_MM } from "../../lib/coordinates";
import { SNAP_OPTIONS } from "../../lib/units";
import type { Unit } from "../../lib/units";
Expand Down Expand Up @@ -325,7 +325,7 @@ export function LabelCanvas({
const objId = node.id();
if (!objId || !stageRef.current) return;

const objs = currentObjects(useLabelStore.getState());
const objs = getCurrentObjects();
const obj = objs.find((o) => o.id === objId);
if (!obj) return;

Expand Down
6 changes: 3 additions & 3 deletions src/components/Canvas/hooks/useCanvasLasso.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useState, useRef } from "react";
import type Konva from "konva";
import { useLabelStore, currentObjects } from "../../../store/labelStore";
import { getCurrentObjects } from "../../../store/labelStore";
import { getIdsIntersectingRect, type LassoRect } from "../lassoGeometry";

interface Options {
Expand Down Expand Up @@ -58,14 +58,14 @@ export function useCanvasLasso({ containerRef, stageRef, spaceDown, selectObject
lassoRectRef.current = null;
setLasso(null);
if (!rect || !stageRef.current) return;
const ids = currentObjects(useLabelStore.getState()).map((o) => o.id);
const ids = getCurrentObjects().map((o) => o.id);
selectObjects(getIdsIntersectingRect(stageRef.current, ids, rect));
};

const onStageMouseDown = (e: Konva.KonvaEventObject<MouseEvent>) => {
if (e.evt.button !== 0 || spaceDown) return;
const targetId = e.target.id();
const onObject = currentObjects(useLabelStore.getState()).some((o) => o.id === targetId);
const onObject = getCurrentObjects().some((o) => o.id === targetId);
if (onObject || e.target.getParent()?.className === "Transformer") return;
const pos = stageRef.current?.getPointerPosition();
if (!pos) return;
Expand Down
13 changes: 10 additions & 3 deletions src/components/Canvas/hooks/useKonvaTransformer.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useRef, useEffect } from "react";
import type Konva from "konva";
import { pxToDots } from "../../../lib/coordinates";
import { useLabelStore, currentObjects } from "../../../store/labelStore";
import { getCurrentObjects } from "../../../store/labelStore";
import { BARCODE_1D_TYPES, STACKED_2D_TYPES, ObjectRegistry } from "../../../registry";
import type { LabelObject } from "../../../registry";
import type { ObjectChanges } from "../../../store/labelStore";
Expand All @@ -22,6 +22,11 @@ import {
type SnapRect,
} from "../../../lib/snapGuides";

/** Minimum bounding-box edge length during a resize, in stage pixels. Below
* this the user is presumed to have flicked past the object and we keep the
* previous box rather than collapsing it to a sliver. */
const MIN_RESIZE_BOX_PX = 10;

/** Pack a Konva clientRect into the SnapRect shape used by snap helpers. */
function toSnapRect(id: string, rect: { x: number; y: number; width: number; height: number }): SnapRect {
return { id, x: rect.x, y: rect.y, width: rect.width, height: rect.height };
Expand Down Expand Up @@ -231,7 +236,9 @@ export function useKonvaTransformer({
};

const boundBoxFunc = (oldBox: BoundingBox, newBox: BoundingBox): BoundingBox => {
if (newBox.width < 10 || newBox.height < 10) return oldBox;
if (newBox.width < MIN_RESIZE_BOX_PX || newBox.height < MIN_RESIZE_BOX_PX) {
return oldBox;
}
if (isUniformScale) newBox = forceSquareBox(oldBox, newBox);
const dotPx = scale / dpmm;
let bbox = applyHeightSnap(oldBox, newBox, dotPx, transformAnchorRef.current);
Expand Down Expand Up @@ -266,7 +273,7 @@ export function useKonvaTransformer({
const nodeHeight = node.height();
node.scaleX(1);
node.scaleY(1);
const obj = currentObjects(useLabelStore.getState()).find((o) => o.id === singleId);
const obj = getCurrentObjects().find((o) => o.id === singleId);
if (!obj) {
cleanupTransformState();
return;
Expand Down
55 changes: 55 additions & 0 deletions src/components/Properties/NumberInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { clampMin } from '../../lib/inputParse';
import { inputCls, labelCls } from './styles';

interface NumberInputProps {
label: string;
value: number;
/** When set, the change handler receives a value clamped to at least `min`,
* guarding against the empty/0 input collapse that bare Number() invites. */
min?: number;
max?: number;
onChange: (next: number) => void;
disabled?: boolean;
readOnly?: boolean;
}

/**
* Standard label + number input pair used by registry properties panels.
* Centralises the layout, the labelCls/inputCls coupling, and the
* empty-or-NaN-to-min sanitisation so individual registries don't repeat
* the boilerplate.
*/
export function NumberInput({
label,
value,
min,
max,
onChange,
disabled,
readOnly,
}: NumberInputProps) {
return (
<div className="flex flex-col gap-1">
<label className={labelCls}>{label}</label>
<input
type="number"
className={inputCls}
value={value}
min={min}
max={max}
disabled={disabled}
readOnly={readOnly}
onChange={(e) => {
const raw = e.target.value;
let next = min !== undefined ? clampMin(raw, min) : Number(raw);
// Drop NaN before it corrupts the store. clampMin already returns
// `min` for unparsable input, so this only matters when `min` is
// undefined and the user pastes a non-numeric string.
if (isNaN(next)) return;
if (max !== undefined && next > max) next = max;
onChange(next);
}}
Comment thread
u8array marked this conversation as resolved.
/>
</div>
);
}
4 changes: 2 additions & 2 deletions src/hooks/useGlobalShortcuts.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useEffect } from "react";
import { useLabelStore, useHistory, currentObjects } from "../store/labelStore";
import { useLabelStore, useHistory, getCurrentObjects } from "../store/labelStore";
import { nextRotation } from "../components/Canvas/rotationGeometry";

export function useGlobalShortcuts() {
Expand All @@ -26,7 +26,7 @@ export function useGlobalShortcuts() {
if (inInput) return;
if (mod && e.code === "KeyA") {
e.preventDefault();
selectObjects(currentObjects(useLabelStore.getState()).map((o) => o.id));
selectObjects(getCurrentObjects().map((o) => o.id));
return;
}
if (mod && e.code === "KeyD") {
Expand Down
39 changes: 15 additions & 24 deletions src/registry/aztec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { inputCls, labelCls } from "../components/Properties/styles";
import { fieldPos, fdField } from "./zplHelpers";
import { type ZplRotation } from "./rotation";
import { RotationSelect } from "../components/Properties/RotationSelect";
import { NumberInput } from "../components/Properties/NumberInput";

export interface AztecProps {
content: string;
Expand Down Expand Up @@ -51,31 +52,21 @@ export const aztec: ObjectTypeDefinition<AztecProps> = {
/>
</div>

<div className="flex flex-col gap-1">
<label className={labelCls}>{loc.magnification}</label>
<input
type="number"
className={inputCls}
value={p.magnification}
min={1}
max={10}
onChange={(e) =>
onChange({ magnification: Number(e.target.value) })
}
/>
</div>
<NumberInput
label={loc.magnification}
value={p.magnification}
min={1}
max={10}
onChange={(magnification) => onChange({ magnification })}
/>

<div className="flex flex-col gap-1">
<label className={labelCls}>{loc.ecLevel}</label>
<input
type="number"
className={inputCls}
value={p.ecLevel}
min={0}
max={232}
onChange={(e) => onChange({ ecLevel: Number(e.target.value) })}
/>
</div>
<NumberInput
label={loc.ecLevel}
value={p.ecLevel}
min={0}
max={232}
onChange={(ecLevel) => onChange({ ecLevel })}
/>

<RotationSelect value={p.rotation} onChange={(rotation) => onChange({ rotation })} />
</div>
Expand Down
39 changes: 16 additions & 23 deletions src/registry/barcode1d.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { commitHeightTransform } from './transformHelpers';
import { filterContent, type ContentSpec } from './contentSpec';
import { type ZplRotation } from './rotation';
import { RotationSelect } from '../components/Properties/RotationSelect';
import { NumberInput } from '../components/Properties/NumberInput';

export interface Barcode1DProps {
content: string;
Expand Down Expand Up @@ -104,30 +105,22 @@ export function createBarcode1D(config: Barcode1DConfig): ObjectTypeDefinition<B
/>
</div>

<div className="flex flex-col gap-1">
<label className={labelCls}>{loc.height}</label>
<input
type="number"
className={inputCls}
value={p.height}
min={1}
disabled={config.heightLocked}
readOnly={config.heightLocked}
onChange={(e) => onChange({ height: Number(e.target.value) })}
/>
</div>
<NumberInput
label={loc.height}
value={p.height}
min={1}
disabled={config.heightLocked}
readOnly={config.heightLocked}
onChange={(height) => onChange({ height })}
/>

<div className="flex flex-col gap-1">
<label className={labelCls}>{loc.moduleWidth}</label>
<input
type="number"
className={inputCls}
value={p.moduleWidth}
min={1}
max={10}
onChange={(e) => onChange({ moduleWidth: Number(e.target.value) })}
/>
</div>
<NumberInput
label={loc.moduleWidth}
value={p.moduleWidth}
min={1}
max={10}
onChange={(moduleWidth) => onChange({ moduleWidth })}
/>

{!config.interpretationLocked && (
<label className="flex items-center gap-2 cursor-pointer">
Expand Down
68 changes: 26 additions & 42 deletions src/registry/box.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useT } from '../lib/useT';
import { inputCls, labelCls } from '../components/Properties/styles';
import { fieldPos } from './zplHelpers';
import { commitWidthHeightTransform } from './transformHelpers';
import { clampMin } from '../lib/inputParse';
import { NumberInput } from '../components/Properties/NumberInput';

export interface BoxProps {
width: number;
Expand Down Expand Up @@ -49,26 +49,18 @@ export const box: ObjectTypeDefinition<BoxProps> = {
return (
<div className="flex flex-col gap-3">
<div className="grid grid-cols-2 gap-2">
<div className="flex flex-col gap-1">
<label className={labelCls}>{t.registry.box.width}</label>
<input
type="number"
className={inputCls}
value={p.width}
min={1}
onChange={(e) => onChange({ width: clampMin(e.target.value, 1) })}
/>
</div>
<div className="flex flex-col gap-1">
<label className={labelCls}>{t.registry.box.height}</label>
<input
type="number"
className={inputCls}
value={p.height}
min={1}
onChange={(e) => onChange({ height: clampMin(e.target.value, 1) })}
/>
</div>
<NumberInput
label={t.registry.box.width}
value={p.width}
min={1}
onChange={(width) => onChange({ width })}
/>
<NumberInput
label={t.registry.box.height}
value={p.height}
min={1}
onChange={(height) => onChange({ height })}
/>
</div>

<label className="flex items-center gap-2 cursor-pointer">
Expand All @@ -82,16 +74,12 @@ export const box: ObjectTypeDefinition<BoxProps> = {
</label>

{!p.filled && (
<div className="flex flex-col gap-1">
<label className={labelCls}>{t.registry.box.thickness}</label>
<input
type="number"
className={inputCls}
value={p.thickness}
min={1}
onChange={(e) => onChange({ thickness: clampMin(e.target.value, 1) })}
/>
</div>
<NumberInput
label={t.registry.box.thickness}
value={p.thickness}
min={1}
onChange={(thickness) => onChange({ thickness })}
/>
)}

<div className="grid grid-cols-2 gap-2">
Expand All @@ -106,17 +94,13 @@ export const box: ObjectTypeDefinition<BoxProps> = {
<option value="W">{t.registry.box.colorW}</option>
</select>
</div>
<div className="flex flex-col gap-1">
<label className={labelCls}>{t.registry.box.rounding}</label>
<input
type="number"
className={inputCls}
value={p.rounding}
min={0}
max={8}
onChange={(e) => onChange({ rounding: clampMin(e.target.value, 0) })}
/>
</div>
<NumberInput
label={t.registry.box.rounding}
value={p.rounding}
min={0}
max={8}
onChange={(rounding) => onChange({ rounding })}
/>
</div>

<label className="flex items-center gap-2 cursor-pointer">
Expand Down
Loading