From d5de798f8a4653be18b05e0c87a16d13a9c97bc3 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Wed, 14 Oct 2026 12:24:32 +0300 Subject: [PATCH] Options: snap marker size (#1770) The object-snap glyphs were fixed at 5 px; a Drafting slider now scales them from 1 to 20 px. Co-Authored-By: Claude Opus 5.5 --- locales/ar-SA/opencadstudio.ftl | 1 + locales/bg-BG/opencadstudio.ftl | 1 + locales/cs-CZ/opencadstudio.ftl | 1 + locales/de-DE/opencadstudio.ftl | 1 + locales/el-GR/opencadstudio.ftl | 1 + locales/en-US/opencadstudio.ftl | 1 + locales/es-ES/opencadstudio.ftl | 1 + locales/fi-FI/opencadstudio.ftl | 1 + locales/fr-FR/opencadstudio.ftl | 1 + locales/hi-IN/opencadstudio.ftl | 1 + locales/hu-HU/opencadstudio.ftl | 1 + locales/it-IT/opencadstudio.ftl | 1 + locales/ja-JP/opencadstudio.ftl | 1 + locales/ko-KR/opencadstudio.ftl | 1 + locales/lo-LA/opencadstudio.ftl | 1 + locales/nl-NL/opencadstudio.ftl | 1 + locales/pl-PL/opencadstudio.ftl | 1 + locales/pt-BR/opencadstudio.ftl | 1 + locales/ru-RU/opencadstudio.ftl | 1 + locales/tr-TR/opencadstudio.ftl | 1 + locales/zh-CN/opencadstudio.ftl | 1 + locales/zh-TW/opencadstudio.ftl | 1 + src/app/config.rs | 3 + src/app/mod.rs | 2 + src/app/update/mod.rs | 6 + src/app/view/mod.rs | 1 + src/locale_catalog.rs | 1 + src/ui/overlay.rs | 639 ++++++++++++++++---------------- src/ui/window/options.rs | 16 + 29 files changed, 375 insertions(+), 315 deletions(-) diff --git a/locales/ar-SA/opencadstudio.ftl b/locales/ar-SA/opencadstudio.ftl index 59eea44e9..58d54f9a0 100644 --- a/locales/ar-SA/opencadstudio.ftl +++ b/locales/ar-SA/opencadstudio.ftl @@ -2444,6 +2444,7 @@ common = .load-linetypes = تحميل أنواع الخطوط .requires-two-positive-values = يتطلب قيمتين موجبتين (x,y) أو قيمة واحدة لكليهما. .snap-marker-color = لون علامة الالتقاط + .snap-marker-size = حجم علامة الالتقاط .command-line-text-color = لون نص سطر الأوامر .operand = المعامل .linetypes-loaded = تم تحميل __ocs_fmt_0__ من أنواع الخطوط. diff --git a/locales/bg-BG/opencadstudio.ftl b/locales/bg-BG/opencadstudio.ftl index 3a4978879..acb1ac860 100644 --- a/locales/bg-BG/opencadstudio.ftl +++ b/locales/bg-BG/opencadstudio.ftl @@ -2431,6 +2431,7 @@ common = .load-linetypes = Зареждане на типове линии .requires-two-positive-values = Изискват се две положителни стойности (x,y) или една за двете. .snap-marker-color = Цвят на маркера за прихващане + .snap-marker-size = Размер на маркера за прихващане .command-line-text-color = Цвят на текста в командния ред .operand = Операнд .linetypes-loaded = Заредени типове линии: __ocs_fmt_0__. diff --git a/locales/cs-CZ/opencadstudio.ftl b/locales/cs-CZ/opencadstudio.ftl index e24946d23..c6f6151c8 100644 --- a/locales/cs-CZ/opencadstudio.ftl +++ b/locales/cs-CZ/opencadstudio.ftl @@ -2431,6 +2431,7 @@ common = .load-linetypes = Načíst typy čar .requires-two-positive-values = Vyžaduje dvě kladné hodnoty (x,y) nebo jednu pro obě. .snap-marker-color = Barva značky uchopení + .snap-marker-size = Velikost značky uchopení .command-line-text-color = Barva textu příkazového řádku .operand = Operand .linetypes-loaded = Načteno typů čar: __ocs_fmt_0__. diff --git a/locales/de-DE/opencadstudio.ftl b/locales/de-DE/opencadstudio.ftl index 4b6dc2379..9686e0aaf 100644 --- a/locales/de-DE/opencadstudio.ftl +++ b/locales/de-DE/opencadstudio.ftl @@ -2432,6 +2432,7 @@ common = .load-linetypes = Linientypen laden .requires-two-positive-values = Erfordert zwei positive Werte (x,y) oder einen für beide. .snap-marker-color = Farbe der Fangmarkierung + .snap-marker-size = Größe der Fangmarkierung .command-line-text-color = Textfarbe der Befehlszeile .operand = Operand .linetypes-loaded = __ocs_fmt_0__ Linientyp(en) geladen. diff --git a/locales/el-GR/opencadstudio.ftl b/locales/el-GR/opencadstudio.ftl index 22723a70a..26eca339a 100644 --- a/locales/el-GR/opencadstudio.ftl +++ b/locales/el-GR/opencadstudio.ftl @@ -2431,6 +2431,7 @@ common = .load-linetypes = Φόρτωση τύπων γραμμής .requires-two-positive-values = Απαιτούνται δύο θετικές τιμές (x,y) ή μία και για τις δύο. .snap-marker-color = Χρώμα δείκτη έλξης + .snap-marker-size = Μέγεθος δείκτη έλξης .command-line-text-color = Χρώμα κειμένου γραμμής εντολών .operand = Τελεστέος .linetypes-loaded = Φορτώθηκαν __ocs_fmt_0__ τύποι γραμμής. diff --git a/locales/en-US/opencadstudio.ftl b/locales/en-US/opencadstudio.ftl index 48ed03b5e..0f4734280 100644 --- a/locales/en-US/opencadstudio.ftl +++ b/locales/en-US/opencadstudio.ftl @@ -2432,6 +2432,7 @@ common = .load-linetypes = Load Linetypes .requires-two-positive-values = Requires two positive values (x,y) or one for both. .snap-marker-color = Snap marker color + .snap-marker-size = Snap marker size .command-line-text-color = Command line text color .operand = Operand .linetypes-loaded = __ocs_fmt_0__ linetype(s) loaded. diff --git a/locales/es-ES/opencadstudio.ftl b/locales/es-ES/opencadstudio.ftl index 5d780d57e..4ae2ed045 100644 --- a/locales/es-ES/opencadstudio.ftl +++ b/locales/es-ES/opencadstudio.ftl @@ -2432,6 +2432,7 @@ common = .load-linetypes = Cargar tipos de línea .requires-two-positive-values = Requiere dos valores positivos (x,y) o uno para ambos. .snap-marker-color = Color del marcador de referencia + .snap-marker-size = Tamaño del marcador de referencia .command-line-text-color = Color del texto de la línea de comandos .operand = Operando .linetypes-loaded = __ocs_fmt_0__ tipo(s) de línea cargado(s). diff --git a/locales/fi-FI/opencadstudio.ftl b/locales/fi-FI/opencadstudio.ftl index 4784f7e19..950164de4 100644 --- a/locales/fi-FI/opencadstudio.ftl +++ b/locales/fi-FI/opencadstudio.ftl @@ -2431,6 +2431,7 @@ common = .load-linetypes = Lataa viivatyypit .requires-two-positive-values = Vaatii kaksi positiivista arvoa (x,y) tai yhden molemmille. .snap-marker-color = Kohdistusmerkin väri + .snap-marker-size = Kohdistusmerkin koko .command-line-text-color = Komentorivin tekstin väri .operand = Operandi .linetypes-loaded = Ladattu __ocs_fmt_0__ viivatyyppiä. diff --git a/locales/fr-FR/opencadstudio.ftl b/locales/fr-FR/opencadstudio.ftl index 83bc2092e..8b24070bd 100644 --- a/locales/fr-FR/opencadstudio.ftl +++ b/locales/fr-FR/opencadstudio.ftl @@ -2432,6 +2432,7 @@ common = .load-linetypes = Charger des types de ligne .requires-two-positive-values = Nécessite deux valeurs positives (x,y) ou une seule pour les deux. .snap-marker-color = Couleur du marqueur d'accrochage + .snap-marker-size = Taille du marqueur d'accrochage .command-line-text-color = Couleur du texte de la ligne de commande .operand = Opérande .linetypes-loaded = __ocs_fmt_0__ type(s) de ligne chargé(s). diff --git a/locales/hi-IN/opencadstudio.ftl b/locales/hi-IN/opencadstudio.ftl index e1fce019f..0aab05d14 100644 --- a/locales/hi-IN/opencadstudio.ftl +++ b/locales/hi-IN/opencadstudio.ftl @@ -2420,6 +2420,7 @@ common = .load-linetypes = लाइनटाइप लोड करें .requires-two-positive-values = दो धनात्मक मान (x,y) या दोनों के लिए एक मान आवश्यक है। .snap-marker-color = स्नैप मार्कर का रंग + .snap-marker-size = स्नैप मार्कर का आकार .command-line-text-color = कमांड लाइन टेक्स्ट रंग .operand = ऑपरेंड .linetypes-loaded = __ocs_fmt_0__ लाइनटाइप लोड हुए। diff --git a/locales/hu-HU/opencadstudio.ftl b/locales/hu-HU/opencadstudio.ftl index 3f333ca15..4565836cd 100644 --- a/locales/hu-HU/opencadstudio.ftl +++ b/locales/hu-HU/opencadstudio.ftl @@ -2437,6 +2437,7 @@ common = .load-linetypes = Vonaltípusok betöltése .requires-two-positive-values = Két pozitív érték (x,y) vagy egy közös érték szükséges. .snap-marker-color = Tárgyraszterjel színe + .snap-marker-size = Tárgyraszterjel mérete .command-line-text-color = Parancssor szövegszíne .operand = Operandus .linetypes-loaded = __ocs_fmt_0__ vonaltípus betöltve. diff --git a/locales/it-IT/opencadstudio.ftl b/locales/it-IT/opencadstudio.ftl index 9d5c0f82d..9f8ace95c 100644 --- a/locales/it-IT/opencadstudio.ftl +++ b/locales/it-IT/opencadstudio.ftl @@ -2431,6 +2431,7 @@ common = .load-linetypes = Carica tipi di linea .requires-two-positive-values = Richiede due valori positivi (x,y) o uno per entrambi. .snap-marker-color = Colore del marcatore di snap + .snap-marker-size = Dimensione del marcatore di snap .command-line-text-color = Colore del testo della riga di comando .operand = Operando .linetypes-loaded = __ocs_fmt_0__ tipo/i di linea caricato/i. diff --git a/locales/ja-JP/opencadstudio.ftl b/locales/ja-JP/opencadstudio.ftl index 0efcad0f9..6703c8b44 100644 --- a/locales/ja-JP/opencadstudio.ftl +++ b/locales/ja-JP/opencadstudio.ftl @@ -2422,6 +2422,7 @@ common = .load-linetypes = 線種をロード .requires-two-positive-values = 2つの正の値 (x,y)、または両方に共通の1つの値が必要です。 .snap-marker-color = スナップ マーカーの色 + .snap-marker-size = スナップ マーカーのサイズ .command-line-text-color = コマンドラインの文字色 .operand = オペランド .linetypes-loaded = __ocs_fmt_0__ 個の線種をロードしました。 diff --git a/locales/ko-KR/opencadstudio.ftl b/locales/ko-KR/opencadstudio.ftl index 1c74a5a48..b9bf8d88f 100644 --- a/locales/ko-KR/opencadstudio.ftl +++ b/locales/ko-KR/opencadstudio.ftl @@ -2431,6 +2431,7 @@ common = .load-linetypes = 선종류 로드 .requires-two-positive-values = 양수 값 두 개(x,y) 또는 둘 다에 쓸 값 하나가 필요합니다. .snap-marker-color = 스냅 마커 색상 + .snap-marker-size = 스냅 마커 크기 .command-line-text-color = 명령줄 텍스트 색상 .operand = 피연산자 .linetypes-loaded = 선종류 __ocs_fmt_0__개를 로드했습니다. diff --git a/locales/lo-LA/opencadstudio.ftl b/locales/lo-LA/opencadstudio.ftl index 52612de34..3fc078836 100644 --- a/locales/lo-LA/opencadstudio.ftl +++ b/locales/lo-LA/opencadstudio.ftl @@ -2432,6 +2432,7 @@ common = .load-linetypes = ໂຫຼດຮູບແບບເສັ້ນ .requires-two-positive-values = ຕ້ອງການຄ່າບວກສອງຄ່າ (x,y) ຫຼື ຄ່າດຽວສຳລັບທັງສອງ. .snap-marker-color = ສີເຄື່ອງໝາຍຈຸດດູດ + .snap-marker-size = ຂະໜາດເຄື່ອງໝາຍຈຸດດູດ .command-line-text-color = ສີຂໍ້ຄວາມແຖວຄຳສັ່ງ .operand = ຕົວດຳເນີນການ .linetypes-loaded = ໂຫຼດຮູບແບບເສັ້ນແລ້ວ __ocs_fmt_0__ ຮູບແບບ. diff --git a/locales/nl-NL/opencadstudio.ftl b/locales/nl-NL/opencadstudio.ftl index 5887187fc..1aba8d7c3 100644 --- a/locales/nl-NL/opencadstudio.ftl +++ b/locales/nl-NL/opencadstudio.ftl @@ -2432,6 +2432,7 @@ common = .load-linetypes = Lijntypen laden .requires-two-positive-values = Vereist twee positieve waarden (x,y) of één voor beide. .snap-marker-color = Kleur van snapmarkering + .snap-marker-size = Grootte van snapmarkering .command-line-text-color = Tekstkleur opdrachtregel .operand = Operand .linetypes-loaded = __ocs_fmt_0__ lijntype(n) geladen. diff --git a/locales/pl-PL/opencadstudio.ftl b/locales/pl-PL/opencadstudio.ftl index 73bf218dc..0aa504dcc 100644 --- a/locales/pl-PL/opencadstudio.ftl +++ b/locales/pl-PL/opencadstudio.ftl @@ -2431,6 +2431,7 @@ common = .load-linetypes = Wczytaj rodzaje linii .requires-two-positive-values = Wymaga dwóch wartości dodatnich (x,y) lub jednej dla obu. .snap-marker-color = Kolor znacznika przyciągania + .snap-marker-size = Rozmiar znacznika przyciągania .command-line-text-color = Kolor tekstu wiersza poleceń .operand = Operand .linetypes-loaded = Wczytano rodzaje linii: __ocs_fmt_0__. diff --git a/locales/pt-BR/opencadstudio.ftl b/locales/pt-BR/opencadstudio.ftl index 3dd6b8040..d68f4385d 100644 --- a/locales/pt-BR/opencadstudio.ftl +++ b/locales/pt-BR/opencadstudio.ftl @@ -2432,6 +2432,7 @@ common = .load-linetypes = Carregar tipos de linha .requires-two-positive-values = Requer dois valores positivos (x,y) ou um para ambos. .snap-marker-color = Cor do marcador de snap + .snap-marker-size = Tamanho do marcador de snap .command-line-text-color = Cor do texto da linha de comando .operand = Operando .linetypes-loaded = __ocs_fmt_0__ tipo(s) de linha carregado(s). diff --git a/locales/ru-RU/opencadstudio.ftl b/locales/ru-RU/opencadstudio.ftl index 80525c86d..25f2d76cc 100644 --- a/locales/ru-RU/opencadstudio.ftl +++ b/locales/ru-RU/opencadstudio.ftl @@ -2435,6 +2435,7 @@ common = .load-linetypes = Загрузить типы линий .requires-two-positive-values = Требуются два положительных значения (x,y) или одно для обоих. .snap-marker-color = Цвет маркера привязки + .snap-marker-size = Размер маркера привязки .command-line-text-color = Цвет текста командной строки .operand = Операнд .linetypes-loaded = Загружено типов линий: __ocs_fmt_0__. diff --git a/locales/tr-TR/opencadstudio.ftl b/locales/tr-TR/opencadstudio.ftl index 3efbe6469..da6152f3a 100644 --- a/locales/tr-TR/opencadstudio.ftl +++ b/locales/tr-TR/opencadstudio.ftl @@ -2416,6 +2416,7 @@ common = .load-linetypes = Çizgi tiplerini yükle .requires-two-positive-values = İki pozitif değer (x,y) ya da ikisi için tek değer gerekir. .snap-marker-color = Yakalama işaretçisi rengi + .snap-marker-size = Yakalama işaretçisi boyutu .command-line-text-color = Komut satırı metin rengi .operand = İşlenen .linetypes-loaded = __ocs_fmt_0__ çizgi tipi yüklendi. diff --git a/locales/zh-CN/opencadstudio.ftl b/locales/zh-CN/opencadstudio.ftl index 1d56f7c08..ea8690e95 100644 --- a/locales/zh-CN/opencadstudio.ftl +++ b/locales/zh-CN/opencadstudio.ftl @@ -2419,6 +2419,7 @@ common = .load-linetypes = 加载线型 .requires-two-positive-values = 需要两个正值 (x,y),或一个同时用于两者的值。 .snap-marker-color = 捕捉标记颜色 + .snap-marker-size = 捕捉标记大小 .command-line-text-color = 命令行文字颜色 .operand = 操作对象 .linetypes-loaded = 已加载 __ocs_fmt_0__ 个线型。 diff --git a/locales/zh-TW/opencadstudio.ftl b/locales/zh-TW/opencadstudio.ftl index fed448760..aa43eb0bf 100644 --- a/locales/zh-TW/opencadstudio.ftl +++ b/locales/zh-TW/opencadstudio.ftl @@ -2431,6 +2431,7 @@ common = .load-linetypes = 載入線型 .requires-two-positive-values = 需要兩個正值 (x,y),或一個同時用於兩者的值。 .snap-marker-color = 鎖點標記顏色 + .snap-marker-size = 鎖點標記大小 .command-line-text-color = 命令列文字顏色 .operand = 運算元 .linetypes-loaded = 已載入 __ocs_fmt_0__ 個線型。 diff --git a/src/app/config.rs b/src/app/config.rs index 22a6c52f7..408fa6679 100644 --- a/src/app/config.rs +++ b/src/app/config.rs @@ -369,6 +369,8 @@ pub struct ModelSpaceThemeConfig { pub grip_hot: u8, /// GRIPHOVER: hovered/warm grip ACI color (0 = Theme Primary Strong, 1..=255 = ACI). pub grip_hover: u8, + /// SNAPMARKERSIZE: object-snap marker half-size in pixels (1–20, default 5). + pub snap_marker_size: u8, } impl Default for ModelSpaceThemeConfig { @@ -390,6 +392,7 @@ impl Default for ModelSpaceThemeConfig { grip_color: 0, grip_hot: 0, grip_hover: 0, + snap_marker_size: 5, } } } diff --git a/src/app/mod.rs b/src/app/mod.rs index 5a7e648d9..b8205f059 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -2609,6 +2609,8 @@ pub enum Message { SelectionHighlightColorChanged(u8), /// Change Grip size in pixels (1–25, GRIPSIZE). GripSizeChanged(u8), + /// Change the object-snap marker size in pixels (1–20, SNAPMARKERSIZE). + SnapMarkerSizeChanged(u8), /// Change Unselected Grip color ACI index (0 = Theme Primary, 1..=255 = ACI, GRIPCOLOR). GripColorChanged(u8), /// Change Selected/Hot Grip color ACI index (0 = Theme Danger, 1..=255 = ACI, GRIPHOT). diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index 51e485f3d..7974cecfa 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -9052,6 +9052,12 @@ impl OpenCADStudio { Task::none() } + Message::SnapMarkerSizeChanged(size) => { + self.model_space.snap_marker_size = size.clamp(1, 20); + self.persist_settings_if_changed(); + Task::none() + } + Message::GripColorChanged(color) => { self.model_space.grip_color = color; self.sync_model_space_theme(false); diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index 3ff6f049a..a91981cc3 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -958,6 +958,7 @@ bg={bg_ms:.1}ms n={view_count}" || c.is_selection_gathering() }), snap_color: self.snap_marker_color, + snap_size: self.model_space.snap_marker_size as f32, }, // MATCHPROP paints properties onto destinations; it gets the // paintbrush cursor instead of the crosshair. diff --git a/src/locale_catalog.rs b/src/locale_catalog.rs index a698b9a68..3d578321d 100644 --- a/src/locale_catalog.rs +++ b/src/locale_catalog.rs @@ -5141,6 +5141,7 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "Load Linetypes" => Some(("common", "load-linetypes")), "Requires two positive values (x,y) or one for both." => Some(("common", "requires-two-positive-values")), "Snap marker color" => Some(("common", "snap-marker-color")), + "Snap marker size" => Some(("common", "snap-marker-size")), "Command line text color" => Some(("common", "command-line-text-color")), "Operand" => Some(("common", "operand")), "{added} linetype(s) loaded." => Some(("common", "linetypes-loaded")), diff --git a/src/ui/overlay.rs b/src/ui/overlay.rs index 8fe973dc8..cbef3af27 100644 --- a/src/ui/overlay.rs +++ b/src/ui/overlay.rs @@ -307,6 +307,8 @@ pub struct CrosshairOptions { /// Explicit object-snap marker colour; `None` picks one that reads on /// the canvas. pub snap_color: Option<[u8; 3]>, + /// Object-snap marker half-size in pixels (SNAPMARKERSIZE, 1–20). + pub snap_size: f32, } /// Rendering style for the viewport grid. @@ -1679,110 +1681,231 @@ impl canvas::Program for SelectionCanvas { style: canvas::Style::Solid(marker), ..Default::default() }; - match snap_type { - SnapType::ObjectPick => { - // Target box + center dot (object-acquisition glyph). - let h = 7.0_f32; - let rect = canvas::Path::rectangle( - Point::new(sp.x - h, sp.y - h), - Size::new(h * 2.0, h * 2.0), - ); - frame.stroke(&rect, stroke.clone()); - let r = 3.0_f32; - frame.fill( - &canvas::Path::circle(sp, r), - Color { - r: 0.95, - g: 0.50, - b: 0.08, - a: 0.85, - }, - ); - } - SnapType::Endpoint => { - let h = 5.0_f32; - let rect = canvas::Path::rectangle( - Point::new(sp.x - h, sp.y - h), - Size::new(h * 2.0, h * 2.0), - ); - frame.stroke(&rect, stroke); - } - SnapType::Midpoint => { - let r = 6.0_f32; - let path = canvas::Path::new(|b| { - b.move_to(Point::new(sp.x, sp.y - r)); - b.line_to(Point::new(sp.x + r * 0.866, sp.y + r * 0.5)); - b.line_to(Point::new(sp.x - r * 0.866, sp.y + r * 0.5)); - b.close(); - }); - frame.stroke(&path, stroke); - } - SnapType::Center => { - let r = 5.5_f32; - let path = canvas::Path::circle(sp, r); - frame.stroke(&path, stroke); - } - SnapType::GeometricCenter => { - // Pentagon around a centre dot: the area centroid. - let r = 6.5_f32; - let path = canvas::Path::new(|b| { - for k in 0..5 { - let a = -std::f32::consts::FRAC_PI_2 - + k as f32 * std::f32::consts::TAU / 5.0; - let p = Point::new(sp.x + r * a.cos(), sp.y + r * a.sin()); - if k == 0 { - b.move_to(p); - } else { - b.line_to(p); + // SNAPMARKERSIZE scales the glyphs drawn for the default 5 px. + let scale = self.crosshair.snap_size.clamp(1.0, 20.0) / 5.0; + frame.with_save(|frame| { + frame.translate(iced::Vector::new(sp.x, sp.y)); + frame.scale(scale); + frame.translate(iced::Vector::new(-sp.x, -sp.y)); + match snap_type { + SnapType::ObjectPick => { + // Target box + center dot (object-acquisition glyph). + let h = 7.0_f32; + let rect = canvas::Path::rectangle( + Point::new(sp.x - h, sp.y - h), + Size::new(h * 2.0, h * 2.0), + ); + frame.stroke(&rect, stroke.clone()); + let r = 3.0_f32; + frame.fill( + &canvas::Path::circle(sp, r), + Color { + r: 0.95, + g: 0.50, + b: 0.08, + a: 0.85, + }, + ); + } + SnapType::Endpoint => { + let h = 5.0_f32; + let rect = canvas::Path::rectangle( + Point::new(sp.x - h, sp.y - h), + Size::new(h * 2.0, h * 2.0), + ); + frame.stroke(&rect, stroke); + } + SnapType::Midpoint => { + let r = 6.0_f32; + let path = canvas::Path::new(|b| { + b.move_to(Point::new(sp.x, sp.y - r)); + b.line_to(Point::new(sp.x + r * 0.866, sp.y + r * 0.5)); + b.line_to(Point::new(sp.x - r * 0.866, sp.y + r * 0.5)); + b.close(); + }); + frame.stroke(&path, stroke); + } + SnapType::Center => { + let r = 5.5_f32; + let path = canvas::Path::circle(sp, r); + frame.stroke(&path, stroke); + } + SnapType::GeometricCenter => { + // Pentagon around a centre dot: the area centroid. + let r = 6.5_f32; + let path = canvas::Path::new(|b| { + for k in 0..5 { + let a = -std::f32::consts::FRAC_PI_2 + + k as f32 * std::f32::consts::TAU / 5.0; + let p = Point::new(sp.x + r * a.cos(), sp.y + r * a.sin()); + if k == 0 { + b.move_to(p); + } else { + b.line_to(p); + } + } + b.close(); + }); + frame.stroke(&path, stroke.clone()); + frame.fill(&canvas::Path::circle(sp, 1.6), marker); + } + SnapType::Node => { + // Circle with an inscribed X. + let r = 5.5_f32; + let cpath = canvas::Path::circle(sp, r); + frame.stroke(&cpath, stroke.clone()); + let d = r * std::f32::consts::FRAC_1_SQRT_2; + let x1 = canvas::Path::new(|b| { + b.move_to(Point::new(sp.x - d, sp.y - d)); + b.line_to(Point::new(sp.x + d, sp.y + d)); + }); + let x2 = canvas::Path::new(|b| { + b.move_to(Point::new(sp.x - d, sp.y + d)); + b.line_to(Point::new(sp.x + d, sp.y - d)); + }); + frame.stroke(&x1, stroke.clone()); + frame.stroke(&x2, stroke); + } + SnapType::Quadrant => { + let r = 6.0_f32; + let path = canvas::Path::new(|b| { + b.move_to(Point::new(sp.x, sp.y - r)); + b.line_to(Point::new(sp.x + r, sp.y)); + b.line_to(Point::new(sp.x, sp.y + r)); + b.line_to(Point::new(sp.x - r, sp.y)); + b.close(); + }); + frame.stroke(&path, stroke); + } + SnapType::Intersection => { + // An extended intersection sets one or two extension bases; + // draw a dashed guide from each endpoint through the crossing + // so both contributing extension paths stay visible. A real + // on-segment crossing carries no bases, so nothing draws here. + // (#247, #259) + for base in [self.snap_ext_base, self.snap_ext_base2] + .into_iter() + .flatten() + { + let dx = sp.x - base.x; + let dy = sp.y - base.y; + let len = (dx * dx + dy * dy).sqrt(); + if len > 1e-3 { + let dash = canvas::Stroke { + line_dash: canvas::LineDash { + segments: &[4.0, 4.0], + offset: 0, + }, + ..canvas::Stroke::default().with_color(marker).with_width(1.0) + }; + let tip = Point::new(sp.x + dx / len * 18.0, sp.y + dy / len * 18.0); + // `base` is a projected entity endpoint: clip before + // stroking (see `clip_seg`). + if let Some((a, b)) = clip_seg(base, tip, bounds) { + frame.stroke(&canvas::Path::line(a, b), dash); + } } } - b.close(); - }); - frame.stroke(&path, stroke.clone()); - frame.fill(&canvas::Path::circle(sp, 1.6), marker); - } - SnapType::Node => { - // Circle with an inscribed X. - let r = 5.5_f32; - let cpath = canvas::Path::circle(sp, r); - frame.stroke(&cpath, stroke.clone()); - let d = r * std::f32::consts::FRAC_1_SQRT_2; - let x1 = canvas::Path::new(|b| { - b.move_to(Point::new(sp.x - d, sp.y - d)); - b.line_to(Point::new(sp.x + d, sp.y + d)); - }); - let x2 = canvas::Path::new(|b| { - b.move_to(Point::new(sp.x - d, sp.y + d)); - b.line_to(Point::new(sp.x + d, sp.y - d)); - }); - frame.stroke(&x1, stroke.clone()); - frame.stroke(&x2, stroke); - } - SnapType::Quadrant => { - let r = 6.0_f32; - let path = canvas::Path::new(|b| { - b.move_to(Point::new(sp.x, sp.y - r)); - b.line_to(Point::new(sp.x + r, sp.y)); - b.line_to(Point::new(sp.x, sp.y + r)); - b.line_to(Point::new(sp.x - r, sp.y)); - b.close(); - }); - frame.stroke(&path, stroke); - } - SnapType::Intersection => { - // An extended intersection sets one or two extension bases; - // draw a dashed guide from each endpoint through the crossing - // so both contributing extension paths stay visible. A real - // on-segment crossing carries no bases, so nothing draws here. - // (#247, #259) - for base in [self.snap_ext_base, self.snap_ext_base2] - .into_iter() - .flatten() - { - let dx = sp.x - base.x; - let dy = sp.y - base.y; - let len = (dx * dx + dy * dy).sqrt(); - if len > 1e-3 { + let r = 5.0_f32; + let p1 = canvas::Path::new(|b| { + b.move_to(Point::new(sp.x - r, sp.y - r)); + b.line_to(Point::new(sp.x + r, sp.y + r)); + }); + let p2 = canvas::Path::new(|b| { + b.move_to(Point::new(sp.x - r, sp.y + r)); + b.line_to(Point::new(sp.x + r, sp.y - r)); + }); + frame.stroke(&p1, stroke.clone()); + frame.stroke(&p2, stroke); + } + SnapType::ApparentIntersection => { + // X like Intersection, framed by a small square so the + // two are visually distinguishable. + let r = 5.0_f32; + let rect = canvas::Path::rectangle( + Point::new(sp.x - r, sp.y - r), + Size::new(r * 2.0, r * 2.0), + ); + frame.stroke(&rect, stroke.clone()); + let xr = r - 1.5; + let p1 = canvas::Path::new(|b| { + b.move_to(Point::new(sp.x - xr, sp.y - xr)); + b.line_to(Point::new(sp.x + xr, sp.y + xr)); + }); + let p2 = canvas::Path::new(|b| { + b.move_to(Point::new(sp.x - xr, sp.y + xr)); + b.line_to(Point::new(sp.x + xr, sp.y - xr)); + }); + frame.stroke(&p1, stroke.clone()); + frame.stroke(&p2, stroke); + } + SnapType::Insertion => { + // Two overlapping rectangles (a small "tag" glyph). + let r = 5.0_f32; + let inner = canvas::Path::rectangle( + Point::new(sp.x - r * 0.5, sp.y - r), + Size::new(r, r * 2.0), + ); + let outer = canvas::Path::rectangle( + Point::new(sp.x - r, sp.y - r * 0.5), + Size::new(r * 2.0, r), + ); + frame.stroke(&outer, stroke.clone()); + frame.stroke(&inner, stroke); + } + SnapType::Perpendicular => { + // Right-angle hook in the lower-left quadrant. + let r = 6.0_f32; + let p = canvas::Path::new(|b| { + b.move_to(Point::new(sp.x - r, sp.y - r)); + b.line_to(Point::new(sp.x - r, sp.y + r)); + b.line_to(Point::new(sp.x + r, sp.y + r)); + }); + let foot = canvas::Path::new(|b| { + b.move_to(Point::new(sp.x - r, sp.y)); + b.line_to(Point::new(sp.x, sp.y)); + b.line_to(Point::new(sp.x, sp.y + r)); + }); + frame.stroke(&p, stroke.clone()); + frame.stroke(&foot, stroke); + } + SnapType::Tangent => { + // Circle with a tangent bar across the top. + let r = 5.5_f32; + let c = canvas::Path::circle(sp, r); + frame.stroke(&c, stroke.clone()); + let bar = canvas::Path::new(|b| { + b.move_to(Point::new(sp.x - r, sp.y - r)); + b.line_to(Point::new(sp.x + r, sp.y - r)); + }); + frame.stroke(&bar, stroke); + } + SnapType::Nearest => { + // Bowtie / hourglass — two opposed triangles meeting at sp. + let r = 5.5_f32; + let path = canvas::Path::new(|b| { + b.move_to(Point::new(sp.x - r, sp.y - r)); + b.line_to(Point::new(sp.x + r, sp.y - r)); + b.line_to(Point::new(sp.x - r, sp.y + r)); + b.line_to(Point::new(sp.x + r, sp.y + r)); + b.close(); + }); + frame.stroke(&path, stroke); + } + SnapType::Extension => { + // Unit direction from the endpoint the snap extends from + // toward the snap point, so the guide line and the three + // dots follow the actual extension path (#238). + let dir = self.snap_ext_base.and_then(|base| { + let dx = sp.x - base.x; + let dy = sp.y - base.y; + let len = (dx * dx + dy * dy).sqrt(); + (len > 1e-3).then(|| (base, Point::new(dx / len, dy / len))) + }); + // Dashed guide line from that endpoint, through the snap + // point and a little beyond, so the extension path is + // visible as the cursor tracks along it. + if let Some((base, u)) = dir { let dash = canvas::Stroke { line_dash: canvas::LineDash { segments: &[4.0, 4.0], @@ -1790,229 +1913,115 @@ impl canvas::Program for SelectionCanvas { }, ..canvas::Stroke::default().with_color(marker).with_width(1.0) }; - let tip = Point::new(sp.x + dx / len * 18.0, sp.y + dy / len * 18.0); + let tip = Point::new(sp.x + u.x * 18.0, sp.y + u.y * 18.0); // `base` is a projected entity endpoint: clip before // stroking (see `clip_seg`). if let Some((a, b)) = clip_seg(base, tip, bounds) { frame.stroke(&canvas::Path::line(a, b), dash); } } - } - let r = 5.0_f32; - let p1 = canvas::Path::new(|b| { - b.move_to(Point::new(sp.x - r, sp.y - r)); - b.line_to(Point::new(sp.x + r, sp.y + r)); - }); - let p2 = canvas::Path::new(|b| { - b.move_to(Point::new(sp.x - r, sp.y + r)); - b.line_to(Point::new(sp.x + r, sp.y - r)); - }); - frame.stroke(&p1, stroke.clone()); - frame.stroke(&p2, stroke); - } - SnapType::ApparentIntersection => { - // X like Intersection, framed by a small square so the - // two are visually distinguishable. - let r = 5.0_f32; - let rect = canvas::Path::rectangle( - Point::new(sp.x - r, sp.y - r), - Size::new(r * 2.0, r * 2.0), - ); - frame.stroke(&rect, stroke.clone()); - let xr = r - 1.5; - let p1 = canvas::Path::new(|b| { - b.move_to(Point::new(sp.x - xr, sp.y - xr)); - b.line_to(Point::new(sp.x + xr, sp.y + xr)); - }); - let p2 = canvas::Path::new(|b| { - b.move_to(Point::new(sp.x - xr, sp.y + xr)); - b.line_to(Point::new(sp.x + xr, sp.y - xr)); - }); - frame.stroke(&p1, stroke.clone()); - frame.stroke(&p2, stroke); - } - SnapType::Insertion => { - // Two overlapping rectangles (a small "tag" glyph). - let r = 5.0_f32; - let inner = canvas::Path::rectangle( - Point::new(sp.x - r * 0.5, sp.y - r), - Size::new(r, r * 2.0), - ); - let outer = canvas::Path::rectangle( - Point::new(sp.x - r, sp.y - r * 0.5), - Size::new(r * 2.0, r), - ); - frame.stroke(&outer, stroke.clone()); - frame.stroke(&inner, stroke); - } - SnapType::Perpendicular => { - // Right-angle hook in the lower-left quadrant. - let r = 6.0_f32; - let p = canvas::Path::new(|b| { - b.move_to(Point::new(sp.x - r, sp.y - r)); - b.line_to(Point::new(sp.x - r, sp.y + r)); - b.line_to(Point::new(sp.x + r, sp.y + r)); - }); - let foot = canvas::Path::new(|b| { - b.move_to(Point::new(sp.x - r, sp.y)); - b.line_to(Point::new(sp.x, sp.y)); - b.line_to(Point::new(sp.x, sp.y + r)); - }); - frame.stroke(&p, stroke.clone()); - frame.stroke(&foot, stroke); - } - SnapType::Tangent => { - // Circle with a tangent bar across the top. - let r = 5.5_f32; - let c = canvas::Path::circle(sp, r); - frame.stroke(&c, stroke.clone()); - let bar = canvas::Path::new(|b| { - b.move_to(Point::new(sp.x - r, sp.y - r)); - b.line_to(Point::new(sp.x + r, sp.y - r)); - }); - frame.stroke(&bar, stroke); - } - SnapType::Nearest => { - // Bowtie / hourglass — two opposed triangles meeting at sp. - let r = 5.5_f32; - let path = canvas::Path::new(|b| { - b.move_to(Point::new(sp.x - r, sp.y - r)); - b.line_to(Point::new(sp.x + r, sp.y - r)); - b.line_to(Point::new(sp.x - r, sp.y + r)); - b.line_to(Point::new(sp.x + r, sp.y + r)); - b.close(); - }); - frame.stroke(&path, stroke); - } - SnapType::Extension => { - // Unit direction from the endpoint the snap extends from - // toward the snap point, so the guide line and the three - // dots follow the actual extension path (#238). - let dir = self.snap_ext_base.and_then(|base| { - let dx = sp.x - base.x; - let dy = sp.y - base.y; - let len = (dx * dx + dy * dy).sqrt(); - (len > 1e-3).then(|| (base, Point::new(dx / len, dy / len))) - }); - // Dashed guide line from that endpoint, through the snap - // point and a little beyond, so the extension path is - // visible as the cursor tracks along it. - if let Some((base, u)) = dir { - let dash = canvas::Stroke { - line_dash: canvas::LineDash { - segments: &[4.0, 4.0], - offset: 0, - }, - ..canvas::Stroke::default().with_color(marker).with_width(1.0) - }; - let tip = Point::new(sp.x + u.x * 18.0, sp.y + u.y * 18.0); - // `base` is a projected entity endpoint: clip before - // stroking (see `clip_seg`). - if let Some((a, b)) = clip_seg(base, tip, bounds) { - frame.stroke(&canvas::Path::line(a, b), dash); + // Three dots at the snap point, strung along the extension + // direction (horizontal fallback when the base is unknown). + let u = dir.map(|(_, u)| u).unwrap_or(Point::new(1.0, 0.0)); + let r = 1.4_f32; + for k in [-7.0_f32, 0.0, 7.0] { + let dot = + canvas::Path::circle(Point::new(sp.x + u.x * k, sp.y + u.y * k), r); + frame.fill(&dot, marker); } } - // Three dots at the snap point, strung along the extension - // direction (horizontal fallback when the base is unknown). - let u = dir.map(|(_, u)| u).unwrap_or(Point::new(1.0, 0.0)); - let r = 1.4_f32; - for k in [-7.0_f32, 0.0, 7.0] { - let dot = - canvas::Path::circle(Point::new(sp.x + u.x * k, sp.y + u.y * k), r); - frame.fill(&dot, marker); + SnapType::Parallel => { + // Two short parallel diagonal bars. + let r = 6.0_f32; + let off = 3.0_f32; + let b1 = canvas::Path::new(|b| { + b.move_to(Point::new(sp.x - r - off, sp.y + r)); + b.line_to(Point::new(sp.x + r - off, sp.y - r)); + }); + let b2 = canvas::Path::new(|b| { + b.move_to(Point::new(sp.x - r + off, sp.y + r)); + b.line_to(Point::new(sp.x + r + off, sp.y - r)); + }); + frame.stroke(&b1, stroke.clone()); + frame.stroke(&b2, stroke); + } + SnapType::Grid => { + let arm = 4.0_f32; + let h = canvas::Path::new(|b| { + b.move_to(Point::new(sp.x - arm, sp.y)); + b.line_to(Point::new(sp.x + arm, sp.y)); + }); + let v = canvas::Path::new(|b| { + b.move_to(Point::new(sp.x, sp.y - arm)); + b.line_to(Point::new(sp.x, sp.y + arm)); + }); + frame.stroke(&h, stroke.clone()); + frame.stroke(&v, stroke); + } + SnapType::Vertex => { + // Filled square: the solid rhyme of Endpoint's hollow box. + let h = 5.0_f32; + frame.fill( + &canvas::Path::rectangle( + Point::new(sp.x - h, sp.y - h), + Size::new(h * 2.0, h * 2.0), + ), + marker, + ); + } + SnapType::EdgeMidpoint => { + // Filled triangle: the solid rhyme of Midpoint's outline. + let r = 6.0_f32; + let path = canvas::Path::new(|b| { + b.move_to(Point::new(sp.x, sp.y - r)); + b.line_to(Point::new(sp.x + r * 0.866, sp.y + r * 0.5)); + b.line_to(Point::new(sp.x - r * 0.866, sp.y + r * 0.5)); + b.close(); + }); + frame.fill(&path, marker); + } + SnapType::FaceCenter => { + // Filled disc: the solid rhyme of Center's outline. + frame.fill(&canvas::Path::circle(sp, 5.5_f32), marker); + } + SnapType::Knot => { + // Filled diamond: distinct from Quadrant's outline. + let r = 5.5_f32; + let path = canvas::Path::new(|b| { + b.move_to(Point::new(sp.x, sp.y - r)); + b.line_to(Point::new(sp.x + r, sp.y)); + b.line_to(Point::new(sp.x, sp.y + r)); + b.line_to(Point::new(sp.x - r, sp.y)); + b.close(); + }); + frame.fill(&path, marker); + } + SnapType::FacePerpendicular => { + // Right-angle hook like the 2D marker, plus a filled foot + // dot marking the face contact. + let r = 6.0_f32; + let p = canvas::Path::new(|b| { + b.move_to(Point::new(sp.x - r, sp.y - r)); + b.line_to(Point::new(sp.x - r, sp.y + r)); + b.line_to(Point::new(sp.x + r, sp.y + r)); + }); + frame.stroke(&p, stroke.clone()); + frame.fill(&canvas::Path::circle(sp, 2.0_f32), marker); + } + SnapType::NearestFace => { + // Filled bowtie: the solid rhyme of Nearest's outline. + let r = 5.5_f32; + let path = canvas::Path::new(|b| { + b.move_to(Point::new(sp.x - r, sp.y - r)); + b.line_to(Point::new(sp.x + r, sp.y - r)); + b.line_to(Point::new(sp.x - r, sp.y + r)); + b.line_to(Point::new(sp.x + r, sp.y + r)); + b.close(); + }); + frame.fill(&path, marker); } } - SnapType::Parallel => { - // Two short parallel diagonal bars. - let r = 6.0_f32; - let off = 3.0_f32; - let b1 = canvas::Path::new(|b| { - b.move_to(Point::new(sp.x - r - off, sp.y + r)); - b.line_to(Point::new(sp.x + r - off, sp.y - r)); - }); - let b2 = canvas::Path::new(|b| { - b.move_to(Point::new(sp.x - r + off, sp.y + r)); - b.line_to(Point::new(sp.x + r + off, sp.y - r)); - }); - frame.stroke(&b1, stroke.clone()); - frame.stroke(&b2, stroke); - } - SnapType::Grid => { - let arm = 4.0_f32; - let h = canvas::Path::new(|b| { - b.move_to(Point::new(sp.x - arm, sp.y)); - b.line_to(Point::new(sp.x + arm, sp.y)); - }); - let v = canvas::Path::new(|b| { - b.move_to(Point::new(sp.x, sp.y - arm)); - b.line_to(Point::new(sp.x, sp.y + arm)); - }); - frame.stroke(&h, stroke.clone()); - frame.stroke(&v, stroke); - } - SnapType::Vertex => { - // Filled square: the solid rhyme of Endpoint's hollow box. - let h = 5.0_f32; - frame.fill( - &canvas::Path::rectangle( - Point::new(sp.x - h, sp.y - h), - Size::new(h * 2.0, h * 2.0), - ), - marker, - ); - } - SnapType::EdgeMidpoint => { - // Filled triangle: the solid rhyme of Midpoint's outline. - let r = 6.0_f32; - let path = canvas::Path::new(|b| { - b.move_to(Point::new(sp.x, sp.y - r)); - b.line_to(Point::new(sp.x + r * 0.866, sp.y + r * 0.5)); - b.line_to(Point::new(sp.x - r * 0.866, sp.y + r * 0.5)); - b.close(); - }); - frame.fill(&path, marker); - } - SnapType::FaceCenter => { - // Filled disc: the solid rhyme of Center's outline. - frame.fill(&canvas::Path::circle(sp, 5.5_f32), marker); - } - SnapType::Knot => { - // Filled diamond: distinct from Quadrant's outline. - let r = 5.5_f32; - let path = canvas::Path::new(|b| { - b.move_to(Point::new(sp.x, sp.y - r)); - b.line_to(Point::new(sp.x + r, sp.y)); - b.line_to(Point::new(sp.x, sp.y + r)); - b.line_to(Point::new(sp.x - r, sp.y)); - b.close(); - }); - frame.fill(&path, marker); - } - SnapType::FacePerpendicular => { - // Right-angle hook like the 2D marker, plus a filled foot - // dot marking the face contact. - let r = 6.0_f32; - let p = canvas::Path::new(|b| { - b.move_to(Point::new(sp.x - r, sp.y - r)); - b.line_to(Point::new(sp.x - r, sp.y + r)); - b.line_to(Point::new(sp.x + r, sp.y + r)); - }); - frame.stroke(&p, stroke.clone()); - frame.fill(&canvas::Path::circle(sp, 2.0_f32), marker); - } - SnapType::NearestFace => { - // Filled bowtie: the solid rhyme of Nearest's outline. - let r = 5.5_f32; - let path = canvas::Path::new(|b| { - b.move_to(Point::new(sp.x - r, sp.y - r)); - b.line_to(Point::new(sp.x + r, sp.y - r)); - b.line_to(Point::new(sp.x - r, sp.y + r)); - b.line_to(Point::new(sp.x + r, sp.y + r)); - b.close(); - }); - frame.fill(&path, marker); - } - } + }); } // ── CAD crosshair cursor ────────────────────────────────────────────── diff --git a/src/ui/window/options.rs b/src/ui/window/options.rs index 45ad8037a..6eccb5cff 100644 --- a/src/ui/window/options.rs +++ b/src/ui/window/options.rs @@ -1578,6 +1578,22 @@ pub fn view_window<'a>( ] .spacing(10) .align_y(iced::Center), + Space::new().height(14), + row![ + text(crate::t!("Snap marker size")).size(12).width(150), + slider( + 1..=20, + model_space.snap_marker_size.clamp(1, 20), + Message::SnapMarkerSizeChanged, + ) + .step(1) + .width(Fill), + text(format!("{} px", model_space.snap_marker_size.clamp(1, 20))) + .size(11) + .width(44), + ] + .spacing(10) + .align_y(iced::Center), Space::new().height(24), text(crate::t!("Snap and Grid")).size(15), Space::new().height(10),