Files
cadcraft/crates/engine/tests/annotate_options.rs
T
Valentin KleinandClaude Opus 5.5 87d1e28345 MLEADER: spline and no-leader types, block content (fixes #408) (#419)
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>
2026-10-10 19:20:02 -04:00

141 lines
5.0 KiB
Rust

//! Options listed at the dimension and multileader prompts take effect (#243).
use cadcraft_engine::Session;
use cadcraft_engine::doc::{DimKind, Dimension, EntityKind, MLeader};
use cadcraft_engine::geom::Vec2;
fn last(s: &Session) -> EntityKind {
s.doc().unwrap().model.last().unwrap().kind.clone()
}
fn last_dim(s: &Session) -> Dimension {
match last(s) {
EntityKind::Dimension(d) => d,
other => panic!("not a dimension: {other:?}"),
}
}
fn last_mleader(s: &Session) -> MLeader {
match last(s) {
EntityKind::MLeader(m) => m,
other => panic!("not a multileader: {other:?}"),
}
}
fn value(s: &Session, d: &Dimension) -> String {
cadcraft_engine::render::dimension_in(s.doc().unwrap(), d).value
}
fn near(a: Vec2, b: Vec2) -> bool {
a.dist(b) < 1e-6
}
#[test]
fn dimlinear_angle_rotates_the_text_and_text_replaces_it() {
let mut s = Session::new();
s.script("dimlinear 0,0 10,0 a 30 t\nX=<>\n5,-3\n").unwrap();
let d = last_dim(&s);
assert_eq!(d.kind, DimKind::Linear { rotation: 0.0 }, "Angle must not rotate the dimension line");
assert!((d.text_rotation - 30f64.to_radians()).abs() < 1e-9, "{}", d.text_rotation);
assert_eq!(d.text, "X=<>");
assert!(s.running.is_none());
}
#[test]
fn dimlinear_vertical_forces_the_orientation() {
let mut s = Session::new();
s.script("dimlinear 0,0 10,5 v").unwrap();
let p = s.prompt_text();
assert!(p.contains("[Mtext/Text/Angle]"), "{p}");
s.script("5,-3\n").unwrap();
assert_eq!(last_dim(&s).kind, DimKind::Linear { rotation: std::f64::consts::FRAC_PI_2 });
}
#[test]
fn dimordinate_datum_and_text_options() {
let mut s = Session::new();
// The leader endpoint alone would make a Y datum; Xdatum forces X, and Text is a text option.
s.script("dimordinate 0,0 x t\nA <>\n10,2\n").unwrap();
let d = last_dim(&s);
assert_eq!(d.kind, DimKind::Ordinate { x_type: true });
assert_eq!(d.text, "A <>");
s.script("dimordinate 0,0 y a 90 2,10\n").unwrap();
let d = last_dim(&s);
assert_eq!(d.kind, DimKind::Ordinate { x_type: false });
assert!((d.text_rotation - std::f64::consts::FRAC_PI_2).abs() < 1e-9);
}
#[test]
fn dimradius_text_options() {
let mut s = Session::new();
s.script("circle 0,0 5\ndimradius 5,0 t\nR=<>\na 45 8,8\n").unwrap();
let d = last_dim(&s);
assert_eq!(d.kind, DimKind::Radius);
assert_eq!(d.text, "R=<>");
assert!((d.text_rotation - 45f64.to_radians()).abs() < 1e-9);
}
#[test]
fn dimangular_quadrant_locks_the_measured_angle() {
let mut s = Session::new();
// Placed at (-3,-4) the arc would measure the 270° side; the quadrant keeps the 90° one.
s.script("dimangular\n\n0,0 10,0 0,10 q 5,5 -3,-4\n").unwrap();
let d = last_dim(&s);
assert_eq!(value(&s, &d), "90°");
assert!(near(d.defpt.xy(), Vec2::new(5.0, 5.0).normalized() * 5.0), "{:?}", d.defpt);
}
#[test]
fn dimarc_partial_measures_part_of_the_arc() {
let mut s = Session::new();
s.script("arc c 0,0 -10,0 10,0\n").unwrap();
assert!(matches!(last(&s), EntityKind::Arc(_)), "{:?}", last(&s));
s.script("dimarc 0,-10 p 10,0 0,-10 5,-12\n").unwrap();
let d = last_dim(&s);
assert_eq!(d.kind, DimKind::ArcLength);
assert!(near(d.p13.xy(), Vec2::new(0.0, -10.0)) && near(d.p14.xy(), Vec2::new(10.0, 0.0)), "{d:?}");
assert_eq!(value(&s, &d), "⌒15.7080");
}
#[test]
fn mleader_landing_first_and_content_first() {
let mut s = Session::new();
s.script("mleader l 10,5 0,0 note\n").unwrap();
let m = last_mleader(&s);
assert_eq!(m.leaders.len(), 1);
assert!(near(m.leaders[0][0].xy(), Vec2::ZERO) && near(m.landing.xy(), Vec2::new(10.0, 5.0)), "{m:?}");
assert_eq!(m.text.unwrap().contents, "note");
s.script("mleader c 10,5 hello\n0,0\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.text.unwrap().contents, "hello");
}
#[test]
fn mleader_options_take_effect() {
let mut s = Session::new();
// Enter opens Options; Maxpoints 3 and no landing line.
s.script("mleader\n\nm 3 a n x 0,0 5,5 10,5 txt\n").unwrap();
let m = last_mleader(&s);
assert_eq!(m.leaders[0].len(), 2, "{m:?}");
assert!(near(m.leaders[0][1].xy(), Vec2::new(5.0, 5.0)) && near(m.landing.xy(), Vec2::new(10.0, 5.0)));
assert_eq!(m.dogleg, 0.0);
assert_eq!(m.text.unwrap().contents, "txt");
// 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();
assert!(near(m.landing.xy(), Vec2::new(r, r) / 2f64.sqrt()), "{:?}", m.landing);
// Leader type sPline keeps the following input and draws a spline leader.
s.script("mleader o l p x 0,0 4,4 hi\n").unwrap();
let m = last_mleader(&s);
assert!(m.spline);
assert_eq!(m.text.unwrap().contents, "hi");
assert!(s.running.is_none());
}