Skip to content

feat(font): PrintLab ZPL Bold + ZPL-correct text positioning#67

Merged
u8array merged 8 commits into
mainfrom
feat/printlab-zpl-font
May 16, 2026
Merged

feat(font): PrintLab ZPL Bold + ZPL-correct text positioning#67
u8array merged 8 commits into
mainfrom
feat/printlab-zpl-font

Conversation

@u8array
Copy link
Copy Markdown
Owner

@u8array u8array commented May 15, 2026

No description provided.

Add a PrintLab ZPL Bold font (Roboto Condensed derivative, Apache-2.0)
with per-glyph advance widths and vertical scale fitted to Zebra's
CG Triumvirate so editor layout matches the printed label. Family is
renamed to avoid collision with upstream Roboto. License and NOTICE
included in src/assets/fonts/.

Rebuild text positioning to bridge two anchor semantics: Konva anchors
at the EM-top-left and rotates the whole node around that point, while
ZPL anchors at the cap-top (^FO) or baseline (^FT) and keeps the cell
extending right-down regardless of rotation. The new shift table in
textPositionTransforms maps every rotation x positionType to the
screen offset needed so the rendered bbox lands where Labelary draws
it. I and B FO branches additionally consume the measured ink width
because Konva's rotation pivots at the anchor.

Add textBoxMatch (our font vs Labelary default) and
textVisualRegression (same font on both sides) suites with
Labelary-generated fixtures.
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a custom "PrintLab ZPL" font and a refined text positioning system to improve rendering accuracy compared to Zebra's native output. Key changes include a new "measureInkWidthPx" utility for precise text measurement and an updated "anchorDelta" transform that handles "^FO" and "^FT" anchors across all rotations. The PR also adds comprehensive visual and bounding-box regression tests. Feedback suggests consolidating duplicated font property calculations and replacing a hardcoded width heuristic in reversed text rendering with the new measurement utility.

Comment thread src/components/Canvas/KonvaObject.tsx Outdated
Comment thread src/components/Canvas/KonvaObject.tsx Outdated
u8array added 6 commits May 16, 2026 07:10
Pre-branch, obj.x/y stored the ZPL anchor (^FO cap-top or ^FT baseline)
and the renderer applied a rotation- and positionType-dependent shift
so the Konva text-bbox landed where Labelary draws. That shift hung off
every interaction path (drag, resize, snap, smart-align), and on rotated
text it interacted poorly with Konva's transformer math — corner-drags
produced 1e15-class scale values, smart-align translated instead of
resizing, and pinInactiveEdges restored the wrong edges.

Move the shift to the I/O boundary. obj.x/y now stores the Konva render
position (= EM top-left of the rendered text) — the same coordinate
system every other shape uses. zplGenerator's textFieldPos converts to
the ZPL anchor on emit, zplParser converts back on import. Editor-side
interactions stay shape-agnostic.

Concrete fixes that fall out:
- Wrapper Group around the rotated Text so the transformer-attached
  node is axis-aligned (Konva's rotated-node bbox math is the source of
  the rotated-text-resize drift).
- Rectangle resize on text + serial: corner-drag updates fontHeight via
  sy and fontWidth via sx independently, mirroring box/ellipse.
- Sanity-clamp on transformer scale so a Konva NaN/Infinity can't blow
  obj coordinates to 1e15.
- useFontCacheVersion bumps on document.fonts loadingdone so Konva's
  internal text-width cache is invalidated when @font-face finishes
  loading; key={fontVersion} on the Text re-mounts it so the cap-top
  position stops drifting between mounts.
- Gemini-PR feedback: shared text metrics (getTextRenderMetrics) used by
  both the renderer and the resize commit; reverse text background uses
  measureInkWidthPx instead of the 0.62 heuristic.

Test suite extended with 32 byte-exact round-trip tests covering
^FO/^FT x N/R/I/B x {h=20,30,50,87}: parse(zpl) -> generate must yield
the input ZPL. Plus a two-pass test that catches drift across multiple
import/export cycles.
The previous FO shift table only applied the small `pad - bias` ascender
correction for R/I/B rotations, which is correct for FT (baseline) but
wrong for FO. ZPL's ^FO is documented as the top-left of the character
field regardless of rotation — but Konva's rotation pivot lands at a
different corner of the rotated bbox per rotation:

  R: ^FO is top-right of the visible field, Konva pivot is bottom-left
     of the rotated bbox → need to shift by -h
  I: ^FO is bottom-right, need shifts by -w and -h
  B: ^FO is bottom-left of visible field, Konva pivot is bottom-right
     of rotated bbox → need to shift by -w

