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 = { )} + +