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/src/app/mod.rs b/src/app/mod.rs
index 0c2cd3e73..a5b58bbc4 100644
--- a/src/app/mod.rs
+++ b/src/app/mod.rs
@@ -1892,9 +1892,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 (AutoCAD `_M2P`).
+ 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 f84da07ef..4be17c12c 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 23ec26ce1..8dc825ed0 100644
--- a/src/app/view/mod.rs
+++ b/src/app/view/mod.rs
@@ -1629,6 +1629,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
@@ -1647,6 +1652,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 014da974f..317967a2f 100644
--- a/src/app/view/overlay.rs
+++ b/src/app/view/overlay.rs
@@ -1102,6 +1102,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))
@@ -1146,6 +1147,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();
@@ -1262,21 +1272,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,
@@ -1312,11 +1320,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/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 {