diff --git a/src/components/Canvas/BarcodeObject.tsx b/src/components/Canvas/BarcodeObject.tsx index 9399f262..c58aa4c5 100644 --- a/src/components/Canvas/BarcodeObject.tsx +++ b/src/components/Canvas/BarcodeObject.tsx @@ -78,7 +78,7 @@ export function BarcodeObject({ let barcodeCanvas: HTMLCanvasElement | null = null; let errorMsg: string | null = null; if (obj.type === "tlc39") { - // bwip-js has no native tlc39 encoder; render the Code 39 + MicroPDF417 + // bwip-js has no native tlc39 encoder, render the Code 39 + MicroPDF417 // composite ourselves. Goes straight to canvas, bypassing buildBwipOptions. barcodeCanvas = renderTlc39Canvas(obj.props, scale, dpmm); if (!barcodeCanvas) errorMsg = "TLC39 render failed"; @@ -188,7 +188,7 @@ export function BarcodeObject({ // selectively skips bwip's internal padding (e.g. GS1 DataBar's // paddingheight rows) so bars fill the bbox at firmware-correct height. const bitmapCrop = dim.bitmapCrop; - // Force-off when the symbology has no HRI in ZPL (e.g. GS1 Databar) — the + // Force-off when the symbology has no HRI in ZPL (e.g. GS1 Databar); the // canvas must match the print output even if a legacy saved object still // carries printInterpretation: true. const rotation = objectRotation(obj.props); @@ -268,7 +268,7 @@ export function BarcodeObject({ }); // EAN/UPC have barTopPx === 0 (no text zone above), so the // helper's bar-relative textY equals the bbox-relative one - // here — render directly without an offset wrapper. + // here; render directly without an offset wrapper. overlayContent = eanOverlay.nodes; } else { // Other 1D: single centered text. textRef is wired only for the @@ -276,7 +276,7 @@ export function BarcodeObject({ // rotated paths the counter-scale math doesn't apply (would // scale along upright-Y which after R/B is screen-X), so the // ref stays undefined and the bars+text just scale together - // during a rotated resize drag — minor visual nit, no broken + // during a rotated resize drag, a minor visual nit with no broken // print output. // Compensate Courier's em-bottom padding only on the ink-height path. const inkShiftPx = hri?.fontDots ? textFontSize * COURIER_BOLD_EM_BOTTOM_PAD : 0; @@ -323,7 +323,7 @@ export function BarcodeObject({ txt.y(textLocalY(1)); }; - // Clip-expansion is upright-EAN/UPC only; absent on other paths + // Clip-expansion is upright-EAN/UPC only, absent on other paths // so Konva computes a natural bbox. Spread-or-empty keeps the // JSX free of four parallel ternaries. const clipProps = isUprightEanUpc && eanOverlay diff --git a/src/components/Canvas/Grid.tsx b/src/components/Canvas/Grid.tsx index 5c58bee2..89fd6e39 100644 --- a/src/components/Canvas/Grid.tsx +++ b/src/components/Canvas/Grid.tsx @@ -30,7 +30,7 @@ export function Grid({ const right = labelOffsetX + labelWidthPx; const bottom = labelOffsetY + labelHeightPx; - // Snap grid (primary — always visible, matches snap interval) + // Snap grid (primary; always visible, matches snap interval) for (let x = labelOffsetX + snapPx; x < right; x += snapPx) { lines.push( & { 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. */ + * before passing; no runtime cast needed here. */ export function ImageObject({ obj, scale, @@ -31,7 +31,7 @@ export function ImageObject({ const w = dotsToPx(p.widthDots, scale, dpmm); // Aspect-lock when a real PNG is cached; fall back to `heightDots` for // recall-only placeholders so the user can shape the box freely. Guard - // against 0-width cached images (malformed file edge case) — div-by- + // against 0-width cached images (malformed file edge case); div-by- // zero would otherwise render NaN-sized canvas nodes. const h = cached && cached.width > 0 ? w * (cached.height / cached.width) diff --git a/src/components/Canvas/KonvaObject.tsx b/src/components/Canvas/KonvaObject.tsx index 04de3f02..138d029f 100644 --- a/src/components/Canvas/KonvaObject.tsx +++ b/src/components/Canvas/KonvaObject.tsx @@ -23,14 +23,8 @@ import type { SerialProps } from "../../registry/serial"; type Props = KonvaObjectProps; -/** - * Selection outline drawn as a separate (non-listening) overlay so the - * underlying shape can keep its own stroke / fill / globalCompositeOperation - * without compromise. Sits at local (0, 0) inside the parent Group so it - * follows drag translations together with the body. Tracing the declared - * bbox (not the inset body) means a thick outline shape gets a marker on - * its outer pixel, matching the user's mental model of selection. - */ +/** Non-listening selection overlay tracing the declared bbox; decoupled + * from the body so thick outlines still get a clean marker. */ function SelectionOverlay({ width, height, @@ -85,25 +79,17 @@ function EllipseSelectionOverlay({ ); } -/** Canvas-side projection of ^FP. V+R never coincide: ^FP's - * direction is a single letter. */ +/** Canvas projection of ^FP (direction is a single letter, V/R exclusive). */ function applyFpToContent(content: string, fpDirection: "H" | "V" | "R" | undefined): string { if (fpDirection === "V") return [...content].join("\n"); if (fpDirection === "R") return [...content].reverse().join(""); return content; } -/** Per Labelary pixel scan: a reversed block's left edge sits - * `FPR_ANCHOR_LEFT_PADDING_RATIO × fontHeight` dots right of - * `FO − totalAdvance`. PrintLab Bold advance widths are calibrated - * to CG Triumvirate (assets/fonts/NOTICE.md), so a fontHeight - * factor stays stable across sizes. */ +/** Reversed block's left edge sits this fraction of fontHeight right of + * `FO - totalAdvance`. Per Labelary pixel scan. */ const FPR_ANCHOR_LEFT_PADDING_RATIO = 0.8; -/** Shared `` props baked once per render of a text/serial field. - * Every Text node in the field (single-line or per-^FB-line) uses - * the same font / scale / rotation / fill / selection-stroke; only - * `key`, `text`, and `(x, y)` vary across them. */ interface BaseTextProps { fontSize: number; fontFamily: string; @@ -114,18 +100,13 @@ interface BaseTextProps { stroke: string | undefined; strokeWidth: number; letterSpacing?: number; - /** Horizontal CSS-px shift applied to every glyph (used by the - * ^FPR anchor offset). */ + /** Horizontal shift per glyph (used by ^FPR). */ offsetXPx?: number; } -/** Render a text/serial field as either a single Konva `` or, - * when `^FB` is active on a text object, one `` per line at a - * Zebra-style alignment offset. Per-line + offset against ZPL's - * documented 5:9 A0 advance (`zebraTextLayout`) keeps the canvas - * centred / right-aligned output aligned with Labelary — Konva's - * own `Text.align` uses canvas `measureText` which over-estimates - * A0 glyph widths and drifts noticeably for centred blocks. */ +/** One `` per line at Zebra alignment offset when ^FB is active. + * Konva's own Text.align uses canvas measureText which over-estimates + * A0 advances and drifts noticeably on centred blocks. */ type TextFieldObj = | (LeafObject & { type: "text"; props: TextProps }) | (LeafObject & { type: "serial"; props: SerialProps }); @@ -174,17 +155,9 @@ function TextFieldContent({ ); } -/** Dashed vertical guide at `^FB` blockWidth so the user sees where - * the printer wraps. Rendered only while the text is selected; - * height matches `blockLines * lineStep` to cover the full block. - * - * An invisible Rect over the full block extent (0,0)–(blockWidth, - * blockHeight) anchors the parent Group's clientRect so the - * Transformer always covers the FO-aligned block area, regardless - * of C/R justify pushing the text spans rightwards inside it. - * Without it the bbox would collapse to the rendered glyphs + - * guide line, shifting the selection's left edge away from the - * field anchor. */ +/** Dashed wrap guide at ^FB blockWidth. The invisible Rect over the + * full block extent anchors the Group's clientRect so the Transformer + * covers the FO-aligned area regardless of C/R justify. */ function BlockWrapGuide({ blockWidthDots, blockLines, @@ -258,13 +231,6 @@ const BARCODE_TYPES = new Set([ ]); export function KonvaObject(props_: Props) { - // Substitute the bound variable's resolved value into `props.content` - // before any per-type renderer touches the obj. Keeps Konva blissfully - // unaware of the binding mechanism: every shape draws what would - // print for the currently active CSV row (or the variable's - // defaultValue when no CSV is in play). `applyBindingToObject` - // is identity-preserving when the obj isn't bound, so memoisation - // downstream isn't affected for the common case. const variables = useLabelStore((s) => s.variables); const csvDataset = useLabelStore((s) => s.csvDataset); const csvMapping = useLabelStore((s) => s.csvMapping); @@ -273,9 +239,6 @@ export function KonvaObject(props_: Props) { const obj = applyBindingToObject(props_.obj, variables, active, csvRenderMode); const renderProps = obj === props_.obj ? props_ : { ...props_, obj }; - // Bounds-tint when bound field is rendering fallback (no CSV cell - // for this variable). The rule lives in lib so it's testable - // without Konva; see shouldShowFallbackTint for the full predicate. const boundVariable = lookupBoundVariable(obj, variables); const showFallbackTint = shouldShowFallbackTint( boundVariable, csvDataset, csvMapping, csvRenderMode, @@ -289,12 +252,7 @@ export function KonvaObject(props_: Props) { if (!showFallbackTint) return shape; - // Read declared bbox from props. Shapes with explicit width/height - // (text, box, ellipse, image, qrcode, datamatrix) produce a visible - // tint; barcodes whose width is derived from content length (Code39, - // Code128, EAN-13, …) silently skip — their bars are visually - // distinctive enough that fallback ambiguity is less acute, and the - // source-state badge in the Variables panel covers them. + // Skip tint for content-derived-width shapes (barcodes); badge covers them. const props = (obj as { props?: { width?: unknown; height?: unknown } }).props; const wDots = typeof props?.width === "number" ? props.width : 0; const hDots = typeof props?.height === "number" ? props.height : 0; @@ -321,25 +279,9 @@ export function KonvaObject(props_: Props) { ); } -/** - * Per-type Konva renderer dispatcher (`KonvaObject` above narrows by - * `obj.type` and routes to the right component / case). - * - * Convention for adding a new shape type: - * - * 1. `id={obj.id}` sits on the **outermost render node**. Every shape - * wraps body plus selection overlay in a `` and puts the id - * there. Stage-level lookups (`stage.findOne(#id)`, snap, alt+click - * cycle) all walk up to the id'd ancestor, so this stays consistent. - * - * 2. Selection visuals: shapes draw a dedicated overlay component - * (`SelectionOverlay` for rectangular bodies, `EllipseSelectionOverlay` - * for ^GE / ^GC) that traces the declared outer bbox. Decoupling the - * marker from the body means a thick outline (or a ^LRY difference- - * blended body) keeps a clean blue marker on its outer pixel rather - * than re-tracing the body's stroke. The overlay sits inside the same - * Group as the body so drag translations move both together. - */ +/** New shape types: put `id={obj.id}` on the outermost Group (stage + * lookups walk up to it) and use the selection-overlay components + * rather than re-tracing the body's stroke. */ function KonvaObjectInner({ obj, scale, @@ -353,21 +295,14 @@ function KonvaObjectInner({ }: Props) { const fontVersion = useFontCacheVersion(); const colors = useColorScheme(); - // Pass the whole label config so the metrics helper can resolve - // either a per-field `fontId` or the label-wide `defaultFontId` to - // an uploaded preview TTF. ZPL emit/parse intentionally call the - // metrics without `label`, so their ink-width stays PrintLab-ZPL - // based and the round-trip is unaffected. + // Pass label so metrics resolves uploaded-TTF previews. ZPL emit/parse + // call without `label` so round-trip stays PrintLab-based. const label = useLabelStore((s) => s.label); const requestContentEditorFocus = useLabelStore((s) => s.requestContentEditorFocus); const setSidebarTab = useLabelStore((s) => s.setSidebarTab); const selectObjects = useLabelStore((s) => s.selectObjects); - // obj.x/y is the Konva render position (top-left of the EM bbox) — - // identical to what every other shape stores. The ZPL anchor (^FO - // cap-top / ^FT baseline) lives at obj.x/y + zplAnchorDelta and is - // applied only at the I/O boundary by zplGenerator / zplParser, so - // every in-editor interaction (drag, resize, snap, smart-align) sees - // a shape-agnostic single coordinate system. + // obj.x/y is the EM-bbox top-left; ZPL anchor delta is applied only + // at the zplGenerator/zplParser boundary. const baseMetrics = getTextRenderMetrics(obj, undefined, label); const textMetrics = baseMetrics && (obj.type === "text" || obj.type === "serial") @@ -431,19 +366,8 @@ function KonvaObjectInner({ } if (obj.type === "text" && obj.props.reverse) { - // ZPL `^A0,h,w` lets `w` differ from `h` to stretch each glyph - // horizontally. `w=0` is Zebra shorthand for "match the height". - // Konva mirrors this with a scaleX on the text node: the FO anchor - // stays at (x, y) but the rendered glyphs occupy (w/h)·advance. - // Use the measured ink width (already in dot space, mirrored to - // CSS px via dotsToPx) so the inverted background bbox tracks the - // actual rendered text rather than a length-based guess. - // Match the printer's ^GB knockout-background exactly: width = - // measured ink width, height = font height in CSS px. Earlier - // versions padded the height to 1.3× for visual breathing room - // but that hid what the printer actually emits — the canvas now - // mirrors the same `^GB inkW, fontHeight` shape the generator - // produces, so the preview matches Labelary and the real device. + // Match the printer's ^GB knockout exactly: width = measured ink, + // height = font height. Mirrors the generator's `^GB inkW, fontHeight`. const approxW = dotsToPx(textMetrics.inkWidthDots, scale, dpmm); const approxH = fontSizePx; return ( @@ -465,10 +389,7 @@ function KonvaObjectInner({ strokeWidth={isSelected ? 1.5 : 0} /> { if (obj.locked) return; selectObjects([obj.id]); @@ -554,20 +469,13 @@ function KonvaObjectInner({ const p = obj.props; const w = dotsToPx(p.width, scale, dpmm); const h = dotsToPx(p.height, scale, dpmm); - // A/B/C have real vector paths sourced from Labelary (pixel-true - // to Zebra firmware). D/E are trademarked UL/CSA logos we can't - // ship — render a clearly-placeholder dashed gray box with the - // letters so the user knows the printer will substitute the real - // logo. Unknown codes (D/E or malformed import) take the same - // placeholder branch via the null path here. + // A/B/C: vector paths from Labelary. D/E (trademarked UL/CSA) and + // unknown codes fall to the placeholder branch. const vectorPath = GS_VECTOR_CODES.has(p.symbol) ? GS_SYMBOL_PATHS[p.symbol as GsVectorCode] : null; - // The field bbox itself doesn't rotate — Zebra keeps the ^FO - // anchor and (w, h) extent fixed and rotates only the glyph - // inside. Skip the outer rotatedGroupTransform; sceneFunc bakes - // rotation into the per-rotation `rel` rect + a canvas rotate - // for the path orientation. + // Zebra rotates only the glyph; bbox stays at (w, h). sceneFunc bakes + // rotation into rel + canvas rotate. return ( - {/* Single hit area for the whole field bbox — picks up clicks - both for the vector-path branch and the placeholder branch - below (both of which render with listening={false}). One - source of truth for "where in this Group counts as a hit" - instead of duplicating a transparent Rect inside each - branch. */} + {/* Hit area for both vector and placeholder branches (both listening=false). */} {vectorPath ? (() => { - // Konva.Path has no fillRule prop, so the holes in ® / © - // (inner ring carved out of the outer disc) can't be - // expressed via Path's nonzero fill. Drop down to a - // custom Shape that builds a Path2D from the d-string - // and fills it with the native even-odd rule — handles - // both the holed glyphs and the multi-letter ™ uniformly. - // - // `rel[rotation]` carries the post-rotation glyph rect - // inside the declared bbox, measured directly from - // Labelary so Zebra's per-rotation anchor offsets match - // pixel-for-pixel. - // Defensive ?? rel.N for the malformed-import case where - // a JSON-restored object slips a non-N/R/I/B rotation - // string past the type system; falling back to the - // upright entry keeps the canvas from crashing on bad - // data instead of throwing inside sceneFunc. + // Custom Shape with Path2D + even-odd fill: Konva.Path has no + // fillRule, so ®/© holes can't be carved via nonzero. `rel` + // is the per-rotation glyph rect measured from Labelary; + // ?? rel.N guards malformed-import rotation strings. const rel = vectorPath.rel[p.rotation] ?? vectorPath.rel.N; const glyphW = vectorPath.bbox.maxX - vectorPath.bbox.minX; const glyphH = vectorPath.bbox.maxY - vectorPath.bbox.minY; @@ -615,11 +506,7 @@ function KonvaObjectInner({ sceneFunc={(ctx) => { ctx.save(); ctx.translate(renderX, renderY); - // For each rotation: pre-translate so the post-rotation - // glyph bbox starts at (0, 0) within the rel rect, - // then rotate, then scale upright path coords to fit - // the (renderW × renderH) target. R/B swap effective - // W/H because the upright glyph is sideways on screen. + // R/B swap effective W/H because upright glyph is sideways. if (p.rotation === "R") { ctx.translate(renderW, 0); ctx.rotate(Math.PI / 2); @@ -638,10 +525,8 @@ function KonvaObjectInner({ ctx.translate(-vectorPath.bbox.minX, -vectorPath.bbox.minY); const path = new Path2D(vectorPath.d); ctx.fillStyle = "#000000"; - // Konva.Context exposes the underlying 2D canvas - // context as `_context`; reach for it directly so - // we can call `fill(path, "evenodd")` — the Konva - // wrapper doesn't surface the path/rule overload. + // Reach into Konva's underlying 2D context for the + // path/rule overload that Konva's wrapper omits. ctx._context.fill(path, "evenodd"); ctx.restore(); }} @@ -698,42 +583,19 @@ function KonvaObjectInner({ const strokeWidth = Math.max(dotsToPx(p.thickness, scale, dpmm), 0.5); const cornerRadius = p.rounding * dotsToPx(Math.min(p.width, p.height) / 8, scale, dpmm); - // Option-A geometry (delegated to lib/shapeGeometry.ts so the Konva - // canvas, the @napi-rs pixel-regression renderer, and any future - // consumer share one definition of ZPL ^GB extrusion). Centred - // stroke on the inset rect places the band exactly inside the - // declared bbox; the firmware's clamp-to-solid rule is handled by - // `renderFilled`. - // promoteFilled=true: see note in shapeRender.ts — ^GB rects extrude - // their solid fill to max(w,t) × max(h,t) per Zebra firmware. The - // ellipse branch below leaves this off because ^GE / ^GC - // collapse to solid at their declared bbox without promotion. + // promoteFilled=true: ^GB extrudes solid to max(w,t) x max(h,t) + // (see shapeRender.ts). ^GE/^GC collapse without promotion. const insetGeom = outlineInset(w, h, strokeWidth, p.filled, true); const renderFilled = insetGeom.renderFilled; const insetCornerRadius = renderFilled ? cornerRadius : Math.max(0, cornerRadius - strokeWidth / 2); - // Inverted (^LRY) paint is delegated to reverseShapeStyle so box - // and ellipse share the same colour rules — the helper covers the - // stroke/fill swap for filled outlines (banding workaround) and - // the difference-blend that produces the print-correct knockout. const { stroke, fill, globalCompositeOperation } = reverseShapeStyle( p.reverse, p.color, renderFilled, ); - // Wrap body + selection overlay in a draggable Group so both move - // together during a drag — without this the selection-stroke rect - // stays at the start position while the body translates, leaving a - // visible ghost outline behind the moving box until drag-end. - // id sits on the Group (not the inner Rect) so onTransformEnd reads the - // Group's absolute position via node.x()/y(); putting id on the Rect - // would return its local (0, 0) and the post-resize commit would land - // off-canvas. The Transformer + altClickCycle + snap all walk up to - // the id'd ancestor, so finding the Group via findOne(#id) is fine. - // Group.width/height defaults to 0; commitWidthHeightTransform doesn't - // need them (it uses obj.props.width * sx). return ( void; } -/** Imperative actions sibling components (PropertiesPanel) need from the - * canvas. The canvas owns the live render bboxes via Konva — co-locating - * the action with the data avoids round-tripping through the store. */ export interface LabelCanvasHandle { alignSelectionToLabel: (axis: AlignAxis) => void; } @@ -97,9 +92,8 @@ export const LabelCanvas = forwardRef(function LabelCa const [guides, setGuides] = useState([]); const [ghost, setGhost] = useState(null); - // Raw pointer position tracked independently of @dnd-kit's scroll-adjusted delta. - // activatorEvent.client + event.delta includes scroll momentum from the palette - // sidebar, which causes a proportional drop-position offset on touch devices. + // Bypasses @dnd-kit scroll-adjusted delta; palette scroll momentum + // would otherwise offset touch-device drops. const lastPointerRef = useRef({ x: 0, y: 0 }); useEffect(() => { const onMove = (e: PointerEvent) => { @@ -127,9 +121,7 @@ export const LabelCanvas = forwardRef(function LabelCa const previewLocks = useLabelStore(selectPreviewLocksEditor); const exitPreviewMode = useLabelStore((s) => s.exitPreviewMode); - // Load the Labelary blob URL into an HTMLImageElement so react-konva's - // `Image` node can draw it. Decoding before mount avoids a one-frame - // flash of empty space when the user toggles preview on. + // Pre-decode so toggling preview on doesn't flash a frame of empty space. const [previewImg, setPreviewImg] = useState(null); const previewUrl = previewMode.status === 'active' ? previewMode.url : null; useEffect(() => { @@ -145,12 +137,7 @@ export const LabelCanvas = forwardRef(function LabelCa }; }, [previewUrl]); - // Render path operates on visible leaves only: groups emit no node of - // their own (v1 has no group transform), and a group with visible=false - // hides its whole subtree. Lock cascades the same way — a leaf inside - // a locked group is stamped as effectively locked so the per-leaf - // draggable / locked-click checks all see one consistent value - // without each consumer having to walk ancestors. + // Leaves only; group lock/visible cascades down so per-leaf checks see one value. const visibleLeaves = useMemo(() => { const out: LeafObject[] = []; const walk = (nodes: LabelObject[], inheritedLocked: boolean) => { @@ -168,10 +155,7 @@ export const LabelCanvas = forwardRef(function LabelCa return out; }, [objects]); - // Konva-side machinery (transformer, snap snapshots) lives on leaves; - // groups have no node of their own. Expanding the selection here is - // what makes "click a child → group is selected" feel like a Figma - // multi-drag without a second drag pathway. + // Expand selection so group-click feels like Figma multi-drag. const allLeaves = useMemo(() => getAllLeaves(objects), [objects]); const attachableIds = useMemo( () => expandSelection(objects, selectedIds), @@ -191,14 +175,9 @@ export const LabelCanvas = forwardRef(function LabelCa return () => observer.disconnect(); }, []); - // Alt+click cycles selection through stacked objects so users can reach - // shapes hidden behind a filled (or inverted) form. useAltClickCycle({ containerRef, stageRef, selectObject }); - // Escape exits preview mode. Stays bound globally (not just to the - // canvas) so the user can return to the editor from anywhere — and - // the existing useGlobalShortcuts already short-circuits the rest of - // its bindings while preview locks, so there's no collision risk. + // Global binding so user can exit preview from anywhere. useEffect(() => { const onKeyDown = (e: KeyboardEvent) => { if (e.code !== 'Escape') return; @@ -215,8 +194,7 @@ export const LabelCanvas = forwardRef(function LabelCa const onKeyDown = (e: KeyboardEvent) => { if (e.code !== "Delete" && e.code !== "Backspace") return; if (isEditableTarget(e.target as HTMLElement)) return; - // Preview overlay shows a frozen Labelary snapshot; editing while - // it's active would silently drift the comparison out of sync. + // Preview is a frozen snapshot; editing would drift the comparison. if (selectPreviewLocksEditor(useLabelStore.getState())) return; const { selectedIds: ids } = useLabelStore.getState(); if (ids.length === 0) return; @@ -250,13 +228,10 @@ export const LabelCanvas = forwardRef(function LabelCa : 1; const screenDx = e.code === "ArrowRight" ? step : e.code === "ArrowLeft" ? -step : 0; const screenDy = e.code === "ArrowDown" ? step : e.code === "ArrowUp" ? -step : 0; - // Map the visual screen direction to label coordinates so arrow keys - // always move the object the way the user sees it on the rotated view. + // Inverse-rotate so arrow direction matches visual. const [dx, dy] = inverseRotateDelta(screenDx, screenDy, viewRotation); - // Expand so arrow keys move every leaf of a selected group, not - // the group node itself (whose x/y is conventionally 0 and has no - // effect on rendered children). + // Expand so a selected group moves its leaves (group x/y is conventionally 0). const expanded = expandSelection(objs, ids); updateObjects( expanded.flatMap((sid) => { @@ -279,9 +254,7 @@ export const LabelCanvas = forwardRef(function LabelCa const labelShiftMm = (label.labelShift ?? 0) / label.dpmm; const effectiveWidthMm = label.widthMm + labelShiftMm; - // zoom=1 → 100% → physical label size on screen (96 dpi CSS convention). - // fitZoom is the multiplier that makes the visually-rotated label fill the - // container, so axes swap at 90°/270°. + // zoom=1 = 100% (96 dpi CSS); fitZoom swaps axes at 90/270. const axisSwapped = isAxisSwapped(viewRotation); const fitWidthMm = axisSwapped ? label.heightMm : effectiveWidthMm; const fitHeightMm = axisSwapped ? effectiveWidthMm : label.heightMm; @@ -292,8 +265,7 @@ export const LabelCanvas = forwardRef(function LabelCa ) : 1; - // On first mount (once container dimensions are known), initialize zoom to - // fit so the label is immediately visible regardless of persisted zoom value. + // Init zoom to fit once container is sized so label is immediately visible. const didInitRef = useRef(false); useEffect(() => { if (didInitRef.current || usableWidth <= 0) return; @@ -340,10 +312,7 @@ export const LabelCanvas = forwardRef(function LabelCa const snap = (dots: number) => snapEnabled ? Math.round(dots / snapUnit) * snapUnit : dots; - // Stable across renders — every consumer that needs a snap snapshot - // calls this with its own id. Defined once (vs. per-object in the - // KonvaObject map) so a 60Hz dragmove that re-renders LabelCanvas - // doesn't churn N closures per frame. + // One stable closure to avoid 60Hz churning N per-object closures. const getOthersSnapshot = useCallback((excludeId: string) => { const stage = stageRef.current; if (!stage) return []; @@ -367,10 +336,8 @@ export const LabelCanvas = forwardRef(function LabelCa onStageMouseDown, } = useCanvasLasso({ containerRef, stageRef, spaceDown, selectObjects }); - // Snap / guide infrastructure used by both drag and resize. The label rect - // here matches the visual (rotation-aware) bounds — snap math operates in - // stage-screen space, so it must reflect what the user sees, not the - // un-rotated layout coordinates. + // Snap math operates in stage-screen space, so the label rect must + // reflect the rotation-aware visual bounds, not layout coords. const transformerSnapLabelRect = useMemo( () => ({ id: "_lbl", @@ -382,12 +349,6 @@ export const LabelCanvas = forwardRef(function LabelCa [visualLabelX, visualLabelY, visualLabelWidthPx, visualLabelHeightPx], ); - // Imperative align-to-label: PropertiesPanel calls this directly via the - // forwarded ref. Co-located with the render data — measure each selected - // node's rendered bbox via Konva clientRect (single source of truth for - // type-specific footprints like text baselines / barcode text-zones), - // compute the centre-delta in screen px, map back through view rotation - // and px-per-dot to model coordinates. useImperativeHandle( ref, () => ({ @@ -398,9 +359,7 @@ export const LabelCanvas = forwardRef(function LabelCa const ids = state.selectedIds; if (ids.length === 0) return; const objs = currentObjects(state); - // Konva nodes only exist for leaves; align operates on the - // measured rendered bboxes, so expand any selected group to its - // leaf ids and feed those into the Konva lookup. + // Konva nodes exist only for leaves; expand group ids. const attachable = expandSelection(objs, ids); const boxes = attachable.flatMap((id) => { @@ -423,9 +382,7 @@ export const LabelCanvas = forwardRef(function LabelCa screenDy, viewRotation, ); - // Round to integer dots — matches the `mmToDots` convention used by - // PropertiesPanel inputs and keeps the store's x/y invariant so ZPL - // emit doesn't see fractional coordinates. + // Integer dots preserves store invariant; mmToDots convention. const pxPerDot = scale / label.dpmm; const dxDots = Math.round(layoutDx / pxPerDot); const dyDots = Math.round(layoutDy / pxPerDot); @@ -467,12 +424,7 @@ export const LabelCanvas = forwardRef(function LabelCa viewRotation, }); - // Quick 90°-rotation button overlay. Only step-rotation objects (those - // with a `rotation: N|R|I|B` prop — text, serial, all barcodes) get the - // affordance; box/ellipse/line/image rotate freely via the - // Transformer or have no rotation. Positioned at the visual top-right - // corner of the selected node, derived from getClientRect so it tracks - // the rendered bbox through both object-rotation and viewRotation. + // Step-rotation only (text/serial/barcodes); box/ellipse/line/image use Transformer. const singleSelected = selectedIds.length === 1 ? objects.find((o) => o.id === selectedIds[0]) ?? null : null; @@ -491,16 +443,8 @@ export const LabelCanvas = forwardRef(function LabelCa setRotationBtnPos(null); return; } - // 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. - // - // Listeners attach to the node itself, not to the stage: Konva's - // `transform` event (fired per-frame during a resize) does NOT - // bubble, so a stage-level listener would only catch `dragmove` - // and the button would freeze in place until the user released. + // Direct node update for 60fps drag; React state kept in sync. + // Per-node listener because Konva's transform doesn't bubble. const update = () => { const rect = node.getClientRect({ relativeTo: stage, skipStroke: true }); const x = rect.x + rect.width + ROTATE_BUTTON_GAP_PX; @@ -531,12 +475,8 @@ export const LabelCanvas = forwardRef(function LabelCa ...(changes.x !== undefined && { x: snap(changes.x) }), ...(changes.y !== undefined && { y: snap(changes.y) }), }; - // Multi-select: propagate position delta to all other selected objects. - // Read fresh state (getState) to avoid stale closure when multiple DragEnd events - // fire simultaneously during a Transformer group drag. - // expandSelection lets a selected group behave like a multi-selection - // of its leaves here, so dragging one leaf moves the whole group via - // the same delta-propagation path used by shift-click selections. + // Fresh getState() guards stale closure across simultaneous DragEnd + // events; expandSelection propagates the delta to group leaves. const state = useLabelStore.getState(); const currentObjs = currentObjects(state); const selIds = expandSelection(currentObjs, state.selectedIds); @@ -590,9 +530,7 @@ export const LabelCanvas = forwardRef(function LabelCa otherRects.push({ id: o.id, x: r.x, y: r.y, width: r.width, height: r.height }); } - // Snap operates in stage (screen) space — getClientRect on rotated - // children already accounts for the Group transform, so the label rect - // must use visual bounds (not un-rotated labelOffset*). + // Stage space; clientRect already accounts for Group transform. const labelRect = { id: "_lbl", x: visualLabelX, @@ -608,8 +546,7 @@ export const LabelCanvas = forwardRef(function LabelCa labelRect, labelRect, ); - // result is in screen space; node.position() is in group-local space, so - // the delta needs inverse-rotation back into the un-rotated frame. + // result is screen-space; node.position() is group-local, so inverse-rotate. const screenDx = result.x - dr.x; const screenDy = result.y - dr.y; if (screenDx !== 0 || screenDy !== 0) { @@ -628,18 +565,7 @@ export const LabelCanvas = forwardRef(function LabelCa if (e.target === e.target.getStage()) selectObjects([]); }; - /** - * Click-passthrough for locked objects (Figma idiom). The locked node - * still listens (so the Alt+click cycle keeps reaching it), but its - * onSelect routes here instead of selecting itself. The next non-locked - * hit at the same point wins; if nothing's left, treat as background. - * - * Pointer source is `lastPointerRef` rather than the original click - * event because `onSelect` deliberately drops the event to keep the - * KonvaObjectProps surface narrow. The document-level pointermove - * listener updates the ref every frame, so by the time the click - * handler fires the ref is at the click position. - */ + /** Figma idiom: locked object passes click through to next non-locked hit. */ const handleLockedClick = (add: boolean) => { const stage = stageRef.current; const cr = containerRef.current?.getBoundingClientRect(); @@ -678,8 +604,7 @@ export const LabelCanvas = forwardRef(function LabelCa const pointerToLabelDots = (clientX: number, clientY: number) => { const rect = containerRef.current?.getBoundingClientRect(); if (!rect) return null; - // Inverse-rotate the pointer around the label center so screen-space - // input maps back into the un-rotated label coordinate frame. + // Inverse-rotate around label center to map screen -> un-rotated frame. const [rx, ry] = inverseRotateDelta( clientX - rect.left - labelCenterX, clientY - rect.top - labelCenterY, @@ -748,10 +673,7 @@ export const LabelCanvas = forwardRef(function LabelCa background: colors.canvasBg, backgroundImage: `radial-gradient(circle, ${colors.canvasDot} 1px, transparent 1px)`, backgroundSize: "24px 24px", - // `not-allowed` while the preview overlay is locking the editor: - // signals at the user's locus of attention (the canvas itself) - // that editing is paused, instead of relying on the toolbar button - // alone for state feedback. + // Locus-of-attention feedback for preview lock. cursor: previewLocks ? 'not-allowed' : cursor, }} onMouseDown={handleMouseDown} @@ -759,10 +681,7 @@ export const LabelCanvas = forwardRef(function LabelCa onMouseUp={handleMouseUp} onMouseLeave={handleMouseUp} > - {/* Preview-mode overlays. Loading is shown DOM-side because - the Konva Image can't render before the bitmap decoded; the - error banner stays inside the canvas region so the user can - dismiss it without leaving the canvas viewport. */} + {/* Loading is DOM (Konva can't render before decode); error is in-canvas. */} {previewMode.status === 'loading' && (
@@ -870,13 +789,7 @@ export const LabelCanvas = forwardRef(function LabelCa > {/* Object layer */} - {/* - * Rotation pivot: x=offsetX=labelCenterX. At 0° this collapses to - * the identity (group-local = screen), and onTransformEnd stays - * valid because Konva keeps node.x()/y() in the group's local - * (un-rotated) frame during drags. Pointer input (pointerToLabelDots) - * is inverse-rotated explicitly since it bypasses Konva. - */} + {/* Pivot at labelCenter; node.x()/y() stays group-local during drags. */} (function LabelCa width={labelWidthPx} height={labelHeightPx} fill="white" - // Preview overlay swaps the default drop-shadow for a - // symmetric amber glow so the paper itself carries the - // mode indicator at the user's locus of attention. Same - // amber the app uses for Labelary-related warnings, so - // the colour language stays consistent. + // Preview: amber glow matches the Labelary-warning palette. shadowColor={previewLocks ? 'rgba(251, 191, 36, 0.55)' : 'rgba(0,0,0,0.4)'} shadowBlur={previewLocks ? 28 : 12} shadowOffsetY={previewLocks ? 0 : 2} @@ -913,11 +822,7 @@ export const LabelCanvas = forwardRef(function LabelCa /> )} - {/* Preview overlay replaces the editor leaves entirely so the - user sees exactly what Labelary would render at the same - scale and position. Falls back to nothing during loading - (loading spinner is rendered as a DOM overlay outside the - Konva stage), so neither view briefly blinks through. */} + {/* Preview replaces editor leaves so user sees the Labelary render at same scale. */} {previewLocks ? ( previewImg && ( (function LabelCa offsetY={labelOffsetY} isSelected={attachableIds.includes(obj.id)} onSelect={(add) => { - // Auto-select-parent: clicking a child of a group - // surfaces the outermost containing group as the - // selection target. Top-level leaves pass through. + // Click child -> select outermost group; lock cascades to handleLockedClick. const target = selectionTargetId(objects, obj.id); - // Lock cascades from the group: a click on a child - // of a locked group routes through handleLockedClick - // (so the next non-locked hit wins) instead of - // selecting through to a leaf the user can't move. const targetObj = target === obj.id ? obj : findObjectById(objects, target); if (targetObj?.locked) handleLockedClick(add); @@ -980,11 +879,7 @@ export const LabelCanvas = forwardRef(function LabelCa )} - {/* Lasso, guides and Transformer live in screen space outside - the Group. Lasso intersection uses getClientRect which - respects the rotation; snap guides come from computeSnap in - stage pixels; the Transformer follows nodes through their - accumulated parent transform. */} + {/* Outside the rotation Group; clientRect/Transformer respect parent transforms. */} {!previewLocks && lassoRect && ( (function LabelCa onTransformStart={onTransformStart} boundBoxFunc={boundBoxFunc} onTransformEnd={onTransformEnd} - // Match the per-shape selection stroke and the line endpoint - // handles so all selection visuals share one colour. borderStroke={colors.selection} anchorStroke={colors.selection} anchorFill="#ffffff" anchorSize={7} anchorStrokeWidth={1} - // Exclude selection stroke from the bbox; otherwise scale-aware - // stroke padding leaks into the resize math and produces sub-dot - // drift in node.x()/y() that surfaces as 1-dot ZPL coordinate - // jumps under pxToDots rounding. + // Stroke-padding drift would surface as 1-dot ZPL jumps. ignoreStroke /> )} @@ -1036,8 +926,7 @@ export const LabelCanvas = forwardRef(function LabelCa )} - {/* Ruler — topmost layer. Tracks the visually-rotated label edges; - labels are reversed when the corresponding axis flips. */} + {/* Ruler tracks visual edges; labels reverse when axis flips. */} ; type Props = Omit & { obj: LineLabelObject }; -/** Line renderer. Hosted as its own component so hooks (useState for - * live endpoint drag) can run conditionally per object type without - * violating rules-of-hooks. The dispatcher in KonvaObject narrows - * `obj` before passing — no runtime cast needed here. */ export function LineObject({ obj, scale, @@ -92,8 +73,7 @@ export function LineObject({ }: Props) { const p = obj.props; const colors = useColorScheme(); - // All positions are absolute stage coordinates — the Group has no offset. - // This eliminates any parent-child draggable conflict. + // Absolute stage coords; Group has no offset (avoids draggable conflict). const x1 = offsetX + dotsToPx(obj.x, scale, dpmm); const y1 = offsetY + dotsToPx(obj.y, scale, dpmm); const rad = (p.angle * Math.PI) / 180; @@ -101,46 +81,25 @@ export function LineObject({ const x2 = x1 + lenPx * Math.cos(rad); const y2 = y1 + lenPx * Math.sin(rad); - // Reverse (^LRY) uses a difference-blend body — print-correct: renders - // black on the white label and inverts darker shapes underneath. The - // body keeps that mode even while selected so the inversion visual - // doesn't disappear and hide whatever is layered behind; the selection - // highlight is rendered as a separate overlay below. + // Reverse (^LRY): difference-blend body kept even while selected so the + // inversion stays visible; selection halo is a separate overlay. const isReverse = !!p.reverse; const strokeColor = isReverse ? "#ffffff" : p.color === "B" ? "#000000" : "#cccccc"; - // Live thickness while the side handle is being dragged. Falls back to - // the stored prop when no drag is in flight; commits to props on - // dragEnd. Wrapping the rendering width in this state means the band, - // selection outline and handle anchors all track the cursor in real - // time without any one-frame delay on release. const [liveThicknessDots, setLiveThicknessDots] = useState(null); const effectiveThicknessDots = liveThicknessDots ?? p.thickness; const rawStrokePx = Math.max(dotsToPx(effectiveThicknessDots, scale, dpmm), 1); - // Option-A geometry (mirrors src/lib/shapeRender.ts): - // - Axis-aligned lines map to ^GB and extrude thickness downward - // (horizontal) or rightward (vertical) from (obj.x, obj.y) — the - // visible body is shifted by t/2 along that axis so the band fills - // y..y+t / x..x+t exactly. Handles stay at the band's start corner. - // - Diagonal lines map to ^GD: the conceptual line is the left long - // edge of a parallelogram and thickness extrudes purely in +x. The - // diagonalPolygonPoints helper builds the four vertices. - // - // The axis-aligned / diagonal pick is derived from the *live* display - // endpoints rather than `p.angle` (which only updates on dragEnd). - // Otherwise dragging a near-horizontal endpoint shows the body locked - // to the horizontal band until release, then snaps to the parallelo- - // gram — a visible jump the user noticed. + // Geometry mirrors shapeRender.ts (axis-aligned -> ^GB band, diagonal + // -> ^GD parallelogram). Axis-aligned check uses live endpoints, not + // p.angle (which only updates on dragEnd) to avoid release-jump. - // Live positions while handles are being dragged (snapped preview) const [livePt1, setLivePt1] = useState<{ x: number; y: number } | null>(null); const [livePt2, setLivePt2] = useState<{ x: number; y: number } | null>(null); - // Live drag delta while the whole line is being dragged const [dragDelta, setDragDelta] = useState<{ x: number; y: number }>({ x: 0, y: 0, @@ -154,20 +113,13 @@ export function LineObject({ const dispX2 = livePt2?.x ?? x2 + dx; const dispY2 = livePt2?.y ?? y2 + dy; - // Mid-drag, an endpoint can be pulled inside the current thickness; the - // onDragEnd commit then snaps thickness down to the new length, which - // would look like a sudden band shrink on release. Cap the visual stroke - // at the live endpoint distance so the band always tracks the t ≤ length - // invariant we commit to. In steady state the data model already - // satisfies this, so the cap is a no-op. + // Cap visual stroke at live length so the band stays within the + // t <= length invariant we commit to on dragEnd. const visualLenPx = Math.hypot(dispX2 - dispX1, dispY2 - dispY1); const lineStrokeWidth = Math.min(rawStrokePx, visualLenPx); const halfStrokePx = lineStrokeWidth / 2; - // Half-pixel epsilon: constrainLine's auto-snap commits 45°-step - // positions where ddx/ddy land exactly on axis-aligned values, but - // float math can leave a tiny residue. <0.5 px collapses to "the - // pixel grid sees this as axis-aligned" without false-positives. + // <0.5 px epsilon for float residue on constrainLine's 45-step snap. const ddxDisp = dispX2 - dispX1; const ddyDisp = dispY2 - dispY1; const isHorizontal = Math.abs(ddyDisp) < 0.5; @@ -176,27 +128,15 @@ export function LineObject({ const visualShiftX = isVertical ? halfStrokePx : 0; const visualShiftY = isHorizontal ? halfStrokePx : 0; - // Shift forces the user-explicit 45°-step constraint; otherwise we use - // Figma-style auto-snap (±5° tolerance to the nearest 45° step). + // Shift = explicit 45-step lock; else Figma-style auto-snap (+-5deg). const resolveMode = (shift: boolean): ConstrainMode => shift ? "shift" : "autoSnap"; - // Cache the other-objects snapshot for the duration of a single endpoint - // drag — captured lazily on the first onDragMove and cleared on - // onDragEnd. Avoids re-querying every Konva node's clientRect per frame. + // Cached per-drag to avoid re-querying every Konva clientRect per frame. const othersSnapshotRef = useRef(null); - /** - * Run the projected endpoint position through object-snap (other shapes' - * edges + label edges). Skips when shift is held — the user-explicit - * 45°-step constraint would otherwise fight the snap-nudge. - * - * The snap pipeline (othersSnapshot, labelRect, returned guides) is in - * stage-screen coords. The line's own drag math is in label-group local - * coords (which coincide with stage at viewRotation=0 but diverge under - * rotation). The `parent` Konva node is used to convert local↔stage so - * the snap stays correct in rotated views. - */ + /** Object-snap for an endpoint; uses parent transform so it stays + * correct in rotated views. Skipped when shift is held. */ function snapEndpoint( localPx: { x: number; y: number }, shift: boolean, @@ -227,15 +167,8 @@ export function LineObject({ setGuides?.([]); } - /** - * Smart-align snap for the thickness side-handle. Treats the band as a - * resize bbox with only the extrusion edge active (`bottom` for horizontal - * lines, `right` for vertical) and delegates to `computeResizeSnap`, the - * same path the Transformer uses for free resizes. Returns the snapped - * thickness in dots. No-ops on diagonals (band axis isn't stage-aligned), - * rotated views (AABB conversion would mislabel the active edge), or - * shift-bypass. - */ + /** Resize-snap for thickness side-handle; no-op on diagonals, rotated + * views, or shift-bypass. */ function snapThickness( rawT: number, shift: boolean, @@ -260,8 +193,7 @@ export function LineObject({ } const transform = parent.getAbsoluteTransform(); const thicknessPx = Math.max(dotsToPx(rawT, scale, dpmm), 1); - // Band bbox in label-local coords, then mapped to stage so the others - // snapshot (already in stage frame) can be compared apples-to-apples. + // Map band bbox local -> stage to match others snapshot frame. const localBand = isHorizontal ? { x: Math.min(dispX1, dispX2), @@ -303,21 +235,13 @@ export function LineObject({ p.length, Math.max(1, Math.round(pxToDots(snappedExtPx, scale, dpmm))), ); - // If the t ≤ length cap forced the band shorter than the snap target, - // the guide would point past the visible band edge — clear it so the - // hint matches what the user sees commit. + // Clear guide when the t<=length cap shortened the band past the snap target. const cappedExtPx = dotsToPx(cappedT, scale, dpmm); setGuides(Math.abs(cappedExtPx - snappedExtPx) < 0.5 ? result.guides : []); return cappedT; } - /** - * Full endpoint-drag pipeline: axis constraint → object snap → final - * geometry derivation. Returns the same shape as `project` so call - * sites stay symmetric; the snapped endpoint may sit slightly off the - * axis the constraint chose, which is the standard Figma compromise - * (snap nudges trump the auto-snap step, but shift still locks). - */ + /** axis-constraint -> object-snap -> derived geometry. */ function endpointDrag( cursorXPx: number, cursorYPx: number, @@ -343,13 +267,7 @@ export function LineObject({ }; } - // Project the cursor (`cursorPx`) toward the line endpoint that should - // stay fixed (`anchorDots`), returning both the constrained line geometry - // and the new "moving" endpoint in display pixels. `forStart=true` means - // the user is dragging the START handle, so the geometry is computed from - // new-start → fixed-end and the new start is `end - projected_delta`. - // `forStart=false` is the END-handle case: start stays fixed, the new - // end follows the projection from start. + // forStart=true: dragging START handle; geometry is new-start -> fixed-end. function project( cursorXPx: number, cursorYPx: number, @@ -360,8 +278,7 @@ export function LineObject({ ) { const cursorXDots = snap(pxToDots(cursorXPx - offsetX, scale, dpmm)); const cursorYDots = snap(pxToDots(cursorYPx - offsetY, scale, dpmm)); - // dx/dy is always the line direction (start → end), so for a - // start-handle drag we flip the input vector. + // dx/dy is always start->end; flip for start-handle drag. const inputDx = forStart ? anchorXDots - cursorXDots : cursorXDots - anchorXDots; @@ -383,18 +300,13 @@ export function LineObject({ }; } - // Diagonal-only: the parallelogram vertex list is reused by the body - // (filled) and the selection outline (stroke), so compute it once. - // Returns garbage for axis-aligned input — but the diagonal branch is - // gated on !isAxisAligned, so it's only consumed when valid. + // Garbage for axis-aligned input; only consumed in the !isAxisAligned branch. const diagPoints = diagonalPolygonPoints( dispX1, dispY1, dispX2, dispY2, lineStrokeWidth, ); - // Thickness handle anchor — sits on the far long edge of the band: - // bottom edge for horizontal lines, right edge otherwise. The handle's - // perpendicular drag direction is then y for horizontal and x for - // anything else, matching ZPL's ^GB / ^GD extrusion conventions. + // Anchored to band's far long edge; perpendicular drag axis matches + // ^GB / ^GD extrusion. const lineCenterX = (dispX1 + dispX2) / 2; const lineCenterY = (dispY1 + dispY2) / 2; const thickHandleX = @@ -404,11 +316,6 @@ export function LineObject({ return ( - {/* Visible line — tracks both whole-drag and handle-drag live. - Difference blend keeps the reverse case print-correct: on the - white label it renders black, over darker shapes it inverts - those pixels. Stays in reverse mode even when selected so the - inversion visualisation isn't masked. */} {isAxisAligned ? ( <> ) : ( <> - {/* Diagonal ^GD body — closed filled parallelogram rather than - a centred stroke so the canvas matches Labelary's flat-top / - pointy-side geometry. Reverse uses the same difference blend - as the stroked case. */} + {/* ^GD: filled parallelogram (not stroke) matches Labelary geometry. */} )} - {/* Wide transparent hit area — handles click-to-select and whole-line drag. - id is here (not on the Group) so the Stage snap handler can find this node - via e.target.id() and apply object-snap correctly. The hit area is - shifted along with the visible body so clicks register where the - user sees the line. */} + {/* Hit area carries the id so Stage snap can find this node. */} { - // Snap the absolute start-point position to the grid (not - // the delta), then derive the delta to apply. Snapping the - // delta would let an off-grid line stay off-grid forever; - // shapes, text and the endpoint handles in this same - // component all snap absolute, so the line should too. + // Snap absolute (not delta) so off-grid lines drift onto the grid. const newX = snap(obj.x + pxToDots(e.target.x(), scale, dpmm)); const newY = snap(obj.y + pxToDots(e.target.y(), scale, dpmm)); const deltaXPx = dotsToPx(newX - obj.x, scale, dpmm); @@ -504,10 +400,7 @@ export function LineObject({ /> {isSelected && ( <> - {/* Start point — dragging moves the origin; end point stays fixed. - Visuals match the Konva Transformer anchors used for other - shapes: small white square with a thin selection-coloured - border. The hit area is the larger transparent Rect's box. */} + {/* Start point: dragging moves origin; end stays fixed. */} length - // → printed `t × t`); cap thickness to the new length so - // the model preserves the t ≤ length invariant. + // Cap thickness to length; t > length triggers ^GB promotion (t x t). onChange({ x: r.movingDotX, y: r.movingDotY, @@ -580,7 +470,7 @@ export function LineObject({ strokeWidth={1} listening={false} /> - {/* End point — dragging changes length & angle */} + {/* End point: dragging changes length & angle */} - {/* Thickness handle — drags perpendicular to the extrusion - axis (y for horizontal, x for everything else). Clamps to - the 1-dot minimum; flip-on-overshoot is deferred. */} + {/* Thickness handle, perpendicular drag; flip-on-overshoot deferred. */} length promotion. const rawT = Math.min( p.length, Math.max(1, Math.round(pxToDots(extPx, scale, dpmm))), @@ -673,9 +558,7 @@ export function LineObject({ e.target.getParent(), ); setLiveThicknessDots(newT); - // Pin the Rect to the (possibly-clamped, possibly-snapped) - // anchor so dragging past the minimum doesn't decouple the - // handle from the band edge. + // Pin Rect to the clamped/snapped anchor so the handle stays on the band edge. const newStroke = Math.max(dotsToPx(newT, scale, dpmm), 1); e.target.position({ x: diff --git a/src/components/Canvas/RotationButton.tsx b/src/components/Canvas/RotationButton.tsx index 63690954..bd930db5 100644 --- a/src/components/Canvas/RotationButton.tsx +++ b/src/components/Canvas/RotationButton.tsx @@ -8,7 +8,7 @@ import type Konva from "konva"; * barcodes) where rotation is N/R/I/B rather than a free angle from the * Konva Transformer. * - * Positioned in stage-space (relative to the Layer) — the caller resolves + * Positioned in stage-space (relative to the Layer); the caller resolves * the selected node's bbox via getClientRect({ relativeTo: stage }) so the * button stays at the visual top-right even when the underlying object is * itself rotated to R/I/B. @@ -21,7 +21,7 @@ interface Props { } const RADIUS = 11; -// Lucide "rotate-cw" (24x24). Single-arrow rotation glyph — the +// Lucide "rotate-cw" (24x24). Single-arrow rotation glyph; the // universally-recognised "rotate 90°" icon (Photoshop / Figma / Word all // use this shape). Drawn as a Konva Path scaled down and centred on the // button origin via a wrapping Group. @@ -36,7 +36,7 @@ export const RotationButton = forwardRef(function RotationBu ) { 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 + // clean up (onMouseLeave never fires in that case; e.g. user hits Delete // or Esc while the cursor is over the button). const cursorStageRef = useRef(null); @@ -78,7 +78,7 @@ export const RotationButton = forwardRef(function RotationBu onClick(); }} > - {/* Invisible hitbox — gives the icon a comfortable click target + {/* Invisible hitbox; gives the icon a comfortable click target even though the icon path itself is thin. */} 294 dots matches exactly (~0.5976). export const PLESSEY_BWIP_TO_ZEBRA_WIDTH_RATIO = 49 / 82; -/** Symbologies whose displayed bbox is dimensionally ZPL-correct but whose - * bitmap diverges visually from Zebra firmware — either through stretching - * to fit the corrected bbox (code93/code11/plessey) or through a different - * bar-pattern encoder (gs1databar: bwip-js and Zebra encode the same data - * with the same module count but at different transition positions). UI - * surfaces a hint when one is selected so users know the layout is correct - * even when the visual rendering is approximate. */ -/** Severity of visual-render approximation per symbology. One - * axis with two levels: - * - "approx": bitmap approximate, dimensions spec-correct - * (we compute the bbox against the ZPL formula) - * - "unverified": bitmap approximate AND dimensions not cross- - * checkable against Labelary because Labelary - * returns the raw text payload instead of a real - * symbol render. The printed footprint may differ - * from the canvas bbox. - * The UI picks a per-severity hint string; absence means the - * canvas render is trusted. */ +/** approx = bitmap visual diverges but bbox is spec-correct. + * unverified = bitmap diverges AND bbox not Labelary-cross-checkable. */ export type BwipApproxSeverity = "approx" | "unverified"; export const BWIP_APPROX_SEVERITY: ReadonlyMap = new Map([ @@ -111,14 +68,8 @@ export const BWIP_APPROX_SEVERITY: ReadonlyMap = new ["codablock", "unverified"], ]); -// Per-symbology spec module heights for GS1 DataBar. bwip-js renders most -// non-stacked variants at the same canvas height as the omni form (33 modules) -// regardless of the actual variant, which doesn't match Zebra firmware. Use -// these spec values to compute the ZPL-correct bbox height instead of trusting -// the bwip canvas dims. Sym 7 (Expanded Stacked) is segments-dependent and -// falls back to the bwip-natural height. -// 1 Omnidirectional, 2 Truncated, 3 Stacked, 4 Stacked Omnidirectional, -// 5 Limited, 6 Expanded — modules from GS1 General Specifications. +// GS1 General Specifications module heights; bwip renders non-stacked +// variants at omni (33) regardless. Sym 7 segments-dependent (falls back). export const GS1_DATABAR_SPEC_HEIGHT_MODULES: Partial< Record<1 | 2 | 3 | 4 | 5 | 6 | 7, number> > = { @@ -130,10 +81,7 @@ 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. */ +/** bwip's paddingheight rows; shared with bitmap-crop logic. */ export const GS1_DATABAR_PADDING_ROWS = 2; export const EAN_UPC_TYPES = new Set([ diff --git a/src/components/Canvas/bwipHelpers.test.ts b/src/components/Canvas/bwipHelpers.test.ts index c049d330..736d8e82 100644 --- a/src/components/Canvas/bwipHelpers.test.ts +++ b/src/components/Canvas/bwipHelpers.test.ts @@ -151,7 +151,7 @@ describe("getDisplaySize gs1databar sym 7 fallback", () => { // Sym 7 (Expanded Stacked) cannot be Labelary-cross-validated due to a // parens-AI input-format mismatch between bwip-js and Zebra firmware. // The implementation falls back to bwip-natural canvas height. This test - // pins that behavior — any change must be intentional and accompanied + // pins that behavior; any change must be intentional and accompanied // by a documented strategy for the missing ground truth. it("derives height from canvas dims (bwip-natural), not from a spec table", () => { const obj: LabelObject = { @@ -170,7 +170,7 @@ describe("getDisplaySize gs1databar sym 7 fallback", () => { }; // Canvas height varies per content+segments; we use a representative // value that bwip-js produced for a 16-char content at default - // segments. The exact pixel size isn't load-bearing — what matters is + // segments. The exact pixel size isn't load-bearing; what matters is // the formula, which derives from `ch`. const ch = 73; const cw = 100; diff --git a/src/components/Canvas/bwipHelpers.ts b/src/components/Canvas/bwipHelpers.ts index b09c41b9..1df075bf 100644 --- a/src/components/Canvas/bwipHelpers.ts +++ b/src/components/Canvas/bwipHelpers.ts @@ -1,16 +1,5 @@ -/** - * ZPL-FIRST SIZING POLICY - * - * The designer is a layout tool, not a scanner. `getDisplaySize` maps the - * bwip-js intrinsic canvas size to the ZPL-correct display pixels so the - * displayed bbox matches what Zebra firmware will print. Bar patterns may - * look slightly distorted because bwip-js' rendering algorithm differs. - * - * Per-symbology rationale (especially for the deliberately-not-corrected - * cases code93, code11, plessey) is in the inline comments at each `case` - * block in getUprightDisplaySize. The static-parse test in - * bwipHelpers.test.ts ensures every BCID-registered type has a case. - */ +// ZPL-first sizing: bbox matches Zebra firmware print, not bwip-js intrinsic +// canvas. Per-symbology rationale lives at each case in getUprightDisplaySize. import bwipjs from "bwip-js/browser"; import { ObjectRegistry, type LeafObject } from "../../registry"; @@ -37,22 +26,13 @@ import { upcSuppTextZoneDots, } from "./bwipConstants"; -/** - * AI 01 followed by exactly 11 numeric digits (13 chars total) is not a valid - * GTIN-14 element string. Zebra firmware does NOT pad it to Method 1; it falls - * back to General Compaction (~149 modules at 8dpmm). bwip-js with `(01)` - * would force Method 1 (~133 modules), so we re-route through `(99)` to get - * General Compaction encoding too. Empirical cutoff (probed against Labelary). - */ +// AI 01 + 11 digits is not a valid GTIN-14. Zebra falls back to General +// Compaction (~149 modules at 8dpmm); routing through (99) matches that +// where bwip-js would otherwise force Method 1. Probed against Labelary. function isAi01ElevenDigitFragment(content: string): boolean { return /^01\d{11}$/.test(content); } -/** - * Bwip-js text for GS1 DataBar Expanded — wraps raw AI input in parens and - * routes the empirically-known length-mismatch case (AI 01 + 11 digits) through - * `(99)` so the rendered bitmap width matches Zebra firmware's print output. - */ function gs1ExpandedBwipText(content: string): string { if (isAi01ElevenDigitFragment(content)) return `(99)${content}`; return wrapGs1AIs(content); @@ -84,9 +64,7 @@ const BCID: Partial> = { logmars: "code39", msi: "msi", plessey: "plessey", - // Placeholder — the actual bcid is resolved per-symbology via - // GS1_DATABAR_BCID below. This entry exists only to pass the - // `if (!bcid) return null` guard at the top of buildBwipOptions. + // Placeholder; real bcid resolved per-symbology via GS1_DATABAR_BCID. gs1databar: "databaromni", planet: "planet", postal: "postnet", @@ -97,8 +75,7 @@ const BCID: Partial> = { maxicode: "maxicode", micropdf417: "micropdf417", codablock: "codablockf", - // Placeholder — actual bcid (ean2 vs ean5) is resolved from the - // content length in the per-type switch in buildBwipOptions. + // Placeholder; ean2 vs ean5 resolved from content length. upcEanExtension: "ean5", code49: "code49", }; @@ -106,33 +83,20 @@ const BCID: Partial> = { export const BWIP_SCALE = 2; const BWIP_2D_INTERNAL_SCALE = 2; -/** Reused unattached canvas for dry-run encoding validators (e.g. - * `validateMaxicodeBwip`). Cheaper than creating a fresh element - * per call and we don't need its pixel output, just the success - * /throw signal from bwip-js. Initialised lazily so SSR-style - * imports don't break — `document` exists only in the browser. */ +// Lazy so SSR imports don't crash on missing `document`. let _validationCanvas: HTMLCanvasElement | null = null; function getValidationCanvas(): HTMLCanvasElement { if (!_validationCanvas) _validationCanvas = document.createElement("canvas"); return _validationCanvas; } -/** Strip the `bwip-js: bwipp.symbology:` prefixes that bwip-js - * decorates every encoder error with, so the bare diagnostic - * ("Expected postcode followed by group separator character", - * etc.) can be shown as-is in the UI. Shared between the canvas - * render (BarcodeObject) and dry-run validators below. */ +/** Strip `bwip-js: bwipp.symbology:` prefixes from encoder errors for UI display. */ export function cleanBwipError(e: unknown): string { const raw = e instanceof Error ? e.message : String(e); return raw.replace(/^bwip-js:\s*/i, "").replace(/^bwipp\.[^:]+:\s*/i, ""); } -/** Dry-run a Maxicode encode against the shared validation canvas - * to check whether the (content, mode) combination would succeed - * in the real render path. Returns null on success or the cleaned - * bwip-js error message on failure (e.g. SCM-format mismatch for - * modes 2/3). Uses the same option shape `buildBwipOptions` - * produces so canvas render and panel-side validation can't drift. */ +/** Dry-run encode; returns null on success or cleaned error message. */ export function validateMaxicodeBwip(content: string, mode: number): string | null { try { const opts = { @@ -148,22 +112,12 @@ export function validateMaxicodeBwip(content: string, mode: number): string | nu } } -/** - * Estimate the number of columns ZPL/Labelary would choose for PDF417 when - * columns=0 (auto). Zebra uses a heuristic to keep the symbol somewhat square. - * This formula is derived empirically by measuring Labelary outputs for - * various content lengths. - */ +// Mirrors Zebra's columns=0 auto-heuristic. Empirically validated against +// Labelary at 8dpmm, secLevel 0 and 1, content 10..49 chars. function estimatePdf417Columns(content: string, securityLevel: number): number { - // Rough estimate of codewords: each 2.3 characters ~ 1 codeword. - // Security level adds (2^(securityLevel+1)) error correction codewords. const dataCodewords = Math.ceil((content.length || 1) / 2.3); const eccCodewords = Math.pow(2, securityLevel + 1); const totalCodewords = dataCodewords + eccCodewords; - - // Zebra heuristic: columns = floor(sqrt(totalCodewords / 4)) - // Empirically validated against Labelary at 8dpmm for secLevel 0 and 1, - // content lengths 10–49 chars. return Math.max(1, Math.min(30, Math.floor(Math.sqrt(totalCodewords / 4)))); } @@ -171,7 +125,7 @@ function estimatePdf417Columns(content: string, securityLevel: number): number { // row count exceeds what the data strictly requires. const BWIP_PDF417_MIN_ROWHEIGHT = 3; -// EAN/UPC bar-pattern module layout (no quiet zones — bwip-js native canvas). +// EAN/UPC bar-pattern module layout (no quiet zones, bwip-js native canvas). // All values are module offsets/widths from the left edge of the bar pattern. // ean13/upca: 3 start | 6×7 (42) | 5 centre | 6×7 (42) | 3 end = 95 modules // ean8: 3 start | 4×7 (28) | 5 centre | 4×7 (28) | 3 end = 67 modules @@ -192,16 +146,12 @@ export interface EanUpcLayout { modulePx: number; /** x-position (display px) of the left/main digit block. */ xLeft: number; - /** x-position (display px) of the right digit block; 0 for upce. */ + /** x-position (display px) of the right digit block, 0 for upce. */ xRight: number; /** Width (display px) of each digit block. */ halfWidth: number; } -/** - * Compute display-pixel positions for the manually-rendered digit labels - * underneath EAN/UPC bar patterns. Pure function; testable without Konva. - */ export function getEanUpcLayout( type: EanUpcType, displayWidth: number, @@ -226,12 +176,7 @@ export function getEanUpcLayout( // and barcode_postal_standard (6 digits) at 8dpmm, moduleWidth=2. const POSTNET_PLANET_WIDTH_RATIO = 0.7525; -/** - * Compute the optimal bwip render scale for 1D barcodes so that each module - * maps to an integer number of display pixels (avoiding anti-aliasing on - * non-integer upscaling). Falls back to BWIP_SCALE when display pixels per - * module round to zero. - */ +/** Integer-aligned per-module render scale; avoids non-integer upscaling. */ export function get1DBwipScale( moduleWidth: number, scale: number, @@ -240,13 +185,6 @@ export function get1DBwipScale( return Math.max(1, Math.round(dotsToPx(moduleWidth, scale, dpmm))); } -/** - * Render-scale for 1D barcodes: integer-aligned per-module scale when the - * caller supplies the canvas scale + dpmm, otherwise the BWIP_SCALE default. - * Wrapping the branch lets per-type cases pass their own (narrowed, - * required-typed) `moduleWidth` instead of going through a cast at the - * generic top of buildBwipOptions. - */ function bwipScale1D( moduleWidth: number, renderScale: number | undefined, @@ -257,17 +195,10 @@ function bwipScale1D( : BWIP_SCALE; } -// Check-digit math now lives in src/lib/barcodeCheckDigits.ts (pure, -// no Canvas deps). Re-export here so existing callers (BarcodeObject) -// keep working without touching every import. export { eanCheckDigit, upceCheckDigit } from "../../lib/barcodeCheckDigits"; -/** - * Encode text as Code 128 subset B using bwip-js raw ^NNN format. - * ZPL's ^BC defaults to subset B for printable ASCII content, so using raw - * Code B here keeps the designer's module count in sync with Labelary. - * Returns null for characters outside Code B range (ASCII 32–126). - */ +// Forcing Code B keeps module count in sync with Labelary (^BC default). +// Returns null for chars outside ASCII 32..126. function toCode128BRaw(text: string): string | null { if (!text) return null; const parts = ["^104"]; // Start B @@ -279,28 +210,9 @@ function toCode128BRaw(text: string): string | null { return parts.join(""); } -/** - * Translate ZPL ^BC field-data escape sequences (`>X`) into bwip-js parsefnc - * syntax. Returns null when no recognized escape is present so the caller can - * stay on the existing raw-Subset-B path. - * - * ZPL Code 128 escapes (Zebra ZPL II Programming Guide): - * >0 → literal `>` - * >5 → FNC1 - * >6 → FNC2 - * >7 → FNC3 - * >8 → FNC4 - * >9 → invoke Code C; inserts FNC1 only when it is the first character - * of the field (per ZPL II manual). Mid-string `>9` is just a - * subset switch which bwip auto-mode handles for digit runs. - * >: → switch to Subset B (dropped; bwip auto-mode chooses the subset) - * >; → switch to Subset A (dropped; bwip auto-mode chooses the subset) - * - * Without this translation, `STRSTR>52316094000242201` is rendered as 21 raw - * Subset-B symbols, while the firmware reads `>5` as FNC1 and switches to - * Subset C for the 16 trailing digits — yielding ~15 symbols and a much - * narrower bbox. The mismatch is what users observe as "Länge stimmt nicht". - */ +// Translate ^BC field-data escapes (`>X`) to bwip parsefnc; null when none. +// ZPL II: >0 literal >, >5..>8 FNC1..FNC4, >9 FNC1 only at pos 0 (else +// subset switch handled by bwip auto), >:/>; subset switches (dropped). export function parseZplCode128Escapes(text: string): string | null { if (!/>[05-9:;]/.test(text)) return null; let out = ""; @@ -318,7 +230,7 @@ export function parseZplCode128Escapes(text: string): string | null { case "8": out += "^FNC4"; i++; continue; case "9": if (i === 0) out += "^FNC1"; i++; continue; case ":": - case ";": i++; continue; // subset switch — bwip auto-mode handles it + case ";": i++; continue; // subset switch, bwip auto-mode handles it } } out += ch; @@ -334,12 +246,7 @@ export function buildBwipOptions( const bcid = BCID[obj.type]; if (!bcid) return null; - // bwip always renders upright; visual rotation is handled in the - // Konva renderer via an inner rotated Group (rotatedGroupTransform). - // Declared without an initializer because every reachable case - // assigns before `break` and the `default` arm returns early — the - // previous `= null` initializer is what ESLint 10's - // no-useless-assignment now flags as dead. + // bwip always renders upright; Konva renderer applies visual rotation. let opts: Record; switch (obj.type) { @@ -362,9 +269,7 @@ export function buildBwipOptions( case "code49": { const p = obj.props; const scale = bwipScale1D(p.moduleWidth, renderScale, renderDpmm); - // Stacked 1D, bwip auto-picks row count. Clamp rowheight to - // bwip's 8..50 range — defensive net for JSON loads that - // bypass the registry's commitTransform/normalize clamps. + // Clamp to bwip's 8..50 range; guards JSON loads that bypass registry. const rawRow = Math.round(p.height / Math.max(p.moduleWidth, 1)); const rowheight = Math.min(50, Math.max(8, rawRow)); opts = { @@ -385,17 +290,9 @@ export function buildBwipOptions( case "upcEanExtension": { const p = obj.props; const scale = bwipScale1D(p.moduleWidth, renderScale, renderDpmm); - // ZPL ^BS uses one command for both lengths; bwip splits the - // bcid. Anything that isn't a 2-digit supplement is rendered - // as the 5-digit variant — matches printer behaviour where - // the 5-digit form is the common case (ISBN price, magazine - // sequence) and bwip-js rejects other lengths outright. - // HRI digits sit ABOVE the bars per Zebra firmware. Rendered - // as a separate Konva Text overlay (same pattern as logmars) - // so all four rotations land at the firmware-correct anchor - // via the inner rotated Group in BarcodeObject; bwip's own - // includetext would bake the text into the bitmap and rotate - // with it. + // bwip splits ^BS into ean2/ean5 by length; non-2 falls back to ean5. + // HRI rendered as separate Konva overlay (Zebra puts it above bars) + // so rotation lands at the firmware anchor. const text = p.content || "00000"; const variantBcid = text.length === 2 ? "ean2" : "ean5"; opts = { @@ -403,10 +300,6 @@ export function buildBwipOptions( text, scale, height: 10, - // bwip-js defaults includetext=true for ean2/ean5 and bakes the - // digits into the bitmap; we render the HRI via Konva so it can - // follow Zebra's Font 0 magnification steps and stay aligned - // under all four rotations. includetext: false, }; break; @@ -415,12 +308,9 @@ export function buildBwipOptions( const p = obj.props; const scale = bwipScale1D(p.moduleWidth, renderScale, renderDpmm); const text = p.content || "0"; - // Note: ZPL ^BC e=Y (checkDigit) only prints the MOD-10 digit in the - // interpretation line — it does NOT append it to the encoded barcode data. - // ZPL escape sequences (e.g. `>5` for FNC1, `>9` for Code-C switch with - // FNC1) require parsefnc auto-mode so bwip emits the same compact symbol - // count Zebra firmware does. Plain ASCII falls through to the raw Code B - // path which keeps the existing module count behaviour unchanged. + // ^BC e=Y only prints MOD-10 in HRI, not in encoded data. ZPL escapes + // need parsefnc auto-mode to match firmware's symbol count; plain ASCII + // stays on the raw Code B path. const escaped = parseZplCode128Escapes(text); if (escaped !== null) { opts = { bcid, text: escaped, parsefnc: true, scale, height: 10 }; @@ -444,11 +334,7 @@ export function buildBwipOptions( case "plessey": { const p = obj.props; const scale = bwipScale1D(p.moduleWidth, renderScale, renderDpmm); - // Code39/Codabar/Plessey only encode uppercase letters. Zebra firmware - // (and Labelary) silently uppercase lowercase input; bwip-js does not and - // throws instead, which would crash the canvas for any imported ZPL with - // lowercase content. Uppercase here so the lib matches firmware behaviour - // without rewriting the user's source. + // Zebra silently uppercases for these symbologies; bwip-js throws. const needsUpper = obj.type === "code39" || obj.type === "codabar" || obj.type === "plessey"; const raw = p.content || "0"; const text = needsUpper ? raw.toUpperCase() : raw; @@ -458,8 +344,7 @@ export function buildBwipOptions( case "msi": { const p = obj.props; const scale = bwipScale1D(p.moduleWidth, renderScale, renderDpmm); - // Zebra always encodes a Mod10 check digit in MSI regardless of the ^BM e=N - // parameter (which only suppresses it in the interpretation line). + // Zebra always encodes Mod10 in MSI; ^BM e=N only suppresses the HRI digit. opts = { bcid, text: p.content || "0", scale, height: 10, includecheck: true }; break; } @@ -487,8 +372,8 @@ export function buildBwipOptions( const scale = bwipScale1D(p.magnification, renderScale, renderDpmm); const sym = p.symbology; const isExpanded = GS1_DATABAR_EXPANDED_SYMBOLOGIES.has(sym); - // bwip-js needs (AI)data parens; canonical model stores raw digits. - // Sym 1–5 require AI 01 + valid 14-digit GTIN with correct check. + // bwip needs (AI)data parens; model stores raw digits. + // Sym 1..5 require AI 01 + valid 14-digit GTIN with check. const text = isExpanded ? gs1ExpandedBwipText(p.content) : `(01)${gtin14WithCheck(p.content)}`; @@ -558,9 +443,6 @@ export function buildBwipOptions( } case "maxicode": { const p = obj.props; - // bwip-js maxicode uses `mode` (number) directly; modes 2/3 - // would also accept a structured carrier message via `parse`, - // but we don't expose SCM editing yet so pass content as-is. opts = { bcid, text: p.content || " ", scale: BWIP_SCALE, mode: p.mode }; break; } @@ -594,26 +476,13 @@ export function buildBwipOptions( return null; } - // Visual rotation is Konva's job (wraps the KImage in a rotated Group - // via rotatedGroupTransform). Asking bwip to rotate would produce - // bitmaps in mismatched orientations across types — keep them all - // upright so the renderer can rotate them uniformly. return opts; } /** - * Display size of a barcode bbox in pixels. - * - * `w` × `h` is the full footprint Zebra firmware reserves on the print — - * this includes any text zone that may sit on one side of the bars. The - * bars themselves occupy a sub-rectangle described by - * `(barLeftPx, barTopPx, barW, barH)`. For symbologies without a text - * zone or for rotations the text zone hasn't been mapped onto, the bar - * rect equals the full bbox. - * - * Renderers should draw the bwip-js bitmap inside the bar sub-rectangle so - * the bars appear at their true height, while the Konva Group / hit area - * spans the full bbox so selection-handles match the printed footprint. + * Barcode bbox in pixels. `(w, h)` is the firmware-reserved footprint + * including text zones; `(barLeftPx, barTopPx, barW, barH)` is the bar + * sub-rect for drawing the bitmap. */ export interface BarcodeDisplaySize { w: number; @@ -622,10 +491,7 @@ export interface BarcodeDisplaySize { barH: number; barLeftPx: number; barTopPx: number; - /** Upright (rotation=N) view of the same layout. Renderers that draw - * inside an inner rotated Group consume this so their geometry stays - * rotation-independent — the inner group's transform handles the - * visual rotation. Always present alongside the rotated fields. */ + /** Upright (rotation=N) layout for inner-rotated-Group renderers. */ upright: { w: number; h: number; @@ -634,18 +500,12 @@ export interface BarcodeDisplaySize { barLeftPx: number; barTopPx: number; }; - /** Sub-rect of the bwip-js canvas to render (in source pixel coords). - * Lets the renderer skip bwip's internal padding, e.g. the - * paddingheight pad on GS1 DataBar that would otherwise leave the - * bars proportionally shorter than the firmware-reserved bbox. - * Undefined = use the full canvas. */ + /** Sub-rect of bwip canvas to render, skipping internal padding (e.g. + * GS1 DataBar paddingheight). Undefined = full canvas. */ bitmapCrop?: { x: number; y: number; width: number; height: number }; } -/** Firmware-reserved text-zone height in dots, keyed by symbology. The - * zone sits below the bars in upright orientation; rotation maps it to - * another side of the bbox in getDisplaySize. Types not listed have no - * reserved zone. */ +/** Firmware-reserved text-zone height in dots (below bars in upright). */ const TEXT_ZONE_DOTS_BY_TYPE: Partial> = { ean13: EAN_TEXT_ZONE_DOTS, ean8: EAN_TEXT_ZONE_DOTS, @@ -667,9 +527,6 @@ export function getDisplaySize( }; } - // bwip-js now always renders upright (the BarcodeObject renderer - // handles visual rotation via a rotated Konva Group), so the canvas - // dimensions are the upright dimensions directly. const rotation = objectRotation(obj.props); const isQuarter = rotation === "R" || rotation === "B"; const upright = getUprightDisplaySize(obj, canvas.width, canvas.height, scale, dpmm); @@ -678,15 +535,8 @@ export function getDisplaySize( const w = isQuarter ? upright.h : upright.w; const h = isQuarter ? upright.w : upright.h; - // Text-zone reservation in upright orientation, on the "below" side of - // the bars per Labelary's bbox. Zero for symbologies without one. - // ^BS supplements reserve the zone ABOVE the bars in upright (N); - // bookkeeping reuses the same px value but flips which side gets - // the offset. - // ^BS reserves the text zone only when printInterpretation=Y; with - // f=N the printer prints bars only and bbox = bar height. Other - // EAN/UPC reserve the 13-dot zone unconditionally (Zebra firmware - // ships a fixed text guard even when N). + // ^BS reserves text zone only when printInterpretation=Y; other EAN/UPC + // reserve the 13-dot zone unconditionally (firmware ships a fixed guard). const textZoneDots = obj.type === "upcEanExtension" ? obj.props.printInterpretation @@ -694,11 +544,8 @@ export function getDisplaySize( : 0 : TEXT_ZONE_DOTS_BY_TYPE[obj.type] ?? 0; const textZonePx = dotsToPx(textZoneDots, scale, dpmm); - // Source of truth for textAbove is the registry's HriBehavior — same - // field BarcodeObject consumes for its overlay positioning. Without - // this the bbox places bars at the top and reserves the zone at the - // bottom, but the renderer draws the text above the bars at negative - // y → text leaks out of the bbox. Bug spotted by gemini on PR #90. + // Must mirror BarcodeObject's HriBehavior, else above-bars text leaks + // out of the bbox (PR #90). const isTextAbove = ObjectRegistry[obj.type]?.hri?.textAbove ?? false; // Map the upright "below the bars" zone onto the rotated bbox: it travels @@ -712,9 +559,6 @@ export function getDisplaySize( let barW = w; let barH = h; if (textZonePx > 0) { - // isTextAbove flips the upright zone from "below the bars" to "above - // the bars" (and the corresponding rotated edges) without duplicating - // the rotation table. if (!isTextAbove) { switch (rotation) { case "N": barH = h - textZonePx; break; @@ -732,13 +576,7 @@ export function getDisplaySize( } } - // GS1 DataBar opts include `paddingheight: N`, which adds whitespace - // rows on top and bottom of the upright 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. Always y-axis now because - // bwip renders upright. + // Crop GS1 DataBar paddingheight rows so bars fill the firmware-reserved height. let bitmapCrop: BarcodeDisplaySize["bitmapCrop"]; if (obj.type === "gs1databar") { const bwipSc = get1DBwipScale(obj.props.magnification, scale, dpmm); @@ -753,11 +591,6 @@ export function getDisplaySize( } } - // Upright view (rotation=N) of the same layout. Text zone sits above - // the bars when isTextAbove (logmars), otherwise implicitly below - // (barTopPx=0, barH = h - textZonePx). Bars span the full width. - // The inner-rotated-Group renderer pattern consumes this so its - // geometry stays rotation-independent. const uprightView = { w: upright.w, h: upright.h, @@ -777,17 +610,11 @@ function getUprightDisplaySize( scale: number, dpmm: number, ): { w: number; h: number } { - // bwip-js at bwipSc=1 renders 1 extra pixel; at bwipSc>=2 it renders the exact module - // count. The extraPx term corrects for this so formulas stay consistent across scales. + // bwip at bwipSc=1 renders 1 extra px; bwipSc>=2 is exact. extraPx corrects. switch (obj.type) { case "code93": case "code11": { - // bwip-js uses a narrower quiet zone than Zebra firmware. The - // shortfall is content-independent — a fixed module count per - // symbology — so we add it to the bwip module count to recover - // the ZPL-correct print width. The bitmap stretches ~10-25% to - // fill the wider bbox; bars look slightly broader than the - // printed output but dimensions match. + // bwip's quiet zone is narrower than Zebra; add the fixed shortfall. const delta = obj.type === "code93" ? CODE93_QUIET_ZONE_DELTA_MODULES : CODE11_QUIET_ZONE_DELTA_MODULES; @@ -798,11 +625,8 @@ function getUprightDisplaySize( return { w, h }; } case "plessey": { - // bwip-js uses a fundamentally different bar encoding from Zebra - // ^BP — bwip renders ~67% wider than Zebra for the same content. - // Both encodings grow linearly with content, so a constant ratio - // suffices. The bitmap squeezes to ~60% of its intrinsic width; - // bars look compressed but the printed footprint matches. + // bwip uses a different bar encoding from ^BP (~67% wider). Constant + // ratio compresses to match the firmware footprint. const modulePx = dotsToPx(obj.props.moduleWidth, scale, dpmm); const bwipSc = get1DBwipScale(obj.props.moduleWidth, scale, dpmm); const w = @@ -826,18 +650,9 @@ function getUprightDisplaySize( const modulePx = dotsToPx(obj.props.magnification, scale, dpmm); const bwipSc = get1DBwipScale(obj.props.magnification, scale, dpmm); const w = (cw / bwipSc) * modulePx; - // bwip-js renders most non-stacked variants at the omni (33-module) - // height regardless of the actual symbology, so trusting `ch` would - // overstate the height for sym 2/5/6 and understate it for sym 4. - // Use the spec-defined module count instead. - // - // Sym 7 (Expanded Stacked) cannot be Labelary-cross-validated: - // bwip-js needs the (AI)data parens-AI input format, Zebra ^BR sym 7 - // silently rejects that input and renders an empty PNG, so neither - // direction can produce a shared ground truth. The bwip-natural - // canvas height is used as a best-effort approximation; the rendered - // size therefore matches what the user sees in bwip's preview but - // is not guaranteed to match Zebra firmware's actual print output. + // bwip renders most non-stacked variants at omni (33-module) height; + // use spec module count instead. Sym 7 cannot be Labelary-validated + // (input format mismatch) so bwip-natural height is best-effort. const specModules = GS1_DATABAR_SPEC_HEIGHT_MODULES[obj.props.symbology]; const h = specModules !== undefined ? specModules * modulePx @@ -855,9 +670,7 @@ function getUprightDisplaySize( case "ean8": case "upca": case "upce": { - // EAN/UPC reserves a 13-dot text zone below the bars in firmware, - // even when printInterpretation=N. Include it in the bbox so the - // selection footprint matches the printed extent. + // 13-dot text zone reserved by firmware even when interpretation=N. const modulePx = dotsToPx(obj.props.moduleWidth, scale, dpmm); const bwipSc = get1DBwipScale(obj.props.moduleWidth, scale, dpmm); const extraPx = bwipSc === 1 ? 1 : 0; @@ -866,12 +679,8 @@ function getUprightDisplaySize( return { w, h }; } case "upcEanExtension": { - // ^BS prints the human-readable digits ABOVE the bars (unlike the - // main EAN/UPC text band below), and Zebra reserves a larger - // vertical zone for it only when printInterpretation=Y. With - // f=N the bbox collapses to bar height (no guard reservation, - // unlike main UPC/EAN which always reserves 13). Measured - // against Labelary at 80-bar height: Y → 98, N → 80. + // ^BS prints HRI ABOVE bars; zone only reserved when interpretation=Y + // (f=N collapses to bar height, unlike main EAN/UPC). Labelary 80h: Y=98, N=80. const modulePx = dotsToPx(obj.props.moduleWidth, scale, dpmm); const bwipSc = get1DBwipScale(obj.props.moduleWidth, scale, dpmm); const extraPx = bwipSc === 1 ? 1 : 0; @@ -883,10 +692,7 @@ function getUprightDisplaySize( return { w, h }; } case "logmars": { - // LOGMARS reserves a text zone above the bars (per spec) regardless of - // printInterpretation. Include LOGMARS_TEXT_ZONE_DOTS so the bbox - // matches the firmware footprint; bwip's bitmap covers only the bar - // portion and is rendered at the bottom of the bbox. + // Spec reserves text zone above bars regardless of printInterpretation. const modulePx = dotsToPx(obj.props.moduleWidth, scale, dpmm); const bwipSc = get1DBwipScale(obj.props.moduleWidth, scale, dpmm); const extraPx = bwipSc === 1 ? 1 : 0; @@ -895,23 +701,14 @@ function getUprightDisplaySize( return { w, h }; } case "code49": { - // Stacked 1D. Labelary's emulator only renders the HRI line - // for ^B4 (not the bars), so bwip-js is the ground truth; - // bbox math is not Labelary-cross-validated. Same as ^BB. + // Labelary only renders HRI for ^B4; bwip is ground truth (same as ^BB). const p = obj.props; - // Mirror buildBwipOptions's 8..50 clamp so numRows recovery - // matches what bwip actually drew. const rawRow = Math.round(p.height / Math.max(p.moduleWidth, 1)); const rowheightUnits = Math.min(50, Math.max(8, rawRow)); const modulePx = dotsToPx(p.moduleWidth, scale, dpmm); const bwipSc = get1DBwipScale(p.moduleWidth, scale, dpmm); - // numRows recovery uses bwipSc (matches the scale bwip was - // called with), not BWIP_SCALE — otherwise the row count is - // wrong whenever rendering at a non-default scale/dpmm. const numRows = Math.max(1, Math.round(ch / (rowheightUnits * bwipSc))); const w = (cw / bwipSc) * modulePx; - // Bbox uses the clamped row height so the preview matches the - // rendered bars when raw h is outside bwip's range. const h = numRows * dotsToPx(rowheightUnits * p.moduleWidth, scale, dpmm); return { w, h }; } @@ -930,14 +727,8 @@ function getUprightDisplaySize( } case "pdf417": { const p = obj.props; - // bwip-js uses a fixed internal row height of 3 for pdf417 const numRows = ch / (BWIP_PDF417_MIN_ROWHEIGHT * BWIP_SCALE); - - // Width check: bwip-js sometimes adds unexpected padding or uses - // different column logic. We force the display width based on the - // actual number of columns. PDF417 width in modules is: - // 17 * start + 17 * left + 17 * columns + 17 * right + 18 * stop - // Total = 17 * (columns + 4) + 1 + // PDF417 module width: 17*(columns+4)+1. const columns = p.columns || estimatePdf417Columns(p.content, p.securityLevel); const modulesW = 17 * (columns + 4) + 1; @@ -965,10 +756,7 @@ function getUprightDisplaySize( return { w: size, h: size }; } case "maxicode": { - // Fixed physical size — no module-magnification prop. bwip-js - // emits the canonical maxicode bitmap at BWIP_SCALE internal - // pixels per dot, so converting cw/ch back to dots and then - // to stage px places the symbol at its true printed size. + // Fixed physical size; convert bwip's px-per-dot back to stage px. const w = dotsToPx(cw / BWIP_SCALE, scale, dpmm); const h = dotsToPx(ch / BWIP_SCALE, scale, dpmm); return { w, h }; @@ -1003,7 +791,7 @@ function getUprightDisplaySize( /** Valid MicroPDF417 row counts in TLC39's linked 4-column geometry. */ export const TLC39_MICROPDF_ROW_COUNTS = [4, 6, 8, 10] as const; -/** Snap to the nearest valid row count; bwip-js throws on any other value. */ +/** Snap to the nearest valid row count, bwip-js throws on any other value. */ export function snapTlc39MicroPdfRows(requested: number): number { if (!Number.isFinite(requested)) return 4; for (const r of TLC39_MICROPDF_ROW_COUNTS) if (requested <= r) return r; @@ -1019,12 +807,7 @@ function micropdfDataRows(canvasHeight: number): number { ); } -/** TLC39 spec splits content on the first comma: ECI (6 digits) for - * the Code 39 base line, serial (≤25 alphanum) for the MicroPDF417 - * block stacked on top. The "T" linkage flag is appended to the - * Code 39 data when a MicroPDF417 follows; the leading "S" data - * identifier (if any) is stripped from the serial before MicroPDF - * encoding. */ +/** Split on first comma: ECI for Code 39, serial for MicroPDF417 (leading "S" stripped). */ export function splitTlc39Content(content: string): { eci: string; serial: string } { if (!content) return { eci: "", serial: "" }; const comma = content.indexOf(","); @@ -1043,9 +826,7 @@ interface Tlc39RenderProps { microPdfRows: number; } -/** TLC39 composite (MicroPDF417 on top, Code 39 base below). bwip-js has - * no native encoder; composes two encoders at dpmm-aware display sizes - * per the TCIF spec (shared width, no separator). */ +/** TLC39 composite (MicroPDF417 on top, Code 39 below; shared width, no separator). */ export function renderTlc39Canvas( props: Tlc39RenderProps, scale: number, @@ -1063,9 +844,6 @@ export function renderTlc39Canvas( bcid: "code39", text: text || " ", scale: bwipScale, - // bwip `height` is in mm at 1x; constant source height - // (stretched to dpmm-correct dots below) matches the standalone - // 1D pattern. height: 10, includetext: false, } as unknown as Parameters[1]); @@ -1099,9 +877,7 @@ export function renderTlc39Canvas( return stretchTo(src, w, code39H); } - // Render MicroPDF first; the "T" linkage flag is only appended to - // Code 39 when the linked MicroPDF actually rendered, otherwise the - // flag would claim a link that does not exist. + // "T" linkage flag only appended after MicroPDF actually renders. const snappedRows = snapTlc39MicroPdfRows(props.microPdfRows); const mpdfSrc = document.createElement("canvas"); let mpdfOk = true; diff --git a/src/components/Canvas/eanUpcDigitNodes.tsx b/src/components/Canvas/eanUpcDigitNodes.tsx index c0567a76..b7d35478 100644 --- a/src/components/Canvas/eanUpcDigitNodes.tsx +++ b/src/components/Canvas/eanUpcDigitNodes.tsx @@ -3,7 +3,7 @@ import type { ReactNode } from "react"; import type { EanUpcType } from "./bwipHelpers"; /** Visual gap in display px between the UPC-E bar end and the floated - * check digit. Chosen by eye to match the Labelary render — the digit + * check digit. Chosen by eye to match the Labelary render; the digit * sits just past the quiet-zone boundary, not flush against the bars. */ const UPCE_TRAIL_DIGIT_GAP_PX = 2; @@ -22,10 +22,10 @@ interface BuildArgs { type: EanUpcType; displayText: string; layout: { xLeft: number; xRight: number; halfWidth: number }; - /** Upright bar width in display px — used as the right anchor for + /** Upright bar width in display px; used as the right anchor for * the UPC-E trail digit (sits 2px past the bar end). */ uprightBarW: number; - /** Upright bar height in display px — text Y = uprightBarH + textGap. */ + /** Upright bar height in display px; text Y = uprightBarH + textGap. */ uprightBarH: number; textGap: number; textFontSize: number; @@ -34,7 +34,7 @@ interface BuildArgs { /** Build the digit-by-digit HRI overlay for EAN-13 / EAN-8 / UPC-A / * UPC-E in upright coords. Single source of truth shared by the * upright printInterp branch and the rotated inner-Group branch in - * BarcodeObject — both produce identical digit positions, only the + * BarcodeObject; both produce identical digit positions, only the * parent wrapper differs (upright clips the bbox; rotated wraps in a * rotatedGroupTransform Group). */ export function buildEanUpcDigitOverlay(args: BuildArgs): EanUpcDigitOverlay { @@ -82,7 +82,7 @@ export function buildEanUpcDigitOverlay(args: BuildArgs): EanUpcDigitOverlay { }; case "upca": // UPC-A HRI: number system (1) | 5 manuf | 5 product | check (1). - // System digit floats left of the bars, check digit floats right — + // System digit floats left of the bars, check digit floats right; // matches Zebra's ^BU default (checkDigit=Y) which the generator // now emits. return { diff --git a/src/components/Canvas/hitTesting.ts b/src/components/Canvas/hitTesting.ts index 371222ab..221d3a81 100644 --- a/src/components/Canvas/hitTesting.ts +++ b/src/components/Canvas/hitTesting.ts @@ -5,7 +5,7 @@ import type Konva from "konva"; * * `stage.getAllIntersections` returns Konva nodes in z-order (front first), * which may be child shapes of a registered object Group rather than the - * Group itself — every `KonvaObject` puts `id={obj.id}` on the *outer* + * Group itself; every `KonvaObject` puts `id={obj.id}` on the *outer* * Group. So we walk each hit upward until we land on a candidate id, dedupe, * and emit in the same z-order Konva produced. * diff --git a/src/components/Canvas/hooks/useAltClickCycle.ts b/src/components/Canvas/hooks/useAltClickCycle.ts index 8abf557a..ea866eeb 100644 --- a/src/components/Canvas/hooks/useAltClickCycle.ts +++ b/src/components/Canvas/hooks/useAltClickCycle.ts @@ -36,7 +36,7 @@ export function useAltClickCycle({ containerRef, stageRef, selectObject }: Optio const rect = el.getBoundingClientRect(); const point = { x: e.clientX - rect.left, y: e.clientY - rect.top }; // Konva's own hit-graph respects view rotation, pan offset, - // per-shape transforms and the listening flag — far cheaper than + // per-shape transforms and the listening flag, far cheaper than // mirroring all of that in our own bbox math. const objIds = new Set(getCurrentObjects().map((o) => o.id)); const hits = objectIdsAtPoint(stage, point, objIds); @@ -45,7 +45,7 @@ export function useAltClickCycle({ containerRef, stageRef, selectObject }: Optio e.preventDefault(); const idx = nextCycleIndex(hits, anchorRef.current, point, ALT_CYCLE_TOL_PX); // `hits[idx]` is `string | undefined` under noUncheckedIndexedAccess - // even though idx is guaranteed valid here — the guard satisfies TS. + // even though idx is guaranteed valid here; the guard satisfies TS. const nextId = hits[idx]; if (!nextId) return; anchorRef.current = { x: point.x, y: point.y, id: nextId }; diff --git a/src/components/Canvas/hooks/useCanvasLasso.ts b/src/components/Canvas/hooks/useCanvasLasso.ts index 3c0cac55..08bfd771 100644 --- a/src/components/Canvas/hooks/useCanvasLasso.ts +++ b/src/components/Canvas/hooks/useCanvasLasso.ts @@ -59,11 +59,11 @@ export function useCanvasLasso({ containerRef, stageRef, spaceDown, selectObject lassoRectRef.current = null; setLasso(null); if (!rect || !stageRef.current) return; - // Figma-style: locked objects opt out of lasso selection — they can't + // Figma-style: locked objects opt out of lasso selection; they can't // be moved or transformed, so grabbing them into a marquee selection // would make the post-lasso drag feel dead. Direct click and the // LayersPanel still target locked items, so bulk-unlock stays possible. - // Leaves are the only Konva-rendered things; intersect on those, then + // Leaves are the only Konva-rendered things, so intersect on those, then // map each captured leaf to its outermost group so a lasso over a // grouped child surfaces the group as the selection unit. // diff --git a/src/components/Canvas/hooks/useKonvaTransformer.ts b/src/components/Canvas/hooks/useKonvaTransformer.ts index 37d4c0ea..2c5fab5e 100644 --- a/src/components/Canvas/hooks/useKonvaTransformer.ts +++ b/src/components/Canvas/hooks/useKonvaTransformer.ts @@ -87,13 +87,13 @@ interface Options { updateObject: (id: string, changes: ObjectChanges) => void; /** Label rect in stage-screen space, used as a snap target. */ labelRect: SnapRect; - /** True when grid-snap is OFF — object-snap during resize mirrors drag. */ + /** True when grid-snap is OFF; object-snap during resize mirrors drag. */ objectSnapEnabled: boolean; /** Pushes resize-time snap guides into the canvas's shared guide state. */ setGuides: (guides: SnapGuide[]) => void; /** Canvas view rotation. When non-zero, the parent group is rotated and * Konva's bbox semantics in boundBoxFunc no longer match our axis-aware - * snap / pin helpers — we fall back to native Konva resize there. */ + * snap / pin helpers; we fall back to native Konva resize there. */ viewRotation: number; } @@ -133,7 +133,7 @@ export function useKonvaTransformer({ // can't move during a resize, so we avoid re-querying Konva on every tick. const othersSnapshotRef = useRef([]); - // Stable key of selected object types — avoids re-running on every drag-move + // Stable key of selected object types, avoids re-running on every drag-move // position update (which changes objects but not the types of selected objects). const selectedTypesKey = selectedIds .map((id) => objects.find((o) => o.id === id)?.type ?? "") @@ -176,7 +176,7 @@ export function useKonvaTransformer({ // 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 + // 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). @@ -234,7 +234,7 @@ export function useKonvaTransformer({ transformAnchorRef.current = obj && STACKED_2D_TYPES.has(obj.type) ? { nodeHeight: node.height(), rowHeight: (obj.props as { rowHeight: number }).rowHeight } : null; - // startBbox is captured lazily on the first boundBoxFunc call — Konva + // startBbox is captured lazily on the first boundBoxFunc call; Konva // passes those bboxes in the transformer's frame, which on rotated // parents differs from getClientRect's stage frame. transformStartBboxRef.current = null; @@ -247,7 +247,7 @@ export function useKonvaTransformer({ } if (isUniformScale) newBox = forceSquareBox(oldBox, newBox); // When the canvas view is rotated, Konva's bbox semantics in this - // callback no longer match an axis-aware snap / pin model — visual + // callback no longer match an axis-aware snap / pin model; visual // bottom-edge becomes node-local-left etc. Skip the height snap, // object-snap and inactive-edge pin in that case and let Konva // resize natively. Stacked-2D row quantisation only matters when @@ -317,7 +317,7 @@ export function useKonvaTransformer({ } const renderedXDots = pxToDots(node.x() - objectsOffsetX, scale, dpmm); const renderedYDots = pxToDots(node.y() - labelOffsetY, scale, dpmm); - // For FT-anchored 1D barcodes, model.y is the bar baseline — needs the + // For FT-anchored 1D barcodes, model.y is the bar baseline, which needs the // post-resize bar height to convert from the bbox top back. Pipe the // scaled height through the same snap() commitBarcodeWidthHeight- // Transform uses so the baseline math sees the *committed* value and @@ -337,7 +337,7 @@ export function useKonvaTransformer({ newBarHeightDots, ); // Only apply snap when the resize actually moved the anchor handle. - // Compare in *rendered* space — for types whose render path applies + // Compare in *rendered* space; for types whose render path applies // a shift (text, QR, FT barcodes), comparing the rendered top-left // to obj.x/y directly always trips (shift ≠ 0 in baseline), so snap // fires on every anchored-corner resize and pulls the visible diff --git a/src/components/Canvas/konvaObjectProps.ts b/src/components/Canvas/konvaObjectProps.ts index b3b43a52..e5074566 100644 --- a/src/components/Canvas/konvaObjectProps.ts +++ b/src/components/Canvas/konvaObjectProps.ts @@ -23,7 +23,7 @@ export function selectionHandlers(onSelect: (add: boolean) => void): { /** Shared props for the per-type renderers under KonvaObject (LineObject, * ImageObject, BarcodeObject, KonvaObjectInner). Per-type renderers - * always receive a leaf — groups have no Konva counterpart and the + * always receive a leaf; groups have no Konva counterpart and the * dispatcher in LabelCanvas filters them out before mapping. Typed as * LeafObject here so the renderers can reach .props without narrowing. */ export interface KonvaObjectProps { diff --git a/src/components/Canvas/reverseShapeStyle.ts b/src/components/Canvas/reverseShapeStyle.ts index 2cb8cb6f..f1dc8ac8 100644 --- a/src/components/Canvas/reverseShapeStyle.ts +++ b/src/components/Canvas/reverseShapeStyle.ts @@ -1,7 +1,7 @@ /** * Per-shape paint values for the field-level `^LR` (reverse) state. * - * Box and ellipse share the same paint rules — black/white knockout via + * Box and ellipse share the same paint rules; black/white knockout via * a `difference` blend, with a fill/stroke swap so a filled outline * doesn't band when the stroke flips to white and the fill flips back * to black. The two render paths agreed in the editor by coincidence; diff --git a/src/components/Canvas/rotatedGroupTransform.ts b/src/components/Canvas/rotatedGroupTransform.ts index 4d2d24ad..ea33d53c 100644 --- a/src/components/Canvas/rotatedGroupTransform.ts +++ b/src/components/Canvas/rotatedGroupTransform.ts @@ -1,28 +1,7 @@ -/** - * Compute the Konva transform that lets an inner Group render content in - * its *upright* (rotation=N) coordinate system while the parent shows it - * rotated by the field's ZPL rotation. The returned `{x, y, rotation}` - * places the inner Group so the rotated content's top-left lands at the - * outer Group's (0, 0) — i.e. the parent's bbox stays a positive - * rectangle regardless of orientation. - * - * Coordinate convention: Konva is y-down, rotation CW positive. - * - * N : no inner group needed (rotation=0, x=0, y=0). - * R : rotate 90° CW. Upright (0..W, 0..H) → (-H..0, 0..W). - * Translate (+H, 0) → bbox (0..H, 0..W). - * I : rotate 180°. Upright (0..W, 0..H) → (-W..0, -H..0). - * Translate (+W, +H) → bbox (0..W, 0..H). - * B : rotate -90°. Upright (0..W, 0..H) → (0..H, -W..0). - * Translate (0, +W) → bbox (0..H, 0..W). - * - * Why an inner-group transform instead of per-element math: - * historically rotated 1D barcode rendering hand-wrote per-rotation - * tx/ty/tRot formulas for every text element. Each bug fix touched N - * symmetric expressions and they drifted apart. Rendering the upright - * layout once inside a rotated container collapses those to a single - * source of truth. - */ +// Inner-Group transform so children render upright; outer bbox stays a +// positive rect. Konva y-down, CW rotation positive. +// R: +90, translate (+H, 0). I: +180, (+W, +H). B: -90, (0, +W). + export type ZplRotation = "N" | "R" | "I" | "B"; export interface RotatedGroupTransform { diff --git a/src/components/Canvas/transformPosition.ts b/src/components/Canvas/transformPosition.ts index ecf7eec0..9fa9c120 100644 --- a/src/components/Canvas/transformPosition.ts +++ b/src/components/Canvas/transformPosition.ts @@ -2,27 +2,8 @@ import { BARCODE_1D_TYPES } from "../../registry"; import type { LeafObject } from "../../registry"; import { QR_FO_Y_OFFSET_DOTS } from "./bwipConstants"; -/** - * Convert the rendered top-left of a Konva node back to the object's stored - * model position, in dots. Inverts per-type render offsets that the renderer - * adds at draw time. - * - * Currently handles: - * - QR (FO): subtracts the hardcoded +10 dot Y-offset that BarcodeObject adds - * to compensate for Zebra firmware artifact. - * - 1D barcodes (FT): adds the new bar height back so obj.y stays on the - * bar baseline that ^FT anchors at. Without the correction every resize - * commits the bar TOP as the new obj.y and the next render shifts the - * barcode up by another bar-height. - * - * Text/serial pass through unchanged. obj.x/y is the position of the - * unrotated wrapper Group around the (possibly rotated) inner Text. On - * resize the Konva transformer pins the opposite corner of the Group's - * axis-aligned clientRect — so obj.x/y naturally tracks the visible - * pinned corner, the ZPL anchor offset is recomputed from the new - * fontHeight at I/O time, and the round-trip stays exact. No inversion - * needed here. - */ +/** QR FO: subtract Zebra-artifact +10 Y. 1D FT: add new bar height back + * so obj.y stays on the FT baseline (else resize walks barcode up). */ export function modelPositionFromRenderedTopLeft( obj: LeafObject, renderedXDots: number, diff --git a/src/components/Canvas/transformerGeometry.test.ts b/src/components/Canvas/transformerGeometry.test.ts index f37978a3..986912ac 100644 --- a/src/components/Canvas/transformerGeometry.test.ts +++ b/src/components/Canvas/transformerGeometry.test.ts @@ -78,7 +78,7 @@ describe("applyHeightSnap", () => { // Captured from a real low-zoom box bottom-edge resize that triggered // the runaway top-anchor pin. dotPx ≈ 0.116 (≈ 4.3 px / dot at fit-zoom), // newBox.y drifted 0.8–10 px from oldBox.y due to Konva FP scale-driven - // node-position updates — comfortably above the prior `dotPx * 0.5` + // node-position updates, comfortably above the prior `dotPx * 0.5` // threshold, which incorrectly identified each frame as top-anchor and // pinned the bottom edge, marching the box upward. const oldBoxLowZoom = { x: 100, y: 346.809, width: 50, height: 27.426, rotation: 0 }; diff --git a/src/components/Canvas/transformerGeometry.ts b/src/components/Canvas/transformerGeometry.ts index 72e8c4a5..fba9828d 100644 --- a/src/components/Canvas/transformerGeometry.ts +++ b/src/components/Canvas/transformerGeometry.ts @@ -11,14 +11,7 @@ export function snapBoxHeight(height: number, stepPx: number): number { return Math.max(stepPx, Math.round(height / stepPx) * stepPx); } -/** - * Forces newBox to be square while keeping the anchor corner pinned. - * - * Konva does not expose the active anchor to boundBoxFunc, so it is inferred - * from which oldBox edges moved: an edge that did not move is the pinned - * side. The new size is the larger of the two requested deltas, so either - * axis the user pulls drives the resize. - */ +/** Anchor inferred from non-moving edge (Konva hides it from boundBoxFunc). */ export function forceSquareBox(oldBox: BoundingBox, newBox: BoundingBox): BoundingBox { const leftMoved = Math.abs(newBox.x - oldBox.x) > 0.001; const topMoved = Math.abs(newBox.y - oldBox.y) > 0.001; @@ -28,10 +21,6 @@ export function forceSquareBox(oldBox: BoundingBox, newBox: BoundingBox): Boundi return { ...newBox, x, y, width: size, height: size }; } -/** - * Adjust newBox so its bottom edge stays at oldBox's bottom (top-anchor resize) - * with a height of snappedH. Used when the user drags the top handle. - */ export function pinBottomEdge( oldBox: BoundingBox, newBox: BoundingBox, @@ -50,19 +39,8 @@ export function isTopAnchorResize( return Math.abs(newBox.y - oldBox.y) > thresholdPx; } -/** - * Phase 1 of resize: row-quantise the height for stacked-2D barcodes - * (PDF417, MicroPDF417, Codablock) where a non-integer row count is - * invalid. Pins the bottom edge if the resize comes from the top anchor - * so the dragged edge tracks the cursor. - * - * No-op for non-stacked-2D shapes — applying a height-snap there used to - * trigger pinBottomEdge whenever Konva's frame-to-frame y drifted past a - * sub-pixel threshold (low zoom = 1 dot < 1 screen pixel), compounding - * into a runaway top-anchor pin that marched the box out of the work - * area. Boxes / barcodes that don't need row-quantised heights run - * unsnapped here; rounding happens in the global onTransformEnd snap. - */ +/** Row-quantise stacked-2D (PDF417/MicroPDF417/Codablock); skip otherwise + * to avoid low-zoom sub-pixel top-anchor runaway. */ export interface RowAnchor { nodeHeight: number; rowHeight: number; @@ -82,11 +60,7 @@ export function applyHeightSnap( : { ...newBox, height: snappedH }; } -/** - * Tolerance for `positionDidMove`. Sized to absorb float rounding from the - * screen-pixel <-> dot conversion; anything within this margin counts as - * "did not move" so the original integer position is preserved. - */ +/** Absorbs px<->dot float rounding so integer positions are preserved. */ export const POSITION_MOVE_TOLERANCE_DOTS = 1; @@ -97,20 +71,7 @@ export interface ActiveEdgeFlags { bottom: boolean; } -/** - * Enforce the resize-invariant: edges the user did NOT grab stay at their - * start-of-drag positions. Konva's per-frame scale-driven node-position - * updates can drift sub-pixel for "stationary" edges even on a pure - * single-edge drag — without this, those drifts compound and the box - * walks away from where the user wanted it pinned. - * - * - Both side-edges inactive → restore start.x and start.width. - * - Only one side-edge active → keep the moving edge's current position - * and extend the size from the corresponding pinned start-edge. - * - Both active (e.g. uniform-scale corner drag) → pass through. - * - * Same logic on the y axis. - */ +/** Pin non-grabbed edges to start so Konva's sub-pixel drift can't walk them. */ export function pinInactiveEdges( bbox: BoundingBox, startBox: BoundingBox, @@ -122,12 +83,10 @@ export function pinInactiveEdges( x = startBox.x; width = startBox.width; } else if (!active.left) { - // right edge moves; left is pinned at start const newRight = x + width; x = startBox.x; width = Math.max(0, newRight - x); } else if (!active.right) { - // left edge moves; right is pinned at start const startRight = startBox.x + startBox.width; width = Math.max(0, startRight - x); } @@ -136,12 +95,10 @@ export function pinInactiveEdges( y = startBox.y; height = startBox.height; } else if (!active.top) { - // bottom edge moves; top is pinned at start const newBottom = y + height; y = startBox.y; height = Math.max(0, newBottom - y); } else if (!active.bottom) { - // top edge moves; bottom is pinned at start const startBottom = startBox.y + startBox.height; height = Math.max(0, startBottom - y); } @@ -149,12 +106,7 @@ export function pinInactiveEdges( return { ...bbox, x, y, width, height }; } -/** - * Decide whether the resize actually moved the object. When the user drags - * a handle whose opposite anchor is the top-left, the position is visually - * unchanged. Without this guard, applying snap to it would pull off-grid - * shapes onto the grid as a side-effect of resizing. - */ +/** Guards snap-on-resize from pulling off-grid shapes to grid as a side-effect. */ export function positionDidMove( rawDots: number, previousDots: number, diff --git a/src/components/Fonts/FontManager.tsx b/src/components/Fonts/FontManager.tsx index b85a863f..a8e0e6fc 100644 --- a/src/components/Fonts/FontManager.tsx +++ b/src/components/Fonts/FontManager.tsx @@ -37,7 +37,7 @@ export function FontManager() { // Partition customFonts into three buckets matched to the three UI // sections. Discriminator is the *presence* of the `path` property - // (m.path === undefined), not its truthiness — a manual mapping + // (m.path === undefined), not its truthiness; a manual mapping // freshly added carries an empty-string path while the user types, // and we must keep that row in the manual section instead of // mis-routing it into the built-in-preview bucket. We also remember @@ -147,7 +147,7 @@ export function FontManager() { const addBuiltinPreview = () => { // Pick the first built-in id that does not already have a binding // so the new row lands on a usable default. If every built-in is - // already bound, fall back to "0" — the user can edit it. + // already bound, fall back to "0"; the user can edit it. const takenAliases = new Set( (customFonts ?? []) .filter((m) => m.path === undefined) @@ -160,7 +160,7 @@ export function FontManager() { // Built-in-preview rows are keyed by alias (one binding per ID). // Patch is applied via spread so an explicit `undefined` actually - // clears the field — the previous `?? m.previewFontName` fallback + // clears the field; the previous `?? m.previewFontName` fallback // turned "Pick a font…" into a no-op because Nullish-coalescing // ignores undefined patches. const updateBuiltinPreview = ( @@ -337,14 +337,14 @@ function FontEntry({ onRequestDelete, }: FontEntryProps) { const t = useT(); - // The embed toggle is only meaningful once an alias is in place — + // The embed toggle is only meaningful once an alias is in place; // ~DY without a matching ^CW would dump bytes onto the printer that // no field can reference. Disable + tooltip when alias is empty so // the constraint is visible instead of silently failing at emit. const embedDisabled = !alias; // Heads-up when the user picks a built-in letter (0, A-H): ^CW with // a built-in alias overrides the factory font on the printer, which - // is rarely what the user wants — the "Built-in font previews" + // is rarely what the user wants; the "Built-in font previews" // section is the right place for an editor-only binding. const overridesBuiltin = isBuiltinFontId(alias); @@ -452,7 +452,7 @@ function ManualMappingsSection({ // Auto-remove rows whose alias AND path are both empty when focus // actually leaves the row container. requestAnimationFrame defers // the check until the new focus has landed, then we confirm the row - // no longer contains it — tabbing between the row's own inputs does + // no longer contains it; tabbing between the row's own inputs does // not count as "leaving". const handleBlur = ( e: FocusEvent, @@ -628,7 +628,7 @@ function AddFontForm({ onDone }: AddFontFormProps) { const [uploadFailed, setUploadFailed] = useState(false); const handleFileChange = useCallback(async (file: File) => { - // Default to the source filename uppercased — Zebra printer storage + // Default to the source filename uppercased; Zebra printer storage // conventionally uses uppercase ALL.TTF style identifiers, and a // freshly-picked file is almost always the user's intended name. const printerName = name.trim() || file.name.toUpperCase(); diff --git a/src/components/Output/PrintToZebraDialog.tsx b/src/components/Output/PrintToZebraDialog.tsx index 6424e5fa..11437903 100644 --- a/src/components/Output/PrintToZebraDialog.tsx +++ b/src/components/Output/PrintToZebraDialog.tsx @@ -68,7 +68,7 @@ export function PrintToZebraDialog({ zpl, onClose }: Props) { return; case "no_response": // Raw-socket printers (port 9100) never reply with HTTP, so a - // timeout is the typical success case — but it is indistinguishable + // timeout is the typical success case; but it is indistinguishable // from an unreachable host. Report honestly instead of green-success. setNetStatus({ type: "success", message: t.zebraPrint.sentNoResponse }); return; diff --git a/src/components/Output/ZPLOutput.tsx b/src/components/Output/ZPLOutput.tsx index c65fa504..7e1b41f2 100644 --- a/src/components/Output/ZPLOutput.tsx +++ b/src/components/Output/ZPLOutput.tsx @@ -87,7 +87,7 @@ export function ZPLOutput({ collapsed, onCollapse, onExpand }: Props) { * actions + copy button) and a `
` body. The Setup-Script
  *  output moved into the Printer Settings modal's docked preview
  *  pane, so this component no longer needs to host a secondary
- *  variant — it just shows the per-label ZPL. */
+ *  variant; it just shows the per-label ZPL. */
 function OutputSection({
   heading,
   content,
diff --git a/src/components/Output/ZplImportModal.tsx b/src/components/Output/ZplImportModal.tsx
index 7b18ad8b..9b41715b 100644
--- a/src/components/Output/ZplImportModal.tsx
+++ b/src/components/Output/ZplImportModal.tsx
@@ -55,7 +55,7 @@ export function ZplImportModal({ onClose }: Props) {
       loadDesign({ ...label, ...labelConfig }, importedPages, importedVariables);
     }
     // Setup-Script fields update the active profile regardless of
-    // append/replace — they are per-installation state, not per-design,
+    // append/replace; they are per-installation state, not per-design,
     // so an import of a ZPL that carries Setup-Script commands should
     // reflect those commands' intent on the user's profile.
     if (Object.keys(printerProfile).length > 0) {
diff --git a/src/components/Output/ZplLine.tsx b/src/components/Output/ZplLine.tsx
index 3e5f6476..96034ca8 100644
--- a/src/components/Output/ZplLine.tsx
+++ b/src/components/Output/ZplLine.tsx
@@ -1,8 +1,6 @@
 /** One line of rendered ZPL with `^CMD` tokens highlighted in the
- *  accent color. Shared between the per-label ZPL pane in
- *  `ZPLOutput` and the Setup-Script preview pane in the Printer
- *  Settings modal so any future tweak (e.g. handling `~CMD` or
- *  lowercase commands) lands in one place. */
+ *  accent color. Shared between the per-label ZPL pane and the
+ *  Setup-Script preview pane. */
 export function ZplLine({ line }: { line: string }) {
   const parts = line.split(/([\^][A-Z0-9]+)/g);
   return (
diff --git a/src/components/Palette/resolveDefaultSize.ts b/src/components/Palette/resolveDefaultSize.ts
index af4d4cea..7139c9c6 100644
--- a/src/components/Palette/resolveDefaultSize.ts
+++ b/src/components/Palette/resolveDefaultSize.ts
@@ -5,10 +5,8 @@ import type { ObjectTypeDefinition } from "../../types/ObjectType";
  *  the active label config. Spec-fixed-physical-size symbols (e.g.
  *  Maxicode) declare `widthMm`/`heightMm` so their footprint stays
  *  correct across dpmm; all other types declare `width`/`height`
- *  directly in dots. Lives next to the palette since that is the
- *  only current drop site, but is its own module so the resolver
- *  stays testable in isolation and any future drop path can reuse
- *  it without depending on the palette component. */
+ *  directly in dots. Extracted from the palette so the resolver
+ *  stays testable in isolation. */
 export function resolveDefaultSizeDots(
   defaultSize: ObjectTypeDefinition["defaultSize"],
   label: LabelConfig,
diff --git a/src/components/PrinterSettings/ClockAndTimeTab.tsx b/src/components/PrinterSettings/ClockAndTimeTab.tsx
index 762a76a4..aea5449c 100644
--- a/src/components/PrinterSettings/ClockAndTimeTab.tsx
+++ b/src/components/PrinterSettings/ClockAndTimeTab.tsx
@@ -38,7 +38,7 @@ const CLOCK_LANGUAGE_LABEL_KEYS = {
   '13': 'clockLanguage13',
 } as const satisfies Record;
 
-/** Tab 3 of the Printer Settings Modal — clock/time setup commands.
+/** Tab 3 of the Printer Settings Modal; clock/time setup commands.
  *  All three live in the Setup-Script output (EEPROM-persistent),
  *  not the per-label ZPL block. ^ST is modelled as a static user-
  *  typed value via HTML5 datetime-local, so generated scripts stay
@@ -108,7 +108,7 @@ export function ClockAndTimeTab() {
         optionLabel={(m) => loc[CLOCK_FORMAT_LABEL_KEYS[m]]}
       />
 
-      {/* All three ^SL inputs live under one parent tag — mode +
+      {/* All three ^SL inputs live under one parent tag; mode +
           (conditional) tolerance share the first positional slot,
           language is the second. ZplSubField keeps the per-row
           labels without duplicating the ^SL command tag (same
@@ -126,7 +126,7 @@ export function ClockAndTimeTab() {
             // Schema's cross-field rule pairs `clockMode === 'TOL'`
             // with a defined `clockTolerance` (and vice versa). Seed
             // the default the moment the user picks TOL, and clear
-            // the tolerance the moment they leave TOL — without
+            // the tolerance the moment they leave TOL; without
             // this the store would briefly hold an invalid combo
             // (TOL without tolerance) which the schema rejects.
             const nextTolerance = nextMode === 'TOL'
diff --git a/src/components/PrinterSettings/EncodingAndLanguageTab.tsx b/src/components/PrinterSettings/EncodingAndLanguageTab.tsx
index 948d65a7..6a33cd85 100644
--- a/src/components/PrinterSettings/EncodingAndLanguageTab.tsx
+++ b/src/components/PrinterSettings/EncodingAndLanguageTab.tsx
@@ -34,7 +34,7 @@ const ZPL_MODE_LABEL_KEYS = {
   '2': 'zplMode2',
 } as const satisfies Record;
 
-/** Tab 4 of the Printer Settings Modal — encoding & language setup.
+/** Tab 4 of the Printer Settings Modal; encoding & language setup.
  *  All three commands belong to the Setup-Script output channel
  *  (EEPROM-persistent printer state), surfaced under the "Setup
  *  Script" rail group. */
@@ -59,7 +59,7 @@ export function EncodingAndLanguageTab() {
       />
 
       {/* ^SE: free-form printer file path (e.g. `E:UHANGUL.DAT`).
-          No primitive — path-like inputs are not used elsewhere
+          No primitive; path-like inputs are not used elsewhere
           in the modal; promote to a primitive only when a second
           caller appears. `SE_PATH_PATTERN` is a soft `:invalid`
           hint (see `sePathPattern.ts` for the shape rationale);
diff --git a/src/components/PrinterSettings/IdentityTab.tsx b/src/components/PrinterSettings/IdentityTab.tsx
index ac9f093d..1c61dce0 100644
--- a/src/components/PrinterSettings/IdentityTab.tsx
+++ b/src/components/PrinterSettings/IdentityTab.tsx
@@ -33,7 +33,7 @@ const CONFIG_UPDATE_LABEL_KEYS = {
  *  commit setup. Emits ^KN (name + description), ^KP (4-digit panel
  *  password) and ^JU (configuration update action) on the Setup-
  *  Script channel. ^SL lives in Tab 3 Clock & Time per its semantic
- *  home. See memory:project_ticket_sl_clock_formatting. */
+ *  home. */
 export function IdentityTab() {
   const t = useT();
   const profile = useLabelStore((s) => s.printerProfile);
diff --git a/src/components/PrinterSettings/PrintQualityTab.tsx b/src/components/PrinterSettings/PrintQualityTab.tsx
index fa58fc70..880e8838 100644
--- a/src/components/PrinterSettings/PrintQualityTab.tsx
+++ b/src/components/PrinterSettings/PrintQualityTab.tsx
@@ -19,18 +19,9 @@ const ORIENTATION_LABEL_KEYS = {
   I: "printOrientationI",
 } as const satisfies Record;
 
-/** Tab 2 of the Printer Settings Modal. Houses everything that
- *  controls per-label print quality: orientation, mirror, speed,
- *  darkness, plus three new commands (^JZ reprint-after-error,
- *  ^JT head-test interval, ~TA tear-off adjust).
- *
- *  Caveat: ^JZ / ^JT / ~TA are persistent in the printer's EEPROM
- *  per the ZPL spec, so emitting them on every label writes flash
- *  on every print. The Printer Settings Modal epic plans to split
- *  per-label commands from setup-script-only commands; until that
- *  ships, these three live in the per-label generator path. See
- *  the project_ticket_printer_settings_modal memory for the
- *  follow-up. */
+/** Tab 2 of the Printer Settings Modal. Per-label print quality
+ *  (orientation, mirror, speed, darkness) plus setup-script-only
+ *  ^JZ / ^JT / ~TA / ^CV which the user provisions once. */
 export function PrintQualityTab() {
   const t = useT();
   const label = useLabelStore((s) => s.label);
@@ -59,7 +50,7 @@ export function PrintQualityTab() {
         onChange={(v) => setLabelConfig({ mirror: v ? "Y" : undefined })}
       />
 
-      {/* Speed triple: ^PR a,b,c — print / slew / backfeed. All
+      {/* Speed triple: ^PR a,b,c; print / slew / backfeed. All
           three share the same 2..14 ips range and one ^PR command,
           so group them under one ZplField + tag header. */}
       
@@ -101,7 +92,7 @@ export function PrintQualityTab() {
         
- {/* ^MD permanent darkness — the EEPROM-persistent set value. */} + {/* ^MD permanent darkness; the EEPROM-persistent set value. */} { }); it('rejects stems at the 9-char boundary and when empty', () => { - // 9 chars pins the exact off-by-one — a hypothetical regex + // 9 chars pins the exact off-by-one; a hypothetical regex // tweak to `{1,12}` or `{1,9}` would slip past a 12-char-only // test, this one catches it. expect(re.test('E:NINECHARS.DAT')).toBe(false); diff --git a/src/components/PrinterSettings/zplFieldPrimitives.tsx b/src/components/PrinterSettings/zplFieldPrimitives.tsx index 73bc3978..c3d6b58b 100644 --- a/src/components/PrinterSettings/zplFieldPrimitives.tsx +++ b/src/components/PrinterSettings/zplFieldPrimitives.tsx @@ -3,16 +3,8 @@ import { labelCls, inputCls } from "../ui/formStyles"; import { clampBoundedInt, readBoundedInt } from "../../lib/inputParse"; import { stripUnsafeChars } from "../../types/PrinterProfile"; -/** Shared muted-monospace class for ZPL command tags so the visual - * weight stays identical across the label-row, checkbox-row and - * any future field primitives that dock a tag rightwards. */ const commandTagCls = "font-mono text-[10px] text-muted/60 tracking-tight"; -/** Field label with a ghost-rendered ZPL command tag docked right. - * The distinctive design move of the printer-settings modal: users - * see exactly which command each control emits, turning the modal - * into a discoverable spec reference without crowding the form. - * Density matches the Properties Panel's `labelCls`. */ export function ZplCommandLabel({ text, command, @@ -32,10 +24,7 @@ export function ZplCommandLabel({ ); } -/** Checkbox row with the same ZPL-command-docked-right treatment as - * `ZplCommandLabel`. The command tag sits outside the `