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 <noreply@anthropic.com>
This commit is contained in:
Valentin Klein 2026-10-11 00:09:20 +02:00 committed by GitHub
parent 8db51c2766
commit de8ed87a6e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 110 additions and 29 deletions

View File

@ -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<CommandSpec> {
vec![
@ -69,7 +69,7 @@ pub fn specs() -> Vec<CommandSpec> {
.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<Vec2>) -> Vec<Vec2> {
let mut pts = self.pts.clone();
@ -1649,6 +1645,7 @@ impl Interactive for MLeaderM {
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
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,

View File

@ -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 }
}
}

View File

@ -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();

View File

@ -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);
}