diff --git a/assets/icons/osnap/mtp.svg b/assets/icons/osnap/mtp.svg new file mode 100644 index 000000000..1005c246d --- /dev/null +++ b/assets/icons/osnap/mtp.svg @@ -0,0 +1 @@ + diff --git a/locales/ar-SA/opencadstudio.ftl b/locales/ar-SA/opencadstudio.ftl index b54a45dad..86443f3b9 100644 --- a/locales/ar-SA/opencadstudio.ftl +++ b/locales/ar-SA/opencadstudio.ftl @@ -632,7 +632,9 @@ common = .middle-right-sentence-case = وسط اليمين .middleleft = وسط اليسار .middleright = وسط اليمين + .mid-between-2-points-m2p = نقطة المنتصف بين نقطتين (M2P) .midpoint = نقطة المنتصف + .mtp-needs-an-active-point-prompt = يتطلب MTP مطالبة نقطة نشطة. .miles = أميال .millimeters = مليمترات .model-title-case = النموذج diff --git a/locales/bg-BG/opencadstudio.ftl b/locales/bg-BG/opencadstudio.ftl index ad0be6bca..3be5e6825 100644 --- a/locales/bg-BG/opencadstudio.ftl +++ b/locales/bg-BG/opencadstudio.ftl @@ -619,7 +619,9 @@ common = .middle-right-sentence-case = Средата вдясно .middleleft = Средата вляво .middleright = Средата вдясно + .mid-between-2-points-m2p = Среда между 2 точки (M2P) .midpoint = Средна точка + .mtp-needs-an-active-point-prompt = MTP изисква активна подкана за точка. .miles = Мили .millimeters = Милиметри .model-title-case = Модел diff --git a/locales/cs-CZ/opencadstudio.ftl b/locales/cs-CZ/opencadstudio.ftl index 4fb2655f0..b885713ea 100644 --- a/locales/cs-CZ/opencadstudio.ftl +++ b/locales/cs-CZ/opencadstudio.ftl @@ -619,7 +619,9 @@ common = .middle-right-sentence-case = Uprostřed vpravo .middleleft = Uprostřed vlevo .middleright =Uprostřed vpravo + .mid-between-2-points-m2p = Střed mezi 2 body (M2P) .midpoint = Střed + .mtp-needs-an-active-point-prompt = MTP vyžaduje aktivní výzvu k zadání bodu. .miles = Míle .millimeters = milimetry .model-title-case = Model diff --git a/locales/de-DE/opencadstudio.ftl b/locales/de-DE/opencadstudio.ftl index cef710d64..867d917d8 100644 --- a/locales/de-DE/opencadstudio.ftl +++ b/locales/de-DE/opencadstudio.ftl @@ -620,7 +620,9 @@ common = .middle-right-sentence-case = Mitte rechts .middleleft = Mitte links .middleright = Mitte rechts + .mid-between-2-points-m2p = Mitte zwischen 2 Punkten (M2P) .midpoint = Mittelpunkt + .mtp-needs-an-active-point-prompt = MTP erfordert eine aktive Punkteingabe. .miles = Meilen .millimeters = Millimeter .model-title-case = Modell diff --git a/locales/el-GR/opencadstudio.ftl b/locales/el-GR/opencadstudio.ftl index 3390d369f..d7fa6130e 100644 --- a/locales/el-GR/opencadstudio.ftl +++ b/locales/el-GR/opencadstudio.ftl @@ -621,7 +621,9 @@ common = .middle-right-sentence-case = Μέση δεξιά .middleleft = Μέση αριστερά .middleright = Μέση δεξιά + .mid-between-2-points-m2p = Μέσο μεταξύ 2 σημείων (M2P) .midpoint = Μέσο + .mtp-needs-an-active-point-prompt = Το MTP απαιτεί ενεργή προτροπή σημείου. .miles = Μίλια .millimeters = Χιλιοστά .model-title-case = Μοντέλο diff --git a/locales/en-US/opencadstudio.ftl b/locales/en-US/opencadstudio.ftl index 4b0008de3..7538de696 100644 --- a/locales/en-US/opencadstudio.ftl +++ b/locales/en-US/opencadstudio.ftl @@ -619,7 +619,9 @@ common = .middle-right-sentence-case = Middle right .middleleft = Middle left .middleright = Middle right + .mid-between-2-points-m2p = Mid Between 2 Points (M2P) .midpoint = Midpoint + .mtp-needs-an-active-point-prompt = MTP needs an active point prompt. .miles = Miles .millimeters = Millimeters .model-title-case = Model diff --git a/locales/es-ES/opencadstudio.ftl b/locales/es-ES/opencadstudio.ftl index c06c6e8d3..5bea5595a 100644 --- a/locales/es-ES/opencadstudio.ftl +++ b/locales/es-ES/opencadstudio.ftl @@ -620,7 +620,9 @@ common = .middle-right-sentence-case = Medio derecha .middleleft = Medio izquierda .middleright = Medio derecha + .mid-between-2-points-m2p = Punto medio entre 2 puntos (M2P) .midpoint = Punto medio + .mtp-needs-an-active-point-prompt = MTP requiere una solicitud de punto activa. .miles = Millas .millimeters = Milímetros .model-title-case = Modelo diff --git a/locales/fi-FI/opencadstudio.ftl b/locales/fi-FI/opencadstudio.ftl index 5944347b6..6272243c7 100644 --- a/locales/fi-FI/opencadstudio.ftl +++ b/locales/fi-FI/opencadstudio.ftl @@ -619,7 +619,9 @@ common = .middle-right-sentence-case = Keski oikealla .middleleft = Keski vasen .middleright =Keski oikealla + .mid-between-2-points-m2p = Kahden pisteen puoliväli (M2P) .midpoint = Keskipiste + .mtp-needs-an-active-point-prompt = MTP vaatii aktiivisen pistekehotteen. .miles = Mailia .millimeters = millimetriä .model-title-case = Malli diff --git a/locales/fr-FR/opencadstudio.ftl b/locales/fr-FR/opencadstudio.ftl index 4458464c8..b4626a974 100644 --- a/locales/fr-FR/opencadstudio.ftl +++ b/locales/fr-FR/opencadstudio.ftl @@ -620,7 +620,9 @@ common = .middle-right-sentence-case = Moyen droit .middleleft = Moyen gauche .middleright = Moyen droit + .mid-between-2-points-m2p = Milieu entre 2 points (M2P) .midpoint = Moyenne + .mtp-needs-an-active-point-prompt = MTP nécessite une invite de point active. .miles = Milles .millimeters = Millimètres .model-title-case = Modèle diff --git a/locales/hi-IN/opencadstudio.ftl b/locales/hi-IN/opencadstudio.ftl index 72f75f672..c4f8573bb 100644 --- a/locales/hi-IN/opencadstudio.ftl +++ b/locales/hi-IN/opencadstudio.ftl @@ -608,7 +608,9 @@ common = .middle-right-sentence-case = मध्य दाएं .middleleft = मध्य बाएं .middleright = मध्य दाएँ + .mid-between-2-points-m2p = 2 बिंदुओं के बीच का मध्यबिंदु (M2P) .midpoint = मिडपॉइंट + .mtp-needs-an-active-point-prompt = MTP के लिए सक्रिय बिंदु प्रॉम्प्ट आवश्यक है। .miles = माइल्स .millimeters = मिलीमीटर .model-title-case = मॉडल diff --git a/locales/hu-HU/opencadstudio.ftl b/locales/hu-HU/opencadstudio.ftl index 286520448..1d7ef1906 100644 --- a/locales/hu-HU/opencadstudio.ftl +++ b/locales/hu-HU/opencadstudio.ftl @@ -619,7 +619,9 @@ common = .middle-right-sentence-case = Jobb középső .middleleft = bal középső .middleright =Jobb középső + .mid-between-2-points-m2p = Két pont közötti felezőpont (M2P) .midpoint = Középpont + .mtp-needs-an-active-point-prompt = Az MTP aktív pontbekérést igényel. .miles = Mérföld .millimeters = Milliméter .model-title-case = Modell diff --git a/locales/it-IT/opencadstudio.ftl b/locales/it-IT/opencadstudio.ftl index ca4425e2d..cb9a7e25d 100644 --- a/locales/it-IT/opencadstudio.ftl +++ b/locales/it-IT/opencadstudio.ftl @@ -619,7 +619,9 @@ common = .middle-right-sentence-case = Centro a destra .middleleft = Centro a sinistra .middleright =Centro a destra + .mid-between-2-points-m2p = Punto medio tra 2 punti (M2P) .midpoint = Punto medio + .mtp-needs-an-active-point-prompt = MTP richiede una richiesta di punto attiva. .miles = Miglia .millimeters = Millimetri .model-title-case = Modello diff --git a/locales/ja-JP/opencadstudio.ftl b/locales/ja-JP/opencadstudio.ftl index da633d8ed..0a06b2921 100644 --- a/locales/ja-JP/opencadstudio.ftl +++ b/locales/ja-JP/opencadstudio.ftl @@ -610,7 +610,9 @@ common = .middle-right-sentence-case = 中央右 .middleleft = 中央左 .middleright = 中央右 + .mid-between-2-points-m2p = 2点間の中点 (M2P) .midpoint = 中点 + .mtp-needs-an-active-point-prompt = MTP にはアクティブな点入力プロンプトが必要です。 .miles = マイル .millimeters = ミリメートル .model-title-case = モデル diff --git a/locales/ko-KR/opencadstudio.ftl b/locales/ko-KR/opencadstudio.ftl index 043b4239c..7ca820393 100644 --- a/locales/ko-KR/opencadstudio.ftl +++ b/locales/ko-KR/opencadstudio.ftl @@ -619,7 +619,9 @@ common = .middle-right-sentence-case = 가운데 오른쪽 .middleleft = 가운데 왼쪽 .middleright = 가운데 오른쪽 + .mid-between-2-points-m2p = 두 점 사이의 중점 (M2P) .midpoint = 중간점 + .mtp-needs-an-active-point-prompt = MTP에는 활성 점 입력 프롬프트가 필요합니다. .miles =마일리지 .millimeters = 밀리미터 .model-title-case = 모델 diff --git a/locales/nl-NL/opencadstudio.ftl b/locales/nl-NL/opencadstudio.ftl index 92eeea1de..a0d35b8a4 100644 --- a/locales/nl-NL/opencadstudio.ftl +++ b/locales/nl-NL/opencadstudio.ftl @@ -618,7 +618,9 @@ common = .middle-right-sentence-case = Middenrechts .middleleft = Midden links .middleright = Middenrechts + .mid-between-2-points-m2p = Midden tussen 2 punten (M2P) .midpoint = Middenpunt + .mtp-needs-an-active-point-prompt = MTP vereist een actieve puntinvoer. .miles = Mijlen .millimeters = Millimeter .model-title-case = Model diff --git a/locales/pl-PL/opencadstudio.ftl b/locales/pl-PL/opencadstudio.ftl index 08c348dd8..0f2548b49 100644 --- a/locales/pl-PL/opencadstudio.ftl +++ b/locales/pl-PL/opencadstudio.ftl @@ -619,7 +619,9 @@ common = .middle-right-sentence-case = Środkowy prawy .middleleft = Środkowy lewy .middleright =Środkowy prawy + .mid-between-2-points-m2p = Środek między 2 punktami (M2P) .midpoint = Punkt środkowy + .mtp-needs-an-active-point-prompt = MTP wymaga aktywnego monitu o punkt. .miles = Mile .millimeters = Milimetry .model-title-case = Modelka diff --git a/locales/pt-BR/opencadstudio.ftl b/locales/pt-BR/opencadstudio.ftl index fc8eb131a..a54b10ee4 100644 --- a/locales/pt-BR/opencadstudio.ftl +++ b/locales/pt-BR/opencadstudio.ftl @@ -620,7 +620,9 @@ common = .middle-right-sentence-case = Meio direito .middleleft = Meio esquerdo .middleright = Meio direito + .mid-between-2-points-m2p = Ponto médio entre 2 pontos (M2P) .midpoint = Ponto médio + .mtp-needs-an-active-point-prompt = O MTP requer uma solicitação de ponto ativa. .miles = Milhas .millimeters = Milímetros .model-title-case = Modelo diff --git a/locales/ru-RU/opencadstudio.ftl b/locales/ru-RU/opencadstudio.ftl index eb21fa689..2eba43cc8 100644 --- a/locales/ru-RU/opencadstudio.ftl +++ b/locales/ru-RU/opencadstudio.ftl @@ -623,7 +623,9 @@ common = .middle-right-sentence-case = Средний правый .middleleft = Средний слева .middleright = Средний правый + .mid-between-2-points-m2p = Середина между 2 точками (M2P) .midpoint = Средняя точка + .mtp-needs-an-active-point-prompt = Для MTP требуется активный запрос точки. .miles = Майлз .millimeters = Миллиметры .model-title-case = Модель diff --git a/locales/tr-TR/opencadstudio.ftl b/locales/tr-TR/opencadstudio.ftl index fc8274482..027a179cc 100644 --- a/locales/tr-TR/opencadstudio.ftl +++ b/locales/tr-TR/opencadstudio.ftl @@ -606,7 +606,9 @@ common = .middle-right-sentence-case = Orta sağ .middleleft = Orta sol .middleright = Orta sağ + .mid-between-2-points-m2p = 2 Nokta Arasındaki Orta Nokta (M2P) .midpoint = Orta Nokta + .mtp-needs-an-active-point-prompt = MTP için etkin bir nokta istemi gerekir. .miles = Mil .millimeters = Milimetre .model-title-case = Model diff --git a/locales/zh-CN/opencadstudio.ftl b/locales/zh-CN/opencadstudio.ftl index b1840049e..028a2101d 100644 --- a/locales/zh-CN/opencadstudio.ftl +++ b/locales/zh-CN/opencadstudio.ftl @@ -607,7 +607,9 @@ common = .middle-right-sentence-case = 右边 .middleleft = 左边 .middleright = 右中边 + .mid-between-2-points-m2p = 两点之间的中点 (M2P) .midpoint = 中点 + .mtp-needs-an-active-point-prompt = MTP 需要活动的点提示。 .miles = 迈尔斯 .millimeters = 毫米数 .model-title-case = 模型 diff --git a/locales/zh-TW/opencadstudio.ftl b/locales/zh-TW/opencadstudio.ftl index acf355918..04c488dfc 100644 --- a/locales/zh-TW/opencadstudio.ftl +++ b/locales/zh-TW/opencadstudio.ftl @@ -619,7 +619,9 @@ common = .middle-right-sentence-case = 右邊 .middleleft = 左邊 .middleright = 右中邊 + .mid-between-2-points-m2p = 兩點之間的中點 (M2P) .midpoint = 中點 + .mtp-needs-an-active-point-prompt = MTP 需要作用中的點提示。 .miles = 邁爾斯 .millimeters = 毫米數 .model-title-case = 模型 diff --git a/src/app/automation.rs b/src/app/automation.rs index 64968ab11..a862a29cf 100644 --- a/src/app/automation.rs +++ b/src/app/automation.rs @@ -1551,6 +1551,23 @@ mod tests { assert!(app.tabs[0].active_cmd.is_none()); } + #[test] + fn mtp_snap_override_starts_the_existing_modifier() { + use crate::app::Message; + let mut app = OpenCADStudio::new_for_test(); + app.automation_op(r#"{"op":"new"}"#); + + let _ = app.update(Message::CommandInput("LINE".to_string())); + let _ = app.update(Message::CommandSubmit); + let _ = app.update(Message::SnapOverrideMtp); + + assert_eq!(app.tabs[0].active_cmd.as_ref().map(|c| c.name()), Some("MTP")); + assert_eq!( + app.tabs[0].suspended_cmd.as_ref().map(|c| c.name()), + Some("LINE") + ); + } + #[test] fn test_mtp_escape_restores_parent() { use crate::app::Message; diff --git a/src/app/mod.rs b/src/app/mod.rs index b7444abc9..dd843d3e1 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1910,9 +1910,12 @@ pub enum Message { WebFieldCopy, /// Web only: the focused field's text — write it to the clipboard. WebFieldCopyText(Option), - /// Pick from the one-shot snap override menu (Shift+RMB): only this snap - /// applies to the next point pick, then the configuration restores (#337). + /// Pick from the one-shot snap override menu (Shift+RMB): only this + /// snap applies to the next point pick, then the configuration restores (#337). SnapOverridePick(crate::snap::SnapType), + /// Mid Between 2 Points from the snap menu: modal 2-pick modifier over + /// the active point prompt. + SnapOverrideMtp, /// Close the one-shot snap override menu without picking. SnapOverrideClose, /// Open a path from the Start tab's recent-documents list (skips the diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index 6064d01f8..259609c43 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -680,6 +680,29 @@ impl OpenCADStudio { Task::none() } + Message::SnapOverrideMtp => { + self.snap_override_popup = None; + self.tabs[self.active_tab] + .scene + .selection + .borrow_mut() + .context_menu = None; + // Same guard as typed MTP/M2P: point step, not entity pick. + let i = self.active_tab; + let allowed = self.tabs[i].active_cmd.as_ref().is_some_and(|c| { + (!c.input_kind().wants_text() || c.point_step_accepts_keywords()) + && !c.needs_entity_pick() + }); + if allowed { + self.start_mtp_modifier(i); + } else { + self.command_line.push_info( + crate::t!("MTP needs an active point prompt.").as_ref(), + ); + } + Task::none() + } + Message::SnapOverrideClose => { self.snap_override_popup = None; Task::none() diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index 42dd3c81e..d91c18608 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -1753,6 +1753,11 @@ bg={bg_ms:.1}ms n={view_count}" }; if let Some(p) = ctx_pos { let has_cmd = tab.active_cmd.is_some(); + // Same guard as typed MTP/M2P and SnapOverrideMtp. + let has_point_step = tab.active_cmd.as_ref().is_some_and(|c| { + (!c.input_kind().wants_text() || c.point_step_accepts_keywords()) + && !c.needs_entity_pick() + }); let has_selection = !tab.scene.selected.is_empty(); let isolation_active = tab.scene.is_isolation_active(); let last_cmds: Vec = self @@ -1771,6 +1776,7 @@ bg={bg_ms:.1}ms n={view_count}" isolation_active, last_cmds, draworder_open, + has_point_step, )); } } diff --git a/src/app/view/overlay.rs b/src/app/view/overlay.rs index 31aaf8cd2..3192b9128 100644 --- a/src/app/view/overlay.rs +++ b/src/app/view/overlay.rs @@ -1117,6 +1117,7 @@ pub(super) fn viewport_context_menu_overlay( isolation_active: bool, last_cmds: Vec, draworder_open: bool, + has_point_step: bool, ) -> Element<'static, Message> { let item = |label: String, msg: Message| -> Element<'static, Message> { button(text(label).size(12)) @@ -1161,6 +1162,15 @@ pub(super) fn viewport_context_menu_overlay( if has_cmd { items.push(item(t!("Cancel").into_owned(), Message::CommandEscape)); items.push(item(t!("Enter").into_owned(), Message::CommandFinalize)); + // MTP (`_M2P`) goes last, after Parallel: two picks, so not a + // `SnapType`, but same icon-only cell with hover tooltip. + if has_point_step { + items.push(sep()); + items.push(item( + t!("Mid Between 2 Points (M2P)").into_owned(), + Message::SnapOverrideMtp, + )); + } } else { if !last_cmds.is_empty() { let last = last_cmds[0].clone(); @@ -1277,21 +1287,19 @@ pub(super) fn viewport_context_menu_overlay( /// One-shot snap override menu (Shift+RMB, #337): a cursor-anchored grid of /// snap ICONS only — the names show as hover tooltips. Picking one applies -/// that snap to just the next point pick. +/// that snap to just the next point pick; the trailing MTP cell instead +/// suspends the prompt for two picks and returns their midpoint. pub(super) fn snap_override_overlay(pos: iced::Point) -> Element<'static, Message> { const COLS: usize = 4; - let cell = |snap_type: crate::snap::SnapType, label: &'static str| -> Element<'static, Message> { - let icon = container(crate::ui::icons::themed::( - crate::ui::icons::osnap(snap_type), - 16.0, - )) + let cell_icon = |icon: &'static [u8], label: String, msg: Message| -> Element<'static, Message> { + let icon = container(crate::ui::icons::themed::(icon, 16.0)) .width(26) .height(26) .align_x(iced::Center) .align_y(iced::Center); let btn = button(icon) - .on_press(Message::SnapOverridePick(snap_type)) + .on_press(msg) .style(|theme: &Theme, status| button::Style { background: matches!( status, @@ -1327,11 +1335,29 @@ pub(super) fn snap_override_overlay(pos: iced::Point) -> Element<'static, Messag .into() }; + // MTP (`_M2P`) goes last, after Parallel: two picks, so not a + // `SnapType`, but same icon-only cell with hover tooltip. + let mut cells: Vec> = Vec::with_capacity( + crate::snap::ALL_SNAP_MODES.len() + 1, + ); + for &(snap_type, _glyph, label) in crate::snap::ALL_SNAP_MODES { + cells.push(cell_icon( + crate::ui::icons::osnap(snap_type), + label.to_string(), + Message::SnapOverridePick(snap_type), + )); + } + cells.push(cell_icon( + crate::ui::icons::mtp_icon(), + t!("Mid Between 2 Points (M2P)").into_owned(), + Message::SnapOverrideMtp, + )); let mut grid = column![].spacing(2); - for chunk in crate::snap::ALL_SNAP_MODES.chunks(COLS) { + while !cells.is_empty() { + let n = cells.len().min(COLS); let mut r = row![].spacing(2); - for &(snap_type, _glyph, label) in chunk { - r = r.push(cell(snap_type, label)); + for c in cells.drain(..n) { + r = r.push(c); } grid = grid.push(r); } diff --git a/src/locale_catalog.rs b/src/locale_catalog.rs index 2814da757..f5fdc2646 100644 --- a/src/locale_catalog.rs +++ b/src/locale_catalog.rs @@ -1677,6 +1677,7 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "MiddleOfText" => Some(("styles", "middleoftext")), "MiddleOfTopLine" => Some(("styles", "middleoftopline")), "MiddleRight" => Some(("common", "middleright")), + "Mid Between 2 Points (M2P)" => Some(("common", "mid-between-2-points-m2p")), "Midpoint" => Some(("common", "midpoint")), "Miles" => Some(("common", "miles")), "Millimeters" => Some(("common", "millimeters")), @@ -1707,6 +1708,7 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "Move Right" => Some(("layout", "move-right")), "Move freely" => Some(("modify", "move-freely")), "Move text & arrows out" => Some(("modify", "move-text-arrows-out")), + "MTP needs an active point prompt." => Some(("common", "mtp-needs-an-active-point-prompt")), "Multi-View\nBlock" => Some(("insert", "multi-view-block")), "MultiLeader" => Some(("properties", "multileader")), "MultiLine" => Some(("properties", "multiline")), diff --git a/src/ui/icons.rs b/src/ui/icons.rs index 384929a93..a9d4227da 100644 --- a/src/ui/icons.rs +++ b/src/ui/icons.rs @@ -43,6 +43,7 @@ static OSNAP_NEAREST: &[u8] = include_bytes!("../../assets/icons/osnap/nearest.s static OSNAP_APPARENT: &[u8] = include_bytes!("../../assets/icons/osnap/apparent.svg"); static OSNAP_PARALLEL: &[u8] = include_bytes!("../../assets/icons/osnap/parallel.svg"); static OSNAP_GRID: &[u8] = include_bytes!("../../assets/icons/osnap/grid.svg"); +static OSNAP_MTP: &[u8] = include_bytes!("../../assets/icons/osnap/mtp.svg"); static LAY_ON: &[u8] = include_bytes!("../../assets/icons/layers/layon.svg"); static LAY_OFF: &[u8] = include_bytes!("../../assets/icons/layers/layoff.svg"); @@ -595,6 +596,11 @@ pub fn osnap(snap: crate::snap::SnapType) -> &'static [u8] { } } +/// MTP menu icon: modal 2-pick modifier, not a persistent `SnapType` mode. +pub fn mtp_icon() -> &'static [u8] { + OSNAP_MTP +} + /// Layer visibility icon bytes (on / off). pub fn layer_visible(visible: bool) -> &'static [u8] { if visible {