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 130ff87f..44ff1492 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 7c2c52d1..7f1cbc9c 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 af9ac738..c61e9c18 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 032386b1..a1dacb1f 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 83b16463..2a171b9e 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 0313d557..951f389d 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 94ce4ced..f6505c36 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 1edb0d32..5f2d53f2 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 946509b1..fc240236 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 57947bfb..fd118a7d 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 b2d1dcce..10aa70aa 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 51fd3101..d944d4ad 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 9e7f086b..61069440 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 eb7d01d8..470a1a49 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 be9d2e4c..7a3200e4 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 bdcc01ce..aa851d00 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 d5816a5c..b65b58f5 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 d5bcc6e8..272ab627 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 857446c9..f88fc60c 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 89ba678a..baad5248 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 5fe0c76c..45b83679 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 0079d09c..bdbc9ee0 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 0fbceff6..6a5ae433 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 955eadc3..935a7972 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 60550b23..6c8ca50f 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 34eee4d9..824624fd 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 4390cceb..32a047a0 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 fb7b8866..587dcd9c 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 1f9e3e1b..37a5a23f 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 8a13cac4..2650af14 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 d31a24d8..13d43ff7 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 37fa2b55..37c1586e 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 6cc82cc4..48fae74b 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 00000000..67441371 --- /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 a158a9b4..be9c6d77 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 0574a576..bef3af2b 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 3644277e..cb891cb7 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 044d8c5a..834fe77d 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 1d554c03..8f0f85bf 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 67441371..1293835a 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 929d18ff..dc89108f 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 4b6f3592..9dd9f4d1 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 00000000..5c186a01 --- /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 00000000..5f53602b --- /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 b55ff420..1c56847c 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 1293835a..e5748502 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 3cd4d427..e5835c1e 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 cc9a06e1..03fc9e3b 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) })} />