diff --git a/src/components/Canvas/KonvaObject.tsx b/src/components/Canvas/KonvaObject.tsx index 38d85a4f..80cd7ceb 100644 --- a/src/components/Canvas/KonvaObject.tsx +++ b/src/components/Canvas/KonvaObject.tsx @@ -1,5 +1,5 @@ import { useFontCacheVersion } from "../../hooks/useFontCacheVersion"; -import { Circle, Ellipse, Group, Rect, Text } from "react-konva"; +import { Ellipse, Group, Rect, Text } from "react-konva"; import { BarcodeObject } from "./BarcodeObject"; import { LineObject } from "./LineObject"; import { ImageObject } from "./ImageObject"; @@ -297,7 +297,7 @@ function KonvaObjectInner({ // `renderFilled`. // 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 + // ellipse branch below leaves 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; @@ -440,46 +440,5 @@ function KonvaObjectInner({ ); } - if (obj.type === "circle") { - const p = obj.props; - const d = dotsToPx(p.diameter, scale, dpmm); - const r = d / 2; - const stroke = p.color === "B" ? "#000000" : "#cccccc"; - const strokeWidth = Math.max(dotsToPx(p.thickness, scale, dpmm), 0.5); - // Option-A geometry — same outlineInset() definition as box/ellipse. - const insetGeom = outlineInset(d, d, strokeWidth, p.filled); - const renderFilled = insetGeom.renderFilled; - const insetR = insetGeom.width / 2; - const fill = renderFilled - ? p.color === "B" - ? "#000000" - : "#ffffff" - : "transparent"; - return ( - - - {isSelected && ( - - )} - - ); - } - return null; } diff --git a/src/components/Canvas/LabelCanvas.tsx b/src/components/Canvas/LabelCanvas.tsx index 45ece4a1..eeb9b887 100644 --- a/src/components/Canvas/LabelCanvas.tsx +++ b/src/components/Canvas/LabelCanvas.tsx @@ -469,7 +469,7 @@ export const LabelCanvas = forwardRef(function LabelCa // Quick 90°-rotation button overlay. Only step-rotation objects (those // with a `rotation: N|R|I|B` prop — text, serial, all barcodes) get the - // affordance; box/ellipse/circle/line/image rotate freely via the + // affordance; box/ellipse/line/image rotate freely via the // Transformer or have no rotation. Positioned at the visual top-right // corner of the selected node, derived from getClientRect so it tracks // the rendered bbox through both object-rotation and viewRotation. @@ -701,11 +701,18 @@ export const LabelCanvas = forwardRef(function LabelCa } const pos = pointerToLabelDots(lastPointerRef.current.x, lastPointerRef.current.y); if (!pos) return; - const type = (event.active.data.current as PaletteDragData | undefined)?.type; + const dragData = event.active.data.current as PaletteDragData | undefined; + const type = dragData?.type; if (!type) return; const def = ObjectRegistry[type]; if (!def) return; - setGhost({ id: "__ghost__", type, ...pos, rotation: 0, props: def.defaultProps } as LeafObject); + setGhost({ + id: "__ghost__", + type, + ...pos, + rotation: 0, + props: { ...def.defaultProps, ...dragData?.propsOverride }, + } as LeafObject); }, onDragEnd(event) { setGhost(null); @@ -713,9 +720,10 @@ export const LabelCanvas = forwardRef(function LabelCa if (event.over?.id !== "canvas") return; const pos = pointerToLabelDots(lastPointerRef.current.x, lastPointerRef.current.y); if (!pos) return; - const type = (event.active.data.current as PaletteDragData | undefined)?.type; + const dragData = event.active.data.current as PaletteDragData | undefined; + const type = dragData?.type; if (!type) return; - addObject(type, pos); + addObject(type, pos, dragData.propsOverride); }, onDragCancel() { setGhost(null); diff --git a/src/components/Canvas/hooks/useKonvaTransformer.ts b/src/components/Canvas/hooks/useKonvaTransformer.ts index 887007af..5b8db139 100644 --- a/src/components/Canvas/hooks/useKonvaTransformer.ts +++ b/src/components/Canvas/hooks/useKonvaTransformer.ts @@ -189,7 +189,12 @@ export function useKonvaTransformer({ : undefined; const resizeEnabled = selectedIds.length <= 1 && !singleSelected?.locked; const singleType = singleSelected?.type ?? ""; - const isUniformScale = !!ObjectRegistry[singleType]?.uniformScale; + const uniformScaleDef = ObjectRegistry[singleType]?.uniformScale; + const isUniformScale = + typeof uniformScaleDef === "function" + ? !!singleSelected && !isGroup(singleSelected) && + uniformScaleDef(singleSelected.props as object) + : !!uniformScaleDef; const enabledAnchors: string[] | undefined = selectedIds.length > 1 ? [] diff --git a/src/components/Palette/ObjectPalette.tsx b/src/components/Palette/ObjectPalette.tsx index 90f9af78..2e8ae2a7 100644 --- a/src/components/Palette/ObjectPalette.tsx +++ b/src/components/Palette/ObjectPalette.tsx @@ -1,7 +1,8 @@ import { useDraggable } from '@dnd-kit/core'; import { ObjectRegistry } from '../../registry'; import { PALETTE_GROUPS } from './paletteGroups'; -import type { ObjectTypeDefinition } from '../../types/ObjectType'; +import { VIRTUAL_PALETTE_ENTRIES, type VirtualPaletteEntry } from './virtualEntries'; +import type { ObjectGroup } from '../../types/ObjectType'; import { useT } from '../../lib/useT'; import { useLabelStore } from '../../store/labelStore'; import { mmToDots } from '../../lib/coordinates'; @@ -10,24 +11,33 @@ import { CollapsibleSection } from '../ui/CollapsibleSection'; import type { PaletteDragData } from '../../dnd/types'; interface PaletteEntryProps { + /** Unique within the palette: registry type or virtual entry id. */ + id: string; + /** Registry type to instantiate. Equals `id` for non-virtual entries. */ type: string; - def: ObjectTypeDefinition; + icon: string; + label: string; + defaultSize: { width: number; height: number }; + propsOverride?: object; } -function PaletteEntry({ type, def }: PaletteEntryProps) { - const t = useT(); +function PaletteEntry({ id, type, icon, label, defaultSize, propsOverride }: PaletteEntryProps) { const addObject = useLabelStore((s) => s.addObject); const { attributes, listeners, setNodeRef, isDragging } = useDraggable({ - id: `palette-${type}`, - data: { type } satisfies PaletteDragData, + id: `palette-${id}`, + data: { type, propsOverride } satisfies PaletteDragData, }); const handleDoubleClick = () => { - const { label } = useLabelStore.getState(); - addObject(type, { - x: Math.round(mmToDots(label.widthMm, label.dpmm) / 2 - def.defaultSize.width / 2), - y: Math.round(mmToDots(label.heightMm, label.dpmm) / 2 - def.defaultSize.height / 2), - }); + const { label: labelConfig } = useLabelStore.getState(); + addObject( + type, + { + x: Math.round(mmToDots(labelConfig.widthMm, labelConfig.dpmm) / 2 - defaultSize.width / 2), + y: Math.round(mmToDots(labelConfig.heightMm, labelConfig.dpmm) / 2 - defaultSize.height / 2), + }, + propsOverride, + ); }; return ( @@ -47,25 +57,54 @@ function PaletteEntry({ type, def }: PaletteEntryProps) { `} > - - {def.icon} - - - {(t.types as Record)[type] ?? def.label} - + {icon} + {label} ); } +interface ResolvedEntry { + id: string; + type: string; + icon: string; + label: string; + defaultSize: { width: number; height: number }; + propsOverride?: object; +} + +function resolveEntries( + group: ObjectGroup, + types: Record, +): ResolvedEntry[] { + const registry = Object.entries(ObjectRegistry) + .filter(([, def]) => def.group === group) + .map(([type, def]): ResolvedEntry => ({ + id: type, + type, + icon: def.icon, + 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() { const t = useT(); return (
{PALETTE_GROUPS.map((group) => { - const entries = Object.entries(ObjectRegistry).filter( - ([, def]) => def.group === group.key, - ); + const entries = resolveEntries(group.key, t.types as Record); if (entries.length === 0) return null; return ( - {entries.map(([type, def]) => ( - + {entries.map((e) => ( + ))} ); diff --git a/src/components/Palette/virtualEntries.ts b/src/components/Palette/virtualEntries.ts new file mode 100644 index 00000000..eb3dbcee --- /dev/null +++ b/src/components/Palette/virtualEntries.ts @@ -0,0 +1,44 @@ +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/dnd/types.ts b/src/dnd/types.ts index e5f8af4a..0a7fb921 100644 --- a/src/dnd/types.ts +++ b/src/dnd/types.ts @@ -1,3 +1,4 @@ export interface PaletteDragData { type: string; + propsOverride?: object; } diff --git a/src/lib/shapeGeometry.ts b/src/lib/shapeGeometry.ts index 109d762a..27d723a5 100644 --- a/src/lib/shapeGeometry.ts +++ b/src/lib/shapeGeometry.ts @@ -4,7 +4,7 @@ * Mirrors Zebra firmware's rendering semantics so that the on-screen * Konva canvas, the @napi-rs/canvas pixel-regression renderer, and the * ZPL output all describe the same shape: - * - Outlines (box / ellipse / circle) extrude thickness *inward* from + * - Outlines (box / ellipse) extrude thickness *inward* from * the declared bbox; thickness ≥ min(w, h)/2 collapses to solid. * - Diagonal lines (^GD) place the conceptual line on the *left long * edge* of a parallelogram and extrude thickness in +x only — both @@ -16,7 +16,7 @@ */ /** - * Inset values for an outline rectangle / ellipse / circle whose + * Inset values for an outline rectangle / ellipse whose * declared bbox is (0, 0, w, h) with stroke thickness t. The caller * uses these to position a *centred-stroke* primitive whose outer * edge lands on the declared bbox. diff --git a/src/lib/shapeRender.ts b/src/lib/shapeRender.ts index 36bbfb51..f0ab145f 100644 --- a/src/lib/shapeRender.ts +++ b/src/lib/shapeRender.ts @@ -1,10 +1,9 @@ import type { LabelObject } from "../types/Group"; 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 - * prop shapes — can each pass their normalised width / height in - * without the call-site needing a union-narrowing ternary. */ +/** Inward-extruded ^GE / ^GC ring or solid disc for the ellipse type + * (circles round-trip as ellipse with `lockAspect:true`, sharing the + * same geometry). */ function drawEllipticalOutline( ctx: CanvasRenderingContext2D, x: number, y: number, @@ -106,16 +105,6 @@ export function renderShape( return; } - case "circle": { - drawEllipticalOutline( - ctx, - obj.x, obj.y, - obj.props.diameter, obj.props.diameter, - obj.props.thickness, obj.props.filled, obj.props.color, - ); - return; - } - case "line": { const p = obj.props; const color = p.color === "B" ? "#000000" : "#ffffff"; diff --git a/src/lib/zplParser.test.ts b/src/lib/zplParser.test.ts index d946be14..8642cd9c 100644 --- a/src/lib/zplParser.test.ts +++ b/src/lib/zplParser.test.ts @@ -487,6 +487,11 @@ describe('parseZPL — ^GE ellipse', () => { const { objects } = parseZPL('^XA^FO0,0^GE100,80,80,B^FS^XZ', 8); expect(props(objects[0]).filled).toBe(true); }); + + it('preserves the original thickness on filled ^GE (lossless round-trip)', () => { + const { objects } = parseZPL('^XA^FO0,0^GE100,80,80,B^FS^XZ', 8); + expect(props(objects[0]).thickness).toBe(80); + }); }); describe('parseZPL — ^GC circle', () => { @@ -497,12 +502,18 @@ describe('parseZPL — ^GC circle', () => { expect(props(objects[0]).width).toBe(100); expect(props(objects[0]).height).toBe(100); expect(props(objects[0]).filled).toBe(false); + expect(props(objects[0]).lockAspect).toBe(true); }); it('creates a filled circle when thickness >= diameter', () => { const { objects } = parseZPL('^XA^FO0,0^GC50,50,B^FS^XZ', 8); expect(props(objects[0]).filled).toBe(true); }); + + it('preserves the original thickness on filled ^GC (lossless round-trip)', () => { + const { objects } = parseZPL('^XA^FO0,0^GC50,50,B^FS^XZ', 8); + expect(props(objects[0]).thickness).toBe(50); + }); }); describe('parseZPL — ^GD diagonal line', () => { diff --git a/src/lib/zplParser.ts b/src/lib/zplParser.ts index 313f26e7..653bb180 100644 --- a/src/lib/zplParser.ts +++ b/src/lib/zplParser.ts @@ -1178,7 +1178,10 @@ export function parseZPL(zpl: string, dpmm = 8): ParsedZPL { { width: w, height: h, - thickness: filled ? 3 : t, + // Preserve the original thickness (same rationale as ^GB) so a + // ZPL round-trip is lossless. UI sets sensible defaults when + // the user toggles `filled` off; the parser stays faithful. + thickness: t, filled, color, } satisfies EllipseProps, @@ -1201,9 +1204,10 @@ export function parseZPL(zpl: string, dpmm = 8): ParsedZPL { { width: d, height: d, - thickness: filled ? 3 : t, + thickness: t, filled, color, + lockAspect: true, } satisfies EllipseProps, undefined, takeComment(), diff --git a/src/locales/ar.ts b/src/locales/ar.ts index d2178429..98fa8b8b 100644 --- a/src/locales/ar.ts +++ b/src/locales/ar.ts @@ -235,11 +235,6 @@ const ar = { }, circle: { diameter: 'القطر (نقاط)', - thickness: 'الحدود (نقاط)', - filled: 'ممتلئ', - color: 'اللون', - colorB: 'B — أسود', - colorW: 'W — أبيض', }, line: { angle: 'الزاوية (°)', diff --git a/src/locales/bg.ts b/src/locales/bg.ts index ccd3846e..c885f736 100644 --- a/src/locales/bg.ts +++ b/src/locales/bg.ts @@ -235,11 +235,6 @@ const bg = { }, circle: { diameter: 'Диаметър (точки)', - thickness: 'Рамка (точки)', - filled: 'Запълнено', - color: 'Цвят', - colorB: 'B — Черно', - colorW: 'W — Бяло', }, line: { angle: 'Ъгъл (°)', diff --git a/src/locales/cs.ts b/src/locales/cs.ts index d56d25b6..811ff1c9 100644 --- a/src/locales/cs.ts +++ b/src/locales/cs.ts @@ -235,11 +235,6 @@ const cs = { }, circle: { diameter: 'Průměr (body)', - thickness: 'Okraj (body)', - filled: 'Vyplněný', - color: 'Barva', - colorB: 'B — Černá', - colorW: 'W — Bílá', }, line: { angle: 'Úhel (°)', diff --git a/src/locales/da.ts b/src/locales/da.ts index 487af003..447ecba2 100644 --- a/src/locales/da.ts +++ b/src/locales/da.ts @@ -235,11 +235,6 @@ const da = { }, circle: { diameter: 'Diameter (punkter)', - thickness: 'Ramme (punkter)', - filled: 'Fyldt', - color: 'Farve', - colorB: 'B — Sort', - colorW: 'W — Hvid', }, line: { angle: 'Vinkel (°)', diff --git a/src/locales/de.ts b/src/locales/de.ts index 889943d0..aa241d5b 100644 --- a/src/locales/de.ts +++ b/src/locales/de.ts @@ -256,11 +256,6 @@ const de = { }, circle: { diameter: 'Durchmesser (Punkte)', - thickness: 'Rahmen (Punkte)', - filled: 'Gefüllt', - color: 'Farbe', - colorB: 'B — Schwarz', - colorW: 'W — Weiß', }, line: { angle: 'Winkel (°)', diff --git a/src/locales/el.ts b/src/locales/el.ts index 8d6bcaa0..e187f44c 100644 --- a/src/locales/el.ts +++ b/src/locales/el.ts @@ -235,11 +235,6 @@ const el = { }, circle: { diameter: 'Διάμετρος (κουκκίδες)', - thickness: 'Περίγραμμα (κουκκίδες)', - filled: 'Γεμάτο', - color: 'Χρώμα', - colorB: 'B — Μαύρο', - colorW: 'W — Λευκό', }, line: { angle: 'Γωνία (°)', diff --git a/src/locales/en.ts b/src/locales/en.ts index 7a5bd0ce..04e1230a 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -256,11 +256,6 @@ const en = { }, circle: { diameter: 'Diameter (dots)', - thickness: 'Border (dots)', - filled: 'Filled', - color: 'Color', - colorB: 'B — Black', - colorW: 'W — White', }, line: { angle: 'Angle (°)', diff --git a/src/locales/es.ts b/src/locales/es.ts index 8a85d90f..5c9c6657 100644 --- a/src/locales/es.ts +++ b/src/locales/es.ts @@ -235,11 +235,6 @@ const es = { }, circle: { diameter: 'Diámetro (puntos)', - thickness: 'Borde (puntos)', - filled: 'Relleno', - color: 'Color', - colorB: 'B — Negro', - colorW: 'W — Blanco', }, line: { angle: 'Ángulo (°)', diff --git a/src/locales/et.ts b/src/locales/et.ts index 576dd45a..e58469e0 100644 --- a/src/locales/et.ts +++ b/src/locales/et.ts @@ -235,11 +235,6 @@ const et = { }, circle: { diameter: 'Läbimõõt (punktid)', - thickness: 'Ääris (punktid)', - filled: 'Täidetud', - color: 'Värv', - colorB: 'B — Must', - colorW: 'W — Valge', }, line: { angle: 'Nurk (°)', diff --git a/src/locales/fa.ts b/src/locales/fa.ts index eea43ac6..703e7946 100644 --- a/src/locales/fa.ts +++ b/src/locales/fa.ts @@ -235,11 +235,6 @@ const fa = { }, circle: { diameter: 'قطر (نقطه)', - thickness: 'حاشیه (نقطه)', - filled: 'پر شده', - color: 'رنگ', - colorB: 'B — سیاه', - colorW: 'W — سفید', }, line: { angle: 'زاویه (°)', diff --git a/src/locales/fi.ts b/src/locales/fi.ts index 05c07255..c3137871 100644 --- a/src/locales/fi.ts +++ b/src/locales/fi.ts @@ -235,11 +235,6 @@ const fi = { }, circle: { diameter: 'Halkaisija (pisteet)', - thickness: 'Reuna (pisteet)', - filled: 'Täytetty', - color: 'Väri', - colorB: 'B — Musta', - colorW: 'W — Valkoinen', }, line: { angle: 'Kulma (°)', diff --git a/src/locales/fr.ts b/src/locales/fr.ts index e08f7c4d..ddb238a1 100644 --- a/src/locales/fr.ts +++ b/src/locales/fr.ts @@ -235,11 +235,6 @@ const fr = { }, circle: { diameter: 'Diamètre (points)', - thickness: 'Bordure (points)', - filled: 'Rempli', - color: 'Couleur', - colorB: 'B — Noir', - colorW: 'W — Blanc', }, line: { angle: 'Angle (°)', diff --git a/src/locales/he.ts b/src/locales/he.ts index f53db94f..16c9b6e9 100644 --- a/src/locales/he.ts +++ b/src/locales/he.ts @@ -235,11 +235,6 @@ const he = { }, circle: { diameter: 'קוטר (נקודות)', - thickness: 'מסגרת (נקודות)', - filled: 'מלא', - color: 'צבע', - colorB: 'B — שחור', - colorW: 'W — לבן', }, line: { angle: 'זווית (°)', diff --git a/src/locales/hr.ts b/src/locales/hr.ts index 90120fc7..a9c82cfd 100644 --- a/src/locales/hr.ts +++ b/src/locales/hr.ts @@ -235,11 +235,6 @@ const hr = { }, circle: { diameter: 'Promjer (točke)', - thickness: 'Obrub (točke)', - filled: 'Ispunjeno', - color: 'Boja', - colorB: 'B — Crno', - colorW: 'W — Bijelo', }, line: { angle: 'Kut (°)', diff --git a/src/locales/hu.ts b/src/locales/hu.ts index b8015e5f..d3a3ac11 100644 --- a/src/locales/hu.ts +++ b/src/locales/hu.ts @@ -235,11 +235,6 @@ const hu = { }, circle: { diameter: 'Átmérő (pontok)', - thickness: 'Keret (pontok)', - filled: 'Kitöltött', - color: 'Szín', - colorB: 'B — Fekete', - colorW: 'W — Fehér', }, line: { angle: 'Szög (°)', diff --git a/src/locales/it.ts b/src/locales/it.ts index 33a310ef..98ec3c53 100644 --- a/src/locales/it.ts +++ b/src/locales/it.ts @@ -235,11 +235,6 @@ const it = { }, circle: { diameter: 'Diametro (punti)', - thickness: 'Bordo (punti)', - filled: 'Riempito', - color: 'Colore', - colorB: 'B — Nero', - colorW: 'W — Bianco', }, line: { angle: 'Angolo (°)', diff --git a/src/locales/ja.ts b/src/locales/ja.ts index 2fd32164..9b42046a 100644 --- a/src/locales/ja.ts +++ b/src/locales/ja.ts @@ -235,11 +235,6 @@ const ja = { }, circle: { diameter: '直径 (ドット)', - thickness: '枠線 (ドット)', - filled: '塗りつぶし', - color: '色', - colorB: 'B — 黒', - colorW: 'W — 白', }, line: { angle: '角度 (°)', diff --git a/src/locales/ko.ts b/src/locales/ko.ts index 5ff7eb31..bcb2a5bb 100644 --- a/src/locales/ko.ts +++ b/src/locales/ko.ts @@ -235,11 +235,6 @@ const ko = { }, circle: { diameter: '지름 (도트)', - thickness: '테두리 (도트)', - filled: '채움', - color: '색상', - colorB: 'B — 검정', - colorW: 'W — 흰색', }, line: { angle: '각도 (°)', diff --git a/src/locales/lt.ts b/src/locales/lt.ts index 0e17b04d..78c1e021 100644 --- a/src/locales/lt.ts +++ b/src/locales/lt.ts @@ -235,11 +235,6 @@ const lt = { }, circle: { diameter: 'Skersmuo (taškai)', - thickness: 'Rėmelis (taškai)', - filled: 'Užpildytas', - color: 'Spalva', - colorB: 'B — Juoda', - colorW: 'W — Balta', }, line: { angle: 'Kampas (°)', diff --git a/src/locales/lv.ts b/src/locales/lv.ts index 760f61b5..2f63af7e 100644 --- a/src/locales/lv.ts +++ b/src/locales/lv.ts @@ -235,11 +235,6 @@ const lv = { }, circle: { diameter: 'Diametrs (punkti)', - thickness: 'Apmale (punkti)', - filled: 'Aizpildīts', - color: 'Krāsa', - colorB: 'B — Melns', - colorW: 'W — Balts', }, line: { angle: 'Leņķis (°)', diff --git a/src/locales/nl.ts b/src/locales/nl.ts index f6d3ca05..5abbef27 100644 --- a/src/locales/nl.ts +++ b/src/locales/nl.ts @@ -235,11 +235,6 @@ const nl = { }, circle: { diameter: 'Diameter (dots)', - thickness: 'Rand (dots)', - filled: 'Gevuld', - color: 'Kleur', - colorB: 'B — Zwart', - colorW: 'W — Wit', }, line: { angle: 'Hoek (°)', diff --git a/src/locales/no.ts b/src/locales/no.ts index 07a8f74b..291dec3f 100644 --- a/src/locales/no.ts +++ b/src/locales/no.ts @@ -235,11 +235,6 @@ const no = { }, circle: { diameter: 'Diameter (punkter)', - thickness: 'Ramme (punkter)', - filled: 'Fylt', - color: 'Farge', - colorB: 'B — Svart', - colorW: 'W — Hvit', }, line: { angle: 'Vinkel (°)', diff --git a/src/locales/pl.ts b/src/locales/pl.ts index cfcf7ade..9db166ee 100644 --- a/src/locales/pl.ts +++ b/src/locales/pl.ts @@ -235,11 +235,6 @@ const pl = { }, circle: { diameter: 'Średnica (punkty)', - thickness: 'Obramowanie (punkty)', - filled: 'Wypełniony', - color: 'Kolor', - colorB: 'B — Czarny', - colorW: 'W — Biały', }, line: { angle: 'Kąt (°)', diff --git a/src/locales/pt.ts b/src/locales/pt.ts index 9fa2abbb..592562e2 100644 --- a/src/locales/pt.ts +++ b/src/locales/pt.ts @@ -235,11 +235,6 @@ const pt = { }, circle: { diameter: 'Diâmetro (pontos)', - thickness: 'Borda (pontos)', - filled: 'Preenchido', - color: 'Cor', - colorB: 'B — Preto', - colorW: 'W — Branco', }, line: { angle: 'Ângulo (°)', diff --git a/src/locales/ro.ts b/src/locales/ro.ts index e0ac1857..c8c0fa4b 100644 --- a/src/locales/ro.ts +++ b/src/locales/ro.ts @@ -235,11 +235,6 @@ const ro = { }, circle: { diameter: 'Diametru (puncte)', - thickness: 'Bordură (puncte)', - filled: 'Umplut', - color: 'Culoare', - colorB: 'B — Negru', - colorW: 'W — Alb', }, line: { angle: 'Unghi (°)', diff --git a/src/locales/sk.ts b/src/locales/sk.ts index f1dc804c..2947df81 100644 --- a/src/locales/sk.ts +++ b/src/locales/sk.ts @@ -235,11 +235,6 @@ const sk = { }, circle: { diameter: 'Priemer (body)', - thickness: 'Okraj (body)', - filled: 'Vyplnený', - color: 'Farba', - colorB: 'B — Čierna', - colorW: 'W — Biela', }, line: { angle: 'Uhol (°)', diff --git a/src/locales/sl.ts b/src/locales/sl.ts index 5e8e0c1a..6607836b 100644 --- a/src/locales/sl.ts +++ b/src/locales/sl.ts @@ -235,11 +235,6 @@ const sl = { }, circle: { diameter: 'Premer (točke)', - thickness: 'Obroba (točke)', - filled: 'Zapolnjeno', - color: 'Barva', - colorB: 'B — Črna', - colorW: 'W — Bela', }, line: { angle: 'Kot (°)', diff --git a/src/locales/sr.ts b/src/locales/sr.ts index 5f6fd929..c382fd19 100644 --- a/src/locales/sr.ts +++ b/src/locales/sr.ts @@ -235,11 +235,6 @@ const sr = { }, circle: { diameter: 'Пречник (тачке)', - thickness: 'Оквир (тачке)', - filled: 'Испуњено', - color: 'Боја', - colorB: 'B — Црна', - colorW: 'W — Бела', }, line: { angle: 'Ugao (°)', diff --git a/src/locales/sv.ts b/src/locales/sv.ts index c6a891b6..d9ddcb73 100644 --- a/src/locales/sv.ts +++ b/src/locales/sv.ts @@ -235,11 +235,6 @@ const sv = { }, circle: { diameter: 'Diameter (punkter)', - thickness: 'Kant (punkter)', - filled: 'Fylld', - color: 'Färg', - colorB: 'B — Svart', - colorW: 'W — Vit', }, line: { angle: 'Vinkel (°)', diff --git a/src/locales/tr.ts b/src/locales/tr.ts index 48784174..2a20accd 100644 --- a/src/locales/tr.ts +++ b/src/locales/tr.ts @@ -235,11 +235,6 @@ const tr = { }, circle: { diameter: 'Çap (nokta)', - thickness: 'Kenarlık (nokta)', - filled: 'Dolu', - color: 'Renk', - colorB: 'B — Siyah', - colorW: 'W — Beyaz', }, line: { angle: 'Açı (°)', diff --git a/src/locales/zh-hans.ts b/src/locales/zh-hans.ts index f133a0b2..e6237271 100644 --- a/src/locales/zh-hans.ts +++ b/src/locales/zh-hans.ts @@ -235,11 +235,6 @@ const zhHans = { }, circle: { diameter: '直径 (点)', - thickness: '边框 (点)', - filled: '填充', - color: '颜色', - colorB: 'B — 黑色', - colorW: 'W — 白色', }, line: { angle: '角度 (°)', diff --git a/src/locales/zh-hant.ts b/src/locales/zh-hant.ts index 46930d2e..6532528f 100644 --- a/src/locales/zh-hant.ts +++ b/src/locales/zh-hant.ts @@ -235,11 +235,6 @@ const zhHant = { }, circle: { diameter: '直徑 (點)', - thickness: '邊框 (點)', - filled: '填充', - color: '顏色', - colorB: 'B — 黑色', - colorW: 'W — 白色', }, line: { angle: '角度 (°)', diff --git a/src/registry/circle.tsx b/src/registry/circle.tsx deleted file mode 100644 index cbf1c6fa..00000000 --- a/src/registry/circle.tsx +++ /dev/null @@ -1,88 +0,0 @@ -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'; - -export interface CircleProps { - diameter: number; - thickness: number; - filled: boolean; - color: 'B' | 'W'; -} - -export const circle: ObjectTypeDefinition = { - label: 'Circle', - icon: '●', - group: 'shape', - defaultProps: { - diameter: 100, - thickness: 3, - filled: false, - color: 'B', - }, - defaultSize: { width: 100, height: 100 }, - uniformScale: true, - - // Force a uniform scale: take the smaller of the two axes so the resized - // circle stays inside the bounding box the user dragged out. - commitTransform: (obj, { sx, sy, snap }) => ({ - diameter: Math.max(1, snap(Math.round(obj.props.diameter * Math.min(sx, sy)))), - }), - - toZPL: (obj) => { - const p = obj.props; - const thick = p.filled ? p.diameter : p.thickness; - return [ - fieldPos(obj), - `^GE${p.diameter},${p.diameter},${thick},${p.color}`, - `^FS`, - ].join(''); - }, - - PropertiesPanel: ({ obj, onChange }) => { - const t = useT(); - const p = obj.props; - return ( -
- onChange({ diameter })} - /> - - - - {!p.filled && ( - onChange({ thickness })} - /> - )} - -
- - -
-
- ); - }, -}; diff --git a/src/registry/ellipse.tsx b/src/registry/ellipse.tsx index c77295f8..eea03806 100644 --- a/src/registry/ellipse.tsx +++ b/src/registry/ellipse.tsx @@ -11,6 +11,10 @@ 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. */ + lockAspect?: boolean; } export const ellipse: ObjectTypeDefinition = { @@ -26,16 +30,31 @@ export const ellipse: ObjectTypeDefinition = { }, defaultSize: { width: 150, height: 100 }, - commitTransform: commitWidthHeightTransform, + uniformScale: (p) => p.lockAspect === true, + + commitTransform: (obj, ctx) => { + // When lockAspect is true, the transformer already constrains the + // bbox to a square via forceSquareBox, so sx === sy here. We still + // collapse to a single axis to keep width === height exact under + // float rounding rather than relying on identical Math.round inputs. + if (obj.props.lockAspect) { + const uniform = { ...ctx, sx: Math.min(ctx.sx, ctx.sy), sy: Math.min(ctx.sx, ctx.sy) }; + return commitWidthHeightTransform(obj, uniform); + } + return commitWidthHeightTransform(obj, ctx); + }, toZPL: (obj) => { const p = obj.props; const thick = p.filled ? Math.min(p.width, p.height) : p.thickness; - return [ - fieldPos(obj), - `^GE${p.width},${p.height},${thick},${p.color}`, - `^FS`, - ].join(''); + // Equal axes round-trip through Zebra's dedicated circle command + // (one parameter shorter, pixel-equivalent). The parser maps either + // ^GC or ^GE to an ellipse on import. + const cmd = + p.width === p.height + ? `^GC${p.width},${thick},${p.color}` + : `^GE${p.width},${p.height},${thick},${p.color}`; + return [fieldPos(obj), cmd, `^FS`].join(''); }, PropertiesPanel: ({ obj, onChange }) => { @@ -43,20 +62,29 @@ export const ellipse: ObjectTypeDefinition = { const p = obj.props; return (
-
+ {p.lockAspect ? ( onChange({ width })} + onChange={(d) => onChange({ width: d, height: d })} /> - onChange({ height })} - /> -
+ ) : ( +
+ onChange({ width })} + /> + onChange({ height })} + /> +
+ )}