Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ Edit an imported label and export it again:

- **Preserved:** everything you didn't touch comes back byte-for-byte, including fields, label settings, comments, whitespace, and commands the editor doesn't model. A zero-edit import/export cycle reproduces the file exactly.
- **Regenerated:** only the objects you edit, add, or delete are re-emitted from the model. The rest of the file is spliced back unchanged.
- **Full-regeneration fallback:** a few constructs make per-object patching unsafe: `^MU` unit scaling, non-default `^CC`/`^CT`/`^CD` command prefixes, non-UTF-8 `^CI` encoding, non-default `^FE` embed delimiters, a bare `^FN` declared outside a field, and a barcode relying on a `^BY` from an earlier field. On such labels the first edit regenerates the whole label; with no edits the export stays byte-for-byte.
- **Full-regeneration fallback:** a few constructs make per-object patching unsafe: `^MU` unit scaling, non-default `^CC`/`^CT`/`^CD` command prefixes, non-UTF-8 `^CI` encoding, non-default `^FE` embed delimiters, a bare `^FN` declared outside a field, an in-span `^JM` density switch, and a barcode relying on a `^BY` from an earlier field. On such labels the first edit regenerates the whole label; with no edits the export stays byte-for-byte.

Byte capture at import is deliberately conservative: when a field can't be mapped cleanly to a single object, the whole label falls back to model regeneration, which keeps the content but not the exact bytes. The captured bytes are stored in saved `.json` designs; a design from an older app version with an outdated capture format is detected and rebuilt.

Expand Down Expand Up @@ -152,7 +152,7 @@ Both `.zpl` and `.json` round-trip cleanly. `.zpl` preserves all printable conte
## Coverage

<!-- coverage:start (generated from docs/zpl-roadmap.md by scripts/gen-coverage.mjs; run `pnpm coverage:gen`) -->
113 of the 225 ZPL II commands tracked in the [roadmap](docs/zpl-roadmap.md) are supported today. Categorical breakdown:
114 of the 225 ZPL II commands tracked in the [roadmap](docs/zpl-roadmap.md) are supported today. Categorical breakdown:

| Area | Supported |
|---|---|
Expand All @@ -169,7 +169,7 @@ Both `.zpl` and `.json` round-trip cleanly. `.zpl` preserves all printable conte
| Text & fonts | 7 / 14 |
| Print quality | 10 / 18 |
| Configuration & persistence | 3 / 5 |
| Hardware / Host comm / RFID / Network | 0 / 87 |
| Hardware / Host comm / RFID / Network | 1 / 87 |
<!-- coverage:end -->

---
Expand Down
2 changes: 1 addition & 1 deletion docs/zpl-roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ These need printer-side feedback or are intrinsically connection bound.
| `[ ]` | `^JJ` | set auxiliary port | `Native build` |
| `[ ]` | `^JS` | sensor select (reflective / transmissive) | `Native build` |
| `[ ]` | `~JL` | set label length | `Native build` |
| `[ ]` | `^JM` | set dots per millimeter (B halves density and doubles format scale; A = normal, default) | `Coming soon` |
| `[x]` | `^JM` | set dots per millimeter (B halves density and doubles format scale; A = normal, default) | |
| `[ ]` | `~JN` | head test fatal | `Native build` |
| `[ ]` | `~JO` | head test non-fatal | `Native build` |
| `[ ]` | `~JP` | pause and cancel format | `Native build` |
Expand Down
5 changes: 3 additions & 2 deletions packages/core/src/lib/barcodeScannability.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { PreflightCtx, PreflightProducerResult } from "../types/preflight";
import { effectiveDpmm } from "../types/LabelConfig";
import { mmToUnitExact, unitLabel, type Unit } from "./units";

