mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-25 19:36:49 -06:00
MLEADER's Options answered "not available yet" for Leader type sPline and None and for Content type Block. Spline leaders now draw as smooth curves through their points (DXF LEADER path type 72 = 1); None places only the content at one picked point; Block asks for a block name and how it attaches (Center extents or Insertion point) and inserts it at the end of the landing. MLeader gains `spline` and `block` (serde defaults); DXF stays LEADER + MTEXT/INSERT. PURGE, RENAME, copy/paste, FLATTEN, grips and extents follow the block content. The choices are remembered like the other MLEADER options. docs/gaps.md: top-10 row 5, F1 and F10 narrowed. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
124 lines
5.6 KiB
Rust
124 lines
5.6 KiB
Rust
//! MLEADER leader types (straight, spline, none) and block content, at the command line and in
|
|
//! the JSON form (#408).
|
|
|
|
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 logged(s: &Session, text: &str) -> bool {
|
|
s.log.iter().any(|l| l.contains(text))
|
|
}
|
|
|
|
/// A session with block "TAG": a 2 x 2 square around its base point (0, 0).
|
|
fn with_block() -> Session {
|
|
let mut s = Session::new();
|
|
let sq = s.execute("pline", &json!({"vertices": [[-1, -1], [1, -1], [1, 1], [-1, 1]], "closed": true})).unwrap()["handle"].clone();
|
|
s.execute("block", &json!({"name": "TAG", "base": [-1, -1], "handles": [sq], "keep": "delete"})).unwrap();
|
|
s
|
|
}
|
|
|
|
#[test]
|
|
fn spline_leader_curves_through_its_points() {
|
|
let mut s = Session::new();
|
|
s.script("mleader o l p m 3 x 0,0 5,5 10,0 note\n").unwrap();
|
|
let m = last_mleader(&s);
|
|
assert!(m.spline && m.leaders[0].len() == 2);
|
|
let path = &m.leader_paths()[0];
|
|
assert!(path.len() > 3, "tessellated: {path:?}");
|
|
assert!(path.first().unwrap().near(Vec2::ZERO, 1e-9) && path.last().unwrap().near(Vec2::new(10.0, 0.0), 1e-9));
|
|
// The straight leader runs along y = x up to the middle point; the curve rounds the corner above it.
|
|
assert!(path.iter().any(|p| p.x > 2.0 && p.x < 4.0 && p.y > p.x + 0.1), "not a smooth curve: {path:?}");
|
|
// Remembered, and Straight switches back.
|
|
s.script("mleader 0,0 5,5 10,0 again\n").unwrap();
|
|
assert!(last_mleader(&s).spline);
|
|
s.script("mleader o l s x 0,0 5,5 10,0 straight\n").unwrap();
|
|
assert!(!last_mleader(&s).spline);
|
|
}
|
|
|
|
#[test]
|
|
fn no_leader_places_only_the_content() {
|
|
let mut s = Session::new();
|
|
s.script("mleader o l n x 3,4 Only text\n").unwrap();
|
|
assert!(s.running.is_none(), "{}", s.prompt_text());
|
|
let m = last_mleader(&s);
|
|
assert!(m.leaders.is_empty() && m.dogleg == 0.0);
|
|
let t = m.text.unwrap();
|
|
assert_eq!(t.contents, "Only text");
|
|
assert!(t.insert.xy().near(Vec2::new(3.0, 4.0), 1e-9) && t.attach == 1);
|
|
// No leader and no content: nothing to draw.
|
|
s.script("mleader o c n x 1,1\n").unwrap();
|
|
assert!(logged(&s, "nothing was drawn"));
|
|
assert_eq!(s.doc().unwrap().model.iter().count(), 1);
|
|
assert!(s.running.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn block_content_inserts_the_block_at_the_landing() {
|
|
let mut s = with_block();
|
|
// Unknown names are reported and asked again; Center extents puts the block's centre at the
|
|
// end of the landing.
|
|
s.script("mleader o c b nosuch\n").unwrap();
|
|
assert!(logged(&s, "Block \"nosuch\" not found."));
|
|
s.script("tag c x 0,0 10,5\n").unwrap();
|
|
assert!(s.running.is_none(), "{}", s.prompt_text());
|
|
let m = last_mleader(&s);
|
|
assert!(m.text.is_none());
|
|
let b = m.block.clone().expect("block content");
|
|
assert_eq!(b.block, "TAG");
|
|
let end = m.landing.xy() + Vec2::new(m.dogleg, 0.0);
|
|
// Block base (-1,-1) maps to the insertion point; the square's centre is base + (1, 1).
|
|
assert!((b.insert.xy() + Vec2::new(1.0, 1.0)).near(end, 1e-9), "{:?} vs {end:?}", b.insert);
|
|
// Insertion point attachment, remembered for the next MLEADER together with the block.
|
|
s.script("mleader o c b tag i x 0,0 10,5\n").unwrap();
|
|
let m = last_mleader(&s);
|
|
assert!(m.block.unwrap().insert.xy().near(m.landing.xy() + Vec2::new(m.dogleg, 0.0), 1e-9));
|
|
s.script("mleader 20,0 30,5\n").unwrap();
|
|
assert!(last_mleader(&s).block.is_some(), "block content remembered");
|
|
// Block content without a leader line.
|
|
s.script("mleader o l n x 50,50\n").unwrap();
|
|
let m = last_mleader(&s);
|
|
assert!(m.leaders.is_empty() && m.block.unwrap().insert.xy().near(Vec2::new(50.0, 50.0), 1e-9));
|
|
}
|
|
|
|
#[test]
|
|
fn block_content_keeps_the_block_alive() {
|
|
let mut s = with_block();
|
|
let h = s.execute("mleader", &json!({"points": [[0, 0], [10, 5]], "block": "tag", "blockAttach": "insertion"})).unwrap()["handle"].clone();
|
|
// PURGE keeps a block used only as multileader content.
|
|
s.execute("purge", &json!({"type": "blocks"})).ok();
|
|
assert!(s.doc().unwrap().block("TAG").is_some(), "purged a block in use");
|
|
// RENAME follows into the multileader.
|
|
s.execute("rename", &json!({"table": "block", "from": "TAG", "to": "NOTE"})).unwrap();
|
|
assert_eq!(last_mleader(&s).block.unwrap().block, "NOTE");
|
|
// MOVE carries the block along.
|
|
let before = last_mleader(&s).block.unwrap().insert.xy();
|
|
s.execute("move", &json!({"handles": [h], "from": [0, 0], "to": [5, 5]})).unwrap();
|
|
assert!(last_mleader(&s).block.unwrap().insert.xy().near(before + Vec2::new(5.0, 5.0), 1e-9));
|
|
}
|
|
|
|
#[test]
|
|
fn json_form_takes_leader_type_and_block() {
|
|
let mut s = with_block();
|
|
s.execute("mleader", &json!({"points": [[0, 0], [5, 5], [10, 0]], "text": "s", "leaderType": "spline"})).unwrap();
|
|
assert!(last_mleader(&s).spline);
|
|
s.execute("mleader", &json!({"points": [[7, 7]], "text": "alone", "leaderType": "none"})).unwrap();
|
|
let m = last_mleader(&s);
|
|
assert!(m.leaders.is_empty() && m.text.unwrap().insert.xy().near(Vec2::new(7.0, 7.0), 1e-9));
|
|
for bad in [
|
|
json!({"points": [[0, 0], [5, 5]], "leaderType": "wavy"}),
|
|
json!({"points": [[0, 0], [5, 5]], "block": "missing"}),
|
|
json!({"points": [[0, 0], [5, 5]], "block": "tag", "blockAttach": "corner"}),
|
|
json!({"points": [[0, 0]]}),
|
|
] {
|
|
assert!(s.execute("mleader", &bad).is_err(), "{bad}");
|
|
}
|
|
}
|