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: 5 additions & 1 deletion src/components/Canvas/KonvaObject.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,11 @@ function KonvaObjectInner({
// stroke on the inset rect places the band exactly inside the
// declared bbox; the firmware's clamp-to-solid rule is handled by
// `renderFilled`.
const insetGeom = outlineInset(w, h, strokeWidth, p.filled);
// promoteFilled=true: see note in shapeRender.ts — ^GB rects extrude
// their solid fill to max(w,t) × max(h,t) per Zebra firmware. The
// ellipse / circle branches below leave this off because ^GE / ^GC
// collapse to solid at their declared bbox without promotion.
const insetGeom = outlineInset(w, h, strokeWidth, p.filled, true);
const renderFilled = insetGeom.renderFilled;
const insetCornerRadius = renderFilled
? cornerRadius
Expand Down
13 changes: 11 additions & 2 deletions src/lib/shapeGeometry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,22 @@ export function outlineInset(
h: number,
t: number,
filled: boolean,
/** When true, a solid-rendered field extends to `max(w, t) × max(h, t)`.
* This per-axis promotion is documented for ^GB rects only ("horizontal
* line" rule and its vertical mirror); ^GE / ^GC just collapse to solid
* at their declared bbox dimensions, so callers from those code paths
* leave this off. Pure single-axis lines hit a different parser branch
* and never reach this helper. */
promoteFilled = false,
): OutlineInset {
const clampsToFilled = !filled && t * 2 >= Math.min(w, h);
const renderFilled = filled || clampsToFilled;
const fillW = promoteFilled ? Math.max(w, t) : w;
const fillH = promoteFilled ? Math.max(h, t) : h;
return {
offset: renderFilled ? 0 : t / 2,
width: renderFilled ? w : Math.max(0, w - t),
height: renderFilled ? h : Math.max(0, h - t),
width: renderFilled ? fillW : Math.max(0, w - t),
height: renderFilled ? fillH : Math.max(0, h - t),
renderFilled,
};
}
Expand Down
19 changes: 8 additions & 11 deletions src/lib/shapeRender.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { LabelObject } from "../types/Group";
import { diagonalPolygonPoints } from "./shapeGeometry";
import { diagonalPolygonPoints, outlineInset } from "./shapeGeometry";

/** Inward-extruded ^GE / ^GC ring or solid disc, shared by ellipse and
* circle. Extracted so the two registry types — which carry different
Expand Down Expand Up @@ -74,18 +74,15 @@ export function renderShape(
// evenodd fill once we have a Labelary fixture with rounding>0
// to validate against; the current fixtures all use rounding=0
// so the four-band approach below is exact.
if (p.filled) {
ctx.fillStyle = color;
ctx.fillRect(obj.x, obj.y, p.width, p.height);
return;
}
const t = Math.max(1, p.thickness);
// Outline that extrudes inward — clamps to filled rect when the
// outline would meet itself in the middle (Zebra firmware does the
// same: ^GB with thickness >= min(w, h)/2 renders solid).
if (t * 2 >= Math.min(p.width, p.height)) {
// promoteFilled=true: ^GB rects extrude solid fields to
// max(w,t) × max(h,t) (Zebra "horizontal line" rule). Without it,
// a 101×92 rect declared with thickness 101 would be drawn 9 dots
// short along its bottom edge compared to Labelary.
const geom = outlineInset(p.width, p.height, t, p.filled, true);
if (geom.renderFilled) {
ctx.fillStyle = color;
ctx.fillRect(obj.x, obj.y, p.width, p.height);
ctx.fillRect(obj.x, obj.y, geom.width, geom.height);
return;
}
// Four filled bands (top, bottom, left, right) avoid the
Expand Down
6 changes: 5 additions & 1 deletion src/lib/zplParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1012,7 +1012,11 @@ export function parseZPL(zpl: string, dpmm = 8): ParsedZPL {
{
width: w,
height: h,
thickness: filled ? 3 : t,
// Preserve the original thickness so a ZPL round-trip is
// lossless and the renderer can apply Zebra's dimension
// promotion (`max(w,t) × max(h,t)`) for fields where
// thickness exceeds the smaller axis.
thickness: t,
filled,
color,
rounding,
Expand Down
10 changes: 9 additions & 1 deletion src/registry/box.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,15 @@ export const box: ObjectTypeDefinition<BoxProps> = {

toZPL: (obj) => {
const p = obj.props;
const t = p.filled ? Math.min(p.width, p.height) : p.thickness;
// Emit `thickness` verbatim so a ZPL round-trip is lossless. Only
// floor it up to `min(w,h)` when the user toggled `filled` but the
// stored thickness is below the firmware's solid threshold; this
// keeps a user-driven "make this solid" intent in the printed
// output even if they never bumped the thickness slider.
const solidThreshold = Math.min(p.width, p.height);
const t = p.filled
? Math.max(p.thickness, solidThreshold)
: p.thickness;
return [
p.reverse ? '^LRY' : '',
fieldPos(obj),
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
57 changes: 57 additions & 0 deletions tests/fixtures/shapeTestCases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,4 +288,61 @@ export const shapeTestCases: ShapeTestCase[] = [
zpl_input: "^XA^FO100,200^GD200,346,6,B,L^FS^XZ",
image_ref: "shape_line_diag_steep.png",
},

// Dimension-promotion cases for ^GB rects where thickness exceeds an
// axis. Zebra firmware extrudes solid fields out to `max(w, t)` /
// `max(h, t)`; renderShape used to draw the literal `w × h` and miss
// the strip along the affected edge. Each case picks a different
// axis so a regression touching only one branch is caught.
{
id: "shape_box_thickness_exceeds_height",
obj: {
id: "20",
type: "box",
// ^GB101,92,101: thickness > height → rect grows downward by
// (t - h) = 9 dots. Reproduces the user-reported case where the
// editor's box bottom was 9 dots above Labelary's. Reverse=true
// mirrors the original ZPL; ^LRY only inverts ink colour and
// does not affect geometry.
x: 144,
y: 160,
rotation: 0,
props: { width: 101, height: 92, thickness: 101, filled: false, color: "B", rounding: 0, reverse: true },
},
zpl_input: "^XA^LRY^FO144,160^GB101,92,101,B,0^FS^LRN^XZ",
image_ref: "shape_box_thickness_exceeds_height.png",
},
{
id: "shape_box_thickness_exceeds_width",
obj: {
id: "21",
type: "box",
// ^GB80,150,120: thickness > width → rect grows rightward by
// (t - w) = 40 dots. Symmetric of the above; catches a fix that
// only handles the height axis.
x: 100,
y: 100,
rotation: 0,
props: { width: 80, height: 150, thickness: 120, filled: false, color: "B", rounding: 0 },
},
zpl_input: "^XA^FO100,100^GB80,150,120,B,0^FS^XZ",
image_ref: "shape_box_thickness_exceeds_width.png",
},
{
id: "shape_box_thickness_exceeds_both",
obj: {
id: "22",
type: "box",
// ^GB60,40,90: thickness exceeds both axes → 90×90 square.
// Square is the firmware's documented "create a square" form
// (w, h, t all equal); the promotion path must collapse to that
// shape when t pulls both axes up to the same value.
x: 100,
y: 100,
rotation: 0,
props: { width: 60, height: 40, thickness: 90, filled: false, color: "B", rounding: 0 },
},
zpl_input: "^XA^FO100,100^GB60,40,90,B,0^FS^XZ",
image_ref: "shape_box_thickness_exceeds_both.png",
},
];