diff --git a/src/components/Canvas/KonvaObject.tsx b/src/components/Canvas/KonvaObject.tsx
index 0468e9c0..19eab610 100644
--- a/src/components/Canvas/KonvaObject.tsx
+++ b/src/components/Canvas/KonvaObject.tsx
@@ -6,6 +6,7 @@ import { ImageObject } from "./ImageObject";
import type Konva from "konva";
import { dotsToPx, pxToDots } from "../../lib/coordinates";
import { outlineInset } from "../../lib/shapeGeometry";
+import { reverseShapeStyle } from "./reverseShapeStyle";
import { useColorScheme } from "../../lib/useColorScheme";
import { useLabelStore } from "../../store/labelStore";
import { ZPL_FONT_HEIGHT_TO_CSS_RATIO } from "./textPositionTransforms";
@@ -312,42 +313,15 @@ function KonvaObjectInner({
? cornerRadius
: Math.max(0, cornerRadius - strokeWidth / 2);
- // Inverted (^LRY) regions print as a knockout. The difference-blend
- // body renders print-correctly: on the white label it produces black
- // (white-on-white inverted = black ink in print), and over darker
- // shapes it inverts those pixels — matching what Zebra firmware
- // actually prints. The body keeps that mode even while selected so
- // the inversion visualisation doesn't disappear and hide whatever
- // is layered behind. The selection outline is rendered as a separate
- // overlay rect with normal blending.
- //
- // Special-cases:
- // - reverse + filled drops the body stroke. Konva renders fill then
- // stroke; with the difference blend the fill flips the destination
- // to black and the (white) stroke then flips back to white inside
- // the rect, producing a b/w/b banding artefact. The stroke and
- // fill carry the same colour anyway so dropping it is visually
- // identical without the artefact.
- // - colour W filled (non-reverse) uses the light-grey shape colour
- // for the fill too, otherwise white-on-white would make filled
- // and outlined indistinguishable on canvas.
- const isReverse = !!p.reverse;
- const shapeColor = p.color === "B" ? "#000000" : "#cccccc";
- // `renderFilled` includes the firmware clamp-to-solid case, so a
- // very-thick outline picks the filled fill/stroke pair instead of
- // collapsing into a degenerate inset rect.
- const stroke = isReverse
- ? renderFilled
- ? "transparent"
- : "#ffffff"
- : shapeColor;
- const fill = isReverse
- ? renderFilled
- ? "#ffffff"
- : "transparent"
- : renderFilled
- ? shapeColor
- : "transparent";
+ // Inverted (^LRY) paint is delegated to reverseShapeStyle so box
+ // and ellipse share the same colour rules — the helper covers the
+ // stroke/fill swap for filled outlines (banding workaround) and
+ // the difference-blend that produces the print-correct knockout.
+ const { stroke, fill, globalCompositeOperation } = reverseShapeStyle(
+ p.reverse,
+ p.color,
+ renderFilled,
+ );
// Wrap body + selection overlay in a draggable Group so both move
// together during a drag — without this the selection-stroke rect
// stays at the start position while the body translates, leaving a
@@ -379,7 +353,7 @@ function KonvaObjectInner({
strokeScaleEnabled={false}
fill={fill}
cornerRadius={insetCornerRadius}
- globalCompositeOperation={isReverse ? "difference" : "source-over"}
+ globalCompositeOperation={globalCompositeOperation}
/>
{isSelected && (
{isSelected && (
diff --git a/src/components/Canvas/reverseShapeStyle.ts b/src/components/Canvas/reverseShapeStyle.ts
new file mode 100644
index 00000000..2cb8cb6f
--- /dev/null
+++ b/src/components/Canvas/reverseShapeStyle.ts
@@ -0,0 +1,47 @@
+/**
+ * Per-shape paint values for the field-level `^LR` (reverse) state.
+ *
+ * Box and ellipse share the same paint rules — black/white knockout via
+ * a `difference` blend, with a fill/stroke swap so a filled outline
+ * doesn't band when the stroke flips to white and the fill flips back
+ * to black. The two render paths agreed in the editor by coincidence;
+ * pulling the colour table out of both call sites makes that agreement
+ * explicit and stops the two from drifting apart on the next colour
+ * tweak.
+ *
+ * Line has its own simpler variant (stroke-only, no fill) and stays
+ * separate.
+ */
+
+export interface ReverseShapeStyle {
+ stroke: string;
+ fill: string;
+ /** Konva's `globalCompositeOperation`. `difference` produces the
+ * print-correct knockout on the white label and inverts darker
+ * shapes underneath; `source-over` is the default upright paint. */
+ globalCompositeOperation: "difference" | "source-over";
+}
+
+export function reverseShapeStyle(
+ reverse: boolean | undefined,
+ color: "B" | "W",
+ renderFilled: boolean,
+): ReverseShapeStyle {
+ const isReverse = !!reverse;
+ const shapeColor = color === "B" ? "#000000" : "#cccccc";
+ return {
+ stroke: isReverse
+ ? renderFilled
+ ? "transparent"
+ : "#ffffff"
+ : shapeColor,
+ fill: isReverse
+ ? renderFilled
+ ? "#ffffff"
+ : "transparent"
+ : renderFilled
+ ? shapeColor
+ : "transparent",
+ globalCompositeOperation: isReverse ? "difference" : "source-over",
+ };
+}
diff --git a/src/components/Palette/ObjectPalette.tsx b/src/components/Palette/ObjectPalette.tsx
index 2e8ae2a7..d77b8285 100644
--- a/src/components/Palette/ObjectPalette.tsx
+++ b/src/components/Palette/ObjectPalette.tsx
@@ -1,7 +1,6 @@
import { useDraggable } from '@dnd-kit/core';
import { ObjectRegistry } from '../../registry';
import { PALETTE_GROUPS } from './paletteGroups';
-import { VIRTUAL_PALETTE_ENTRIES, type VirtualPaletteEntry } from './virtualEntries';
import type { ObjectGroup } from '../../types/ObjectType';
import { useT } from '../../lib/useT';
import { useLabelStore } from '../../store/labelStore';
@@ -76,7 +75,7 @@ function resolveEntries(
group: ObjectGroup,
types: Record,
): ResolvedEntry[] {
- const registry = Object.entries(ObjectRegistry)
+ return Object.entries(ObjectRegistry)
.filter(([, def]) => def.group === group)
.map(([type, def]): ResolvedEntry => ({
id: type,
@@ -85,17 +84,6 @@ function resolveEntries(
label: types[type] ?? def.label,
defaultSize: def.defaultSize,
}));
- const virtual = VIRTUAL_PALETTE_ENTRIES
- .filter((v) => v.group === group)
- .map((v: VirtualPaletteEntry): ResolvedEntry => ({
- id: v.id,
- type: v.type,
- icon: v.icon,
- label: types[v.labelKey] ?? v.fallbackLabel,
- defaultSize: v.defaultSize,
- propsOverride: v.propsOverride,
- }));
- return [...registry, ...virtual];
}
export function ObjectPalette() {
diff --git a/src/components/Palette/virtualEntries.ts b/src/components/Palette/virtualEntries.ts
deleted file mode 100644
index eb3dbcee..00000000
--- a/src/components/Palette/virtualEntries.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import type { ObjectGroup } from '../../types/ObjectType';
-import type { EllipseProps } from '../../registry/ellipse';
-
-/**
- * Palette-only sugar entries: surface alternative starting configs for a
- * registry type without inflating the type union. The "Circle" entry
- * instantiates an `ellipse` with `lockAspect: true` so the transformer
- * keeps it square; round-trips through ^GC on export and ^GC on import
- * preserve the flag, so the file format stays canonical.
- */
-export interface VirtualPaletteEntry {
- /** Unique key inside the palette ("circle"). Does NOT collide with
- * registry types — those use their own key directly. */
- id: string;
- /** Registry type to instantiate. */
- type: string;
- group: ObjectGroup;
- icon: string;
- /** Key into `t.types` for the visible label. */
- labelKey: string;
- /** Display label fallback when the locale is missing the key. */
- fallbackLabel: string;
- /** Default size used by the drop-on-canvas position centring math. */
- defaultSize: { width: number; height: number };
- /** Merged on top of the registry type's `defaultProps` at creation. */
- propsOverride: object;
-}
-
-export const VIRTUAL_PALETTE_ENTRIES: VirtualPaletteEntry[] = [
- {
- id: 'circle',
- type: 'ellipse',
- group: 'shape',
- icon: '●',
- labelKey: 'circle',
- fallbackLabel: 'Circle',
- defaultSize: { width: 100, height: 100 },
- propsOverride: {
- width: 100,
- height: 100,
- lockAspect: true,
- } satisfies Partial,
- },
-];
diff --git a/src/lib/zplParser.ts b/src/lib/zplParser.ts
index 7872806c..da815e84 100644
--- a/src/lib/zplParser.ts
+++ b/src/lib/zplParser.ts
@@ -1220,6 +1220,7 @@ export function parseZPL(zpl: string, dpmm = 8): ParsedZPL {
thickness: t,
filled,
color,
+ reverse: getReverseFlag(),
} satisfies EllipseProps,
undefined,
takeComment(),
@@ -1244,6 +1245,7 @@ export function parseZPL(zpl: string, dpmm = 8): ParsedZPL {
filled,
color,
lockAspect: true,
+ reverse: getReverseFlag(),
} satisfies EllipseProps,
undefined,
takeComment(),
diff --git a/src/locales/ar.ts b/src/locales/ar.ts
index 0d29468d..12fd90b3 100644
--- a/src/locales/ar.ts
+++ b/src/locales/ar.ts
@@ -19,7 +19,6 @@ const ar = {
datamatrix: 'DataMatrix',
box: 'مستطيل',
ellipse: 'قطع ناقص',
- circle: 'دائرة',
line: 'خط',
serial: 'رقم تسلسلي',
image: 'صورة',
@@ -263,9 +262,11 @@ const ar = {
height: 'الارتفاع (نقطة)',
thickness: 'الحدود (نقطة)',
filled: 'ممتلئ',
+ lockAspect: 'دائرة (تثبيت نسبة العرض إلى الارتفاع)',
color: 'اللون',
colorB: 'B — أسود',
colorW: 'W — أبيض',
+ reverse: 'عكس',
},
circle: {
diameter: 'القطر (نقاط)',
diff --git a/src/locales/bg.ts b/src/locales/bg.ts
index 3d92f752..77c02658 100644
--- a/src/locales/bg.ts
+++ b/src/locales/bg.ts
@@ -19,7 +19,6 @@ const bg = {
datamatrix: 'DataMatrix',
box: 'Правоъгълник',
ellipse: 'Елипса',
- circle: 'Кръг',
line: 'Линия',
serial: 'Сериен №',
image: 'Изображение',
@@ -263,9 +262,11 @@ const bg = {
height: 'Височина (точки)',
thickness: 'Рамка (точки)',
filled: 'Запълнен',
+ lockAspect: 'Кръг (заключи съотношението)',
color: 'Цвят',
colorB: 'B — Черен',
colorW: 'W — Бял',
+ reverse: 'Обърни',
},
circle: {
diameter: 'Диаметър (точки)',
diff --git a/src/locales/cs.ts b/src/locales/cs.ts
index d85150be..87fe8a1e 100644
--- a/src/locales/cs.ts
+++ b/src/locales/cs.ts
@@ -19,7 +19,6 @@ const cs = {
datamatrix: 'DataMatrix',
box: 'Obdélník',
ellipse: 'Elipsa',
- circle: 'Kruh',
line: 'Čára',
serial: 'Sériové číslo',
image: 'Obrázek',
@@ -263,9 +262,11 @@ const cs = {
height: 'Výška (body)',
thickness: 'Rámeček (body)',
filled: 'Vyplněný',
+ lockAspect: 'Kruh (uzamknout poměr stran)',
color: 'Barva',
colorB: 'B — Černá',
colorW: 'W — Bílá',
+ reverse: 'Invertovat',
},
circle: {
diameter: 'Průměr (body)',
diff --git a/src/locales/da.ts b/src/locales/da.ts
index 070c6ae0..d6ba9f5b 100644
--- a/src/locales/da.ts
+++ b/src/locales/da.ts
@@ -19,7 +19,6 @@ const da = {
datamatrix: 'DataMatrix',
box: 'Rektangel',
ellipse: 'Ellipse',
- circle: 'Cirkel',
line: 'Linje',
serial: 'Serienr.',
image: 'Billede',
@@ -263,9 +262,11 @@ const da = {
height: 'Højde (punkter)',
thickness: 'Kant (punkter)',
filled: 'Udfyldt',
+ lockAspect: 'Cirkel (lås formatforhold)',
color: 'Farve',
colorB: 'B — Sort',
colorW: 'W — Hvid',
+ reverse: 'Invertere',
},
circle: {
diameter: 'Diameter (punkter)',
diff --git a/src/locales/de.ts b/src/locales/de.ts
index 4f93f7ea..b58853bf 100644
--- a/src/locales/de.ts
+++ b/src/locales/de.ts
@@ -19,7 +19,6 @@ const de = {
datamatrix: 'DataMatrix',
box: 'Box',
ellipse: 'Ellipse',
- circle: 'Kreis',
line: 'Linie',
serial: 'Seriennummer',
image: 'Bild',
@@ -284,9 +283,11 @@ const de = {
height: 'Höhe (Punkte)',
thickness: 'Rahmen (Punkte)',
filled: 'Gefüllt',
+ lockAspect: 'Kreis (Seitenverhältnis sperren)',
color: 'Farbe',
colorB: 'B — Schwarz',
colorW: 'W — Weiß',
+ reverse: 'Invertieren',
},
circle: {
diameter: 'Durchmesser (Punkte)',
diff --git a/src/locales/el.ts b/src/locales/el.ts
index 7c1af2a3..89c93ea7 100644
--- a/src/locales/el.ts
+++ b/src/locales/el.ts
@@ -19,7 +19,6 @@ const el = {
datamatrix: 'DataMatrix',
box: 'Ορθογώνιο',
ellipse: 'Έλλειψη',
- circle: 'Κύκλος',
line: 'Γραμμή',
serial: 'Σειριακός αρ.',
image: 'Εικόνα',
@@ -263,9 +262,11 @@ const el = {
height: 'Ύψος (κουκκίδες)',
thickness: 'Πλαίσιο (κουκκίδες)',
filled: 'Γεμισμένο',
+ lockAspect: 'Κύκλος (κλείδωμα αναλογίας διαστάσεων)',
color: 'Χρώμα',
colorB: 'B — Μαύρο',
colorW: 'W — Λευκό',
+ reverse: 'Αντιστροφή',
},
circle: {
diameter: 'Διάμετρος (κουκκίδες)',
diff --git a/src/locales/en.ts b/src/locales/en.ts
index c801945d..d760ea61 100644
--- a/src/locales/en.ts
+++ b/src/locales/en.ts
@@ -19,7 +19,6 @@ const en = {
datamatrix: 'DataMatrix',
box: 'Box',
ellipse: 'Ellipse',
- circle: 'Circle',
line: 'Line',
serial: 'Serial',
image: 'Image',
@@ -284,9 +283,11 @@ const en = {
height: 'Height (dots)',
thickness: 'Border (dots)',
filled: 'Filled',
+ lockAspect: 'Circle (lock aspect ratio)',
color: 'Color',
colorB: 'B — Black',
colorW: 'W — White',
+ reverse: 'Invert',
},
circle: {
diameter: 'Diameter (dots)',
diff --git a/src/locales/es.ts b/src/locales/es.ts
index eefc0492..0a76c2cd 100644
--- a/src/locales/es.ts
+++ b/src/locales/es.ts
@@ -19,7 +19,6 @@ const es = {
datamatrix: 'DataMatrix',
box: 'Rectángulo',
ellipse: 'Elipse',
- circle: 'Círculo',
line: 'Línea',
serial: 'Serie',
image: 'Imagen',
@@ -263,9 +262,11 @@ const es = {
height: 'Alto (puntos)',
thickness: 'Borde (puntos)',
filled: 'Relleno',
+ lockAspect: 'Círculo (bloquear relación de aspecto)',
color: 'Color',
colorB: 'B — Negro',
colorW: 'W — Blanco',
+ reverse: 'Invertir',
},
circle: {
diameter: 'Diámetro (puntos)',
diff --git a/src/locales/et.ts b/src/locales/et.ts
index 9abebbb4..eb8cd0c4 100644
--- a/src/locales/et.ts
+++ b/src/locales/et.ts
@@ -19,7 +19,6 @@ const et = {
datamatrix: 'DataMatrix',
box: 'Ristkülik',
ellipse: 'Ellips',
- circle: 'Ring',
line: 'Joon',
serial: 'Seerianr',
image: 'Pilt',
@@ -263,9 +262,11 @@ const et = {
height: 'Kõrgus (punkti)',
thickness: 'Ääris (punkti)',
filled: 'Täidetud',
+ lockAspect: 'Ring (lukusta kuvasuhe)',
color: 'Värvus',
colorB: 'B — Must',
colorW: 'W — Valge',
+ reverse: 'Pööra',
},
circle: {
diameter: 'Läbimõõt (punktid)',
diff --git a/src/locales/fa.ts b/src/locales/fa.ts
index ffd5b2fa..1a027f70 100644
--- a/src/locales/fa.ts
+++ b/src/locales/fa.ts
@@ -19,7 +19,6 @@ const fa = {
datamatrix: 'DataMatrix',
box: 'مستطیل',
ellipse: 'بیضی',
- circle: 'دایره',
line: 'خط',
serial: 'شماره سریال',
image: 'تصویر',
@@ -263,9 +262,11 @@ const fa = {
height: 'ارتفاع (نقطه)',
thickness: 'حاشیه (نقطه)',
filled: 'پر شده',
+ lockAspect: 'دایره (قفل کردن نسبت ابعاد)',
color: 'رنگ',
colorB: 'B — مشکی',
colorW: 'W — سفید',
+ reverse: 'معکوس کردن',
},
circle: {
diameter: 'قطر (نقطه)',
diff --git a/src/locales/fi.ts b/src/locales/fi.ts
index 435ef73d..9e788a07 100644
--- a/src/locales/fi.ts
+++ b/src/locales/fi.ts
@@ -19,7 +19,6 @@ const fi = {
datamatrix: 'DataMatrix',
box: 'Suorakulmio',
ellipse: 'Ellipsi',
- circle: 'Ympyrä',
line: 'Viiva',
serial: 'Sarjanro',
image: 'Kuva',
@@ -263,9 +262,11 @@ const fi = {
height: 'Korkeus (pistettä)',
thickness: 'Reunus (pistettä)',
filled: 'Täytetty',
+ lockAspect: 'Ympyrä (lukitse kuvasuhde)',
color: 'Väri',
colorB: 'B — Musta',
colorW: 'W — Valkoinen',
+ reverse: 'Käännä',
},
circle: {
diameter: 'Halkaisija (pisteet)',
diff --git a/src/locales/fr.ts b/src/locales/fr.ts
index 52f1736b..936e6d7f 100644
--- a/src/locales/fr.ts
+++ b/src/locales/fr.ts
@@ -19,7 +19,6 @@ const fr = {
datamatrix: 'DataMatrix',
box: 'Rectangle',
ellipse: 'Ellipse',
- circle: 'Cercle',
line: 'Ligne',
serial: 'Série',
image: 'Image',
@@ -263,9 +262,11 @@ const fr = {
height: 'Hauteur (points)',
thickness: 'Bordure (points)',
filled: 'Rempli',
+ lockAspect: 'Cercle (verrouiller le rapport d\'aspect)',
color: 'Couleur',
colorB: 'B — Noir',
colorW: 'W — Blanc',
+ reverse: 'Inverser',
},
circle: {
diameter: 'Diamètre (points)',
diff --git a/src/locales/he.ts b/src/locales/he.ts
index 5f2388e6..f9e9d06b 100644
--- a/src/locales/he.ts
+++ b/src/locales/he.ts
@@ -19,7 +19,6 @@ const he = {
datamatrix: 'DataMatrix',
box: 'מלבן',
ellipse: 'אליפסה',
- circle: 'עיגול',
line: 'קו',
serial: 'מס. סידורי',
image: 'תמונה',
@@ -263,9 +262,11 @@ const he = {
height: 'גובה (נקודות)',
thickness: 'גבול (נקודות)',
filled: 'מלא',
+ lockAspect: 'עיגול (נעל יחס גובה-רוחב)',
color: 'צבע',
colorB: 'B — שחור',
colorW: 'W — לבן',
+ reverse: 'הפוך',
},
circle: {
diameter: 'קוטר (נקודות)',
diff --git a/src/locales/hr.ts b/src/locales/hr.ts
index 71db443a..99d602c6 100644
--- a/src/locales/hr.ts
+++ b/src/locales/hr.ts
@@ -19,7 +19,6 @@ const hr = {
datamatrix: 'DataMatrix',
box: 'Pravokutnik',
ellipse: 'Elipsa',
- circle: 'Krug',
line: 'Linija',
serial: 'Serijski br.',
image: 'Slika',
@@ -263,9 +262,11 @@ const hr = {
height: 'Visina (točke)',
thickness: 'Okvir (točke)',
filled: 'Ispunjen',
+ lockAspect: 'Krug (zaključaj omjer stranica)',
color: 'Boja',
colorB: 'B — Crna',
colorW: 'W — Bijela',
+ reverse: 'Invertiraj',
},
circle: {
diameter: 'Promjer (točke)',
diff --git a/src/locales/hu.ts b/src/locales/hu.ts
index 857de47f..273ef35e 100644
--- a/src/locales/hu.ts
+++ b/src/locales/hu.ts
@@ -19,7 +19,6 @@ const hu = {
datamatrix: 'DataMatrix',
box: 'Téglalap',
ellipse: 'Ellipszis',
- circle: 'Kör',
line: 'Vonal',
serial: 'Sorszám',
image: 'Kép',
@@ -263,9 +262,11 @@ const hu = {
height: 'Magasság (pont)',
thickness: 'Keret (pont)',
filled: 'Kitöltött',
+ lockAspect: 'Kör (oldalarány rögzítése)',
color: 'Szín',
colorB: 'B — Fekete',
colorW: 'W — Fehér',
+ reverse: 'Invertálás',
},
circle: {
diameter: 'Átmérő (pontok)',
diff --git a/src/locales/it.ts b/src/locales/it.ts
index 5bf09103..96ebf328 100644
--- a/src/locales/it.ts
+++ b/src/locales/it.ts
@@ -19,7 +19,6 @@ const it = {
datamatrix: 'DataMatrix',
box: 'Rettangolo',
ellipse: 'Ellisse',
- circle: 'Cerchio',
line: 'Linea',
serial: 'Seriale',
image: 'Immagine',
@@ -263,9 +262,11 @@ const it = {
height: 'Altezza (punti)',
thickness: 'Bordo (punti)',
filled: 'Riempito',
+ lockAspect: 'Cerchio (blocca proporzioni)',
color: 'Colore',
colorB: 'B — Nero',
colorW: 'W — Bianco',
+ reverse: 'Inverti',
},
circle: {
diameter: 'Diametro (punti)',
diff --git a/src/locales/ja.ts b/src/locales/ja.ts
index 0eb26d0e..a67e90be 100644
--- a/src/locales/ja.ts
+++ b/src/locales/ja.ts
@@ -19,7 +19,6 @@ const ja = {
datamatrix: 'DataMatrix',
box: '矩形',
ellipse: '楕円',
- circle: '円',
line: '線',
serial: 'シリアル',
image: '画像',
@@ -263,9 +262,11 @@ const ja = {
height: '高さ (ドット)',
thickness: '枠線 (ドット)',
filled: '塗りつぶし',
+ lockAspect: '円(アスペクト比を固定)',
color: '色',
colorB: 'B — 黒',
colorW: 'W — 白',
+ reverse: '反転',
},
circle: {
diameter: '直径 (ドット)',
diff --git a/src/locales/ko.ts b/src/locales/ko.ts
index 9880b511..646286f4 100644
--- a/src/locales/ko.ts
+++ b/src/locales/ko.ts
@@ -19,7 +19,6 @@ const ko = {
datamatrix: 'DataMatrix',
box: '사각형',
ellipse: '타원',
- circle: '원',
line: '선',
serial: '일련번호',
image: '이미지',
@@ -263,9 +262,11 @@ const ko = {
height: '높이 (점)',
thickness: '테두리 (점)',
filled: '채우기',
+ lockAspect: '원 (가로세로 비율 고정)',
color: '색상',
colorB: 'B — 검정',
colorW: 'W — 흰색',
+ reverse: '반전',
},
circle: {
diameter: '지름 (도트)',
diff --git a/src/locales/lt.ts b/src/locales/lt.ts
index a7196bed..6c829ae8 100644
--- a/src/locales/lt.ts
+++ b/src/locales/lt.ts
@@ -19,7 +19,6 @@ const lt = {
datamatrix: 'DataMatrix',
box: 'Stačiakampis',
ellipse: 'Elipsė',
- circle: 'Apskritimas',
line: 'Linija',
serial: 'Serijinis nr.',
image: 'Vaizdas',
@@ -263,9 +262,11 @@ const lt = {
height: 'Aukštis (taškai)',
thickness: 'Rėmelis (taškai)',
filled: 'Užpildytas',
+ lockAspect: 'Apskritimas (užfiksuoti kraštinių santykį)',
color: 'Spalva',
colorB: 'B — Juoda',
colorW: 'W — Balta',
+ reverse: 'Apversti',
},
circle: {
diameter: 'Skersmuo (taškai)',
diff --git a/src/locales/lv.ts b/src/locales/lv.ts
index c930c049..70e8e5e2 100644
--- a/src/locales/lv.ts
+++ b/src/locales/lv.ts
@@ -19,7 +19,6 @@ const lv = {
datamatrix: 'DataMatrix',
box: 'Taisnstūris',
ellipse: 'Elipse',
- circle: 'Aplis',
line: 'Līnija',
serial: 'Sērijas nr.',
image: 'Attēls',
@@ -263,9 +262,11 @@ const lv = {
height: 'Augstums (punkti)',
thickness: 'Apmale (punkti)',
filled: 'Aizpildīts',
+ lockAspect: 'Aplis (fiksēt malu attiecību)',
color: 'Krāsa',
colorB: 'B — Melna',
colorW: 'W — Balta',
+ reverse: 'Apgriezt',
},
circle: {
diameter: 'Diametrs (punkti)',
diff --git a/src/locales/nl.ts b/src/locales/nl.ts
index da274dbf..7f5a71b6 100644
--- a/src/locales/nl.ts
+++ b/src/locales/nl.ts
@@ -19,7 +19,6 @@ const nl = {
datamatrix: 'DataMatrix',
box: 'Rechthoek',
ellipse: 'Ellips',
- circle: 'Cirkel',
line: 'Lijn',
serial: 'Serienummer',
image: 'Afbeelding',
@@ -263,9 +262,11 @@ const nl = {
height: 'Hoogte (punten)',
thickness: 'Rand (punten)',
filled: 'Gevuld',
+ lockAspect: 'Cirkel (beeldverhouding vergrendelen)',
color: 'Kleur',
colorB: 'B — Zwart',
colorW: 'W — Wit',
+ reverse: 'Inverteren',
},
circle: {
diameter: 'Diameter (dots)',
diff --git a/src/locales/no.ts b/src/locales/no.ts
index f5fb89ca..83262d4f 100644
--- a/src/locales/no.ts
+++ b/src/locales/no.ts
@@ -19,7 +19,6 @@ const no = {
datamatrix: 'DataMatrix',
box: 'Rektangel',
ellipse: 'Ellipse',
- circle: 'Sirkel',
line: 'Linje',
serial: 'Serienr.',
image: 'Bilde',
@@ -263,9 +262,11 @@ const no = {
height: 'Høyde (punkter)',
thickness: 'Kant (punkter)',
filled: 'Fylt',
+ lockAspect: 'Sirkel (lås sideforhold)',
color: 'Farge',
colorB: 'B — Svart',
colorW: 'W — Hvit',
+ reverse: 'Inverter',
},
circle: {
diameter: 'Diameter (punkter)',
diff --git a/src/locales/pl.ts b/src/locales/pl.ts
index cc75ff95..c7c36c38 100644
--- a/src/locales/pl.ts
+++ b/src/locales/pl.ts
@@ -19,7 +19,6 @@ const pl = {
datamatrix: 'DataMatrix',
box: 'Prostokąt',
ellipse: 'Elipsa',
- circle: 'Okrąg',
line: 'Linia',
serial: 'Seria',
image: 'Obraz',
@@ -263,9 +262,11 @@ const pl = {
height: 'Wysokość (punkty)',
thickness: 'Kontur (punkty)',
filled: 'Wypełniony',
+ lockAspect: 'Okrąg (zablokuj proporcje)',
color: 'Kolor',
colorB: 'B — Czarny',
colorW: 'W — Biały',
+ reverse: 'Odwróć',
},
circle: {
diameter: 'Średnica (punkty)',
diff --git a/src/locales/pt.ts b/src/locales/pt.ts
index 17a9cddc..98b178c1 100644
--- a/src/locales/pt.ts
+++ b/src/locales/pt.ts
@@ -19,7 +19,6 @@ const pt = {
datamatrix: 'DataMatrix',
box: 'Retângulo',
ellipse: 'Elipse',
- circle: 'Círculo',
line: 'Linha',
serial: 'Série',
image: 'Imagem',
@@ -263,9 +262,11 @@ const pt = {
height: 'Altura (pontos)',
thickness: 'Borda (pontos)',
filled: 'Preenchido',
+ lockAspect: 'Círculo (bloquear proporção)',
color: 'Cor',
colorB: 'B — Preto',
colorW: 'W — Branco',
+ reverse: 'Inverter',
},
circle: {
diameter: 'Diâmetro (pontos)',
diff --git a/src/locales/ro.ts b/src/locales/ro.ts
index c3f1932c..8c885e90 100644
--- a/src/locales/ro.ts
+++ b/src/locales/ro.ts
@@ -19,7 +19,6 @@ const ro = {
datamatrix: 'DataMatrix',
box: 'Dreptunghi',
ellipse: 'Elipsă',
- circle: 'Cerc',
line: 'Linie',
serial: 'Serie',
image: 'Imagine',
@@ -263,9 +262,11 @@ const ro = {
height: 'Înălțime (puncte)',
thickness: 'Chenar (puncte)',
filled: 'Umplut',
+ lockAspect: 'Cerc (blochează raportul de aspect)',
color: 'Culoare',
colorB: 'B — Negru',
colorW: 'W — Alb',
+ reverse: 'Inversează',
},
circle: {
diameter: 'Diametru (puncte)',
diff --git a/src/locales/sk.ts b/src/locales/sk.ts
index 78510bbd..20f7f339 100644
--- a/src/locales/sk.ts
+++ b/src/locales/sk.ts
@@ -19,7 +19,6 @@ const sk = {
datamatrix: 'DataMatrix',
box: 'Obdĺžnik',
ellipse: 'Elipsa',
- circle: 'Kruh',
line: 'Čiara',
serial: 'Sériové číslo',
image: 'Obrázok',
@@ -263,9 +262,11 @@ const sk = {
height: 'Výška (body)',
thickness: 'Rámček (body)',
filled: 'Vyplnený',
+ lockAspect: 'Kruh (uzamknúť pomer strán)',
color: 'Farba',
colorB: 'B — Čierna',
colorW: 'W — Biela',
+ reverse: 'Invertovať',
},
circle: {
diameter: 'Priemer (body)',
diff --git a/src/locales/sl.ts b/src/locales/sl.ts
index c9c83298..51cbc744 100644
--- a/src/locales/sl.ts
+++ b/src/locales/sl.ts
@@ -19,7 +19,6 @@ const sl = {
datamatrix: 'DataMatrix',
box: 'Pravokotnik',
ellipse: 'Elipsa',
- circle: 'Krog',
line: 'Črta',
serial: 'Zaporedna št.',
image: 'Slika',
@@ -263,9 +262,11 @@ const sl = {
height: 'Višina (točke)',
thickness: 'Okvir (točke)',
filled: 'Zapolnjen',
+ lockAspect: 'Krog (zakleni razmerje stranic)',
color: 'Barva',
colorB: 'B — Črna',
colorW: 'W — Bela',
+ reverse: 'Obrni',
},
circle: {
diameter: 'Premer (točke)',
diff --git a/src/locales/sr.ts b/src/locales/sr.ts
index 4c131f76..ef4340d6 100644
--- a/src/locales/sr.ts
+++ b/src/locales/sr.ts
@@ -19,7 +19,6 @@ const sr = {
datamatrix: 'DataMatrix',
box: 'Правоугаоник',
ellipse: 'Елипса',
- circle: 'Круг',
line: 'Линија',
serial: 'Серијски бр.',
image: 'Слика',
@@ -263,9 +262,11 @@ const sr = {
height: 'Visina (tačke)',
thickness: 'Okvir (tačke)',
filled: 'Popunjen',
+ lockAspect: 'Круг (закључај однос страница)',
color: 'Boja',
colorB: 'B — Crna',
colorW: 'W — Bela',
+ reverse: 'Инвертуј',
},
circle: {
diameter: 'Пречник (тачке)',
diff --git a/src/locales/sv.ts b/src/locales/sv.ts
index bd95a933..a993a888 100644
--- a/src/locales/sv.ts
+++ b/src/locales/sv.ts
@@ -19,7 +19,6 @@ const sv = {
datamatrix: 'DataMatrix',
box: 'Rektangel',
ellipse: 'Ellips',
- circle: 'Cirkel',
line: 'Linje',
serial: 'Serienr.',
image: 'Bild',
@@ -263,9 +262,11 @@ const sv = {
height: 'Höjd (punkter)',
thickness: 'Kant (punkter)',
filled: 'Fylld',
+ lockAspect: 'Cirkel (lås sidförhållande)',
color: 'Färg',
colorB: 'B — Svart',
colorW: 'W — Vit',
+ reverse: 'Invertera',
},
circle: {
diameter: 'Diameter (punkter)',
diff --git a/src/locales/tr.ts b/src/locales/tr.ts
index a9cd7fe6..8bc73384 100644
--- a/src/locales/tr.ts
+++ b/src/locales/tr.ts
@@ -19,7 +19,6 @@ const tr = {
datamatrix: 'DataMatrix',
box: 'Dikdörtgen',
ellipse: 'Elips',
- circle: 'Daire',
line: 'Çizgi',
serial: 'Seri No',
image: 'Görsel',
@@ -263,9 +262,11 @@ const tr = {
height: 'Yükseklik (nokta)',
thickness: 'Kenarlık (nokta)',
filled: 'Dolu',
+ lockAspect: 'Daire (en boy oranını kilitle)',
color: 'Renk',
colorB: 'B — Siyah',
colorW: 'W — Beyaz',
+ reverse: 'Tersine çevir',
},
circle: {
diameter: 'Çap (nokta)',
diff --git a/src/locales/zh-hans.ts b/src/locales/zh-hans.ts
index ff637fca..46609ec0 100644
--- a/src/locales/zh-hans.ts
+++ b/src/locales/zh-hans.ts
@@ -19,7 +19,6 @@ const zhHans = {
datamatrix: 'DataMatrix',
box: '矩形',
ellipse: '椭圆',
- circle: '圆',
line: '线条',
serial: '序列号',
image: '图片',
@@ -263,9 +262,11 @@ const zhHans = {
height: '高度 (点)',
thickness: '边框 (点)',
filled: '填充',
+ lockAspect: '圆形(锁定纵横比)',
color: '颜色',
colorB: 'B — 黑色',
colorW: 'W — 白色',
+ reverse: '反转',
},
circle: {
diameter: '直径 (点)',
diff --git a/src/locales/zh-hant.ts b/src/locales/zh-hant.ts
index b0a47096..49d456c6 100644
--- a/src/locales/zh-hant.ts
+++ b/src/locales/zh-hant.ts
@@ -19,7 +19,6 @@ const zhHant = {
datamatrix: 'DataMatrix',
box: '矩形',
ellipse: '橢圓',
- circle: '圓',
line: '線條',
serial: '序號',
image: '圖片',
@@ -263,9 +262,11 @@ const zhHant = {
height: '高度 (點)',
thickness: '框線 (點)',
filled: '填滿',
+ lockAspect: '圓形(鎖定長寬比)',
color: '顏色',
colorB: 'B — 黑色',
colorW: 'W — 白色',
+ reverse: '反轉',
},
circle: {
diameter: '直徑 (點)',
diff --git a/src/registry/box.tsx b/src/registry/box.tsx
index a793ca83..4ac1c44f 100644
--- a/src/registry/box.tsx
+++ b/src/registry/box.tsx
@@ -1,7 +1,7 @@
import type { ObjectTypeDefinition } from '../types/ObjectType';
import { useT } from '../lib/useT';
import { inputCls, labelCls } from '../components/Properties/styles';
-import { fieldPos } from './zplHelpers';
+import { fieldPos, wrapReverse } from './zplHelpers';
import { commitWidthHeightTransform } from './transformHelpers';
import { NumberInput } from '../components/Properties/NumberInput';
@@ -42,13 +42,10 @@ export const box: ObjectTypeDefinition = {
const t = p.filled
? Math.max(p.thickness, solidThreshold)
: p.thickness;
- return [
- p.reverse ? '^LRY' : '',
- fieldPos(obj),
- `^GB${p.width},${p.height},${t},${p.color},${p.rounding}`,
- '^FS',
- p.reverse ? '^LRN' : '',
- ].filter(Boolean).join('');
+ return wrapReverse(
+ p.reverse,
+ `${fieldPos(obj)}^GB${p.width},${p.height},${t},${p.color},${p.rounding}^FS`,
+ );
},
PropertiesPanel: ({ obj, onChange }) => {
diff --git a/src/registry/ellipse.tsx b/src/registry/ellipse.tsx
index eea03806..4138e441 100644
--- a/src/registry/ellipse.tsx
+++ b/src/registry/ellipse.tsx
@@ -2,7 +2,7 @@ import type { ObjectTypeDefinition } from '../types/ObjectType';
import { useT } from '../lib/useT';
import { inputCls, labelCls } from '../components/Properties/styles';
import { NumberInput } from '../components/Properties/NumberInput';
-import { fieldPos } from './zplHelpers';
+import { fieldPos, wrapReverse } from './zplHelpers';
import { commitWidthHeightTransform } from './transformHelpers';
export interface EllipseProps {
@@ -11,10 +11,15 @@ export interface EllipseProps {
thickness: number;
filled: boolean;
color: 'B' | 'W';
- /** When true, resize keeps width === height. Set by the "Circle"
- * palette entry and by the parser when an object round-trips through
- * ^GC. Used by the transformer to force uniform scale anchors. */
+ /** When true, resize keeps width === height. Set by the parser when
+ * an object round-trips through ^GC, by the "Circle" Properties-
+ * Panel toggle, or by the user. The transformer reads this to
+ * force uniform scale anchors. */
lockAspect?: boolean;
+ /** Field-level inversion via `^LRY`/`^LRN` wrap on emit. Round-trips
+ * through the parser's `^LR` state and matches the box/line/text
+ * reverse semantics. */
+ reverse?: boolean;
}
export const ellipse: ObjectTypeDefinition = {
@@ -54,7 +59,7 @@ export const ellipse: ObjectTypeDefinition = {
p.width === p.height
? `^GC${p.width},${thick},${p.color}`
: `^GE${p.width},${p.height},${thick},${p.color}`;
- return [fieldPos(obj), cmd, `^FS`].join('');
+ return wrapReverse(p.reverse, `${fieldPos(obj)}${cmd}^FS`);
},
PropertiesPanel: ({ obj, onChange }) => {
@@ -86,6 +91,32 @@ export const ellipse: ObjectTypeDefinition = {
)}
+
+