/** Recommended minimum barcode module / X-dimension in mm for reliable general
Expand Down Expand Up @@ -34,7 +35,7 @@ export function moduleTooSmallFindings(
export function moduleTooSmallPreflight<P extends object>(
prop: keyof P & string,
): (obj: { props: P }, ctx: PreflightCtx) => PreflightProducerResult[] {
return (obj, ctx) => moduleTooSmallFindings(obj.props[prop] as number, ctx.label.dpmm, ctx.unit);
return (obj, ctx) => moduleTooSmallFindings(obj.props[prop] as number, effectiveDpmm(ctx.label), ctx.unit);
}

/** Registry `preflight` for legacy/niche symbologies (Code 49, TLC39): the
Expand All @@ -44,7 +45,7 @@ export function limitedSupportPreflight<P extends object>(
prop: keyof P & string,
): (obj: { props: P }, ctx: PreflightCtx) => PreflightProducerResult[] {
return (obj, ctx) => [
...moduleTooSmallFindings(obj.props[prop] as number, ctx.label.dpmm, ctx.unit),
...moduleTooSmallFindings(obj.props[prop] as number, effectiveDpmm(ctx.label), ctx.unit),
{ kind: "printerSupportLimited" },
];
}
40 changes: 35 additions & 5 deletions packages/core/src/lib/designFile.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { z } from "zod";
import { labelConfigSchema, type LabelConfig } from "../types/LabelConfig";
import { JM_DENSITY_VALUES, labelConfigSchema, type JmDensity, type LabelConfig } from "../types/LabelConfig";
import { labelObjectBaseSchema } from "../types/LabelObject";
import {
variableSchema,
Expand All @@ -9,7 +9,8 @@ import {
} from "../types/Variable";
import { dbSourceRefSchema, type DbSourceRef } from "../types/DataSource";
import type { LabelObject } from "../types/Group";
import { blockOverlaySchema, type BlockOverlay } from "./zplOverlay/overlay";
import { blockOverlaySchema, overlayText, type BlockOverlay } from "./zplOverlay/overlay";
import { reconstructLegacyBlockHeads } from "./zplHeadScan";
import { visitLeavesInPages, foldSerialLeaf, bindSingleMarkerLeaf, sanitiseVariableNames, safeUniqueNameById } from "./objectTree";
import { insertReverseBackingBoxes, pageNeedsReverseBacking } from "./reverseBacking";
import { ok, err, type Result } from "./result";
Expand All @@ -19,10 +20,10 @@ import { ok, err, type Result } from "./result";
* migrator below and dispatch on `schemaVersion` in `parseDesignFile`.
* The persist middleware in `labelStore` has its own independent
* version for localStorage state; do not conflate. */
export const CURRENT_DESIGN_SCHEMA_VERSION = 3;
export const CURRENT_DESIGN_SCHEMA_VERSION = 4;

