From de8ed87a6e41694040696348fc01c4607921743a Mon Sep 17 00:00:00 2001 From: Valentin Klein Date: Sun, 11 Oct 2026 00:09:20 +0200 Subject: [PATCH] MLEADER remembers the placement order and Options (fixes #362) (#365) The next MLEADER started again with "arrowhead first", Maxpoints 2 and a landing line. As in AutoCAD, the order (arrowhead, landing or content first) and the Options values (Maxpoints, leader landing, content type, first and second angle) are now kept in Session::last_used for the rest of the session and are where the next MLEADER starts. JSON calls keep their documented defaults. Co-authored-by: Claude Opus 5.5 --- crates/engine/src/cmd/annotate.rs | 54 ++++++++++++------------- crates/engine/src/lib.rs | 33 ++++++++++++++- crates/engine/tests/annotate_options.rs | 4 +- crates/engine/tests/mleader_remember.rs | 48 ++++++++++++++++++++++ 4 files changed, 110 insertions(+), 29 deletions(-) create mode 100644 crates/engine/tests/mleader_remember.rs diff --git a/crates/engine/src/cmd/annotate.rs b/crates/engine/src/cmd/annotate.rs index 683c2e7..715513c 100644 --- a/crates/engine/src/cmd/annotate.rs +++ b/crates/engine/src/cmd/annotate.rs @@ -9,7 +9,7 @@ use serde_json::{Value, json}; use super::helpers::v3; use super::machines::SelectRun; use super::*; -use crate::{Accept, EngineError, Input, Interactive, Prompt, Result, Session, Step}; +use crate::{Accept, EngineError, Input, Interactive, LeaderOrder, MLeaderOptions, Prompt, Result, Session, Step}; pub fn specs() -> Vec { vec![ @@ -69,7 +69,7 @@ pub fn specs() -> Vec { .menu(&["Dimension", "Multileader"]) .alias(&["mld"]) .params("{points: [[arrow], ..., [landing]], text}") - .interactive(|_| Ok(Box::new(MLeaderM::default()))), + .interactive(|s| Ok(Box::new(MLeaderM::new(s)))), CommandSpec::new("leader", "Leader", run_leader).alias(&["lead"]).params("{points: [[x,y]...], text?}"), CommandSpec::new("dimstyle.update", "Update", run_update) .menu(&["Dimension", "Update"]) @@ -1455,15 +1455,6 @@ impl Interactive for ChainM { } } -/// Which end of the multileader MLEADER places first. -#[derive(Clone, Copy, Default, PartialEq)] -enum LeaderOrder { - #[default] - Arrowhead, - Landing, - Content, -} - /// The MLEADER Options prompt being answered. #[derive(Clone, Copy, PartialEq)] enum LeaderOpt { @@ -1495,23 +1486,28 @@ struct MLeaderM { content: bool, } -impl Default for MLeaderM { - fn default() -> Self { +impl MLeaderM { + /// Starts with the order and Options chosen the last time (as AutoCAD does). + fn new(s: &Session) -> Self { + let o = s.last_used.mleader; + let max_points = o.max_points.clamp(2, MAX_LEADER_POINTS); MLeaderM { - order: LeaderOrder::Arrowhead, + order: o.order, pts: Vec::new(), leader_done: false, text: None, opt: None, - max_points: 2, - angles: [0.0; 2], - landing: true, - content: true, + max_points, + angles: o.angles, + landing: o.landing, + content: o.content, } } -} - -impl MLeaderM { + /// Keep the order and Options for the next MLEADER. + fn remember(&self, s: &mut Session) { + s.last_used.mleader = + MLeaderOptions { order: self.order, max_points: self.max_points, angles: self.angles, landing: self.landing, content: self.content }; + } /// The leader from arrowhead to landing, with `extra` placed next. fn leader(&self, extra: Option) -> Vec { let mut pts = self.pts.clone(); @@ -1649,6 +1645,7 @@ impl Interactive for MLeaderM { fn input(&mut self, s: &mut Session, i: Input) -> Result { if let Some(o) = self.opt { self.opt = self.option(s, o, i); + self.remember(s); return Ok(Step::Continue); } if self.wants_text() { @@ -1663,12 +1660,15 @@ impl Interactive for MLeaderM { self.leader_done = true; } } - Input::Keyword(k) if self.pts.is_empty() => match k.as_str() { - "leader Landing first" => self.order = LeaderOrder::Landing, - "leader arrowHead first" => self.order = LeaderOrder::Arrowhead, - "Content first" => self.order = LeaderOrder::Content, - _ => self.opt = Some(LeaderOpt::Menu), - }, + Input::Keyword(k) if self.pts.is_empty() => { + match k.as_str() { + "leader Landing first" => self.order = LeaderOrder::Landing, + "leader arrowHead first" => self.order = LeaderOrder::Arrowhead, + "Content first" => self.order = LeaderOrder::Content, + _ => self.opt = Some(LeaderOpt::Menu), + } + self.remember(s); + } Input::Enter if self.pts.is_empty() => self.opt = Some(LeaderOpt::Menu), // Enter at "next point" ends the leader at the last point. Input::Enter if self.pts.len() >= 2 => self.leader_done = true, diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index 0c31636..6328697 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -346,11 +346,42 @@ pub struct LastUsed { pub rotate_angle: f64, /// SCALE factor. pub scale_factor: f64, + /// MLEADER placement order and Options. + pub mleader: MLeaderOptions, } impl Default for LastUsed { fn default() -> Self { - LastUsed { hatch_select: false, polygon_circumscribed: false, rotate_angle: 0.0, scale_factor: 1.0 } + LastUsed { hatch_select: false, polygon_circumscribed: false, rotate_angle: 0.0, scale_factor: 1.0, mleader: MLeaderOptions::default() } + } +} + +/// Which part of a multileader MLEADER places first. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum LeaderOrder { + #[default] + Arrowhead, + Landing, + Content, +} + +/// The MLEADER placement order and the values set at its Options prompt. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct MLeaderOptions { + pub order: LeaderOrder, + /// Maximum points of the leader line (2..=64). + pub max_points: usize, + /// First and second angle constraints (radians; 0 = free). + pub angles: [f64; 2], + /// Draw the landing line (dogleg). + pub landing: bool, + /// Ask for multiline text content (Content type None turns it off). + pub content: bool, +} + +impl Default for MLeaderOptions { + fn default() -> Self { + MLeaderOptions { order: LeaderOrder::Arrowhead, max_points: 2, angles: [0.0; 2], landing: true, content: true } } } diff --git a/crates/engine/tests/annotate_options.rs b/crates/engine/tests/annotate_options.rs index 4910a8a..70256c6 100644 --- a/crates/engine/tests/annotate_options.rs +++ b/crates/engine/tests/annotate_options.rs @@ -123,7 +123,9 @@ fn mleader_options_take_effect() { assert_eq!(m.dogleg, 0.0); assert_eq!(m.text.unwrap().contents, "txt"); - // First angle constraint snaps the leader to 45° steps. + // First angle constraint snaps the leader to 45° steps (a new session: MLEADER would keep the + // Maxpoints and landing chosen above). + let mut s = Session::new(); s.script("mleader o f 45 x 0,0 10,8 note\n").unwrap(); let m = last_mleader(&s); let r = Vec2::new(10.0, 8.0).len(); diff --git a/crates/engine/tests/mleader_remember.rs b/crates/engine/tests/mleader_remember.rs new file mode 100644 index 0000000..e54d375 --- /dev/null +++ b/crates/engine/tests/mleader_remember.rs @@ -0,0 +1,48 @@ +//! MLEADER starts with the placement order and Options chosen the last time (#247 follow-up). + +use cadcraft_engine::Session; +use cadcraft_engine::doc::{EntityKind, MLeader}; +use cadcraft_engine::geom::Vec2; +use serde_json::json; + +fn last_mleader(s: &Session) -> MLeader { + match &s.doc().unwrap().model.last().unwrap().kind { + EntityKind::MLeader(m) => m.clone(), + other => panic!("not a multileader: {other:?}"), + } +} + +fn near(a: Vec2, b: Vec2) -> bool { + a.dist(b) < 1e-6 +} + +#[test] +fn mleader_remembers_order_and_options() { + let mut s = Session::new(); + // Landing first, then Options: Maxpoints 3, no landing line. + s.script("mleader l o m 3 a n x 10,5 5,5 0,0 one\n").unwrap(); + let m = last_mleader(&s); + assert!(near(m.leaders[0][0].xy(), Vec2::ZERO) && near(m.landing.xy(), Vec2::new(10.0, 5.0)), "{m:?}"); + assert_eq!(m.dogleg, 0.0); + + // The next MLEADER asks for the landing first and keeps both options. + s.cmdline("mleader").unwrap(); + assert!(s.prompt_text().contains("landing location"), "{}", s.prompt_text()); + s.script("20,5 15,5 10,0 two\n").unwrap(); + let m = last_mleader(&s); + assert_eq!(m.leaders[0].len(), 2, "three points (Maxpoints 3): {m:?}"); + assert!(near(m.leaders[0][0].xy(), Vec2::new(10.0, 0.0)) && near(m.landing.xy(), Vec2::new(20.0, 5.0)), "{m:?}"); + assert_eq!(m.dogleg, 0.0); + assert_eq!(m.text.unwrap().contents, "two"); + + // Switching back to arrowhead first is remembered too, also when the command is cancelled. + s.script("mleader h\n").unwrap(); + s.cancel(); + s.cmdline("mleader").unwrap(); + assert!(s.prompt_text().contains("arrowhead location"), "{}", s.prompt_text()); + s.cancel(); + + // JSON calls keep their documented defaults (landing line drawn). + s.execute("mleader", &json!({"points": [[0, 0], [5, 5]], "text": "json"})).unwrap(); + assert!(last_mleader(&s).dogleg > 0.0); +}