feat(command): add MTP/M2P midpoint-between-two-points modifier

Add MTP (Midpoint Between Two Points) as a keyword that can be typed
during any point-picking command (e.g. LINE) to suspend the current
command, prompt for two points, and return the midpoint to the parent.

- New Mid2PointCommand registered under both "MTP" and "M2P" aliases
- ReturnPoint CmdResult variant feeds the resolved midpoint back to
  the suspended parent command
- MTP/M2P typed during a point step suspends the active command and
  starts the midpoint modifier; Escape restores the parent command
- Preview wires show a rubber-band line with a triangle marker at
  the midpoint
- Character-by-character typing is routed correctly even with dynamic
  input active (digits no longer leak into dyn fields)
- Three automation tests cover the full workflow, ESC restoration, and
  dynamic-input character routing
This commit is contained in:
Karim Jerbi committed 2026-09-10 20:30:18 +01:00
1 parent f4eeba75df
commit f77c337af4
4 files changed
+330 -9

No files matched your search

+124
View File
@@ -744,6 +744,88 @@ impl CadCommand for TwoValuePromptCommand {
}
}
// ── Mid between 2 points (MTP / M2P) ────────────────────────────────────────
/// Point-entry modifier that prompts for two points, draws a preview connecting line
/// with a midpoint marker, and returns the midpoint to the caller.
#[derive(Debug, Default)]
pub struct Mid2PointCommand {
pub first_point: Option<DVec3>,
}
impl Mid2PointCommand {
pub fn new() -> Self {
Self { first_point: None }
}
}
impl CadCommand for Mid2PointCommand {
fn name(&self) -> &'static str {
"MTP"
}
fn prompt(&self) -> String {
if self.first_point.is_none() {
crate::t!("_mtp Specify first point of mid:").into_owned()
} else {
crate::t!("_mtp Specify second point of mid:").into_owned()
}
}
fn on_point(&mut self, pt: DVec3) -> CmdResult {
if let Some(first) = self.first_point {
let mid = (first + pt) * 0.5;
CmdResult::ReturnPoint(mid)
} else {
self.first_point = Some(pt);
CmdResult::NeedPoint
}
}
fn on_enter(&mut self) -> CmdResult {
CmdResult::Cancel
}
fn on_escape(&mut self) -> CmdResult {
CmdResult::Cancel
}
fn on_preview_wires(&mut self, pt: DVec3) -> Vec<WireModel> {
let mut wires = Vec::new();
if let Some(first) = self.first_point {
// Rubber-band line connecting the first point to the cursor
wires.push(WireModel::solid_f64(
"mtp_rubber_band".to_string(),
vec![[first.x, first.y, first.z], [pt.x, pt.y, pt.z]],
WireModel::CYAN,
false,
));
// Midpoint marker (AutoCAD style triangle glyph)
let mid = (first + pt) * 0.5;
let dist = (pt - first).length();
let s = (dist * 0.02).clamp(0.5, 10.0);
let h = s * 1.5;
let w = s * 1.0;
wires.push(WireModel::solid_f64(
"mtp_mid_triangle".to_string(),
vec![
[mid.x, mid.y + h * (2.0 / 3.0), mid.z],
[mid.x - w, mid.y - h * (1.0 / 3.0), mid.z],
[mid.x + w, mid.y - h * (1.0 / 3.0), mid.z],
],
WireModel::CYAN,
true,
));
}
wires
}
}
inventory::submit!(CommandRegistration {
names: &["MTP", "M2P"],
});
/// Generic interactive front-end for a keyword command that operates on the
/// current selection (CHPROP, ADJUST, XDATA, UNDERLAY, DRAWORDER…). If nothing
/// is selected when it starts it first gathers a selection (Enter confirms),
@@ -1629,6 +1711,8 @@ pub enum CmdResult {
UndoDocument,
/// Sets the TEXTEDITMODE system variable and ends the command.
SetTexteditMode(bool),
/// Return a resolved point back to a suspended parent command (e.g. MTP / M2P).
ReturnPoint(DVec3),
}
/// What kind of value the active command is currently asking for. Drives
@@ -2385,3 +2469,43 @@ pub fn all_registered_command_names() -> Vec<&'static str> {
.flat_map(|r| r.names.iter().copied())
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mid2point_command() {
let mut cmd = Mid2PointCommand::new();
assert_eq!(cmd.name(), "MTP");
assert_eq!(cmd.prompt(), "_mtp Specify first point of mid:");
// First point
let res = cmd.on_point(DVec3::new(10.0, 20.0, 0.0));
assert!(matches!(res, CmdResult::NeedPoint));
assert_eq!(cmd.prompt(), "_mtp Specify second point of mid:");
// Preview wires
let wires = cmd.on_preview_wires(DVec3::new(30.0, 40.0, 0.0));
assert!(!wires.is_empty());
// Second point
let res = cmd.on_point(DVec3::new(30.0, 40.0, 0.0));
assert!(matches!(res, CmdResult::ReturnPoint(mid) if mid == DVec3::new(20.0, 30.0, 0.0)));
}
#[test]
fn test_mid2point_cancel() {
let mut cmd = Mid2PointCommand::new();
assert!(matches!(cmd.on_escape(), CmdResult::Cancel));
assert!(matches!(cmd.on_enter(), CmdResult::Cancel));
}
#[test]
fn test_mtp_registered() {
let names = all_registered_command_names();
assert!(names.contains(&"MTP"));
assert!(names.contains(&"M2P"));
}
}