diff --git a/src/components/Canvas/LabelCanvas.tsx b/src/components/Canvas/LabelCanvas.tsx index 63dff325..59981fec 100644 --- a/src/components/Canvas/LabelCanvas.tsx +++ b/src/components/Canvas/LabelCanvas.tsx @@ -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"; @@ -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; diff --git a/src/components/Canvas/hooks/useCanvasLasso.ts b/src/components/Canvas/hooks/useCanvasLasso.ts index 44f403c4..d62a1549 100644 --- a/src/components/Canvas/hooks/useCanvasLasso.ts +++ b/src/components/Canvas/hooks/useCanvasLasso.ts @@ -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 { @@ -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) => { 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; diff --git a/src/components/Canvas/hooks/useKonvaTransformer.ts b/src/components/Canvas/hooks/useKonvaTransformer.ts index cb891cb7..fc104e08 100644 --- a/src/components/Canvas/hooks/useKonvaTransformer.ts +++ b/src/components/Canvas/hooks/useKonvaTransformer.ts @@ -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"; @@ -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 }; @@ -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); @@ -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; diff --git a/src/components/Properties/NumberInput.tsx b/src/components/Properties/NumberInput.tsx new file mode 100644 index 00000000..53654963 --- /dev/null +++ b/src/components/Properties/NumberInput.tsx @@ -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 ( +
+ + { + 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); + }} + /> +
+ ); +} diff --git a/src/hooks/useGlobalShortcuts.ts b/src/hooks/useGlobalShortcuts.ts index 877fdf42..0a2ce7d5 100644 --- a/src/hooks/useGlobalShortcuts.ts +++ b/src/hooks/useGlobalShortcuts.ts @@ -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() { @@ -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") { diff --git a/src/registry/aztec.tsx b/src/registry/aztec.tsx index 1b527886..70e59a80 100644 --- a/src/registry/aztec.tsx +++ b/src/registry/aztec.tsx @@ -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; @@ -51,31 +52,21 @@ export const aztec: ObjectTypeDefinition = { /> -
- - - onChange({ magnification: Number(e.target.value) }) - } - /> -
+ onChange({ magnification })} + /> -
- - onChange({ ecLevel: Number(e.target.value) })} - /> -
+ onChange({ ecLevel })} + /> onChange({ rotation })} /> diff --git a/src/registry/barcode1d.tsx b/src/registry/barcode1d.tsx index 7d290913..32f76a94 100644 --- a/src/registry/barcode1d.tsx +++ b/src/registry/barcode1d.tsx @@ -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; @@ -104,30 +105,22 @@ export function createBarcode1D(config: Barcode1DConfig): ObjectTypeDefinition -
- - onChange({ height: Number(e.target.value) })} - /> -
+ onChange({ height })} + /> -
- - onChange({ moduleWidth: Number(e.target.value) })} - /> -
+ onChange({ moduleWidth })} + /> {!config.interpretationLocked && (