From 2fa586369fe5fdfe98c1998fc2a944f76ffb7a48 Mon Sep 17 00:00:00 2001 From: u8array Date: Thu, 7 May 2026 19:41:03 +0200 Subject: [PATCH 1/3] feat(shape): add circle as a dedicated shape type A circle is structurally an ellipse with width=height, but enforcing that constraint manually in the existing ellipse panel is error-prone. Adds a Circle type with a single diameter prop, a commitTransform that clamps to the smaller scale axis (so the resized circle stays inside the user's drag box), and ZPL output that emits ^GE with the diameter on both axes. Locale keys added in all 32 languages via the existing add_locale_key.local.py script. Parser is unchanged: imported ^GE remains an ellipse since ZPL has no separate circle command. --- src/components/Canvas/KonvaObject.tsx | 39 +++++++++++ src/locales/ar.ts | 9 +++ src/locales/bg.ts | 9 +++ src/locales/cs.ts | 9 +++ src/locales/da.ts | 9 +++ src/locales/de.ts | 9 +++ src/locales/el.ts | 9 +++ src/locales/en.ts | 9 +++ src/locales/es.ts | 9 +++ src/locales/et.ts | 9 +++ src/locales/fa.ts | 9 +++ src/locales/fi.ts | 9 +++ src/locales/fr.ts | 9 +++ src/locales/he.ts | 9 +++ src/locales/hr.ts | 9 +++ src/locales/hu.ts | 9 +++ src/locales/it.ts | 9 +++ src/locales/ja.ts | 9 +++ src/locales/ko.ts | 9 +++ src/locales/lt.ts | 9 +++ src/locales/lv.ts | 9 +++ src/locales/nl.ts | 9 +++ src/locales/no.ts | 9 +++ src/locales/pl.ts | 9 +++ src/locales/pt.ts | 9 +++ src/locales/ro.ts | 9 +++ src/locales/sk.ts | 9 +++ src/locales/sl.ts | 9 +++ src/locales/sr.ts | 9 +++ src/locales/sv.ts | 9 +++ src/locales/tr.ts | 9 +++ src/locales/zh-hans.ts | 9 +++ src/locales/zh-hant.ts | 9 +++ src/registry/circle.tsx | 95 +++++++++++++++++++++++++++ src/registry/index.ts | 4 ++ src/registry/registry.test.ts | 42 +++++++++++- 36 files changed, 467 insertions(+), 1 deletion(-) create mode 100644 src/registry/circle.tsx diff --git a/src/components/Canvas/KonvaObject.tsx b/src/components/Canvas/KonvaObject.tsx index 130ff87..44ff149 100644 --- a/src/components/Canvas/KonvaObject.tsx +++ b/src/components/Canvas/KonvaObject.tsx @@ -668,5 +668,44 @@ function KonvaObjectInner({ ); } + if (obj.type === "circle") { + const p = obj.props; + const r = dotsToPx(p.diameter, scale, dpmm) / 2; + const stroke = p.color === "B" ? "#000000" : "#cccccc"; + const strokeWidth = Math.max(dotsToPx(p.thickness, scale, dpmm), 0.5); + const fill = p.filled + ? p.color === "B" + ? "#000000" + : "#ffffff" + : "transparent"; + return ( + + onSelect(e.evt.shiftKey || e.evt.ctrlKey || e.evt.metaKey) + } + onTap={() => onSelect(false)} + onDragMove={(e) => { + const snapped = snapPos(e.target.x() - r, e.target.y() - r); + e.target.position({ x: snapped.x + r, y: snapped.y + r }); + }} + onDragEnd={(e) => { + onChange({ + x: pxToDots(e.target.x() - r - offsetX, scale, dpmm), + y: pxToDots(e.target.y() - r - offsetY, scale, dpmm), + }); + }} + /> + ); + } + return null; } diff --git a/src/locales/ar.ts b/src/locales/ar.ts index 7c2c52d..7f1cbc9 100644 --- a/src/locales/ar.ts +++ b/src/locales/ar.ts @@ -19,6 +19,7 @@ const ar = { datamatrix: 'DataMatrix', box: 'مستطيل', ellipse: 'قطع ناقص', + circle: 'دائرة', line: 'خط', serial: 'رقم تسلسلي', image: 'صورة', @@ -200,6 +201,14 @@ const ar = { colorB: 'B — أسود', colorW: 'W — أبيض', }, + circle: { + diameter: 'القطر (نقاط)', + thickness: 'الحدود (نقاط)', + filled: 'ممتلئ', + color: 'اللون', + colorB: 'B — أسود', + colorW: 'W — أبيض', + }, line: { angle: 'الزاوية (°)', length: 'الطول (نقطة)', diff --git a/src/locales/bg.ts b/src/locales/bg.ts index af9ac73..c61e9c1 100644 --- a/src/locales/bg.ts +++ b/src/locales/bg.ts @@ -19,6 +19,7 @@ const bg = { datamatrix: 'DataMatrix', box: 'Правоъгълник', ellipse: 'Елипса', + circle: 'Кръг', line: 'Линия', serial: 'Сериен №', image: 'Изображение', @@ -200,6 +201,14 @@ const bg = { colorB: 'B — Черен', colorW: 'W — Бял', }, + circle: { + diameter: 'Диаметър (точки)', + thickness: 'Рамка (точки)', + filled: 'Запълнено', + color: 'Цвят', + colorB: 'B — Черно', + colorW: 'W — Бяло', + }, line: { angle: 'Ъгъл (°)', length: 'Дължина (точки)', diff --git a/src/locales/cs.ts b/src/locales/cs.ts index 032386b..a1dacb1 100644 --- a/src/locales/cs.ts +++ b/src/locales/cs.ts @@ -19,6 +19,7 @@ const cs = { datamatrix: 'DataMatrix', box: 'Obdélník', ellipse: 'Elipsa', + circle: 'Kruh', line: 'Čára', serial: 'Sériové číslo', image: 'Obrázek', @@ -200,6 +201,14 @@ const cs = { colorB: 'B — Černá', colorW: 'W — Bílá', }, + circle: { + diameter: 'Průměr (body)', + thickness: 'Okraj (body)', + filled: 'Vyplněný', + color: 'Barva', + colorB: 'B — Černá', + colorW: 'W — Bílá', + }, line: { angle: 'Úhel (°)', length: 'Délka (body)', diff --git a/src/locales/da.ts b/src/locales/da.ts index 83b1646..2a171b9 100644 --- a/src/locales/da.ts +++ b/src/locales/da.ts @@ -19,6 +19,7 @@ const da = { datamatrix: 'DataMatrix', box: 'Rektangel', ellipse: 'Ellipse', + circle: 'Cirkel', line: 'Linje', serial: 'Serienr.', image: 'Billede', @@ -200,6 +201,14 @@ const da = { colorB: 'B — Sort', colorW: 'W — Hvid', }, + circle: { + diameter: 'Diameter (punkter)', + thickness: 'Ramme (punkter)', + filled: 'Fyldt', + color: 'Farve', + colorB: 'B — Sort', + colorW: 'W — Hvid', + }, line: { angle: 'Vinkel (°)', length: 'Længde (punkter)', diff --git a/src/locales/de.ts b/src/locales/de.ts index 0313d55..951f389 100644 --- a/src/locales/de.ts +++ b/src/locales/de.ts @@ -19,6 +19,7 @@ const de = { datamatrix: 'DataMatrix', box: 'Box', ellipse: 'Ellipse', + circle: 'Kreis', line: 'Linie', serial: 'Seriennummer', image: 'Bild', @@ -220,6 +221,14 @@ const de = { colorB: 'B — Schwarz', colorW: 'W — Weiß', }, + circle: { + diameter: 'Durchmesser (Punkte)', + thickness: 'Rahmen (Punkte)', + filled: 'Gefüllt', + color: 'Farbe', + colorB: 'B — Schwarz', + colorW: 'W — Weiß', + }, line: { angle: 'Winkel (°)', length: 'Länge (Punkte)', diff --git a/src/locales/el.ts b/src/locales/el.ts index 94ce4ce..f6505c3 100644 --- a/src/locales/el.ts +++ b/src/locales/el.ts @@ -19,6 +19,7 @@ const el = { datamatrix: 'DataMatrix', box: 'Ορθογώνιο', ellipse: 'Έλλειψη', + circle: 'Κύκλος', line: 'Γραμμή', serial: 'Σειριακός αρ.', image: 'Εικόνα', @@ -200,6 +201,14 @@ const el = { colorB: 'B — Μαύρο', colorW: 'W — Λευκό', }, + circle: { + diameter: 'Διάμετρος (κουκκίδες)', + thickness: 'Περίγραμμα (κουκκίδες)', + filled: 'Γεμάτο', + color: 'Χρώμα', + colorB: 'B — Μαύρο', + colorW: 'W — Λευκό', + }, line: { angle: 'Γωνία (°)', length: 'Μήκος (κουκκίδες)', diff --git a/src/locales/en.ts b/src/locales/en.ts index 1edb0d3..5f2d53f 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -19,6 +19,7 @@ const en = { datamatrix: 'DataMatrix', box: 'Box', ellipse: 'Ellipse', + circle: 'Circle', line: 'Line', serial: 'Serial', image: 'Image', @@ -220,6 +221,14 @@ const en = { colorB: 'B — Black', colorW: 'W — White', }, + circle: { + diameter: 'Diameter (dots)', + thickness: 'Border (dots)', + filled: 'Filled', + color: 'Color', + colorB: 'B — Black', + colorW: 'W — White', + }, line: { angle: 'Angle (°)', length: 'Length (dots)', diff --git a/src/locales/es.ts b/src/locales/es.ts index 946509b..fc24023 100644 --- a/src/locales/es.ts +++ b/src/locales/es.ts @@ -19,6 +19,7 @@ const es = { datamatrix: 'DataMatrix', box: 'Rectángulo', ellipse: 'Elipse', + circle: 'Círculo', line: 'Línea', serial: 'Serie', image: 'Imagen', @@ -200,6 +201,14 @@ const es = { colorB: 'B — Negro', colorW: 'W — Blanco', }, + circle: { + diameter: 'Diámetro (puntos)', + thickness: 'Borde (puntos)', + filled: 'Relleno', + color: 'Color', + colorB: 'B — Negro', + colorW: 'W — Blanco', + }, line: { angle: 'Ángulo (°)', length: 'Longitud (puntos)', diff --git a/src/locales/et.ts b/src/locales/et.ts index 57947bf..fd118a7 100644 --- a/src/locales/et.ts +++ b/src/locales/et.ts @@ -19,6 +19,7 @@ const et = { datamatrix: 'DataMatrix', box: 'Ristkülik', ellipse: 'Ellips', + circle: 'Ring', line: 'Joon', serial: 'Seerianr', image: 'Pilt', @@ -200,6 +201,14 @@ const et = { colorB: 'B — Must', colorW: 'W — Valge', }, + circle: { + diameter: 'Läbimõõt (punktid)', + thickness: 'Ääris (punktid)', + filled: 'Täidetud', + color: 'Värv', + colorB: 'B — Must', + colorW: 'W — Valge', + }, line: { angle: 'Nurk (°)', length: 'Pikkus (punkti)', diff --git a/src/locales/fa.ts b/src/locales/fa.ts index b2d1dcc..10aa70a 100644 --- a/src/locales/fa.ts +++ b/src/locales/fa.ts @@ -19,6 +19,7 @@ const fa = { datamatrix: 'DataMatrix', box: 'مستطیل', ellipse: 'بیضی', + circle: 'دایره', line: 'خط', serial: 'شماره سریال', image: 'تصویر', @@ -200,6 +201,14 @@ const fa = { colorB: 'B — مشکی', colorW: 'W — سفید', }, + circle: { + diameter: 'قطر (نقطه)', + thickness: 'حاشیه (نقطه)', + filled: 'پر شده', + color: 'رنگ', + colorB: 'B — سیاه', + colorW: 'W — سفید', + }, line: { angle: 'زاویه (°)', length: 'طول (نقطه)', diff --git a/src/locales/fi.ts b/src/locales/fi.ts index 51fd310..d944d4a 100644 --- a/src/locales/fi.ts +++ b/src/locales/fi.ts @@ -19,6 +19,7 @@ const fi = { datamatrix: 'DataMatrix', box: 'Suorakulmio', ellipse: 'Ellipsi', + circle: 'Ympyrä', line: 'Viiva', serial: 'Sarjanro', image: 'Kuva', @@ -200,6 +201,14 @@ const fi = { colorB: 'B — Musta', colorW: 'W — Valkoinen', }, + circle: { + diameter: 'Halkaisija (pisteet)', + thickness: 'Reuna (pisteet)', + filled: 'Täytetty', + color: 'Väri', + colorB: 'B — Musta', + colorW: 'W — Valkoinen', + }, line: { angle: 'Kulma (°)', length: 'Pituus (pistettä)', diff --git a/src/locales/fr.ts b/src/locales/fr.ts index 9e7f086..6106944 100644 --- a/src/locales/fr.ts +++ b/src/locales/fr.ts @@ -19,6 +19,7 @@ const fr = { datamatrix: 'DataMatrix', box: 'Rectangle', ellipse: 'Ellipse', + circle: 'Cercle', line: 'Ligne', serial: 'Série', image: 'Image', @@ -200,6 +201,14 @@ const fr = { colorB: 'B — Noir', colorW: 'W — Blanc', }, + circle: { + diameter: 'Diamètre (points)', + thickness: 'Bordure (points)', + filled: 'Rempli', + color: 'Couleur', + colorB: 'B — Noir', + colorW: 'W — Blanc', + }, line: { angle: 'Angle (°)', length: 'Longueur (points)', diff --git a/src/locales/he.ts b/src/locales/he.ts index eb7d01d..470a1a4 100644 --- a/src/locales/he.ts +++ b/src/locales/he.ts @@ -19,6 +19,7 @@ const he = { datamatrix: 'DataMatrix', box: 'מלבן', ellipse: 'אליפסה', + circle: 'עיגול', line: 'קו', serial: 'מס. סידורי', image: 'תמונה', @@ -200,6 +201,14 @@ const he = { colorB: 'B — שחור', colorW: 'W — לבן', }, + circle: { + diameter: 'קוטר (נקודות)', + thickness: 'מסגרת (נקודות)', + filled: 'מלא', + color: 'צבע', + colorB: 'B — שחור', + colorW: 'W — לבן', + }, line: { angle: 'זווית (°)', length: 'אורך (נקודות)', diff --git a/src/locales/hr.ts b/src/locales/hr.ts index be9d2e4..7a3200e 100644 --- a/src/locales/hr.ts +++ b/src/locales/hr.ts @@ -19,6 +19,7 @@ const hr = { datamatrix: 'DataMatrix', box: 'Pravokutnik', ellipse: 'Elipsa', + circle: 'Krug', line: 'Linija', serial: 'Serijski br.', image: 'Slika', @@ -200,6 +201,14 @@ const hr = { colorB: 'B — Crna', colorW: 'W — Bijela', }, + circle: { + diameter: 'Promjer (točke)', + thickness: 'Obrub (točke)', + filled: 'Ispunjeno', + color: 'Boja', + colorB: 'B — Crno', + colorW: 'W — Bijelo', + }, line: { angle: 'Kut (°)', length: 'Duljina (točke)', diff --git a/src/locales/hu.ts b/src/locales/hu.ts index bdcc01c..aa851d0 100644 --- a/src/locales/hu.ts +++ b/src/locales/hu.ts @@ -19,6 +19,7 @@ const hu = { datamatrix: 'DataMatrix', box: 'Téglalap', ellipse: 'Ellipszis', + circle: 'Kör', line: 'Vonal', serial: 'Sorszám', image: 'Kép', @@ -200,6 +201,14 @@ const hu = { colorB: 'B — Fekete', colorW: 'W — Fehér', }, + 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 (°)', length: 'Hossz (pont)', diff --git a/src/locales/it.ts b/src/locales/it.ts index d5816a5..b65b58f 100644 --- a/src/locales/it.ts +++ b/src/locales/it.ts @@ -19,6 +19,7 @@ const it = { datamatrix: 'DataMatrix', box: 'Rettangolo', ellipse: 'Ellisse', + circle: 'Cerchio', line: 'Linea', serial: 'Seriale', image: 'Immagine', @@ -200,6 +201,14 @@ const it = { colorB: 'B — Nero', colorW: 'W — Bianco', }, + circle: { + diameter: 'Diametro (punti)', + thickness: 'Bordo (punti)', + filled: 'Riempito', + color: 'Colore', + colorB: 'B — Nero', + colorW: 'W — Bianco', + }, line: { angle: 'Angolo (°)', length: 'Lunghezza (punti)', diff --git a/src/locales/ja.ts b/src/locales/ja.ts index d5bcc6e..272ab62 100644 --- a/src/locales/ja.ts +++ b/src/locales/ja.ts @@ -19,6 +19,7 @@ const ja = { datamatrix: 'DataMatrix', box: '矩形', ellipse: '楕円', + circle: '円', line: '線', serial: 'シリアル', image: '画像', @@ -200,6 +201,14 @@ const ja = { colorB: 'B — 黒', colorW: 'W — 白', }, + circle: { + diameter: '直径 (ドット)', + thickness: '枠線 (ドット)', + filled: '塗りつぶし', + color: '色', + colorB: 'B — 黒', + colorW: 'W — 白', + }, line: { angle: '角度 (°)', length: '長さ (ドット)', diff --git a/src/locales/ko.ts b/src/locales/ko.ts index 857446c..f88fc60 100644 --- a/src/locales/ko.ts +++ b/src/locales/ko.ts @@ -19,6 +19,7 @@ const ko = { datamatrix: 'DataMatrix', box: '사각형', ellipse: '타원', + circle: '원', line: '선', serial: '일련번호', image: '이미지', @@ -200,6 +201,14 @@ const ko = { colorB: 'B — 검정', colorW: 'W — 흰색', }, + circle: { + diameter: '지름 (도트)', + thickness: '테두리 (도트)', + filled: '채움', + color: '색상', + colorB: 'B — 검정', + colorW: 'W — 흰색', + }, line: { angle: '각도 (°)', length: '길이 (점)', diff --git a/src/locales/lt.ts b/src/locales/lt.ts index 89ba678..baad524 100644 --- a/src/locales/lt.ts +++ b/src/locales/lt.ts @@ -19,6 +19,7 @@ const lt = { datamatrix: 'DataMatrix', box: 'Stačiakampis', ellipse: 'Elipsė', + circle: 'Apskritimas', line: 'Linija', serial: 'Serijinis nr.', image: 'Vaizdas', @@ -200,6 +201,14 @@ const lt = { colorB: 'B — Juoda', colorW: 'W — Balta', }, + circle: { + diameter: 'Skersmuo (taškai)', + thickness: 'Rėmelis (taškai)', + filled: 'Užpildytas', + color: 'Spalva', + colorB: 'B — Juoda', + colorW: 'W — Balta', + }, line: { angle: 'Kampas (°)', length: 'Ilgis (taškai)', diff --git a/src/locales/lv.ts b/src/locales/lv.ts index 5fe0c76..45b8367 100644 --- a/src/locales/lv.ts +++ b/src/locales/lv.ts @@ -19,6 +19,7 @@ const lv = { datamatrix: 'DataMatrix', box: 'Taisnstūris', ellipse: 'Elipse', + circle: 'Aplis', line: 'Līnija', serial: 'Sērijas nr.', image: 'Attēls', @@ -200,6 +201,14 @@ const lv = { colorB: 'B — Melna', colorW: 'W — Balta', }, + circle: { + diameter: 'Diametrs (punkti)', + thickness: 'Apmale (punkti)', + filled: 'Aizpildīts', + color: 'Krāsa', + colorB: 'B — Melns', + colorW: 'W — Balts', + }, line: { angle: 'Leņķis (°)', length: 'Garums (punkti)', diff --git a/src/locales/nl.ts b/src/locales/nl.ts index 0079d09..bdbc9ee 100644 --- a/src/locales/nl.ts +++ b/src/locales/nl.ts @@ -19,6 +19,7 @@ const nl = { datamatrix: 'DataMatrix', box: 'Rechthoek', ellipse: 'Ellips', + circle: 'Cirkel', line: 'Lijn', serial: 'Serienummer', image: 'Afbeelding', @@ -200,6 +201,14 @@ const nl = { colorB: 'B — Zwart', colorW: 'W — Wit', }, + circle: { + diameter: 'Diameter (dots)', + thickness: 'Rand (dots)', + filled: 'Gevuld', + color: 'Kleur', + colorB: 'B — Zwart', + colorW: 'W — Wit', + }, line: { angle: 'Hoek (°)', length: 'Lengte (punten)', diff --git a/src/locales/no.ts b/src/locales/no.ts index 0fbceff..6a5ae43 100644 --- a/src/locales/no.ts +++ b/src/locales/no.ts @@ -19,6 +19,7 @@ const no = { datamatrix: 'DataMatrix', box: 'Rektangel', ellipse: 'Ellipse', + circle: 'Sirkel', line: 'Linje', serial: 'Serienr.', image: 'Bilde', @@ -200,6 +201,14 @@ const no = { colorB: 'B — Svart', colorW: 'W — Hvit', }, + circle: { + diameter: 'Diameter (punkter)', + thickness: 'Ramme (punkter)', + filled: 'Fylt', + color: 'Farge', + colorB: 'B — Svart', + colorW: 'W — Hvit', + }, line: { angle: 'Vinkel (°)', length: 'Lengde (punkter)', diff --git a/src/locales/pl.ts b/src/locales/pl.ts index 955eadc..935a797 100644 --- a/src/locales/pl.ts +++ b/src/locales/pl.ts @@ -19,6 +19,7 @@ const pl = { datamatrix: 'DataMatrix', box: 'Prostokąt', ellipse: 'Elipsa', + circle: 'Okrąg', line: 'Linia', serial: 'Seria', image: 'Obraz', @@ -200,6 +201,14 @@ const pl = { colorB: 'B — Czarny', colorW: 'W — Biały', }, + circle: { + diameter: 'Średnica (punkty)', + thickness: 'Obramowanie (punkty)', + filled: 'Wypełniony', + color: 'Kolor', + colorB: 'B — Czarny', + colorW: 'W — Biały', + }, line: { angle: 'Kąt (°)', length: 'Długość (punkty)', diff --git a/src/locales/pt.ts b/src/locales/pt.ts index 60550b2..6c8ca50 100644 --- a/src/locales/pt.ts +++ b/src/locales/pt.ts @@ -19,6 +19,7 @@ const pt = { datamatrix: 'DataMatrix', box: 'Retângulo', ellipse: 'Elipse', + circle: 'Círculo', line: 'Linha', serial: 'Série', image: 'Imagem', @@ -200,6 +201,14 @@ const pt = { colorB: 'B — Preto', colorW: 'W — Branco', }, + circle: { + diameter: 'Diâmetro (pontos)', + thickness: 'Borda (pontos)', + filled: 'Preenchido', + color: 'Cor', + colorB: 'B — Preto', + colorW: 'W — Branco', + }, line: { angle: 'Ângulo (°)', length: 'Comprimento (pontos)', diff --git a/src/locales/ro.ts b/src/locales/ro.ts index 34eee4d..824624f 100644 --- a/src/locales/ro.ts +++ b/src/locales/ro.ts @@ -19,6 +19,7 @@ const ro = { datamatrix: 'DataMatrix', box: 'Dreptunghi', ellipse: 'Elipsă', + circle: 'Cerc', line: 'Linie', serial: 'Serie', image: 'Imagine', @@ -200,6 +201,14 @@ const ro = { colorB: 'B — Negru', colorW: 'W — Alb', }, + circle: { + diameter: 'Diametru (puncte)', + thickness: 'Bordură (puncte)', + filled: 'Umplut', + color: 'Culoare', + colorB: 'B — Negru', + colorW: 'W — Alb', + }, line: { angle: 'Unghi (°)', length: 'Lungime (puncte)', diff --git a/src/locales/sk.ts b/src/locales/sk.ts index 4390cce..32a047a 100644 --- a/src/locales/sk.ts +++ b/src/locales/sk.ts @@ -19,6 +19,7 @@ const sk = { datamatrix: 'DataMatrix', box: 'Obdĺžnik', ellipse: 'Elipsa', + circle: 'Kruh', line: 'Čiara', serial: 'Sériové číslo', image: 'Obrázok', @@ -200,6 +201,14 @@ const sk = { colorB: 'B — Čierna', colorW: 'W — Biela', }, + circle: { + diameter: 'Priemer (body)', + thickness: 'Okraj (body)', + filled: 'Vyplnený', + color: 'Farba', + colorB: 'B — Čierna', + colorW: 'W — Biela', + }, line: { angle: 'Uhol (°)', length: 'Dĺžka (body)', diff --git a/src/locales/sl.ts b/src/locales/sl.ts index fb7b886..587dcd9 100644 --- a/src/locales/sl.ts +++ b/src/locales/sl.ts @@ -19,6 +19,7 @@ const sl = { datamatrix: 'DataMatrix', box: 'Pravokotnik', ellipse: 'Elipsa', + circle: 'Krog', line: 'Črta', serial: 'Zaporedna št.', image: 'Slika', @@ -200,6 +201,14 @@ const sl = { colorB: 'B — Črna', colorW: 'W — Bela', }, + circle: { + diameter: 'Premer (točke)', + thickness: 'Obroba (točke)', + filled: 'Zapolnjeno', + color: 'Barva', + colorB: 'B — Črna', + colorW: 'W — Bela', + }, line: { angle: 'Kot (°)', length: 'Dolžina (točke)', diff --git a/src/locales/sr.ts b/src/locales/sr.ts index 1f9e3e1..37a5a23 100644 --- a/src/locales/sr.ts +++ b/src/locales/sr.ts @@ -19,6 +19,7 @@ const sr = { datamatrix: 'DataMatrix', box: 'Правоугаоник', ellipse: 'Елипса', + circle: 'Круг', line: 'Линија', serial: 'Серијски бр.', image: 'Слика', @@ -200,6 +201,14 @@ const sr = { colorB: 'B — Crna', colorW: 'W — Bela', }, + circle: { + diameter: 'Пречник (тачке)', + thickness: 'Оквир (тачке)', + filled: 'Испуњено', + color: 'Боја', + colorB: 'B — Црна', + colorW: 'W — Бела', + }, line: { angle: 'Ugao (°)', length: 'Dužina (tačke)', diff --git a/src/locales/sv.ts b/src/locales/sv.ts index 8a13cac..2650af1 100644 --- a/src/locales/sv.ts +++ b/src/locales/sv.ts @@ -19,6 +19,7 @@ const sv = { datamatrix: 'DataMatrix', box: 'Rektangel', ellipse: 'Ellips', + circle: 'Cirkel', line: 'Linje', serial: 'Serienr.', image: 'Bild', @@ -200,6 +201,14 @@ const sv = { colorB: 'B — Svart', colorW: 'W — Vit', }, + circle: { + diameter: 'Diameter (punkter)', + thickness: 'Kant (punkter)', + filled: 'Fylld', + color: 'Färg', + colorB: 'B — Svart', + colorW: 'W — Vit', + }, line: { angle: 'Vinkel (°)', length: 'Längd (punkter)', diff --git a/src/locales/tr.ts b/src/locales/tr.ts index d31a24d..13d43ff 100644 --- a/src/locales/tr.ts +++ b/src/locales/tr.ts @@ -19,6 +19,7 @@ const tr = { datamatrix: 'DataMatrix', box: 'Dikdörtgen', ellipse: 'Elips', + circle: 'Daire', line: 'Çizgi', serial: 'Seri No', image: 'Görsel', @@ -200,6 +201,14 @@ const tr = { colorB: 'B — Siyah', colorW: 'W — Beyaz', }, + circle: { + diameter: 'Çap (nokta)', + thickness: 'Kenarlık (nokta)', + filled: 'Dolu', + color: 'Renk', + colorB: 'B — Siyah', + colorW: 'W — Beyaz', + }, line: { angle: 'Açı (°)', length: 'Uzunluk (nokta)', diff --git a/src/locales/zh-hans.ts b/src/locales/zh-hans.ts index 37fa2b5..37c1586 100644 --- a/src/locales/zh-hans.ts +++ b/src/locales/zh-hans.ts @@ -19,6 +19,7 @@ const zhHans = { datamatrix: 'DataMatrix', box: '矩形', ellipse: '椭圆', + circle: '圆', line: '线条', serial: '序列号', image: '图片', @@ -200,6 +201,14 @@ const zhHans = { colorB: 'B — 黑色', colorW: 'W — 白色', }, + circle: { + diameter: '直径 (点)', + thickness: '边框 (点)', + filled: '填充', + color: '颜色', + colorB: 'B — 黑色', + colorW: 'W — 白色', + }, line: { angle: '角度 (°)', length: '长度 (点)', diff --git a/src/locales/zh-hant.ts b/src/locales/zh-hant.ts index 6cc82cc..48fae74 100644 --- a/src/locales/zh-hant.ts +++ b/src/locales/zh-hant.ts @@ -19,6 +19,7 @@ const zhHant = { datamatrix: 'DataMatrix', box: '矩形', ellipse: '橢圓', + circle: '圓', line: '線條', serial: '序號', image: '圖片', @@ -200,6 +201,14 @@ const zhHant = { colorB: 'B — 黑色', colorW: 'W — 白色', }, + circle: { + diameter: '直徑 (點)', + thickness: '邊框 (點)', + filled: '填充', + color: '顏色', + colorB: 'B — 黑色', + colorW: 'W — 白色', + }, line: { angle: '角度 (°)', length: '長度 (點)', diff --git a/src/registry/circle.tsx b/src/registry/circle.tsx new file mode 100644 index 0000000..6744137 --- /dev/null +++ b/src/registry/circle.tsx @@ -0,0 +1,95 @@ +import type { ObjectTypeDefinition } from '../types/ObjectType'; +import { useT } from '../lib/useT'; +import { inputCls, labelCls } from '../components/Properties/styles'; +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 }, + nodeOrigin: 'center', + + // 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: Number(e.target.value) })} + /> +
+ + + + {!p.filled && ( +
+ + onChange({ thickness: Number(e.target.value) })} + /> +
+ )} + +
+ + +
+
+ ); + }, +}; diff --git a/src/registry/index.ts b/src/registry/index.ts index a158a9b..be9c6d7 100644 --- a/src/registry/index.ts +++ b/src/registry/index.ts @@ -15,6 +15,8 @@ import { box } from './box.tsx'; import type { BoxProps } from './box.tsx'; import { ellipse } from './ellipse.tsx'; import type { EllipseProps } from './ellipse.tsx'; +import { circle } from './circle.tsx'; +import type { CircleProps } from './circle.tsx'; import { line } from './line.tsx'; import type { LineProps } from './line.tsx'; import { serial } from './serial.tsx'; @@ -69,6 +71,7 @@ export type LabelObject = | (LabelObjectBase & { type: 'datamatrix'; props: DataMatrixProps }) | (LabelObjectBase & { type: 'box'; props: BoxProps }) | (LabelObjectBase & { type: 'ellipse'; props: EllipseProps }) + | (LabelObjectBase & { type: 'circle'; props: CircleProps }) | (LabelObjectBase & { type: 'line'; props: LineProps }) | (LabelObjectBase & { type: 'serial'; props: SerialProps }) | (LabelObjectBase & { type: 'image'; props: ImageProps }) @@ -134,6 +137,7 @@ export const ObjectRegistry: Record> = { // shape box, ellipse, + circle, line, serial, image, diff --git a/src/registry/registry.test.ts b/src/registry/registry.test.ts index 0574a57..bef3af2 100644 --- a/src/registry/registry.test.ts +++ b/src/registry/registry.test.ts @@ -170,6 +170,46 @@ describe('ellipse.toZPL', () => { }); }); +// ── circle ──────────────────────────────────────────────────────────────────── + +describe('circle.toZPL', () => { + const def = defined(ObjectRegistry['circle']); + + it('emits ^GE with diameter for both axes', () => { + const zpl = def.toZPL(makeObj('circle', { + diameter: 80, thickness: 3, filled: false, color: 'B', + })); + expect(zpl).toContain('^GE80,80,3,B'); + }); + + it('uses diameter as thickness when filled', () => { + const zpl = def.toZPL(makeObj('circle', { + diameter: 80, thickness: 3, filled: true, color: 'B', + })); + expect(zpl).toContain('^GE80,80,80,B'); + }); +}); + +describe('circle.commitTransform', () => { + const def = defined(ObjectRegistry['circle']); + + it('uses the smaller scale axis to keep the circle inside the drag box', () => { + const result = def.commitTransform!( + makeObj('circle', { diameter: 100, thickness: 3, filled: false, color: 'B' }), + { sx: 2, sy: 1.5, snap: (n) => n, nodeHeight: 0, anchor: null }, + ); + expect(result).toEqual({ diameter: 150 }); + }); + + it('clamps the diameter to at least 1', () => { + const result = def.commitTransform!( + makeObj('circle', { diameter: 100, thickness: 3, filled: false, color: 'B' }), + { sx: 0, sy: 0, snap: (n) => n, nodeHeight: 0, anchor: null }, + ); + expect(result).toEqual({ diameter: 1 }); + }); +}); + // ── code128 ─────────────────────────────────────────────────────────────────── describe('code128.toZPL', () => { @@ -352,7 +392,7 @@ describe('ObjectRegistry', () => { const expectedTypes = [ 'text', 'code128', 'code39', 'ean13', 'upca', 'ean8', 'upce', 'interleaved2of5', 'code93', 'qrcode', 'datamatrix', 'pdf417', - 'box', 'ellipse', 'line', 'serial', 'image', + 'box', 'ellipse', 'circle', 'line', 'serial', 'image', ]; it('contains all expected object types', () => { From 7b01adf04cdbe3af499dbbc31f604e245f75fe85 Mon Sep 17 00:00:00 2001 From: u8array Date: Thu, 7 May 2026 21:41:28 +0200 Subject: [PATCH 2/3] feat(canvas): uniform-scale flag enforces square resize for circle Adds an optional uniformScale flag on ObjectTypeDefinition. When set, the transformer restricts to corner anchors and forceSquareBox clamps the resize bbox to the larger of the two axes while pinning the inferred anchor corner. Visual feedback during drag now matches the uniform commitTransform applied on release; circle is the first consumer. forceSquareBox lives in transformerGeometry next to its sibling helpers and has unit coverage for all four corner anchors. --- .../Canvas/hooks/useKonvaTransformer.ts | 7 +++- .../Canvas/transformerGeometry.test.ts | 35 +++++++++++++++++++ src/components/Canvas/transformerGeometry.ts | 17 +++++++++ src/registry/circle.tsx | 1 + src/types/ObjectType.ts | 7 ++++ 5 files changed, 66 insertions(+), 1 deletion(-) diff --git a/src/components/Canvas/hooks/useKonvaTransformer.ts b/src/components/Canvas/hooks/useKonvaTransformer.ts index 3644277..cb891cb 100644 --- a/src/components/Canvas/hooks/useKonvaTransformer.ts +++ b/src/components/Canvas/hooks/useKonvaTransformer.ts @@ -11,6 +11,7 @@ import { isTopAnchorResize, transformNodeTopLeft, positionDidMove, + forceSquareBox, type BoundingBox, } from "../transformerGeometry"; import { modelPositionFromRenderedTopLeft } from "../transformPosition"; @@ -194,6 +195,7 @@ export function useKonvaTransformer({ selectedIds.length === 1 ? objects.find((o) => o.id === selectedIds[0])?.type ?? "" : ""; + const isUniformScale = !!ObjectRegistry[singleType]?.uniformScale; const enabledAnchors: string[] | undefined = selectedIds.length > 1 ? [] @@ -201,7 +203,9 @@ export function useKonvaTransformer({ ? [] : BARCODE_1D_TYPES.has(singleType) ? ["top-center", "bottom-center"] - : undefined; + : isUniformScale + ? ["top-left", "top-right", "bottom-left", "bottom-right"] + : undefined; const isFreeResize = enabledAnchors === undefined; /** Reset all transform-time state. Idempotent; safe to call from any exit path. */ @@ -228,6 +232,7 @@ export function useKonvaTransformer({ const boundBoxFunc = (oldBox: BoundingBox, newBox: BoundingBox): BoundingBox => { if (newBox.width < 10 || newBox.height < 10) return oldBox; + if (isUniformScale) newBox = forceSquareBox(oldBox, newBox); const dotPx = scale / dpmm; let bbox = applyHeightSnap(oldBox, newBox, dotPx, transformAnchorRef.current); diff --git a/src/components/Canvas/transformerGeometry.test.ts b/src/components/Canvas/transformerGeometry.test.ts index 044d8c5..834fe77 100644 --- a/src/components/Canvas/transformerGeometry.test.ts +++ b/src/components/Canvas/transformerGeometry.test.ts @@ -5,6 +5,7 @@ import { isTopAnchorResize, transformNodeTopLeft, positionDidMove, + forceSquareBox, } from "./transformerGeometry"; describe("snapBoxHeight", () => { @@ -93,3 +94,37 @@ describe("positionDidMove", () => { expect(positionDidMove(80, 100)).toBe(true); }); }); + +describe("forceSquareBox", () => { + const oldBox = { x: 100, y: 100, width: 50, height: 50, rotation: 0 }; + + it("clamps to max axis when dragging the bottom-right corner", () => { + const newBox = { x: 100, y: 100, width: 80, height: 60, rotation: 0 }; + expect(forceSquareBox(oldBox, newBox)).toEqual({ + x: 100, y: 100, width: 80, height: 80, rotation: 0, + }); + }); + + it("pins the bottom-right corner when dragging the top-left", () => { + const newBox = { x: 70, y: 80, width: 80, height: 70, rotation: 0 }; + // Bottom-right of oldBox = (150, 150). Square of size 80 must end there. + expect(forceSquareBox(oldBox, newBox)).toEqual({ + x: 70, y: 70, width: 80, height: 80, rotation: 0, + }); + }); + + it("pins the bottom-left corner when dragging the top-right", () => { + const newBox = { x: 100, y: 80, width: 70, height: 70, rotation: 0 }; + expect(forceSquareBox(oldBox, newBox)).toEqual({ + x: 100, y: 80, width: 70, height: 70, rotation: 0, + }); + }); + + it("pins the top-right corner when dragging the bottom-left", () => { + const newBox = { x: 80, y: 100, width: 70, height: 70, rotation: 0 }; + // Top-right of oldBox = (150, 100). Square of size 70 stays there. + expect(forceSquareBox(oldBox, newBox)).toEqual({ + x: 80, y: 100, width: 70, height: 70, rotation: 0, + }); + }); +}); diff --git a/src/components/Canvas/transformerGeometry.ts b/src/components/Canvas/transformerGeometry.ts index 1d554c0..8f0f85b 100644 --- a/src/components/Canvas/transformerGeometry.ts +++ b/src/components/Canvas/transformerGeometry.ts @@ -11,6 +11,23 @@ export function snapBoxHeight(height: number, stepPx: number): number { return Math.max(stepPx, Math.round(height / stepPx) * stepPx); } +/** + * Forces newBox to be square while keeping the anchor corner pinned. + * + * Konva does not expose the active anchor to boundBoxFunc, so it is inferred + * from which oldBox edges moved: an edge that did not move is the pinned + * side. The new size is the larger of the two requested deltas, so either + * axis the user pulls drives the resize. + */ +export function forceSquareBox(oldBox: BoundingBox, newBox: BoundingBox): BoundingBox { + const leftMoved = Math.abs(newBox.x - oldBox.x) > 0.001; + const topMoved = Math.abs(newBox.y - oldBox.y) > 0.001; + const size = Math.max(Math.abs(newBox.width), Math.abs(newBox.height)); + const x = leftMoved ? oldBox.x + oldBox.width - size : oldBox.x; + const y = topMoved ? oldBox.y + oldBox.height - size : oldBox.y; + return { ...newBox, x, y, width: size, height: size }; +} + /** * Adjust newBox so its bottom edge stays at oldBox's bottom (top-anchor resize) * with a height of snappedH. Used when the user drags the top handle. diff --git a/src/registry/circle.tsx b/src/registry/circle.tsx index 6744137..1293835 100644 --- a/src/registry/circle.tsx +++ b/src/registry/circle.tsx @@ -22,6 +22,7 @@ export const circle: ObjectTypeDefinition = { }, defaultSize: { width: 100, height: 100 }, nodeOrigin: 'center', + 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. diff --git a/src/types/ObjectType.ts b/src/types/ObjectType.ts index 929d18f..dc89108 100644 --- a/src/types/ObjectType.ts +++ b/src/types/ObjectType.ts @@ -77,6 +77,13 @@ export interface ObjectTypeDefinition

{ * renderer suppresses the text so the designer matches the print output. */ interpretationLocked?: boolean; + /** + * True if the shape requires a 1:1 aspect ratio (e.g. circle: a single + * diameter). The transformer restricts to corner anchors and forces the + * resize bbox to stay square so visual feedback during drag matches the + * uniform `commitTransform` applied on release. + */ + uniformScale?: boolean; toZPL: (obj: LabelObjectBase & { props: P }) => string; /** * Optional hook to enforce type-specific invariants on incoming changes From 4d97c7ea20593b385b7179dfe2146820762a6189 Mon Sep 17 00:00:00 2001 From: u8array Date: Thu, 7 May 2026 22:02:00 +0200 Subject: [PATCH 3/3] refactor(input): extract clampMin helper for shape number inputs Adds src/lib/inputParse.ts with clampMin and parseIntOrUndef. clampMin replaces the bare `Number(e.target.value)` pattern in the shape PropertiesPanels (box, ellipse, circle, line) so an empty or sub-floor input no longer collapses dimensions to 0. parseIntOrUndef moves out of PropertiesPanel.tsx into the shared module since it has the same sanitisation purpose. Other registries keep their unclamped pattern for now; pulling them in is independent and can happen organically as files get touched. --- src/components/Properties/PropertiesPanel.tsx | 7 +-- src/lib/inputParse.test.ts | 57 +++++++++++++++++++ src/lib/inputParse.ts | 29 ++++++++++ src/registry/box.tsx | 9 +-- src/registry/circle.tsx | 5 +- src/registry/ellipse.tsx | 7 ++- src/registry/line.tsx | 5 +- 7 files changed, 102 insertions(+), 17 deletions(-) create mode 100644 src/lib/inputParse.test.ts create mode 100644 src/lib/inputParse.ts diff --git a/src/components/Properties/PropertiesPanel.tsx b/src/components/Properties/PropertiesPanel.tsx index 4b6f359..9dd9f4d 100644 --- a/src/components/Properties/PropertiesPanel.tsx +++ b/src/components/Properties/PropertiesPanel.tsx @@ -11,6 +11,7 @@ import { } from "../../lib/units"; import type { Unit } from "../../lib/units"; import { useT } from "../../lib/useT"; +import { parseIntOrUndef } from "../../lib/inputParse"; import { inputCls, labelCls } from "./styles"; import type { LabelConfig } from "../../types/ObjectType"; @@ -464,9 +465,3 @@ function LabelConfigPanel({ ); } - -function parseIntOrUndef(raw: string): number | undefined { - if (raw.trim() === "") return undefined; - const n = parseInt(raw, 10); - return isNaN(n) ? undefined : n; -} diff --git a/src/lib/inputParse.test.ts b/src/lib/inputParse.test.ts new file mode 100644 index 0000000..5c186a0 --- /dev/null +++ b/src/lib/inputParse.test.ts @@ -0,0 +1,57 @@ +import { describe, it, expect } from 'vitest'; +import { parseIntOrUndef, clampMin } from './inputParse'; + +describe('parseIntOrUndef', () => { + it('returns undefined for empty input', () => { + expect(parseIntOrUndef('')).toBeUndefined(); + expect(parseIntOrUndef(' ')).toBeUndefined(); + }); + + it('returns undefined for unparsable input', () => { + expect(parseIntOrUndef('abc')).toBeUndefined(); + }); + + it('parses positive integers', () => { + expect(parseIntOrUndef('42')).toBe(42); + }); + + it('parses negative integers', () => { + expect(parseIntOrUndef('-7')).toBe(-7); + }); + + it('preserves 0 as a valid value', () => { + expect(parseIntOrUndef('0')).toBe(0); + }); + + it('truncates fractional input toward zero', () => { + expect(parseIntOrUndef('3.7')).toBe(3); + }); +}); + +describe('clampMin', () => { + it('returns the parsed value when above min', () => { + expect(clampMin('5', 1)).toBe(5); + }); + + it('returns min when input is empty', () => { + expect(clampMin('', 1)).toBe(1); + }); + + it('returns min when input is below the floor', () => { + expect(clampMin('0', 1)).toBe(1); + expect(clampMin('-3', 1)).toBe(1); + }); + + it('returns min when input is unparsable', () => { + expect(clampMin('abc', 1)).toBe(1); + }); + + it('preserves fractional inputs above the floor', () => { + expect(clampMin('2.5', 1)).toBe(2.5); + }); + + it('respects custom floors other than 1', () => { + expect(clampMin('5', 10)).toBe(10); + expect(clampMin('15', 10)).toBe(15); + }); +}); diff --git a/src/lib/inputParse.ts b/src/lib/inputParse.ts new file mode 100644 index 0000000..5f53602 --- /dev/null +++ b/src/lib/inputParse.ts @@ -0,0 +1,29 @@ +/** + * Helpers for sanitising raw `` values into typed model fields. + * + * `` enforces nothing on the value the change + * handler receives — `min` is only a UI hint and `Number("")` collapses to 0. + * These helpers give callers a one-liner that turns the raw string into + * a value the model can safely accept. + */ + +/** + * Parses an integer from a raw input value, returning `undefined` when the + * field is empty or unparsable. Use for optional number fields where + * "absent" is a valid persisted state. + */ +export function parseIntOrUndef(raw: string): number | undefined { + if (raw.trim() === '') return undefined; + const n = parseInt(raw, 10); + return isNaN(n) ? undefined : n; +} + +/** + * Parses a number from a raw input value and clamps it to at least `min`. + * Empty / NaN / sub-min inputs collapse to `min`. Use for required number + * fields that need a hard lower bound (shape dimensions, line widths). + */ +export function clampMin(raw: string, min: number): number { + const n = Number(raw); + return isNaN(n) || n < min ? min : n; +} diff --git a/src/registry/box.tsx b/src/registry/box.tsx index b55ff42..1c56847 100644 --- a/src/registry/box.tsx +++ b/src/registry/box.tsx @@ -3,6 +3,7 @@ import { useT } from '../lib/useT'; import { inputCls, labelCls } from '../components/Properties/styles'; import { fieldPos } from './zplHelpers'; import { commitWidthHeightTransform } from './transformHelpers'; +import { clampMin } from '../lib/inputParse'; export interface BoxProps { width: number; @@ -55,7 +56,7 @@ export const box: ObjectTypeDefinition = { className={inputCls} value={p.width} min={1} - onChange={(e) => onChange({ width: Number(e.target.value) })} + onChange={(e) => onChange({ width: clampMin(e.target.value, 1) })} />

@@ -65,7 +66,7 @@ export const box: ObjectTypeDefinition = { className={inputCls} value={p.height} min={1} - onChange={(e) => onChange({ height: Number(e.target.value) })} + onChange={(e) => onChange({ height: clampMin(e.target.value, 1) })} />
@@ -88,7 +89,7 @@ export const box: ObjectTypeDefinition = { className={inputCls} value={p.thickness} min={1} - onChange={(e) => onChange({ thickness: Number(e.target.value) })} + onChange={(e) => onChange({ thickness: clampMin(e.target.value, 1) })} /> )} @@ -113,7 +114,7 @@ export const box: ObjectTypeDefinition = { value={p.rounding} min={0} max={8} - onChange={(e) => onChange({ rounding: Number(e.target.value) })} + onChange={(e) => onChange({ rounding: clampMin(e.target.value, 0) })} /> diff --git a/src/registry/circle.tsx b/src/registry/circle.tsx index 1293835..e574850 100644 --- a/src/registry/circle.tsx +++ b/src/registry/circle.tsx @@ -1,5 +1,6 @@ import type { ObjectTypeDefinition } from '../types/ObjectType'; import { useT } from '../lib/useT'; +import { clampMin } from '../lib/inputParse'; import { inputCls, labelCls } from '../components/Properties/styles'; import { fieldPos } from './zplHelpers'; @@ -52,7 +53,7 @@ export const circle: ObjectTypeDefinition = { className={inputCls} value={p.diameter} min={1} - onChange={(e) => onChange({ diameter: Number(e.target.value) })} + onChange={(e) => onChange({ diameter: clampMin(e.target.value, 1) })} /> @@ -74,7 +75,7 @@ export const circle: ObjectTypeDefinition = { className={inputCls} value={p.thickness} min={1} - onChange={(e) => onChange({ thickness: Number(e.target.value) })} + onChange={(e) => onChange({ thickness: clampMin(e.target.value, 1) })} /> )} diff --git a/src/registry/ellipse.tsx b/src/registry/ellipse.tsx index 3cd4d42..e5835c1 100644 --- a/src/registry/ellipse.tsx +++ b/src/registry/ellipse.tsx @@ -3,6 +3,7 @@ import { useT } from '../lib/useT'; import { inputCls, labelCls } from '../components/Properties/styles'; import { fieldPos } from './zplHelpers'; import { commitWidthHeightTransform } from './transformHelpers'; +import { clampMin } from '../lib/inputParse'; export interface EllipseProps { width: number; @@ -51,7 +52,7 @@ export const ellipse: ObjectTypeDefinition = { className={inputCls} value={p.width} min={1} - onChange={(e) => onChange({ width: Number(e.target.value) })} + onChange={(e) => onChange({ width: clampMin(e.target.value, 1) })} />
@@ -61,7 +62,7 @@ export const ellipse: ObjectTypeDefinition = { className={inputCls} value={p.height} min={1} - onChange={(e) => onChange({ height: Number(e.target.value) })} + onChange={(e) => onChange({ height: clampMin(e.target.value, 1) })} />
@@ -84,7 +85,7 @@ export const ellipse: ObjectTypeDefinition = { className={inputCls} value={p.thickness} min={1} - onChange={(e) => onChange({ thickness: Number(e.target.value) })} + onChange={(e) => onChange({ thickness: clampMin(e.target.value, 1) })} /> )} diff --git a/src/registry/line.tsx b/src/registry/line.tsx index cc9a06e..03fc9e3 100644 --- a/src/registry/line.tsx +++ b/src/registry/line.tsx @@ -1,5 +1,6 @@ import type { ObjectTypeDefinition } from '../types/ObjectType'; import { useT } from '../lib/useT'; +import { clampMin } from '../lib/inputParse'; import { inputCls, labelCls } from '../components/Properties/styles'; export interface LineProps { @@ -70,7 +71,7 @@ export const line: ObjectTypeDefinition = { className={inputCls} value={p.length} min={1} - onChange={(e) => onChange({ length: Number(e.target.value) })} + onChange={(e) => onChange({ length: clampMin(e.target.value, 1) })} />
@@ -93,7 +94,7 @@ export const line: ObjectTypeDefinition = { className={inputCls} value={p.thickness} min={1} - onChange={(e) => onChange({ thickness: Number(e.target.value) })} + onChange={(e) => onChange({ thickness: clampMin(e.target.value, 1) })} />