export type DesignFileError = "parse_error" | "invalid_schema";
export interface DesignFilePage { objects: LabelObject[]; overlay?: BlockOverlay }
export interface DesignFilePage { objects: LabelObject[]; overlay?: BlockOverlay; jmDensity?: JmDensity }
export interface DesignFile {
label: LabelConfig;
pages: DesignFilePage[];
Expand Down Expand Up @@ -64,10 +65,13 @@ const labelObjectSchema: z.ZodType<unknown> = z.union([groupSchema, leafSchema])
const pageSchema = z.object({
objects: z.array(labelObjectSchema),
overlay: blockOverlaySchema.optional().catch(undefined),
jmDensity: z.enum(JM_DENSITY_VALUES).optional(),
});

const designFileSchema = z.object({
schemaVersion: z.literal(3),
// v3 predates ^JM: it loads through the legacy JM reconstruction. v4 carries
// the persisted density fields; both are read, anything else is rejected.
schemaVersion: z.union([z.literal(3), z.literal(4)]),
label: labelConfigSchema,
pages: z.array(pageSchema),
variables: z.array(variableSchema).optional(),
Expand All @@ -93,6 +97,7 @@ export function parseDesignFile(text: string): Result<DesignFile, DesignFileErro
const parsed = designFileSchema.safeParse(json);
if (parsed.success) {
const pages = parsed.data.pages as unknown as DesignFilePage[];
reconstructLegacyJmDensity(parsed.data.label, pages);
const variables = parsed.data.variables ?? [];
// Enforce the marker-safe name invariant: an old/foreign name like `clock:Y`
// can't be a single-bind marker, so rename offenders + rewrite their markers.
Expand Down Expand Up @@ -187,6 +192,31 @@ function migrateSingleBindToMarker(json: unknown): void {
j.schemaVersion = 3;
}

/** Payloads without ^JM modeling carried a head ^JM only in the raw overlay
* bytes, which a full regen would drop. Reconstructs the fold a fresh import
* would yield; no-op once any label/page density or recorded head is present. */
export function reconstructLegacyJmDensity(
label: { jmDensity?: JmDensity },
pages: DesignFilePage[],
): void {
if (label.jmDensity !== undefined) return;
for (const page of pages) {
if (page.jmDensity !== undefined || page.overlay?.head) return;
}
const heads = reconstructLegacyBlockHeads(
pages.map((page) => (page.overlay ? overlayText(page.overlay) : undefined)),
);
const anchorIndex = pages.findIndex((p) => p.objects.length > 0);
if (anchorIndex >= 0) label.jmDensity = heads[anchorIndex]?.density;
const designJm: JmDensity = label.jmDensity ?? "A";
pages.forEach((page, i) => {
const { density, head } = heads[i] ?? {};
const blockJm: JmDensity = density ?? "A";
if (blockJm !== designJm) page.jmDensity = blockJm;
if (head && page.overlay) page.overlay.head = head;
});
}

interface SerializedDesign {
schemaVersion: number;
label: LabelConfig;
Expand Down
6 changes: 4 additions & 2 deletions packages/core/src/lib/objectBounds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { getAllLeaves, isGroup } from "../types/Group";
import type { LeafObject } from "../registry";
import { BARCODE_1D_TYPES, STACKED_2D_TYPES, getEntry } from "../registry";
import type { LabelConfig } from "../types/LabelConfig";
import { effectiveDpmm, type JmDensity } from "../types/LabelConfig";
import { isAxisSwapped, objectRotation, type ZplRotation } from "../registry/rotation";
import { resolveTextMode } from "../registry/text";
import { blockBoundsDots, EMPTY_TEXT_PLACEHOLDER_GLYPHS, isBlankText, rotatedLineOffset, tbBoundsDots, zebraLineWidthDots } from "./zebraTextLayout";
Expand Down Expand Up @@ -344,14 +345,15 @@ export function printableRectDots(label: {
widthMm: number;
heightMm: number;
dpmm: number;
jmDensity?: JmDensity;
labelShift?: number;
}): BoundingBoxDots {
const shift = label.labelShift ?? 0;
return {
x: shift,
y: 0,
width: mmToDots(label.widthMm, label.dpmm),
height: mmToDots(label.heightMm, label.dpmm),
width: mmToDots(label.widthMm, effectiveDpmm(label)),
height: mmToDots(label.heightMm, effectiveDpmm(label)),
};
}

Expand Down
5 changes: 3 additions & 2 deletions packages/core/src/lib/resolveDefaultSize.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { mmToDots } from "./coordinates";
import type { LabelConfig } from "../types/LabelConfig";
import { effectiveDpmm } from "../types/LabelConfig";
import type { ObjectTypeCore } from "../types/ObjectType";
/** Resolve a registry `defaultSize` declaration to dot units against
* the active label config. Spec-fixed-physical-size symbols (e.g.
Expand All @@ -13,8 +14,8 @@ export function resolveDefaultSizeDots(
): { width: number; height: number } {
if ("widthMm" in defaultSize) {
return {
width: mmToDots(defaultSize.widthMm, label.dpmm),
height: mmToDots(defaultSize.heightMm, label.dpmm),
width: mmToDots(defaultSize.widthMm, effectiveDpmm(label)),
height: mmToDots(defaultSize.heightMm, effectiveDpmm(label)),
};
}
// Shallow-copy the dots-branch so callers can't accidentally
Expand Down
Loading
Loading