From 5e98335e9c4fb7652b09fe8cd207620d8e2f955d Mon Sep 17 00:00:00 2001 From: u8array Date: Thu, 15 Oct 2026 17:01:08 +0200 Subject: [PATCH] docs: inline comments state each rationale once --- packages/core/src/lib/code128Plan.ts | 4 ++-- packages/core/src/lib/code128Subset.test.ts | 3 +-- packages/core/src/lib/designFile.ts | 3 --- packages/core/src/lib/emittedAnchor.ts | 8 ++++---- packages/core/src/lib/gfaDecode.test.ts | 3 +-- packages/core/src/lib/labelGeometry/deviceFonts.ts | 14 +++++++------- packages/core/src/lib/objectTree.ts | 3 ++- packages/core/src/lib/qrFd.test.ts | 3 --- packages/core/src/lib/reverseBacking.ts | 7 +++---- packages/core/src/lib/rfidPosition.ts | 2 +- packages/core/src/lib/variableField.ts | 9 --------- packages/core/src/lib/zebraTextLayout.ts | 9 ++++----- packages/core/src/lib/zplGenerator.ts | 14 ++++---------- packages/core/src/lib/zplImportService.ts | 6 +++--- packages/core/src/lib/zplParser.ts | 3 +-- packages/core/src/lib/zplParser/context.ts | 4 ++-- packages/core/src/lib/zplParser/flushField.ts | 14 +++++--------- .../core/src/lib/zplParser/handlers/barcodes.ts | 7 +++---- .../core/src/lib/zplParser/handlers/graphics.ts | 14 ++++---------- .../core/src/lib/zplParser/handlers/labelConfig.ts | 4 ++-- packages/core/src/lib/zplParser/helpers.ts | 6 ++---- packages/core/src/registry/aztec.ts | 1 - packages/core/src/registry/barcode1d.ts | 9 +++------ packages/core/src/registry/datamatrix.ts | 1 - packages/core/src/registry/image.ts | 13 ++++++------- packages/core/src/registry/line.ts | 7 ++----- packages/core/src/registry/micropdf417.ts | 1 - packages/core/src/registry/pdf417.ts | 1 - packages/core/src/registry/placeholderContent.ts | 3 --- packages/core/src/registry/qrcode.ts | 1 - packages/core/src/registry/text.ts | 2 -- packages/core/src/registry/tlc39.ts | 4 +--- packages/core/src/registry/zplHelpers.ts | 13 ++++++------- packages/core/src/types/ObjectType.ts | 3 ++- packages/core/src/types/clockMarker.ts | 1 - packages/core/src/types/controlKey.ts | 3 +-- packages/core/src/types/preflight.ts | 7 +++---- packages/mcp-server/src/boundaryWritePaths.test.ts | 2 -- packages/mcp-server/src/report.ts | 4 +--- packages/mcp-server/src/tools.test.ts | 12 ++---------- src-tauri/src/db.rs | 3 +-- src-tauri/src/excel.rs | 3 +-- src-tauri/src/usb.rs | 12 +++--------- .../Canvas/BarcodeObject.eanGuardZone.test.tsx | 12 ++---------- .../Canvas/BarcodeObject.fallbackSize.test.tsx | 9 ++------- .../Canvas/BarcodeObject.hriControlBytes.test.tsx | 9 ++------- .../Canvas/BarcodeObject.scmAlarm.test.tsx | 11 ++--------- .../Canvas/BarcodeObject.stateFrame.test.tsx | 11 ++--------- .../Canvas/BarcodeObject.symbologySwitch.test.tsx | 14 ++------------ src/components/Canvas/BarcodeObject.tsx | 5 +---- .../Canvas/ImageObject.gfaFallback.test.tsx | 12 ++---------- src/components/Canvas/ImageObject.tsx | 1 - src/components/Canvas/KonvaObject.tsx | 1 - src/components/Canvas/LabelCanvas.tsx | 5 ++--- src/components/Canvas/captureLook.test.tsx | 10 ++-------- src/components/Canvas/hooks/useKonvaTransformer.ts | 2 -- .../Canvas/lassoGeometry.integration.test.ts | 12 ++---------- .../PrinterSettings/PreviewSettingsTab.tsx | 6 ++---- .../PrinterSettings/zplFieldPrimitives.tsx | 5 ++--- src/components/Properties/LayersPanel.tsx | 2 -- src/components/Properties/PropertiesPanel.tsx | 2 +- src/components/Properties/UnitNumberInput.tsx | 2 +- src/components/Properties/VariableCsvPanel.tsx | 5 ++--- src/components/Properties/VariableInspector.tsx | 4 +--- src/hooks/useColorScheme.test.ts | 5 +---- src/hooks/useColorScheme.ts | 6 ++---- src/hooks/useCsvImportActions.ts | 5 ++--- src/hooks/useExcelImportActions.ts | 4 ++-- src/hooks/useSqliteConnectActions.ts | 4 ++-- src/lib/db.ts | 5 ++--- src/lib/excel.ts | 4 ++-- src/lib/labelRaster.ts | 4 +--- src/lib/mcpBridge.ts | 3 +-- src/lib/multiResize.test.ts | 3 --- src/lib/preflight.test.ts | 3 --- src/lib/sourceEditText.ts | 3 +-- src/lib/variableBinding.test.ts | 2 +- src/lib/zplGenerator.test.ts | 2 -- src/lib/zplParser.test.ts | 3 --- src/main.tsx | 6 ++---- src/registry/text.test.ts | 5 ----- src/store/labelStore.ts | 12 +----------- src/store/slices/dataSlice.ts | 7 ++----- src/test/helpers.ts | 11 +++++++++++ src/test/labelarySync.test.ts | 1 - src/test/visualRegression.test.ts | 1 - src/test/zplRoundtrip.test.ts | 1 - 87 files changed, 146 insertions(+), 340 deletions(-) diff --git a/packages/core/src/lib/code128Plan.ts b/packages/core/src/lib/code128Plan.ts index a4767907..ed5e53b9 100644 --- a/packages/core/src/lib/code128Plan.ts +++ b/packages/core/src/lib/code128Plan.ts @@ -12,8 +12,8 @@ import { C0_RE, resolveControlMarkers } from "../types/controlKey"; export type Code128FdMode = "whole" | "template" | "templateValue" | "sharedRaw"; export type Code128FdLoss = - /** Control bytes cannot ride this mode's FD: they fall to ^FH, which the - * firmware drops from the symbol (ZD230). */ + /** Control bytes cannot ride this mode's FD, they fall to ^FH, see + * code128Subset. */ | { kind: "controlBytesDropped" } /** `>` before an invocation char stays verbatim (import round-trip channel), * so the firmware reads an invocation, not text. */ diff --git a/packages/core/src/lib/code128Subset.test.ts b/packages/core/src/lib/code128Subset.test.ts index 17f954ab..f275aa89 100644 --- a/packages/core/src/lib/code128Subset.test.ts +++ b/packages/core/src/lib/code128Subset.test.ts @@ -260,8 +260,7 @@ describe("printed vs rendered module count", () => { }); it("drops the control bytes a template payload cannot print", () => { - // The ^FE path keeps the ^FH escape, whose C0 bytes the firmware strips - // from the symbol; the preview must show the same symbol. + // The preview must show the symbol the printer prints. const obj = code128("A«sku»B«ctrl:TAB»"); const variables = [{ id: "v1", name: "sku", fnNumber: 1, defaultValue: "X" }]; const resolved = resolveForMeasure(obj as LabelObject, variables); diff --git a/packages/core/src/lib/designFile.ts b/packages/core/src/lib/designFile.ts index c322790c..c07d34ed 100644 --- a/packages/core/src/lib/designFile.ts +++ b/packages/core/src/lib/designFile.ts @@ -172,9 +172,6 @@ function migrateReverseTextBackground(json: unknown): void { const p = pg && typeof pg === "object" ? (pg as Record) : {}; if (!Array.isArray(p.objects)) return p; const objects = p.objects as LabelObject[]; - // Inserting a model object the overlay doesn't link would force a full - // regeneration on export and lose the overlay's preserved bytes; drop the - // overlay on a touched page so it regenerates cleanly from the new model. const next: Record = { ...p, objects: insertReverseBackingBoxes(objects, label), diff --git a/packages/core/src/lib/emittedAnchor.ts b/packages/core/src/lib/emittedAnchor.ts index 417468ea..a012cad5 100644 --- a/packages/core/src/lib/emittedAnchor.ts +++ b/packages/core/src/lib/emittedAnchor.ts @@ -6,10 +6,10 @@ import { objectBoundsDots, type BoundingBoxDots, type ObjectBoundsCtx } from "./ /** The ^FO/^FT origin (dots) the generator actually emits for this leaf, i.e. * the coordinate the printer sees. The single source the off-label preflight * shares with emission, so "is the position off-label" can't drift from what - * prints: text uses the cap-top/baseline transform, graphics anchor at their - * bbox (top-left under ^FO, a bottom corner under ^FT), barcodes/symbol emit at - * the model coord (a gated R 1D at the measured right edge). Pass `box` when the caller already has the bbox to skip a - * recompute. Known minor gap: an ^FT right-justified diagonal line or image uses + * prints: text uses the cap-top/baseline transform, graphics go through + * {@link graphicAnchorCoords}, barcodes/symbol emit at the model coord, a + * gated R 1D at the measured right edge. Pass `box` when the caller already + * has the bbox to skip a recompute. Known minor gap: an ^FT right-justified diagonal line or image uses * the visual bbox width, a few dots off the emitted ^GD/^GF width, which only * shifts the near-left edge for a field whose right corner sits at ~x=0. */ export function emittedAnchorDots( diff --git a/packages/core/src/lib/gfaDecode.test.ts b/packages/core/src/lib/gfaDecode.test.ts index 41414017..ba875645 100644 --- a/packages/core/src/lib/gfaDecode.test.ts +++ b/packages/core/src/lib/gfaDecode.test.ts @@ -113,8 +113,7 @@ describe("the header count, not the stream", () => { }); it("refuses a header without the count, which prints nothing", () => { - // Labelary: omitting b renders identically, omitting c produces no label - // at all, because the firmware never learns where the graphic ends. + // Labelary: omitting b renders identically, omitting c produces no label. expect(rasterFromGfa("^GFA,,,2,C3D4FFFF")).toBeNull(); expect(rasterFromGfa("^GFA,,4,2,C3D4FFFF")?.heightDots).toBe(2); }); diff --git a/packages/core/src/lib/labelGeometry/deviceFonts.ts b/packages/core/src/lib/labelGeometry/deviceFonts.ts index d9aa3933..a165cad7 100644 --- a/packages/core/src/lib/labelGeometry/deviceFonts.ts +++ b/packages/core/src/lib/labelGeometry/deviceFonts.ts @@ -85,8 +85,8 @@ function deviceFontMags( /** Deterministic field extent in dots for the ^FO anchor of rotated (I/B) * bitmap fields: cells are monospaced, and Labelary measures the printed - * extent at n*advance - gap/2 within 2 dots across fonts. Null for - * Font 0 / unknown ids (measured PrintLab width applies there). */ + * extent at n*advance - gap/2 within 2 dots across fonts. Null as in + * deviceFontMags, the measured PrintLab width applies there. */ export function deviceFontInkWidthDots( fontId: string | undefined, heightDots: number, @@ -102,8 +102,8 @@ export function deviceFontInkWidthDots( return magW * (n * spec.advancePerMag - gap / 2); } -/** Snapped character-cell width (the effective ^A font width), or null - * for Font 0 / unknown ids. */ +/** Snapped character-cell width (the effective ^A font width), or null as in + * deviceFontMags. */ export function deviceFontSnappedWidthDots( fontId: string | undefined, heightDots: number, @@ -113,9 +113,9 @@ export function deviceFontSnappedWidthDots( return mags ? mags.magW * mags.spec.magWidthStep : null; } -/** Requested ^A height snapped to the cell grid, or null for Font 0 / - * unknown ids / non-positive heights. The firmware anchors a bitmap field - * by its snapped cell, so the anchor transform needs this too. */ +/** Requested ^A height snapped to the cell grid, null as in deviceFontMags. + * The firmware anchors a bitmap field by its snapped cell, so the anchor + * transform needs this too. */ export function deviceFontSnappedHeightDots( fontId: string | undefined, heightDots: number, diff --git a/packages/core/src/lib/objectTree.ts b/packages/core/src/lib/objectTree.ts index 39f477d5..2ccb63f9 100644 --- a/packages/core/src/lib/objectTree.ts +++ b/packages/core/src/lib/objectTree.ts @@ -13,7 +13,8 @@ interface UnknownNode { /** Apply `visit` to every leaf (non-group) node found under `pages`. * `pages` is treated permissively: any nested `objects`/`children` - * arrays are walked. Mutates leaves in-place via the visitor. */ + * arrays are walked. Mutates leaves in place, which is safe because + * the callers hand it the persisted payload, never the live store. */ export function visitLeavesInPages(pages: unknown, visit: (leaf: UnknownNode) => void): void { if (!Array.isArray(pages)) return; for (const page of pages) walkNode(page, visit); diff --git a/packages/core/src/lib/qrFd.test.ts b/packages/core/src/lib/qrFd.test.ts index 041c8c4b..55570cfb 100644 --- a/packages/core/src/lib/qrFd.test.ts +++ b/packages/core/src/lib/qrFd.test.ts @@ -35,9 +35,6 @@ describe("qrFdToModel", () => { }); } - // ZD230-measured: a count that does not land on a comma loses only the - // overhanging bytes; the firmware resyncs at the next comma and reads on - // (`QM,B0002abc,N12` prints `ab12`). it("resyncs at the next comma when a byte count overruns its segment", () => { expect(qrFdToModel("QM,B0002abc,N12").content).toBe("ab12"); }); diff --git a/packages/core/src/lib/reverseBacking.ts b/packages/core/src/lib/reverseBacking.ts index 59a1119f..d2475755 100644 --- a/packages/core/src/lib/reverseBacking.ts +++ b/packages/core/src/lib/reverseBacking.ts @@ -271,10 +271,9 @@ export function precedingBackingExists( return false; } -/** True when a page has at least one reverse text that the migration would give - * a new backing (i.e. none covering it already). The migration uses this to - * invalidate that page's overlay, since inserting a model object the overlay - * doesn't know about would otherwise silently force a full regeneration. */ +/** True when a page has at least one reverse text with no backing covering it + * yet, so the migration would add one. The caller drops that page's overlay, + * see designOps. */ export function pageNeedsReverseBacking( objects: readonly LabelObject[], label?: FontLabel, diff --git a/packages/core/src/lib/rfidPosition.ts b/packages/core/src/lib/rfidPosition.ts index dff8145c..5452344c 100644 --- a/packages/core/src/lib/rfidPosition.ts +++ b/packages/core/src/lib/rfidPosition.ts @@ -14,7 +14,7 @@ export interface RfidPositionValue { } /** Media-motion distance the position declares, in mm off the label top. - * Dots are physical head dots like ^ML (^JM does not scale the feed). */ + * Dots are physical head dots like ^ML, ^JM does not scale the feed. */ export function rfidPositionValue( position: string | undefined, dpmm: number, diff --git a/packages/core/src/lib/variableField.ts b/packages/core/src/lib/variableField.ts index f4c39af9..e5017bdd 100644 --- a/packages/core/src/lib/variableField.ts +++ b/packages/core/src/lib/variableField.ts @@ -11,15 +11,6 @@ import { extractClockTokens } from "./fcTemplate"; export type BindableLeaf = LabelObjectBase & { props: { content: string } }; export const asLabelObject = (obj: BindableLeaf): LabelObject => obj as unknown as LabelObject; -/** - * Single source of truth for the unified token field. `content` is the only - * state: content that is exactly one known variable marker is Single-Bind - * (emits the inline `^FN{n}^FD{default}^FS`); any other `«name»`/`«clock:T»` - * marker shape is Template; no markers is Literal. Preview - * (`applyBindingToObject`) and export (`fdFieldFor`) consume the same - * `classifyField` decision, so they can never diverge. - */ - /** True when content is exactly one `«marker»` and nothing else: the shape that * emits inline (single-bind) rather than expanding to `^FE` embeds. */ export function isLoneMarker(content: string): boolean { diff --git a/packages/core/src/lib/zebraTextLayout.ts b/packages/core/src/lib/zebraTextLayout.ts index 4a305a20..51c18f67 100644 --- a/packages/core/src/lib/zebraTextLayout.ts +++ b/packages/core/src/lib/zebraTextLayout.ts @@ -158,11 +158,10 @@ export function tbBlockExtentDots( return Math.max(0, blockHeightDots - effH * (1 - EM_TOP_ABOVE_CAP)); } -/** Anchor extent of a block beyond its first line, ^TB or ^FB. The single - * tb-vs-fb dispatch shared by generator, parser and the box-match gate; - * a one-sided change here shifts anchors silently. tbHeightDots > 0 - * selects ^TB (the parser's native signal: ^TB is stream state, not a - * stored mode). */ +/** Anchor extent of a block beyond its first line: ^TB is a fixed clip height, + * ^FB stacks lines. The single tb-vs-fb dispatch shared by generator, parser + * and the box-match gate, so a one-sided change shifts anchors silently. + * tbHeightDots > 0 selects ^TB, the parser's native signal. */ export function blockAnchorExtentDots(args: { tbHeightDots: number; blockWidthDots: number; diff --git a/packages/core/src/lib/zplGenerator.ts b/packages/core/src/lib/zplGenerator.ts index 1a137e1d..8e8653f0 100644 --- a/packages/core/src/lib/zplGenerator.ts +++ b/packages/core/src/lib/zplGenerator.ts @@ -136,8 +136,7 @@ export function planTemplateHeader( } // Mode-D-exclusive slot: > needs its >0 invocation (parser reverses). // Plain-^BC-exclusive slot likewise: >/^/~ need their invocation - // literals; the ^FH hex fdField would otherwise use is dropped from - // the symbol. + // literals, see code128Subset. const def = modeDFns.has(fn) ? escapeGs1FdValue(v.defaultValue) : plain128Fns.has(fn) @@ -926,11 +925,6 @@ export function generateBatchZpl( return [...(lead === '' ? [] : [lead]), ...recallBlocks].join('\n'); } -// ^FT graphics anchor at a bottom corner (spec p.205), so the drop test uses -// that anchor for GRAPHIC_ANCHOR_TYPES. Barcodes emit ^FT at the model coord -// (the plain check holds); rotated text ^FT uses a baseline anchor, where the -// top-left check is only approximate (a pre-existing edge, untouched here). - /** Home-relative to a parsed ^LH/^LT frame, not the label's own home/top (see OverlayFrame). */ export function shiftIntoFrame(objects: LabelObject[], frame: OverlayFrame | undefined, label: PageLabel): LabelObject[] { return frame ? shiftObjectsByHome(objects, frame.homeX, frame.homeY, frame.top, label) : objects; @@ -978,6 +972,7 @@ function shiftObjectsByHome( const ax = printerAnchoredX(obj, label); if (ax !== null) return ax - homeX < 0 || y < 0 ? [] : [{ ...obj, x, y }]; } + // ^FT text emits at a baseline, so for it this top-left check is approximate. return x < 0 || y < 0 ? [] : [{ ...obj, x, y }]; }; return objects.flatMap(shiftOrDrop); @@ -1042,9 +1037,8 @@ function generateZplBlock( /** Shared across a document's blocks so a font ships once. A replayed block's own ~DY stays invisible to it, which costs size only. */ shippedFonts = new Set(), ): PageBlock { - // ^PW/^LL are consumed in physical head dots (ZD230-verified), even under - // ^JMB: the body's object dots emit in the effective (halved) scale, but the - // print width/length stay physical. + // ^PW/^LL are physical head dots, see deriveUnitScale, while the body's object + // dots emit in the effective ^JM scale. const widthDots = mmToDots(label.widthMm, label.dpmm); const heightDots = mmToDots(label.heightMm, label.dpmm); diff --git a/packages/core/src/lib/zplImportService.ts b/packages/core/src/lib/zplImportService.ts index cd34d84f..70dd60bd 100644 --- a/packages/core/src/lib/zplImportService.ts +++ b/packages/core/src/lib/zplImportService.ts @@ -83,9 +83,9 @@ function foldRecallPages(pages: readonly ParsedPage[]): RecallFold | null { } export function importZplText(zpl: string, dpmm: number): ZplImportResult { - // Single pass: stream-persistent state (^MU, ^CC/^CT/^CD, ^CI, ^CW/uploads, - // ^CF/^BY, ^LH/^LT/^LR) carries across ^XA blocks, and the parser owns the - // page boundaries (prefix-aware, unlike a literal ^XA split). + // Single pass: stream-persistent state carries across ^XA blocks, see + // resetFormatScopedState, and the parser owns the page boundaries, + // prefix-aware unlike a literal ^XA split. const r = parseZPL(zpl, dpmm, { captureOverlay: true }); // storedGraphicShips below reads the cache. const imageOwner = newId(); diff --git a/packages/core/src/lib/zplParser.ts b/packages/core/src/lib/zplParser.ts index c8b76ca9..acdc775a 100644 --- a/packages/core/src/lib/zplParser.ts +++ b/packages/core/src/lib/zplParser.ts @@ -557,8 +557,7 @@ export function parseZPL( const dispatch = ({ cmd, rest, start, end }: ZplToken): void => { // Read by the finding push sites (notePartial, pushBrowserLimit). End is - // trimmed: the tokenizer's end runs to the next command's prefix, and a - // span must not swallow the line break plus following indent. + // trimmed because the tokenizer's end runs to the next command's prefix. s.result.prevTokenEnd = s.result.tokenSpan?.end ?? 0; s.result.tokenSpan = { start, end: trimmedSpanEnd(zpl, start, end) }; s.result.tokenEnd = end; diff --git a/packages/core/src/lib/zplParser/context.ts b/packages/core/src/lib/zplParser/context.ts index eaa72ae5..9c0accc5 100644 --- a/packages/core/src/lib/zplParser/context.ts +++ b/packages/core/src/lib/zplParser/context.ts @@ -256,9 +256,9 @@ export interface FieldState { /** ^FN seen since this opener. Such a slot dies with the field, while a slot * armed before the opener survives it, measured on a format recalled by ^XF. */ fnArmedSinceOpener: boolean; - /** ^FE armed for the next ^FD only (spec p.191); reset at ^FS. */ + /** See consumeOneShotArms. */ feArmed: boolean; - /** ^FC armed for the next ^FD only (spec p.1614); reset at ^FS. */ + /** See consumeOneShotArms. */ fcArmed: boolean; /** ^FR: single-field reverse, reset at ^FS. */ frActive: boolean; diff --git a/packages/core/src/lib/zplParser/flushField.ts b/packages/core/src/lib/zplParser/flushField.ts index 3e19de5a..1b29c09e 100644 --- a/packages/core/src/lib/zplParser/flushField.ts +++ b/packages/core/src/lib/zplParser/flushField.ts @@ -247,8 +247,6 @@ export function createCloseField( const hexControl = recallValue === undefined ? s.field.pendingHexControl : (s.declaredHexControl.get(s.comment.fnNumber ?? -1) ?? null); if (hexControl && truncatesAtHexControl(s, rawDecoded)) s.result.hexControl.push(hexControl); // FN embeds → markers, then ^FC clock tokens (order matters: `%FN#1#%Y`). - // Both decode only when armed for THIS field: firmware honours ^FE/^FC - // solely for the next ^FD (spec p.191/p.1614), unarmed data is literal. // Taken as a function because ^BQ replays it on a different string. const expandTokens = (raw: string): string => { const withFn = s.field.feArmed ? applyFnEmbeds(raw) : raw; @@ -287,10 +285,9 @@ export function createCloseField( if (s.field.fieldType !== "text") commitPendingReverseBg(); switch (s.field.fieldType) { case "text": { - // FT pins the last baseline, so the EM-top sits one block-extent above; - // FO R/I stack the same way. ^TB is a fixed clip height, ^FB stacks - // lines. Must match the generator's blockExtentFor for byte-exact - // round-trips and matching WYSIWYG. + // FT pins the last baseline, so the EM-top sits one block-extent above, + // FO R/I stack the same way. Must match the generator's blockExtentFor + // for byte-exact round-trips and matching WYSIWYG. // Same resolution as the generator so anchors round-trip. const anchorFontId = resolveDeviceFontId( s.field.pendingFontId, @@ -751,9 +748,8 @@ export function createCloseField( const variable = upsertVariable(s.comment.fnNumber, varDefault, s.comment.fnComment); // Serial wins over the binding, but only when actually applied // (serialisable type); else a 2D field's ^FN binding would be silently - // dropped. The upsert still runs: the slot is shared, so a later field - // or embed may reference it and must see this default. If none does, - // the end-of-parse sweep drops the orphan. + // dropped. The upsert still runs so a later reference sees this + // default. const serialApplied = s.field.snPending && lastSerialisable; if (serialApplied) { s.serialStrippedFns.add(s.comment.fnNumber); diff --git a/packages/core/src/lib/zplParser/handlers/barcodes.ts b/packages/core/src/lib/zplParser/handlers/barcodes.ts index 6a4a1d8b..035a3484 100644 --- a/packages/core/src/lib/zplParser/handlers/barcodes.ts +++ b/packages/core/src/lib/zplParser/handlers/barcodes.ts @@ -100,8 +100,8 @@ export function createBarcodeHandlers(s: ParserState): Record { : "A"; }, - // MSI ^BMN,{e},{h},{f},{g},{e2}; e: A=no check, B/C/D=checked (default - // B; the firmware also treats invalid values as B, ZD230-measured). + // MSI ^BMN,{e},{h},{f},{g},{e2}. The firmware reads an invalid e as B, + // ZD230-measured, see msiCoreConfig for the e values. BM(p) { s.field.fieldType = "msi"; s.field.bcRotation = readRotation(p[0]); @@ -218,8 +218,7 @@ export function createBarcodeHandlers(s: ParserState): Record { s.field.cbColumns = clampCodablockColumns(int(p[3], 0) || undefined); }, - // ^BTo,w1,r1,h1,w2,h2 (spec p.140). The MicroPDF row count has no - // param: the firmware derives it from the serial (always 4 columns). + // ^BTo,w1,r1,h1,w2,h2 (spec p.140). The row count has no param, see Tlc39MicroPdf. BT(p) { s.field.fieldType = "tlc39"; s.field.bcRotation = readRotation(p[0]); diff --git a/packages/core/src/lib/zplParser/handlers/graphics.ts b/packages/core/src/lib/zplParser/handlers/graphics.ts index 26679903..ac3a062d 100644 --- a/packages/core/src/lib/zplParser/handlers/graphics.ts +++ b/packages/core/src/lib/zplParser/handlers/graphics.ts @@ -79,11 +79,7 @@ export function createGraphicsHandlers(s: ParserState, helpers: GraphicsHelpers) color: "B" | "W", rounding: number, reverseFlag: boolean | undefined, comment: string | undefined, positionType: "FO" | "FT", justify: "L" | "R", ): LabelObject => { - // ^FT graphic origin is a bottom corner (spec p.205): bottom-left, or - // bottom-right when z=1 (justify R). The model stores the top-left, so - // lift by the height and, for R, shift left by the width. ^FO is top-left. - const topLeftY = positionType === "FT" ? gy - h : gy; - const topLeftX = positionType === "FT" && justify === "R" ? gx - w : gx; + const { x: topLeftX, y: topLeftY } = ftTopLeft(gx, gy, w, h, positionType, justify); let obj; if (h === t && w > t) { obj = makeObj( @@ -315,9 +311,7 @@ export function createGraphicsHandlers(s: ParserState, helpers: GraphicsHelpers) const gdColor = readColor(p[3]); const gdOri = (p[4] ?? "L").toUpperCase(); const gdLen = Math.round(Math.sqrt(gdW * gdW + gdH * gdH)); - // ^FT anchors the ^GD bounding box at a bottom corner; lift to the box - // top-left before recovering the start point (gdOri is the diagonal - // direction, distinct from the z-justify "R"). + // gdOri is the diagonal direction, distinct from the z-justify "R". const posType = getPosType(s.field); const justify = s.field.justify; const { x: boxX, y: boxY } = ftTopLeft(s.field.x, s.field.y, gdW, gdH, posType, justify); @@ -535,8 +529,8 @@ export function createGraphicsHandlers(s: ParserState, helpers: GraphicsHelpers) if (parsed) recallStoredGraphic("^IL", parsed, { x: s.label.lhX, y: s.label.lhY + s.label.ltY }); }, - // ^GS{rotation},{height},{width}: selects the internal-font - // legal-symbol glyph (^FD picks which: A=®, B=©, C=™, D=UL, E=CSA). + // ^GS{rotation},{height},{width}: selects the internal-font legal-symbol + // glyph, ^FD picks which, see SymbolProps.symbol. GS(p) { s.field.fieldType = "symbol"; s.field.symRot = readRotation(p[0]); diff --git a/packages/core/src/lib/zplParser/handlers/labelConfig.ts b/packages/core/src/lib/zplParser/handlers/labelConfig.ts index d34a91bf..270dfbc0 100644 --- a/packages/core/src/lib/zplParser/handlers/labelConfig.ts +++ b/packages/core/src/lib/zplParser/handlers/labelConfig.ts @@ -18,8 +18,8 @@ export function createLabelConfigHandlers( ): Record { const labelConfig = s.result.labelConfig; const { dotsOrUndef } = dotsFor(s); - // ^PW/^LL/^ML are physical head dots, ^JM-independent (ZD230-verified): omit - // jmDensity from the scale so the un-halved ^MU multiplier applies. + // ^PW/^LL/^ML read at the physical density, see deriveUnitScale, so the + // scale omits jmDensity and the un-halved ^MU multiplier applies. const physDots = (raw: string | undefined): number | undefined => intDotsOrUndef(raw, deriveUnitScale({ muMode: s.format.muMode }, dpmm)); return { diff --git a/packages/core/src/lib/zplParser/helpers.ts b/packages/core/src/lib/zplParser/helpers.ts index 5cd1bb5c..42e06a0b 100644 --- a/packages/core/src/lib/zplParser/helpers.ts +++ b/packages/core/src/lib/zplParser/helpers.ts @@ -326,10 +326,8 @@ export function makeObj( } as unknown as LabelObject; } -/** Convert an imported graphic anchor to the model's top-left. `^FT` anchors a - * bottom corner (spec p.205): bottom-left, or bottom-right (z=1) when justify - * is "R". Lift by the height and, for "R", shift left by the width. `^FO` is - * already top-left. Mirrors the generator's {@link graphicAnchor}. */ +/** Convert an imported graphic anchor to the model's top-left, the inverse of + * the generator's {@link graphicAnchorCoords}. */ export function ftTopLeft( x: number, y: number, diff --git a/packages/core/src/registry/aztec.ts b/packages/core/src/registry/aztec.ts index a2c0df27..1649391e 100644 --- a/packages/core/src/registry/aztec.ts +++ b/packages/core/src/registry/aztec.ts @@ -38,7 +38,6 @@ export interface AztecProps { rotation: ZplRotation; } -// One switch for the capability flag and the emitter's chip resolution. const CONTROL_CHARS = true; const magnificationTooSmall = moduleTooSmallPreflight('magnification'); diff --git a/packages/core/src/registry/barcode1d.ts b/packages/core/src/registry/barcode1d.ts index f4b2dae8..f567ef07 100644 --- a/packages/core/src/registry/barcode1d.ts +++ b/packages/core/src/registry/barcode1d.ts @@ -27,8 +27,6 @@ export interface Barcode1DProps { printInterpretationAbove?: boolean; checkDigit: boolean; rotation: ZplRotation; - /** Per-field firmware counter (^SN/^SF). When set, the ^FD payload is the - * seed and serializes per label. Mutually exclusive with variable binding. */ serial?: SerialMode; /** Template snapshot from before serial mode replaced `content` with the * seed; consumed by the serial-off restore. Never emitted. */ @@ -77,8 +75,8 @@ export interface Barcode1DCoreConfig { /** See {@link ObjectTypeCore.controlChars}. */ controlChars?: boolean; /** Encode a resolved literal payload that carries control bytes into the - * symbology's own escape form, because the firmware drops ^FH-escaped C0 - * bytes from the symbol. Null falls back to the ^FH emit. */ + * symbology's own escape form, see code128Subset. Null falls back to the + * ^FH emit. */ ctrlFdEncode?: (payload: string) => string | null; /** Escape the characters the symbology's own ^FD grammar would swallow * (Code 128 reads `>` as an invocation code). Applied to every non-GS1 @@ -264,8 +262,7 @@ export function createBarcode1DCore(config: Barcode1DCoreConfig): ObjectTypeCore config.controlChars === true && !p.gs1); return [ fieldPos1d(obj, ctx), - // In-span (after the field opener) so the overlay's bare-^BY check sees - // it and an own export round-trips regenSafe; mirrors the QR emitter. + // In-span after the field opener, see qrcode. byCmd, config.zplCommand(p), fieldData, diff --git a/packages/core/src/registry/datamatrix.ts b/packages/core/src/registry/datamatrix.ts index 9e168a9e..70b853d5 100644 --- a/packages/core/src/registry/datamatrix.ts +++ b/packages/core/src/registry/datamatrix.ts @@ -50,7 +50,6 @@ export interface DataMatrixProps { rows?: number; } -// One switch for the capability flag and the emitter's chip resolution. const CONTROL_CHARS = true; export const DATAMATRIX_PROP_SPECS: PropSpecs = { diff --git a/packages/core/src/registry/image.ts b/packages/core/src/registry/image.ts index 18811327..6fc7912e 100644 --- a/packages/core/src/registry/image.ts +++ b/packages/core/src/registry/image.ts @@ -63,9 +63,9 @@ export function imageEmitDims(p: ImageProps): { width: number; height: number } export interface ImageProps { /** ID into the image cache */ imageId: string; - /** 90-degree orientation. `^GF` has no orientation letter, so a non-N - * rotation is baked into the emitted bitmap (see toZPL); the canvas shows it - * via rotatedGroupTransform. Only cached (editable) images honour it. + /** 90-degree orientation. A non-N rotation is baked into the emitted bitmap + * by rasterizeMono, the canvas shows it via rotatedGroupTransform. Only + * cached (editable) images honour it. * Optional: designs saved before image rotation existed omit it (read as * 'N' via objectRotation). New images seed it from defaultProps. */ rotation?: ZplRotation; @@ -496,10 +496,9 @@ export const image: ObjectTypeCore = { toZPL: (obj) => { const p = obj.props; const cached = getImage(p.imageId); - // ^FT anchors the graphic's bottom-left (spec p.205); right-justified ^FT - // keys its x off the byte-padded ^GF width. imageEmitDims applies the R/B - // axis swap (cached only), and the same helper feeds the home-shift drop - // check so the two agree. ^FO ignores the footprint. + // Right-justified ^FT keys its x off the byte-padded ^GF width. + // imageEmitDims applies the R/B axis swap for cached images and also feeds + // the home-shift drop check, so the two agree. const d = imageEmitDims(p); const anchor = graphicFieldPos(obj, d.width, d.height); // Opaque graphic: re-emit the original ^GF verbatim at the (possibly moved) diff --git a/packages/core/src/registry/line.ts b/packages/core/src/registry/line.ts index f223fc31..433438d5 100644 --- a/packages/core/src/registry/line.ts +++ b/packages/core/src/registry/line.ts @@ -81,9 +81,8 @@ export const line: ObjectTypeCore = { // Pure horizontal: angle 180 means the line extends LEFT from (obj.x, obj.y), // so the ^GB box must start at (obj.x - l) to overlap the same pixels. - // ^FT graphic origin is a bottom corner (spec p.205): lower by the ^GB box - // height (band t horizontal, length l vertical) and, when right-justified - // (z=1), shift to the right edge (band width l horizontal, t vertical). + // ^FT lowers by the ^GB box height, band t horizontal and length l vertical, + // and a right-justified field shifts by its width, see graphicAnchorCoords. const right = obj.fieldJustify === 'R'; if (a === 0 || a === 180) { const bx = a === 180 ? obj.x - l : obj.x; @@ -119,8 +118,6 @@ export const line: ObjectTypeCore = { const orientation = dx * dy >= 0 ? 'L' : 'R'; const boxX = obj.x + (dx < 0 ? Math.round(dx) : 0); const boxY = obj.y + (dy < 0 ? Math.round(dy) : 0); - // ^FT anchors the ^GD bounding box at a bottom corner (spec p.205); ^FO - // emits the box top-left verbatim. const anchor = graphicAnchor(boxX, boxY, w, h, obj.positionType, obj.fieldJustify); return wrapReverse( p.reverse, diff --git a/packages/core/src/registry/micropdf417.ts b/packages/core/src/registry/micropdf417.ts index a7069fa2..12e3971f 100644 --- a/packages/core/src/registry/micropdf417.ts +++ b/packages/core/src/registry/micropdf417.ts @@ -13,7 +13,6 @@ export interface MicroPdf417Props { rotation: ZplRotation; } -// One switch for the capability flag and the emitter's chip resolution. const CONTROL_CHARS = true; interface Micropdf417Mode { diff --git a/packages/core/src/registry/pdf417.ts b/packages/core/src/registry/pdf417.ts index 540348ea..6a41b262 100644 --- a/packages/core/src/registry/pdf417.ts +++ b/packages/core/src/registry/pdf417.ts @@ -17,7 +17,6 @@ export interface Pdf417Props { rotation: ZplRotation; } -// One switch for the capability flag and the emitter's chip resolution. const CONTROL_CHARS = true; export const PDF417_PROP_SPECS: PropSpecs = { diff --git a/packages/core/src/registry/placeholderContent.ts b/packages/core/src/registry/placeholderContent.ts index 3713f077..ae7d9b2b 100644 --- a/packages/core/src/registry/placeholderContent.ts +++ b/packages/core/src/registry/placeholderContent.ts @@ -10,9 +10,6 @@ export function placeholderContentFor(type: string, props: object): string | und return entry?.placeholderContent; } -/** Props for sample rendering: the type's sampleProps overrides merged in, so - * the sample stays encodable where the object's own props would reject it - * (see ObjectTypeCore.sampleProps). */ export function samplePropsFor

(type: string, props: P): P { const overrides = getEntry(type)?.sampleProps; return overrides ? { ...props, ...overrides } : props; diff --git a/packages/core/src/registry/qrcode.ts b/packages/core/src/registry/qrcode.ts index f0d0619f..0d6478a5 100644 --- a/packages/core/src/registry/qrcode.ts +++ b/packages/core/src/registry/qrcode.ts @@ -52,7 +52,6 @@ export interface QrCodeProps { byHeight?: number; } -// One switch for the capability flag and the emitter's chip resolution. const CONTROL_CHARS = true; export const QRCODE_PROP_SPECS: PropSpecs = { diff --git a/packages/core/src/registry/text.ts b/packages/core/src/registry/text.ts index abf11c8b..8ea0975d 100644 --- a/packages/core/src/registry/text.ts +++ b/packages/core/src/registry/text.ts @@ -148,8 +148,6 @@ export const text: ObjectTypeCore = { // encodeTbContent), so wrap the collapsed form there to avoid false overset. const content = mode === "tb" ? (p.content ?? "").replace(/\n/g, " ") : (p.content ?? ""); const lines = wrapBlockLines(content, p.blockWidth ?? 0, lineWidth); - // ^TB clips at blockHeight; N lines occupy (N-1) steps plus the last line's - // own height, not N steps (the trailing 0.25 line-gap doesn't render). const tbHeight = (p.blockHeight ?? 0) > 0 ? tbStackHeightDots(lines.length, p.fontHeight, deviceFontId) : 0; diff --git a/packages/core/src/registry/tlc39.ts b/packages/core/src/registry/tlc39.ts index 1016774a..495fd61c 100644 --- a/packages/core/src/registry/tlc39.ts +++ b/packages/core/src/registry/tlc39.ts @@ -19,9 +19,7 @@ export interface Tlc39Props { height: number; /** MicroPDF417 narrow bar width in dots (^BT w2, 1-10, default 2). */ microPdfModuleWidth: number; - /** MicroPDF417 row height in dots (^BT h2, 1-255, default 4). The row - * COUNT has no ZPL param: the firmware derives it from the serial - * (always 4 columns, smallest version from 6 rows up, ZD230-measured). */ + /** MicroPDF417 row height in dots (^BT h2, 1-255, default 4). The row count has no param, see Tlc39MicroPdf. */ microPdfRowHeight: number; rotation: ZplRotation; } diff --git a/packages/core/src/registry/zplHelpers.ts b/packages/core/src/registry/zplHelpers.ts index 6e69c6cb..61cc9e16 100644 --- a/packages/core/src/registry/zplHelpers.ts +++ b/packages/core/src/registry/zplHelpers.ts @@ -62,12 +62,11 @@ export function fieldPosZ(obj: LabelObjectBase): string { return `${fieldPos(obj)}${z}`; } -/** Graphic leaf types whose ^FO/^FT origin comes from {@link graphicAnchor} - * (the footprint top-left under ^FO, a bottom corner under ^FT). For box / - * ellipse / image the model x/y already is that top-left; a line's x/y is an - * endpoint, so its emitted origin is the bbox top-left instead. Single source - * shared by the generator's home-shift drop test and the preflight off-label - * check, so the two can't disagree on which types anchor this way. */ +/** Graphic leaf types whose ^FO/^FT origin comes from {@link graphicAnchorCoords}. + * For box, ellipse and image the model x/y already is that top-left. A line's + * x/y is an endpoint, so its emitted origin is the bbox top-left instead. + * Single source shared by the generator's home-shift drop test and the + * preflight off-label check. */ export const GRAPHIC_ANCHOR_TYPES: ReadonlySet = new Set([ "box", "ellipse", @@ -135,7 +134,7 @@ interface TextLikeObjForFieldPos extends LabelObjectBase { } /** Vertical extent of a block beyond its first line, in dots. Shifts the - * FT baseline / FO-R/I anchor. ^TB is a fixed clip height; ^FB stacks lines. */ + * FT baseline / FO-R/I anchor. */ function blockExtentFor( p: TextLikeObjForFieldPos["props"], mode: TextMode, diff --git a/packages/core/src/types/ObjectType.ts b/packages/core/src/types/ObjectType.ts index 83bb34a7..1291109f 100644 --- a/packages/core/src/types/ObjectType.ts +++ b/packages/core/src/types/ObjectType.ts @@ -80,7 +80,8 @@ export interface ObjectTypeCore

{ gs1EnterProps?: Partial

; /** Symbology can encode raw control bytes (TAB/CR/...): gates the content * editor's control-key chips and the import's byte-to-chip tokenising. - * False/absent for fixed-charset symbologies (EAN/UPC, Code 39) and text. */ + * False/absent for fixed-charset symbologies (EAN/UPC, Code 39) and text. + * Each registry feeds the same constant to its emitter, so flag and chips never desync. */ controlChars?: boolean; /** Control bytes reach this symbology's symbol only through its own escape * form (Code 128 invocations), never through ^FH hex, which the firmware diff --git a/packages/core/src/types/clockMarker.ts b/packages/core/src/types/clockMarker.ts index b0394d31..e477cced 100644 --- a/packages/core/src/types/clockMarker.ts +++ b/packages/core/src/types/clockMarker.ts @@ -7,7 +7,6 @@ * Capture groups: 1 = channel suffix (`""`|`"2"`|`"3"`), 2 = token letter. */ export const CLOCK_BODY_SRC = "clock([23]?):([A-Za-z])"; -/** Whole-string match: the entire marker body IS a clock token. */ export const CLOCK_BODY_RE = new RegExp(`^${CLOCK_BODY_SRC}$`); /** A `«clock…»` marker embedded in content. Caller adds the `g` flag via diff --git a/packages/core/src/types/controlKey.ts b/packages/core/src/types/controlKey.ts index 28fd7486..095e160f 100644 --- a/packages/core/src/types/controlKey.ts +++ b/packages/core/src/types/controlKey.ts @@ -21,10 +21,9 @@ export const CONTROL_KEY_NAMES = Object.keys(CONTROL_KEYS) as ControlKeyName[]; const CTRL_BODY_SRC = `ctrl:(${CONTROL_KEY_NAMES.join("|")})`; -/** Whole-string match: the entire marker body IS a control key. */ export const CTRL_BODY_RE = new RegExp(`^${CTRL_BODY_SRC}$`); -/** Fresh global regex per call (stateful `lastIndex`, so never share one). */ +/** See clockMarkerReGlobal. */ export const ctrlMarkerReGlobal = (): RegExp => new RegExp(`«${CTRL_BODY_SRC}»`, "g"); export function controlKeyBody(key: ControlKeyName): string { diff --git a/packages/core/src/types/preflight.ts b/packages/core/src/types/preflight.ts index 9534a8e6..d15f2f5a 100644 --- a/packages/core/src/types/preflight.ts +++ b/packages/core/src/types/preflight.ts @@ -72,14 +72,13 @@ export const PREFLIGHT_SEVERITY: Record = { // A blank field is legal ZPL and a normal drafting state; it prints a gap, // so warn rather than block. Mirrors the canvas placeholder. emptyContent: 'warning', - // A legacy/niche symbology (Code 49, TLC39) many printers, especially - // entry-level, do not implement; the ZPL is valid but may print nothing. + // A legacy/niche symbology whose ZPL is valid but may print nothing, see + // limitedSupportPreflight. printerSupportLimited: 'warning', // A rotated QR prints as a static graphic: variable/serial values are baked // at export, the printer no longer substitutes them per label. qrRotatedStatic: 'warning', - // The graphic encoder only produces Model 2 symbols, so a rotated Model 1 - // QR prints as Model 2 (legacy scanners may reject it). + // A rotated Model 1 QR prints as Model 2, which legacy scanners may reject. qrRotatedModel2: 'warning', // Modes 2/3 need a structured carrier message (GS-separated postcode, // country, service); without it the symbol cannot encode. diff --git a/packages/mcp-server/src/boundaryWritePaths.test.ts b/packages/mcp-server/src/boundaryWritePaths.test.ts index 3388f13d..b2748f5e 100644 --- a/packages/mcp-server/src/boundaryWritePaths.test.ts +++ b/packages/mcp-server/src/boundaryWritePaths.test.ts @@ -239,8 +239,6 @@ describe("a variable referenced only by a hidden object", () => { describe("a page past the geometry cap", () => { it("says the probes were skipped instead of showing a clean report", () => { - // Off-label checks judge registry-default boxes there; a clean warnings - // array on such a page is partiality, not health. const objects = Array.from({ length: 2001 }, (_, i) => ({ id: `t${i}`, type: "text", x: 0, y: 0, rotation: 0, props: { content: "x" }, })); diff --git a/packages/mcp-server/src/report.ts b/packages/mcp-server/src/report.ts index 5e746f5b..2459aa36 100644 --- a/packages/mcp-server/src/report.ts +++ b/packages/mcp-server/src/report.ts @@ -280,9 +280,7 @@ function measuredBarcodes( return map; } -/** These objects' own x is the printed RIGHT edge while the bounds row shows - * the ink left edge, so a patch aimed at the reported x moves them a full - * width. Named only when such an object exists, to keep clean reports quiet. */ +/** Names right-anchored objects only when one exists, to keep clean reports quiet. */ function rightAnchorNotes(pages: Page[]): string[] { const ids = pages.flatMap((page) => exportableLeaves(page.objects).filter(isRightAnchoredField).map((l) => l.id), diff --git a/packages/mcp-server/src/tools.test.ts b/packages/mcp-server/src/tools.test.ts index ee0dd2e2..119acd24 100644 --- a/packages/mcp-server/src/tools.test.ts +++ b/packages/mcp-server/src/tools.test.ts @@ -410,8 +410,7 @@ describe("mcp-server tools", () => { }); it("keeps a ^GFA cache that has no source image, and takes fresh bytes", () => { - // Without a source image the cache is the graphic's only copy, and the - // header (not widthDots) is what the emit anchors by. + // The header, not widthDots, is what the emit anchors by. const entry = ObjectRegistry.image; const obj = { id: "i", type: "image", x: 0, y: 0, rotation: 0, @@ -1782,8 +1781,6 @@ describe("a ^GFB header whose declared count does not match the payload bytes", expect(withRaw("^GFB,3,3,10," + "\u20ac".repeat(3) + "^XA~JB").ok).toBe(false); }); it("refuses an empty graphic-count slot, which prints nothing at all", () => { - // Labelary: b may be omitted (renders identically), c may not. Without it - // the firmware never learns where the graphic ends and eats the stream. expect(withRaw("^GFB,,,2,ABCD").ok).toBe(false); expect(withRaw("^GFB,,4,2,ABCD").ok).toBe(true); }); @@ -2129,10 +2126,7 @@ describe("an update on a block that cannot replay around it", () => { }); describe("a structural patch on a page that carries captured import bytes", () => { - // Structural edits DROP the capture (an added object is not in the bytes, a - // removed one still is); the roundtrip rule only demands the drop be loud. - // captureLost used to read the post-drop pages, where the overlay this note - // is about was already gone, so no structural drop ever produced a note. + // The roundtrip rule only demands the drop be loud. const source = "^XA^FWR^ZZ99,7^LT30^FO10,10^A0N,20,20^FDx^FS^XZ"; it("says the capture is lost on an addVariable", () => { @@ -2280,8 +2274,6 @@ describe("the ^GF header's own value ranges", () => { objects: [{ type: "image", x: 10, y: 10, props: { imageId: "", widthDots: 8, rawGf } }], }); - // The p.215 range (1-99999) is documentation, not a wire limit: our own - // encoder emits b/c past it, so gating on it emptied every full-size graphic. it("takes a count no payload could satisfy, which breaks the label, not the wire", () => { expect(withRaw("^GFB,99999999999999999999,8,1,AB^XZ").ok).toBe(true); expect(withRaw("^GFA,,4000000,1,FFFF").ok).toBe(true); diff --git a/src-tauri/src/db.rs b/src-tauri/src/db.rs index 61c27987..dad6fbc5 100644 --- a/src-tauri/src/db.rs +++ b/src-tauri/src/db.rs @@ -514,8 +514,7 @@ async fn run_list_tables(spec: &DbSpec) -> Result, DbError> { out } -/// Refuse a sqlite path the user never granted via the native pick command -/// (scope::PathGrants); network specs carry no path. +/// Refuse a sqlite path scope::PathGrants never granted, network specs carry no path. fn check_path_scope(app: &tauri::AppHandle, spec: &DbSpec) -> Result<(), DbError> { use tauri::Manager; match spec { diff --git a/src-tauri/src/excel.rs b/src-tauri/src/excel.rs index 4df9b4b8..d56ae7d6 100644 --- a/src-tauri/src/excel.rs +++ b/src-tauri/src/excel.rs @@ -76,8 +76,7 @@ async fn timed_read( parsed? } -/// Refuse a path the user never granted via the native pick command -/// (scope::PathGrants). +/// Refuse a path scope::PathGrants never granted. fn check_path_scope(app: &tauri::AppHandle, path: &str) -> Result<(), ExcelError> { use tauri::Manager; if !app.state::().is_granted(path) { diff --git a/src-tauri/src/usb.rs b/src-tauri/src/usb.rs index 70152344..1f0b66d0 100644 --- a/src-tauri/src/usb.rs +++ b/src-tauri/src/usb.rs @@ -51,7 +51,7 @@ fn zebra_first(a: &UsbPrinter, b: &UsbPrinter) -> std::cmp::Ordering { #[serde(tag = "kind", rename_all = "snake_case")] pub enum UsbSendResult { Sent, - /// Linux only: the udev uaccess rule has not applied yet. + /// See classify_lp_io_err. #[cfg(target_os = "linux")] PermissionDenied, NotFound, @@ -66,7 +66,7 @@ pub enum UsbQueryResult { Data { body: String, }, - /// Linux only: the udev uaccess rule has not applied yet. + /// See classify_lp_io_err. #[cfg(target_os = "linux")] PermissionDenied, NotFound, @@ -74,7 +74,6 @@ pub enum UsbQueryResult { #[cfg(any(target_os = "linux", target_os = "macos"))] impl UsbQueryResult { - /// ~DY payloads are ASCII (ZB64); lossy keeps any stray control bytes benign. fn from_reply(body: Vec) -> Self { Self::Data { body: String::from_utf8_lossy(&body).into_owned(), @@ -349,7 +348,7 @@ fn resolve_node(usbmisc_root: &Path, dev_root: &Path, id: &str) -> Result(e: std::io::Error, on_perm: T, on_missing: T) -> Result { match e.kind() { @@ -797,8 +796,6 @@ mod hardware_tests { } } -// Talks to real hardware; excluded from the default run. With the ZD230 -// attached: cd src-tauri && cargo test -- --ignored #[cfg(all(test, target_os = "linux"))] mod linux_hardware_tests { #[test] @@ -811,7 +808,6 @@ mod linux_hardware_tests { rt.block_on(async { let printers = super::list_usb_printers().await.unwrap(); let printer = printers.first().expect("a USB printer should be attached"); - // ~HI answers with the model string, e.g. "ZD230-203dpi,V84.20.18Z,...". let res = super::query_zpl_usb(printer.id.clone(), "~HI".to_string()) .await .unwrap(); @@ -822,8 +818,6 @@ mod linux_hardware_tests { } other => panic!("expected data, got {other:?}"), } - // The preview flow the frontend runs: store the rendered format without - // printing (^IS..,N), then upload its bitmap (^HY answers with ~DY). let preview = "^XA^FO30,30^A0N,40,40^FDUSB Preview^FS^ISR:PRE.GRF,N^XZ\n^XA^HYR:PRE.GRF^XZ"; let res = super::query_zpl_usb(printer.id.clone(), preview.to_string()) .await diff --git a/src/components/Canvas/BarcodeObject.eanGuardZone.test.tsx b/src/components/Canvas/BarcodeObject.eanGuardZone.test.tsx index ad4ca412..efdd7a9b 100644 --- a/src/components/Canvas/BarcodeObject.eanGuardZone.test.tsx +++ b/src/components/Canvas/BarcodeObject.eanGuardZone.test.tsx @@ -1,22 +1,14 @@ // @vitest-environment jsdom import { describe, it, expect, beforeAll, afterEach } from "vitest"; import { render, cleanup } from "@testing-library/react"; +import { stubKonvaCanvas2d } from "../../test/helpers"; import { Stage, Layer } from "react-konva"; import type Konva from "konva"; import { BarcodeObject } from "./BarcodeObject"; import { ObjectRegistry, type LeafType } from "@zplab/core/registry"; import type { LeafObject } from "@zplab/core/registry"; -beforeAll(() => { - const noop = () => undefined; - HTMLCanvasElement.prototype.getContext = (() => - new Proxy({ - getImageData: () => ({ data: new Uint8ClampedArray(4) }), - measureText: () => ({ width: 10 }), - }, { - get: (target, prop) => (prop in target ? target[prop as keyof typeof target] : noop), - })) as unknown as typeof HTMLCanvasElement.prototype.getContext; -}); +beforeAll(stubKonvaCanvas2d); afterEach(cleanup); diff --git a/src/components/Canvas/BarcodeObject.fallbackSize.test.tsx b/src/components/Canvas/BarcodeObject.fallbackSize.test.tsx index ca0d3572..be5c1e75 100644 --- a/src/components/Canvas/BarcodeObject.fallbackSize.test.tsx +++ b/src/components/Canvas/BarcodeObject.fallbackSize.test.tsx @@ -1,19 +1,14 @@ // @vitest-environment jsdom import { describe, it, expect, beforeAll, afterEach } from "vitest"; import { render, cleanup } from "@testing-library/react"; +import { stubKonvaCanvas2d } from "../../test/helpers"; import { Stage, Layer } from "react-konva"; import type Konva from "konva"; import { BarcodeObject } from "./BarcodeObject"; import type { LeafObject } from "@zplab/core/registry"; import type { LabelObject } from "@zplab/core/types/Group"; -beforeAll(() => { - const noop = () => undefined; - HTMLCanvasElement.prototype.getContext = (() => - new Proxy({ getImageData: () => ({ data: new Uint8ClampedArray(4) }), measureText: () => ({ width: 0 }) }, { - get: (target, prop) => (prop in target ? target[prop as keyof typeof target] : noop), - })) as unknown as typeof HTMLCanvasElement.prototype.getContext; -}); +beforeAll(stubKonvaCanvas2d); afterEach(cleanup); diff --git a/src/components/Canvas/BarcodeObject.hriControlBytes.test.tsx b/src/components/Canvas/BarcodeObject.hriControlBytes.test.tsx index 2643ef56..c2800e11 100644 --- a/src/components/Canvas/BarcodeObject.hriControlBytes.test.tsx +++ b/src/components/Canvas/BarcodeObject.hriControlBytes.test.tsx @@ -1,19 +1,14 @@ // @vitest-environment jsdom import { describe, it, expect, beforeAll, afterEach } from "vitest"; import { render, cleanup } from "@testing-library/react"; +import { stubKonvaCanvas2d } from "../../test/helpers"; import { Stage, Layer } from "react-konva"; import type Konva from "konva"; import { BarcodeObject } from "./BarcodeObject"; import type { LeafObject } from "@zplab/core/registry"; import type { LabelObject } from "@zplab/core/types/Group"; -beforeAll(() => { - const noop = () => undefined; - HTMLCanvasElement.prototype.getContext = (() => - new Proxy({ getImageData: () => ({ data: new Uint8ClampedArray(4) }), measureText: () => ({ width: 0 }) }, { - get: (target, prop) => (prop in target ? target[prop as keyof typeof target] : noop), - })) as unknown as typeof HTMLCanvasElement.prototype.getContext; -}); +beforeAll(stubKonvaCanvas2d); afterEach(cleanup); diff --git a/src/components/Canvas/BarcodeObject.scmAlarm.test.tsx b/src/components/Canvas/BarcodeObject.scmAlarm.test.tsx index 712c6f6b..98b94210 100644 --- a/src/components/Canvas/BarcodeObject.scmAlarm.test.tsx +++ b/src/components/Canvas/BarcodeObject.scmAlarm.test.tsx @@ -6,16 +6,9 @@ import type Konva from "konva"; import { BarcodeObject } from "./BarcodeObject"; import type { LeafObject } from "@zplab/core/registry"; import type { LabelObject } from "@zplab/core/types/Group"; +import { stubKonvaCanvas2d } from "../../test/helpers"; -beforeAll(() => { - // jsdom has no 2d context; Konva needs one for Stage/Layer plumbing and - // sceneFunc draws. Geometry never reads pixels back. - const noop = () => undefined; - HTMLCanvasElement.prototype.getContext = (() => - new Proxy({ getImageData: () => ({ data: new Uint8ClampedArray(4) }), measureText: () => ({ width: 0 }) }, { - get: (target, prop) => (prop in target ? target[prop as keyof typeof target] : noop), - })) as unknown as typeof HTMLCanvasElement.prototype.getContext; -}); +beforeAll(stubKonvaCanvas2d); afterEach(cleanup); diff --git a/src/components/Canvas/BarcodeObject.stateFrame.test.tsx b/src/components/Canvas/BarcodeObject.stateFrame.test.tsx index 65320037..bb570039 100644 --- a/src/components/Canvas/BarcodeObject.stateFrame.test.tsx +++ b/src/components/Canvas/BarcodeObject.stateFrame.test.tsx @@ -6,6 +6,7 @@ import type Konva from "konva"; import { StateFrame } from "./BarcodeObject"; import { stateFrameProps } from "./bwipHelpers"; import { naturalRect } from "./nodeRect"; +import { stubKonvaCanvas2d } from "../../test/helpers"; describe("stateFrameProps", () => { it("frames the bar rect below-zone (EAN/UPC)", () => { @@ -24,15 +25,7 @@ describe("stateFrameProps", () => { }); }); -beforeAll(() => { - // jsdom has no 2d context; Konva needs one for Stage/Layer plumbing and - // sceneFunc draws. Geometry (getClientRect) never reads pixels back. - const noop = () => undefined; - HTMLCanvasElement.prototype.getContext = (() => - new Proxy({ getImageData: () => ({ data: new Uint8ClampedArray(4) }) }, { - get: (target, prop) => (prop in target ? target[prop as keyof typeof target] : noop), - })) as unknown as typeof HTMLCanvasElement.prototype.getContext; -}); +beforeAll(stubKonvaCanvas2d); afterEach(cleanup); diff --git a/src/components/Canvas/BarcodeObject.symbologySwitch.test.tsx b/src/components/Canvas/BarcodeObject.symbologySwitch.test.tsx index 3089ae5b..10cd21ce 100644 --- a/src/components/Canvas/BarcodeObject.symbologySwitch.test.tsx +++ b/src/components/Canvas/BarcodeObject.symbologySwitch.test.tsx @@ -8,19 +8,9 @@ import { naturalRect } from "./nodeRect"; import { convertSymbologyMapper } from "../../lib/symbologySwitch"; import { ObjectRegistry, type LeafType } from "@zplab/core/registry"; import type { LeafObject } from "@zplab/core/registry"; +import { stubKonvaCanvas2d } from "../../test/helpers"; -beforeAll(() => { - // jsdom has no 2d context; Konva needs one for Stage/Layer plumbing. - // Geometry (getClientRect) never reads pixels back. - const noop = () => undefined; - HTMLCanvasElement.prototype.getContext = (() => - new Proxy({ - getImageData: () => ({ data: new Uint8ClampedArray(4) }), - measureText: () => ({ width: 10 }), - }, { - get: (target, prop) => (prop in target ? target[prop as keyof typeof target] : noop), - })) as unknown as typeof HTMLCanvasElement.prototype.getContext; -}); +beforeAll(stubKonvaCanvas2d); afterEach(cleanup); diff --git a/src/components/Canvas/BarcodeObject.tsx b/src/components/Canvas/BarcodeObject.tsx index 1d795931..2de51f09 100644 --- a/src/components/Canvas/BarcodeObject.tsx +++ b/src/components/Canvas/BarcodeObject.tsx @@ -212,7 +212,6 @@ export function BarcodeObject({ // render shift (-barLeftPx, -barTopPx) so objectBounds lands the same origin. const footprintBarLeftDots = pxToDots(dim.barLeftPx, scale, dpmm); const footprintBarTopDots = pxToDots(dim.barTopPx, scale, dpmm); - // Upright (unrotated) bar-rect size, for the rotation-aware ^FT anchor. const uprightBarWDots = pxToDots(dim.upright.barW, scale, dpmm); const uprightBarHDots = pxToDots(dim.upright.barH, scale, dpmm); useEffect(() => { @@ -243,7 +242,7 @@ export function BarcodeObject({ : BARCODE_1D_TYPES.has(obj.type) ? (obj.props as { height: number }).height : 0; - // A rotated QR prints as a shift-free ^GFA, so the ^BQ artifacts below are N-only. + // Shift-free as a graphic, see qrPrintsAsGraphic, so the ^BQ artifacts below are N-only. const graphicQr = qrPrintsAsGraphic(obj); const ftOffset = barcodeFtAnchorOffset(graphicQr ? "N" : rotation, uprightBarWDots, ftBarHDots); // ^FT QR rises exactly 3 modules above the anchor (^BY-independent, both @@ -275,8 +274,6 @@ export function BarcodeObject({ const showStateFrame = warnBlank || invalid; const stateFrameColor = invalid ? colors.error : CANVAS_WARNING; - // Whole-object drag (snap + commit) is centralized in the drag controller; - // the commit is a pure translation, so the bar-anchor offset math is moot. const handleDragMove = dragHandlers?.onDragMove; const handleDragEnd = dragHandlers?.onDragEnd; diff --git a/src/components/Canvas/ImageObject.gfaFallback.test.tsx b/src/components/Canvas/ImageObject.gfaFallback.test.tsx index fbfd5341..fd2cc2a8 100644 --- a/src/components/Canvas/ImageObject.gfaFallback.test.tsx +++ b/src/components/Canvas/ImageObject.gfaFallback.test.tsx @@ -1,13 +1,14 @@ // @vitest-environment jsdom import { describe, it, expect, beforeAll, afterEach } from "vitest"; import { render, cleanup } from "@testing-library/react"; +import { stubKonvaCanvas2d } from "../../test/helpers"; import { Stage, Layer } from "react-konva"; import type Konva from "konva"; import { ImageObject } from "./ImageObject"; import type { LabelObject } from "@zplab/core/types/Group"; beforeAll(() => { - const noop = () => undefined; + stubKonvaCanvas2d(); // jsdom ships no ImageData; the preview builds one from the decoded raster. globalThis.ImageData = class { data: Uint8ClampedArray; @@ -19,15 +20,6 @@ beforeAll(() => { this.height = height; } } as unknown as typeof globalThis.ImageData; - HTMLCanvasElement.prototype.getContext = (() => - new Proxy( - { - getImageData: () => ({ data: new Uint8ClampedArray(4) }), - measureText: () => ({ width: 0 }), - putImageData: noop, - }, - { get: (target, prop) => (prop in target ? target[prop as keyof typeof target] : noop) }, - )) as unknown as typeof HTMLCanvasElement.prototype.getContext; }); afterEach(cleanup); diff --git a/src/components/Canvas/ImageObject.tsx b/src/components/Canvas/ImageObject.tsx index 881dd821..6aba7418 100644 --- a/src/components/Canvas/ImageObject.tsx +++ b/src/components/Canvas/ImageObject.tsx @@ -128,7 +128,6 @@ export function ImageObject({ }, [obj.id, footprintWDots, footprintHDots]); useEffect(() => () => clearMeasuredBounds(obj.id), [obj.id]); - // Whole-object drag (snap + commit) is centralized in the drag controller. const handleDragMove = dragHandlers?.onDragMove; const handleDragEnd = dragHandlers?.onDragEnd; diff --git a/src/components/Canvas/KonvaObject.tsx b/src/components/Canvas/KonvaObject.tsx index c846941e..be8a816b 100644 --- a/src/components/Canvas/KonvaObject.tsx +++ b/src/components/Canvas/KonvaObject.tsx @@ -606,7 +606,6 @@ function KonvaObjectInner({ return () => clearMeasuredBounds(obj.id); }, [obj.id, isSingleLineText]); - // Whole-object drag (snap + commit) is centralized in the drag controller. const handleDragMove = dragHandlers?.onDragMove; const handleDragEnd = dragHandlers?.onDragEnd; diff --git a/src/components/Canvas/LabelCanvas.tsx b/src/components/Canvas/LabelCanvas.tsx index c95a4f8e..6a78de0d 100644 --- a/src/components/Canvas/LabelCanvas.tsx +++ b/src/components/Canvas/LabelCanvas.tsx @@ -515,9 +515,8 @@ export const LabelCanvas = forwardRef(function LabelCa const labelWidthPx = effectiveWidthMm * scale; const physicalWidthPx = label.widthMm * scale; const labelHeightPx = label.heightMm * scale; - // Printer bitmap is physical head-raster dots: the preview stream carries - // ^JMB, but ^PW/^LL stay physical (ZD230-verified), so this projection uses - // physical dpmm, not effective. + // ^PW/^LL are physical head dots, see deriveUnitScale, so this projection + // uses physical dpmm, not effective. const toPreviewPx = (dots: number) => dotsToPx(dots, scale, label.dpmm); const printerLayout = previewMode.status === 'active' && previewMode.printerDims diff --git a/src/components/Canvas/captureLook.test.tsx b/src/components/Canvas/captureLook.test.tsx index ba0c32f6..4dcf93c0 100644 --- a/src/components/Canvas/captureLook.test.tsx +++ b/src/components/Canvas/captureLook.test.tsx @@ -9,15 +9,9 @@ import { ObjectRegistry, type LeafType } from "@zplab/core/registry"; import type { LeafObject } from "@zplab/core/registry"; import { useLabelStore } from "../../store/labelStore"; import { CANVAS_WARNING } from "../../hooks/useColorScheme"; +import { stubKonvaCanvas2d } from "../../test/helpers"; -beforeAll(() => { - // jsdom has no 2d context. Konva needs one for Stage/Layer plumbing and sceneFunc draws. - const noop = () => undefined; - HTMLCanvasElement.prototype.getContext = (() => - new Proxy({ getImageData: () => ({ data: new Uint8ClampedArray(4) }), measureText: () => ({ width: 10 }) }, { - get: (target, prop) => (prop in target ? target[prop as keyof typeof target] : noop), - })) as unknown as typeof HTMLCanvasElement.prototype.getContext; -}); +beforeAll(stubKonvaCanvas2d); afterEach(cleanup); diff --git a/src/components/Canvas/hooks/useKonvaTransformer.ts b/src/components/Canvas/hooks/useKonvaTransformer.ts index da964fa2..7ce752db 100644 --- a/src/components/Canvas/hooks/useKonvaTransformer.ts +++ b/src/components/Canvas/hooks/useKonvaTransformer.ts @@ -233,8 +233,6 @@ export function useKonvaTransformer({ // edges are moving relative to the start state (oldBox in boundBoxFunc is the // previous frame, which would always look "everything moved"). const transformStartBboxRef = useRef(null); - // Snapshot of the other objects' bboxes at transform start. Other objects - // can't move during a resize, so we avoid re-querying Konva on every tick. const othersSnapshotRef = useRef([]); // Anchor name at drag start; commit skips grid-snap on inactive axes // so the opposite corner stays put. diff --git a/src/components/Canvas/lassoGeometry.integration.test.ts b/src/components/Canvas/lassoGeometry.integration.test.ts index e44a170f..0ba3ffb5 100644 --- a/src/components/Canvas/lassoGeometry.integration.test.ts +++ b/src/components/Canvas/lassoGeometry.integration.test.ts @@ -3,17 +3,9 @@ import { describe, it, expect, beforeAll } from "vitest"; import Konva from "konva"; import { getIdsIntersectingRect } from "./lassoGeometry"; import { shapeHitProps } from "./konvaObjectProps"; +import { stubKonvaCanvas2d } from "../../test/helpers"; -beforeAll(() => { - // jsdom has no 2d context; Konva probes one once per Shape construction - // (canvas-farbling detection). Scene-graph math itself never draws. - const noop = () => undefined; - HTMLCanvasElement.prototype.getContext = (() => ({ - clearRect: noop, - fillRect: noop, - getImageData: () => ({ data: new Uint8ClampedArray(4) }), - })) as unknown as typeof HTMLCanvasElement.prototype.getContext; -}); +beforeAll(stubKonvaCanvas2d); // Production-path slice on real Konva nodes: shapeHitProps stamps the marker // exactly as the box renderer spreads it, findOne resolves it off the live diff --git a/src/components/PrinterSettings/PreviewSettingsTab.tsx b/src/components/PrinterSettings/PreviewSettingsTab.tsx index 8942cfc3..da4d2ce5 100644 --- a/src/components/PrinterSettings/PreviewSettingsTab.tsx +++ b/src/components/PrinterSettings/PreviewSettingsTab.tsx @@ -56,8 +56,7 @@ export function PreviewSettingsTab() { // so the field is write-only there: it shows the draft, never a stored value. const keyValue = keyDraft ?? (isDesktopShell ? '' : storeKey); const [keySaveFailed, setKeySaveFailed] = useState(false); - // A keychain write can raise an OS unlock prompt and take seconds; block a - // second save (and its duplicate prompt) until this one settles. + // A keychain write can raise an OS unlock prompt and take seconds, so block a second save. const [keySaving, setKeySaving] = useState(false); const keyDirty = isDesktopShell ? keyDraft !== null : keyValue.trim() !== storeKey; const saveKey = () => { @@ -189,8 +188,7 @@ export function PreviewSettingsTab() { <> {/* The notice must stand wherever Labelary renders or is about to: a stored printer choice degrades to it on the web. */} {chosen === "labelary" || provider === "labelary" ? t.output.previewNoticeBody : loc.labelaryIdleHint}{" "} - {/* The plans/retention link is about the public service; a - custom endpoint is the operator's own, so omit it there. */} + {/* The plans/retention link is about the public service only. */} ReactNode; - /** Replaces the cell layout entirely (not merged), e.g. `fieldGridCell`. */ + /** Replaces the default layout, not merged. */ className?: string; }) { const id = useId(); @@ -286,12 +286,11 @@ export function ZplEnumSubCustomSelect({ values: readonly T[]; value: T | undefined; onChange: (next: T | undefined) => void; - /** Omit for a required field (no "unset" segment). */ defaultLabel?: string; optionLabel: (v: T) => string; optionBadge?: (v: T) => string; disabled?: boolean; - /** Replaces the cell layout entirely (not merged), e.g. `fieldGridCell`. */ + /** Replaces the default layout, not merged. */ className?: string; }) { const groups = [ diff --git a/src/components/Properties/LayersPanel.tsx b/src/components/Properties/LayersPanel.tsx index 3d7ac11a..73317e3d 100644 --- a/src/components/Properties/LayersPanel.tsx +++ b/src/components/Properties/LayersPanel.tsx @@ -55,8 +55,6 @@ export function LayersPanel() { }, [rows]); const allRowIds = useMemo(() => rows.map((r) => r.obj.id), [rows]); - // Drives the row highlight and tint (selecting all of a group's members reads - // as the group being selected too). NOT used for the drag block. const effectiveSelected = useMemo( () => effectiveSelection(objects, selectedIds), [objects, selectedIds], diff --git a/src/components/Properties/PropertiesPanel.tsx b/src/components/Properties/PropertiesPanel.tsx index aeae2b92..e2bb0d0b 100644 --- a/src/components/Properties/PropertiesPanel.tsx +++ b/src/components/Properties/PropertiesPanel.tsx @@ -590,7 +590,7 @@ function LabelConfigPanel({ - {/* ^PW/^LL are physical head dots (never ^JM-scaled); bridge via raw dpmm. + {/* ^PW/^LL are physical head dots, see deriveUnitScale, so bridge via raw dpmm. No allowUnset: a transient clear keeps the last valid value, not 0. */}

void; }) { const t = useT(); diff --git a/src/components/Properties/VariableInspector.tsx b/src/components/Properties/VariableInspector.tsx index 57ed3a9c..d4dc1267 100644 --- a/src/components/Properties/VariableInspector.tsx +++ b/src/components/Properties/VariableInspector.tsx @@ -23,8 +23,6 @@ export function VariableInspector({ }: { selected: SelectedMarker | null; onRemoveSelected: () => void; - /** Close the modal (content is already live); used by deep-links out to the - * Variables tab. */ onLeave: () => void; /** Renaming a variable here ripples through the store; the modal must also * rewrite its uncommitted content draft so Apply doesn't revert the marker. */ @@ -129,7 +127,7 @@ function VariableState({ } }; - // Delete / CSV mapping live in the Variables tab (one source of truth). + // Delete and CSV mapping live in the Variables tab. const goManage = () => { onLeave(); setSidebarTab("variables"); diff --git a/src/hooks/useColorScheme.test.ts b/src/hooks/useColorScheme.test.ts index e6cb7308..4b725d5e 100644 --- a/src/hooks/useColorScheme.test.ts +++ b/src/hooks/useColorScheme.test.ts @@ -3,10 +3,7 @@ import { fileURLToPath } from 'node:url'; import { describe, it, expect } from 'vitest'; import { DARK_COLORS, LIGHT_COLORS, type CanvasColors } from './useColorScheme'; -// Canvas chrome mirrors a handful of DOM theme tokens as JS literals because -// Konva can't read CSS variables (and jsdom can't resolve them in tests). -// index.css is the source of truth; this test fails if the JS copy drifts. -// See project_ticket_theme_token_single_source. +// Pins the JS copy to index.css, see useColorScheme and project_ticket_theme_token_single_source. const css = readFileSync(fileURLToPath(new URL('../index.css', import.meta.url)), 'utf8'); /** CanvasColors key -> CSS custom property in index.css. */ diff --git a/src/hooks/useColorScheme.ts b/src/hooks/useColorScheme.ts index fae2f4ae..31bc8fbd 100644 --- a/src/hooks/useColorScheme.ts +++ b/src/hooks/useColorScheme.ts @@ -26,10 +26,8 @@ export interface CanvasColors { * on the canvas; e.g. the ^FB wrap-guide line so the user can tell * it apart from selection/transformer chrome. */ accent: string; - /** Panel surface / muted text / hairline / destructive — mirror the DOM - * theme tokens in index.css so canvas chrome (e.g. the action bar pill) - * matches. index.css is the source of truth; useColorScheme.test.ts pins - * these (+ accent) to it so the two can't drift. Konva can't read CSS + /** Panel surface / muted text / hairline / destructive, mirroring the DOM theme + * tokens in index.css. index.css is the source of truth, Konva can't read CSS * variables and jsdom can't resolve them in tests, hence the JS copy. */ surface: string; surface2: string; diff --git a/src/hooks/useCsvImportActions.ts b/src/hooks/useCsvImportActions.ts index 36822afa..46c43270 100644 --- a/src/hooks/useCsvImportActions.ts +++ b/src/hooks/useCsvImportActions.ts @@ -29,9 +29,8 @@ export type PendingImportKind = "same" | "different"; export interface PendingImport { kind: PendingImportKind; parsed: ParsedImport; - /** datasetFetchToken at dialog-open; a mismatch on confirm means the data - * context (or the whole document) changed underneath, so the import is - * dropped rather than applied to the new context. */ + /** datasetFetchToken at dialog-open, see isCurrentDataContext. A stale token + * drops the import rather than applying it to the new context. */ token: number; /** Display name of the dataset being replaced (filename or db link). */ replacingFilename: string; diff --git a/src/hooks/useExcelImportActions.ts b/src/hooks/useExcelImportActions.ts index d9d48ecc..528c26b0 100644 --- a/src/hooks/useExcelImportActions.ts +++ b/src/hooks/useExcelImportActions.ts @@ -10,8 +10,8 @@ export interface PendingExcelImport { path: string; filename: string; sheets: string[]; - /** datasetFetchToken at pick time; a mismatch on Load means the data context - * (or the whole document) changed underneath, so the sheet is not committed. */ + /** datasetFetchToken at pick time, see isCurrentDataContext. A stale token + * leaves the sheet uncommitted. */ token: number; } diff --git a/src/hooks/useSqliteConnectActions.ts b/src/hooks/useSqliteConnectActions.ts index 3e350f87..abbb7353 100644 --- a/src/hooks/useSqliteConnectActions.ts +++ b/src/hooks/useSqliteConnectActions.ts @@ -12,8 +12,8 @@ import type { SqliteProfile } from '../lib/db'; export interface PendingSqliteImport { profile: SqliteProfile; tables: string[]; - /** datasetFetchToken at pick time; a mismatch on Load means the data context - * changed underneath, so the table is not committed. */ + /** datasetFetchToken at pick time, see isCurrentDataContext. A stale token + * leaves the table uncommitted. */ token: number; } diff --git a/src/lib/db.ts b/src/lib/db.ts index f4fcef1d..55eb5a0d 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -66,9 +66,8 @@ const specOf = (profile: DbProfile): DbSpec => sslMode: profile.sslMode ?? 'prefer', }; -/** Native pick that also grants the path Rust-side (persisted): the sqlite - * db_* commands refuse paths that did not come from this dialog. Returns the - * canonical path (store it verbatim), null on cancel; `suggest` preseeds. */ +/** Native pick the db_* commands require a path to come from, see scope::PathGrants. + * Returns the canonical path to store verbatim, null on cancel. `suggest` preseeds. */ export async function pickSqliteFile(suggest?: string): Promise { const { invoke } = await import('@tauri-apps/api/core'); return invoke('pick_sqlite_file', { suggest: suggest ?? null }); diff --git a/src/lib/excel.ts b/src/lib/excel.ts index e7e51698..73d9ff09 100644 --- a/src/lib/excel.ts +++ b/src/lib/excel.ts @@ -1,8 +1,8 @@ import type { DatasetInput } from '@zplab/core/types/DataSource'; import type { DbRows } from './db'; -/** Native pick that also grants the path Rust-side (session): excel_* commands - * refuse paths that did not come from this dialog. Null on cancel. */ +/** Native pick the excel_* commands require a path to come from, see scope::PathGrants. + * Null on cancel. */ export async function pickExcelFile(): Promise { const { invoke } = await import('@tauri-apps/api/core'); return invoke('pick_excel_file'); diff --git a/src/lib/labelRaster.ts b/src/lib/labelRaster.ts index bbef2ac9..ef6354cc 100644 --- a/src/lib/labelRaster.ts +++ b/src/lib/labelRaster.ts @@ -6,9 +6,7 @@ import type { PrinterBitmap } from "./zebraGraphic"; export type MonoRaster = Pick; -/** The label's size in physical head dots, the raster every renderer is brought to. ^JMB halves the - * format's density, but ^PW and ^LL stay physical (ZD230-verified), so the head and Labelary keep rendering - * at the printer's own dpmm. */ +/** The label's size in physical head dots, the raster every renderer is brought to. */ export function labelDots(label: LabelConfig): { width: number; height: number } { return { width: mmToDots(label.widthMm, label.dpmm), height: mmToDots(label.heightMm, label.dpmm) }; } diff --git a/src/lib/mcpBridge.ts b/src/lib/mcpBridge.ts index 30ab201a..1c4a2519 100644 --- a/src/lib/mcpBridge.ts +++ b/src/lib/mcpBridge.ts @@ -107,8 +107,7 @@ export async function respondToEditDesign(line: string): Promise { /** Apply a pushed draft and tell the sidecar whether it took, so open_in_app * reports what happened instead of assuming it arrived. */ export async function respondToOpenDraft(id: number, designText: string): Promise { - // Refusing loudly beats destruction: a dirty buffer is unsaved work that - // exists nowhere else; an untouched session just closes. + // An untouched session just closes, see applyZplImport. if (selectSourceEditDirty(useLabelStore.getState())) { await postDraftReceipt({ id, diff --git a/src/lib/multiResize.test.ts b/src/lib/multiResize.test.ts index f20d2255..b9157e75 100644 --- a/src/lib/multiResize.test.ts +++ b/src/lib/multiResize.test.ts @@ -129,9 +129,6 @@ describe("projectMultiResize", () => { }); describe("a right-justified member of the selection", () => { - // Its model x IS the printed right edge while the union bbox is ink space, so - // projecting the raw x walked the field right by its own box width and out of - // the selection frame. it("keeps its ink edge flush with a left-anchored twin", () => { const symbol = leaf("s", "symbol", 400, 100, { width: 120, height: 40, symbol: "A", rotation: "N" }); (symbol as unknown as { fieldJustify: string }).fieldJustify = "R"; diff --git a/src/lib/preflight.test.ts b/src/lib/preflight.test.ts index 1a457904..25f25a6a 100644 --- a/src/lib/preflight.test.ts +++ b/src/lib/preflight.test.ts @@ -835,9 +835,6 @@ describe("markerValueFindings (mode-D shared ^FN slot)", () => { }); it("flags control bytes in a template-consumed plain slot, not in a lone bind", () => { - // A template keeps the ^FH path and the firmware drops the C0 byte from - // the symbol; an EXCLUSIVE lone bind encodes it losslessly as an - // invocation, but a SHARED lone bind emits raw/^FH and loses it too. const dirty = [{ id: "v", name: "batch", fnNumber: 1, defaultValue: "A\tB" }]; const deps = { variables: dirty, dataset: null, columnMapping: null }; const out = markerValueFindings([code128("c", "X«batch»Y", false)], deps); diff --git a/src/lib/sourceEditText.ts b/src/lib/sourceEditText.ts index b1798e11..631c8477 100644 --- a/src/lib/sourceEditText.ts +++ b/src/lib/sourceEditText.ts @@ -5,8 +5,7 @@ export type { SourceRefusal }; /** Reason→message for the refusals with no position. A located imbalance is * named per kind by buildSourceDiagnostics, which is why 'unbalanced' is - * excluded here rather than answered twice. ZPL command names stay out of - * the locale strings (translator safety) and interpolate here. */ + * excluded here rather than answered twice. */ export function sourceRefusalText( reason: Exclude, t: Translations, diff --git a/src/lib/variableBinding.test.ts b/src/lib/variableBinding.test.ts index 9ee9dfc5..1063c24a 100644 --- a/src/lib/variableBinding.test.ts +++ b/src/lib/variableBinding.test.ts @@ -255,7 +255,7 @@ describe('applyBindingToObject', () => { it('drops the chip byte when a clock marker forces the lossy ^FH path', () => { // The emitter gates ctrlFdEncode BEFORE ^FC token substitution, so a clock - // marker keeps ^FH and the firmware drops the C0 byte from the symbol. + // marker keeps the ^FH path. const o = { id: 'c1', type: 'code128', x: 0, y: 0, rotation: 0, props: { content: 'A«ctrl:TAB»B«clock:Y»' } } as unknown as LabelObject; const out = applyBindingToObject(o, [], null, 'preview', undefined, 'byteOutsideTemplate'); diff --git a/src/lib/zplGenerator.test.ts b/src/lib/zplGenerator.test.ts index cc8ea42f..37d9e7f0 100644 --- a/src/lib/zplGenerator.test.ts +++ b/src/lib/zplGenerator.test.ts @@ -2104,8 +2104,6 @@ describe('generateBatchZpl', () => { }] as unknown as LabelObject[]; it('emits a control-key chip as a Code 128 subset invocation, not ^FH hex', () => { - // ^FH hex reaches the printer but ^BC drops every C0 byte from the symbol - // (ZD230-verified, pixel-identical to no chip at all). const out = generateZPL(baseLabel, code128Chip('A«ctrl:GS»B«ctrl:CR»')); expect(out).toContain('^FD>933933477^FS'); expect(out).not.toContain('^FH_'); diff --git a/src/lib/zplParser.test.ts b/src/lib/zplParser.test.ts index c2a49501..cd5f836f 100644 --- a/src/lib/zplParser.test.ts +++ b/src/lib/zplParser.test.ts @@ -281,8 +281,6 @@ describe('parseZPL — text field position', () => { }); describe('parseZPL — ^FT graphic box/line bottom-left anchor', () => { - // Spec p.205: a ^FT graphic origin is the bottom-left corner; the model stores - // the top-left, so it lifts by the box height. ^FO is the top-left verbatim. it('lifts a ^FT box by its height to the top-left', () => { const { objects } = parseSingle('^XA^FT100,200^GB50,40,3,B,0^FS^XZ', 8); expect(objects[0]?.type).toBe('box'); @@ -2645,7 +2643,6 @@ describe('parseZPL — ^BT TLC39', () => { }); it('maps the ^BT w2/h2 slots to MicroPDF module width and row height', () => { - // Row count has no ^BT param; the firmware derives it from the serial. const zpl = '^XA^FO0,0^BTN,2,2,40,3,6^FD123456,X^FS^XZ'; const p = props(parseSingle(zpl, 8).objects[0]); expect(p.microPdfModuleWidth).toBe(3); diff --git a/src/main.tsx b/src/main.tsx index ebf09a77..d42fc6d6 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -36,10 +36,8 @@ async function bootstrap() { hydrateLabelaryApiKey, migrateLabelaryKey, } = useLabelStore.getState(); - // Migrate a legacy unbound key into the host-bound rust-only credential (so - // an existing key doesn't stay IPC-readable), then load it. Startup, not - // settings-open, so a user who never visits Preview settings is still - // covered. Fire-and-forget; a keychain failure must not delay paint or throw. + // Migrate then load at startup, not on settings-open, so a user who never visits + // Preview settings is still covered. A keychain failure must not delay paint or throw. void migrateLabelaryKey().catch(() => undefined).then(hydrateLabelaryApiKey); // Stamp the build's sidecar capability so the settings rail and MCP tab can // read it synchronously; fire-and-forget like the key hydration above. diff --git a/src/registry/text.test.ts b/src/registry/text.test.ts index 61469d4e..d2fea0ff 100644 --- a/src/registry/text.test.ts +++ b/src/registry/text.test.ts @@ -113,11 +113,6 @@ describe('text — normalizeChanges ^FP un-emit shape', () => { }); }); -/** - * Serial is a plain single-line counter that suppresses block/reverse/^FP at - * emit and render, but must not DESTROY those props: turning serial off has to - * restore the formatting. So they lie dormant on the model while serial is on. - */ describe('text — serial suppresses block/reverse without destroying them', () => { const serial = { increment: 1, zplMode: 'SN' as const }; diff --git a/src/store/labelStore.ts b/src/store/labelStore.ts index 8ca84f37..1b2e2a12 100644 --- a/src/store/labelStore.ts +++ b/src/store/labelStore.ts @@ -187,9 +187,7 @@ export function migrateLegacy(persistedState: unknown, version: number): unknown } } - // v7→v8: drop path-less canvas-only font bindings left by the removed - // built-in preview feature. They still resolve on canvas via alias but - // only persist in saved JSON; scrub them so legacy sessions are clean. + // v7→v8: scrub the path-less font bindings. if (version < 8) { const label = s.label; if (label && typeof label === 'object') { @@ -261,8 +259,6 @@ export function migrateLegacy(persistedState: unknown, version: number): unknown const p = pg && typeof pg === 'object' ? (pg as Record) : {}; if (!Array.isArray(p.objects)) return p; const objects = p.objects as LabelObject[]; - // Drop the overlay on a touched page: a new model object the overlay - // doesn't link would force full regeneration and lose its bytes. const next: Record = { ...p, objects: insertReverseBackingBoxes(objects, label), @@ -412,8 +408,6 @@ function migrateMaintenanceTypeCodes(pp: Record): Record, ): unknown { - // Mutates in place via the shared leaf walker; pages is the persisted payload, - // not the live store, so mutation is safe. visitLeavesInPages(pages, (leaf) => bindSingleMarkerLeaf(leaf, nameById)); return pages; } function migrateGs1DatabarInPages(pages: unknown): unknown { - // Mutates in place via the shared leaf walker; pages is the persisted - // payload not the live store, so mutation is safe. visitLeavesInPages(pages, (leaf) => { if ( leaf.type !== 'gs1databar' || diff --git a/src/store/slices/dataSlice.ts b/src/store/slices/dataSlice.ts index d1bf4e70..9eb56ccf 100644 --- a/src/store/slices/dataSlice.ts +++ b/src/store/slices/dataSlice.ts @@ -100,9 +100,7 @@ export const createDataSlice: StateCreator = (set datasetFetchToken: 0, pendingDatasetReplace: null, - // Replacing the dataset invalidates any active preview snapshot; tear the - // preview down and apply, rather than no-op under the lock (which would let - // callers close dialogs / clear state as if the load had succeeded). + // Replacing the dataset invalidates any active preview snapshot. loadDataset: (result) => { get().exitPreviewMode(); set((s) => ({ @@ -172,8 +170,7 @@ export const createDataSlice: StateCreator = (set ? 0 : Math.max(0, Math.min(activeRowIndex, rows.length - 1)); // Renaming an existing variable here must ripple to its `«name»` markers, - // else bound fields orphan and print the marker literally. Diff by id, then - // rewrite in ONE pass against the original names so a name swap is safe. + // else bound fields orphan and print the marker literally. Diff by id. const oldNameById = new Map(state.variables.map((v) => [v.id, v.name])); const renames = new Map(); for (const v of variables) { diff --git a/src/test/helpers.ts b/src/test/helpers.ts index c2defac2..86f0a663 100644 --- a/src/test/helpers.ts +++ b/src/test/helpers.ts @@ -3,6 +3,17 @@ import { zlibSync } from 'fflate'; import type { SerialMode } from '@zplab/core/registry/serialField'; import { parseZPL, type ImportFindingKind } from '@zplab/core/lib/zplParser'; +/** jsdom has no 2d context. Konva needs one for Stage/Layer plumbing, sceneFunc + * draws and its per-Shape probe, and geometry never reads pixels back. */ +export function stubKonvaCanvas2d(): void { + const noop = () => undefined; + HTMLCanvasElement.prototype.getContext = (() => + new Proxy( + { getImageData: () => ({ data: new Uint8ClampedArray(4) }), measureText: () => ({ width: 10 }) }, + { get: (target, prop) => (prop in target ? target[prop as keyof typeof target] : noop) }, + )) as unknown as typeof HTMLCanvasElement.prototype.getContext; +} + /** Parse a single-label stream: the sole page merged with the document-wide * fields. Asserts exactly one page; multi-page tests read `pages` directly. */ export function parseSingle(zpl: string, dpmm = 8, opts?: { captureOverlay?: boolean }) { diff --git a/src/test/labelarySync.test.ts b/src/test/labelarySync.test.ts index 6545ca5b..3fd84ead 100644 --- a/src/test/labelarySync.test.ts +++ b/src/test/labelarySync.test.ts @@ -163,7 +163,6 @@ describe("Labelary Sync - Canvas Dimension Logic", () => { if (obj.positionType === "FT") { // Mirror the real render/bounds anchor (objectBounds.barcodeTopLeft): // rotation-aware bar-base offset + HRI text-zone shift (px*8 -> dots). - // Mirrors BarcodeObject: a rotated QR prints as a shift-free ^GFA. const gQr = qrPrintsAsGraphic(obj); const off = barcodeFtAnchorOffset( gQr ? "N" : rotation, diff --git a/src/test/visualRegression.test.ts b/src/test/visualRegression.test.ts index cc243e42..fb499a4a 100644 --- a/src/test/visualRegression.test.ts +++ b/src/test/visualRegression.test.ts @@ -137,7 +137,6 @@ describe("Visual Regression - bwip-js vs Labelary", () => { // then apply rotatedGroupTransform-equivalent rotation around that. // FT barcodes anchor at the rotation-aware bar base (mirrors the render); // FO uses drawY above. px == dots here (getDisplaySize scale 8, dpmm 8). - // Mirrors BarcodeObject: a rotated QR prints as a shift-free ^GFA. const gQr = qrPrintsAsGraphic(obj); const ftOff = obj.positionType === "FT" && isBarcode(obj) diff --git a/src/test/zplRoundtrip.test.ts b/src/test/zplRoundtrip.test.ts index faa3dee5..50ebddee 100644 --- a/src/test/zplRoundtrip.test.ts +++ b/src/test/zplRoundtrip.test.ts @@ -510,7 +510,6 @@ describe('round-trip — ^LH label home offset', () => { // ── MSI barcode ────────────────────────────────────────────────────────────── describe('round-trip — MSI barcode', () => { - // ^BMo,e,h,f,g; e: A=no check, B=1 Mod 10 (default), C/D=multi-check. it('parses ^BM height from third parameter (new format)', () => { const { first } = roundtrip( '^XA^PW784^LL264^CI28^BY2^FO0,0^BMN,A,80,Y,N^FD12345678^FS^XZ',