zplAnchorDelta now takes inkWidthDots and adds those h/w jumps for FO
R/I/B. zplHelpers.textFieldPos (emit) and zplParser (parse) both feed
the measured ink width through so the conversion sees the same value.

Side effects:
- text.tsx / serial.tsx commitTransform swaps sx/sy for R/B rotations
  so the user's vertical mouse drag still controls fontHeight regardless
  of which axis Konva considers "the height" post-rotation.
- measureTextDots gains a typeof-guard for environments where
  CanvasRenderingContext2D has no measureText (unit tests in jsdom).

Verified visually against Labelary for h=441 R, h=275 I, h=275 B —
editor and preview now land at identical dot positions. All 881 tests
green, including the rotation×positionType byte-exact round-trips.
Match the codebase's single-quote / single-line-args style (zplGenerator,
zplParser, lib helpers etc), and add explicit assertions for the two FO
rotations that previously only had round-trip coverage: FO/I subtracts
the measured ink width on X and FO/B subtracts it on Y, both on top of
the (h ± pad ∓ bias) jump.

Without these, a sign error on the inkWidth term in FO/I or FO/B would
slip through — the round-trip loop pairs the same expression on both
sides and would not catch a consistently-wrong direction.
…able types

The text/serial fix for "vertical mouse drag controls fontHeight even
when rotation R/B swaps the axes in screen space" applies the same way
to every other rotatable shape whose commit math distinguishes the two
scale axes: 1D barcodes (height vs. moduleWidth) and stacked 2D codes
(rowHeight vs. moduleWidth). The 2D codes that use
`commitUniformScaleTransform` are symmetric in sx/sy and stay untouched.

Extract the swap into `effectiveScale(rotation, ctx)` in
transformHelpers and route all four commit paths through it:
- text.tsx, serial.tsx (already had inline swaps — collapsed to shared)
- commitBarcodeWidthHeightTransform
- commitStacked2DTransform

Also de-duplicate the text-metrics derivation: zplParser was
recomputing fontFamily / fontScaleX / inkWidthDots inline with the same
formula `getTextRenderMetrics` uses. Both now route through a new
`computeTextRenderMetrics` primitive that takes raw text parameters, so
emit and parse measure the same way by construction.

Drive-by: drop the stale `objectToDisplay` / `displayToObject` mentions
in `textRenderMetrics.ts` jsdoc.
The clamp on `node.scaleX/scaleY()` to (0.01, 100) was defensive against
the Konva transformer producing 1e15-class scale values when applied to
a *rotated* node (its bbox math hits a near-zero divisor on certain
corner drags). With every rotatable type now wrapped in an unrotated
outer Group — text/serial via KonvaObject, barcodes already did so —
the transformer never sees a rotated node directly, so the pathological
condition the clamp guarded against doesn't arise.

Tests can't catch a Konva-internal NaN since it only happens during
live drag interaction; the empirical signal is that the resize-induced
1e15 coordinates in the bug reports stopped reproducing once the
wrapper Group landed.
@u8array
Copy link
Copy Markdown
Owner Author

u8array commented May 16, 2026

/gemini review

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces the 'PrintLab ZPL Bold' font and refactors the text and serial object coordinate systems to use the visual top-left for internal storage, applying ZPL anchor shifts only during import and export. Key changes include new text measurement utilities, rotation-aware scaling logic, and updated Konva rendering to prevent drift during resizing. Feedback identifies a potential issue in transformPosition.ts where text rotation offsets might still cause jumps if not explicitly handled, and a discrepancy in useKonvaTransformer.ts between the implementation and comments regarding visible-glyph snapping.

Comment thread src/components/Canvas/transformPosition.ts Outdated
Comment thread src/components/Canvas/hooks/useKonvaTransformer.ts Outdated
- useKonvaTransformer.ts: stale block describing a visible-glyph
  bbox-conversion that was prototyped, reverted, and accidentally left
  documented. Code went back to passing the EM bbox directly to the
  snap helpers, so the comment is gone now.
- transformPosition.ts: the "pass-through for text" line was accurate
  but didn't say *why* it's safe. Spelled out that obj.x/y is the
  wrapper Group's position and Konva pins the Group's axis-aligned
  clientRect, so the model stays in sync with the visible pinned
  corner without an inversion step here.
@u8array u8array merged commit c6a2a3d into main May 16, 2026
2 checks passed
@u8array u8array deleted the feat/printlab-zpl-font branch May 17, 2026 16:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant