Skip to content

Feat/shape pixel tests#55

Merged
u8array merged 13 commits into
mainfrom
feat/shape-pixel-tests
May 11, 2026
Merged

Feat/shape pixel tests#55
u8array merged 13 commits into
mainfrom
feat/shape-pixel-tests

Conversation

@u8array
Copy link
Copy Markdown
Owner

@u8array u8array commented May 11, 2026

No description provided.

u8array added 11 commits May 11, 2026 17:11
…abelary

Mirrors the bwip-js barcode regression infrastructure for the geometric
primitives. A pure 2D-canvas renderer in src/lib/shapeRender.ts produces
Option-A ZPL-aligned geometry (outline thickness extrudes inward for
^GB/^GE/^GC, downward/rightward for axis-aligned ^GB lines) and the test
diffs it pixel-for-pixel against Labelary references.

Coverage: 15 active fixtures (boxes outline/filled/thickness sweep,
horizontal/vertical lines with reverse-direction cases, ellipse, circle),
plus 4 fetched-but-skipped diagonal fixtures awaiting the GD renderer.

The Konva canvas in production still uses centred-stroke geometry and
therefore mismatches the renderer / Labelary; the follow-up commit
aligns KonvaObject/LineObject to renderShape.
Parallelogram with horizontal short sides (top edge flat, sides pointy),
thickness extruded downward from the diagonal centreline. Matches Zebra
firmware exactly to within rasterisation tolerance; the four previously
skipped diagonal fixtures (slash/backslash 45 deg, shallow 30 deg, steep
60 deg) now run as active regression cases.

Bumps the per-test diff budget to 1500 px and the pixelmatch
per-pixel threshold to 0.3 to absorb the AA halo @napi-rs/canvas
produces along diagonal edges (Labelary renders 1-bit binary, so the
halo is unavoidable on our side without a custom rasteriser). Axis-
aligned cases still finish at sub-50 px diff so the budget is not
meaningfully eroded for them.
The Konva canvas previously rendered shape outlines with a centred
stroke that straddled the declared bounding box. ZPL extrudes thickness
inward from the box for ^GB/^GE/^GC and downward/rightward from (x, y)
for axis-aligned ^GB lines, so the designer view drifted from what
Labelary and the printer produce.

Box / ellipse / circle now inset the body by t/2 (centred stroke fills
the outer band exactly), with the firmware's 'clamp to solid when
2t >= min(w, h)' rule applied so very-thick outlines collapse cleanly.
Axis-aligned lines shift the visible body by t/2 perpendicular along
the ZPL extrusion axis; handles stay at the conceptual band corner.
The box selection overlay is decoupled from the body thickness (now
constant 1.5 px) so a thick outline no longer gets a thick selection
halo.

Diagonal lines still fall back to the centred-stroke rendering and
remain visibly off vs. ^GD's parallelogram geometry; a follow-up
commit replaces them with a closed Konva.Line polygon.
The previous parallelogram straddled the bbox diagonal, putting half
the thickness above and half below the centreline. Pixel inspection of
Labelary references shows Zebra actually places the conceptual line
along the *left long edge* of the band and extrudes thickness in +x
only — both endpoints sit on the same side, the band overhangs the
declared bbox on the right by t.

Updates renderShape.ts and the matching Konva polygon in LineObject.tsx,
so the canvas preview reflects what gets printed. Tests now pass at
strict tolerance (200 px / threshold 0.1) including all four diagonal
fixtures — previously these only passed at the AA-absorbing 1500 / 0.3
budget because the geometry was visibly off at the corners.
The Konva render switched between the axis-aligned band shape and the
diagonal parallelogram based on p.angle, which only commits on dragEnd.
Dragging a near-horizontal endpoint slightly off-axis therefore showed
the body locked to the horizontal band until release, then snapped to
the parallelogram in one frame.

Reading isHorizontal / isVertical off the live dispX/dispY pair (with a
0.5 px epsilon to absorb constrainLine's auto-snap residue) makes the
preview track the geometry that will actually be committed, so the
release no longer changes shape.
The diagonal-polygon and outline-inset math previously lived in two
places: the @napi-rs pixel-regression renderer (lib/shapeRender.ts) and
the Konva canvas components (LineObject.tsx, KonvaObject.tsx). Both
described the same ZPL semantics but were maintained independently, so
a future change to the firmware-clamp threshold or the ^GD parallelogram
shape risked silent drift between the two render paths.

New module lib/shapeGeometry.ts owns the pure helpers (outlineInset for
^GB/^GE/^GC bounding-box adjustments, diagonalPolygonPoints for ^GD
parallelogram vertices). Both renderers import from there, so the
pixel-regression tests transitively validate the Konva canvas too.

No behaviour change; all 680 tests still green.
Direct unit tests for the pure helpers — the pixel-regression suite
covers the rendered output but logic errors in the helpers would only
surface as cross-test diffs there. Cheap to add, faster signal.
Adds a draggable square on the far long edge of the band (bottom edge
for horizontal lines, right edge for vertical / diagonal) that resizes
thickness in real time. The handle follows the band edge as the user
drags, clamped to a 1-dot minimum.

Implementation lives entirely in LineObject; thickness during the drag
is tracked in component state and committed on dragEnd, so the live
preview matches what gets stored — no release-time snap. The flip-on-
overshoot affordance the user described earlier (rotate 180 deg to put
thickness on the other side, skipping zero) is intentionally deferred
to a follow-up.
…rence

The 15-dot rotation offset compensated for the previous off-by-t/2
centred-stroke line geometry rather than a real Konva-vs-ZPL pivot
mismatch — once the lines render at the correct ZPL position, rotated
text sits where it should without any extra shift.

Removes the offset constant, the rotationOffsetDelta helper, and the
related dead branches in objectToDisplay / displayToObject. The
remaining transform is the ^FT baseline correction; ^FO becomes the
identity. Tests updated accordingly.
Three smells from a clean-code pass over the branch:

1. LineObject had a thicknessDragRef whose value was written on
   dragStart and cleared on dragEnd but never read — dragMove computed
   everything from the live cursor position via the JSX closure. Dead
   weight, removed.

2. The thickness handle was rendered through an IIFE inside the JSX
   body just to scope four geometry locals. Hoisted those into the
   component body and flattened the JSX.

3. diagonalPolygonPoints returned a generic number[], which forced
   shapeRender to either non-null-assert each index or cast to an 8-
   tuple. Typed the return as ParallelogramPoints so destructuring is
   ergonomic without the cast.

No behaviour change; all 689 tests still green.
EllipseProps and CircleProps carry differently-named size keys
(width/height vs diameter) — sharing one switch arm forced a
narrowing ternary on the union, which strictTS in CI rejected.
Extract the actual drawing into drawEllipticalOutline so each case
arm pushes the type-specific keys through a single normalised
signature.
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 implements accurate ZPL shape rendering for boxes, ellipses, circles, and lines by mirroring Zebra firmware semantics, including inward outline extrusion and parallelogram geometry for diagonal lines. It introduces a shared geometry library, a 2D canvas renderer, and a visual regression suite using Labelary references. Review feedback identifies a bug where selection highlights disappear on filled ellipses, suggests optimizing diagonal line point calculations, and notes that the new box renderer currently ignores the rounding property.

Comment thread src/components/Canvas/KonvaObject.tsx
Comment thread src/components/Canvas/LineObject.tsx Outdated
Comment thread src/lib/shapeRender.ts
1. (HIGH) Ellipse/circle selection visual disappeared whenever
   renderFilled forced strokeWidth to 0 — i.e. filled shapes and very
   thick outlines that hit the firmware clamp. Reordered the ternary
   so isSelected always wins, giving the selection a 1.5 px halo
   regardless of fill state.

2. (MEDIUM) diagonalPolygonPoints was called twice per render for
   selected diagonals (body + outline). Hoisted the call to the
   component body so both <KLine>s share the same vertex array.

3. (MEDIUM) renderShape ignores ^GB rounding. No regression-fixture
   exercises rounding>0 today, so a speculative implementation would
   not be validated. Added a TODO that points at the existing
   KonvaObject rounding formula and outlines the evenodd-roundRect
   approach for whenever the fixture is added.
@u8array
Copy link
Copy Markdown
Owner Author

u8array commented May 11, 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 standardized geometric rendering for ZPL shape primitives (box, line, ellipse, circle) to ensure consistency between the Konva canvas, ZPL output, and a new pixel-regression test suite. Key changes include the addition of a shapeGeometry library for calculating inward extrusion and parallelogram vertices, a shapeRender utility for 2D canvas rendering, and a visual regression suite that compares local renders against Labelary reference images. Additionally, text position transforms were simplified by removing empirical rotation offsets, and interactive thickness handles were added to the line component. Feedback was provided regarding the error handling in the shape renderer, suggesting a more graceful failure mode for unsupported object types to improve future UI integration.

Comment thread src/lib/shapeRender.ts
Reviewer suggested graceful failure for the unsupported-type case,
based on the Phase-2 TODO in the file header that anticipated Konva
consuming renderShape directly. That refactor took a different shape:
both renderShape and the Konva components share lib/shapeGeometry,
and renderShape itself stayed test-only. With that scope, a loud
throw is the right behaviour — any non-shape object reaching this
function is a test-author bug, not a runtime condition.

Updated the header comment to reflect the actual architecture and
expanded the default-case comment to spell out why the throw stays.
@u8array u8array merged commit 9a86256 into main May 11, 2026
1 check passed
@u8array u8array deleted the feat/shape-pixel-tests branch May 11, 2026 21:14
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