DIMORDINATE Xdatum/Ydatum/Mtext/Text/Angle, DIMRADIUS/DIMDIAMETER Mtext/Text/Angle, DIMANGULAR Mtext/Text/Angle/Quadrant and DIMARC Mtext/Text/Angle/Partial were listed but ignored (any DIMORDINATE keyword other than Xdatum forced a Y datum). DIMLINEAR/DIMALIGNED Angle rotated the dimension line instead of the text. A shared DimText handles Mtext/Text (override text, <> = measurement) and Angle (text rotation) for every placement prompt. Quadrant locks the measured angle to a picked point's side; Partial dimensions the part of the arc between two picked points. After Horizontal/Vertical/ Rotated only the text options remain listed. MLEADER: leader Landing first / Content first switch the placement order, Enter or Options opens the options loop (Leader type, leader lAnding, Content type, Maxpoints, First/Second angle). Spline leaders, leaderless and block content say "not available yet". Angle prompts take numbers so scripts keep splitting the following tokens. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
d433112f1e
commit
c26150ec07
|
|
@ -27,12 +27,12 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
.menu(&["Dimension", "Radius"])
|
||||
.alias(&["dra", "dimrad"])
|
||||
.params("{handle, at?} | {center, point}")
|
||||
.interactive(|_| Ok(Box::new(RadialM { diameter: false, target: None }))),
|
||||
.interactive(|_| Ok(Box::new(RadialM::new(false)))),
|
||||
CommandSpec::new("dimdiameter", "Diameter", run_diameter)
|
||||
.menu(&["Dimension", "Diameter"])
|
||||
.alias(&["ddi", "dimdia"])
|
||||
.params("{handle, at?} | {center, point}")
|
||||
.interactive(|_| Ok(Box::new(RadialM { diameter: true, target: None }))),
|
||||
.interactive(|_| Ok(Box::new(RadialM::new(true)))),
|
||||
CommandSpec::new("dimangular", "Angular", run_angular)
|
||||
.menu(&["Dimension", "Angular"])
|
||||
.alias(&["dan", "dimang"])
|
||||
|
|
@ -411,20 +411,22 @@ fn run_qdim(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
Ok(json!({ "handles": out }))
|
||||
}
|
||||
|
||||
fn mleader_kind(s: &Session, pts: &[Vec2], text: &str) -> Option<EntityKind> {
|
||||
let (landing, arrow_pts) = pts.split_last()?;
|
||||
/// A multileader from arrowhead to landing; `landing` false leaves out the landing line (dogleg).
|
||||
fn mleader_kind(s: &Session, pts: &[Vec2], text: &str, landing: bool) -> Option<EntityKind> {
|
||||
let (landing_pt, arrow_pts) = pts.split_last()?;
|
||||
let d = s.doc().ok()?;
|
||||
let cur = d.header.str("CMLEADERSTYLE", "Standard");
|
||||
let st = d.mleader_styles.iter().find(|m| m.name.eq_ignore_ascii_case(&cur)).or(d.mleader_styles.first()).cloned().unwrap_or_default();
|
||||
let k = d.header.f64("DIMSCALE", 1.0);
|
||||
let th = st.text_height * k;
|
||||
let dir = if arrow_pts.first().is_some_and(|a| a.x > landing.x) { -1.0 } else { 1.0 };
|
||||
let dir = if arrow_pts.first().is_some_and(|a| a.x > landing_pt.x) { -1.0 } else { 1.0 };
|
||||
let attach = if dir > 0.0 { 4 } else { 6 };
|
||||
let tpos = *landing + Vec2::new((st.dogleg * k + st.landing_gap * k) * dir, 0.0);
|
||||
let dogleg = if landing { st.dogleg * k } else { 0.0 };
|
||||
let tpos = *landing_pt + Vec2::new((dogleg + st.landing_gap * k) * dir, 0.0);
|
||||
Some(EntityKind::MLeader(MLeader {
|
||||
leaders: vec![arrow_pts.iter().map(|p| v3(*p)).collect()],
|
||||
landing: v3(*landing),
|
||||
dogleg: st.dogleg * k,
|
||||
landing: v3(*landing_pt),
|
||||
dogleg,
|
||||
text: (!text.is_empty()).then(|| MText {
|
||||
insert: v3(tpos),
|
||||
height: th,
|
||||
|
|
@ -445,7 +447,7 @@ fn run_mleader(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
if pts.len() < 2 {
|
||||
return Err(bad("mleader", "need an arrowhead and a landing point"));
|
||||
}
|
||||
let k = mleader_kind(s, &pts, str_param(p, "text").unwrap_or("")).ok_or_else(|| bad("mleader", "bad points"))?;
|
||||
let k = mleader_kind(s, &pts, str_param(p, "text").unwrap_or(""), true).ok_or_else(|| bad("mleader", "bad points"))?;
|
||||
Ok(json!({ "handle": s.add_entity(k)?.hex() }))
|
||||
}
|
||||
|
||||
|
|
@ -788,18 +790,78 @@ fn segment_at(s: &Session, h: Handle, p: Vec2) -> Option<Segment> {
|
|||
}
|
||||
}
|
||||
|
||||
/// What a dimension placement prompt asks after Mtext, Text or Angle.
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
enum DimAsk {
|
||||
Text,
|
||||
Angle,
|
||||
}
|
||||
|
||||
/// The Mtext / Text / Angle options of the dimension placement prompts: replace the measured text
|
||||
/// (`<>` stands for the measurement) and rotate the text.
|
||||
#[derive(Default)]
|
||||
struct DimText {
|
||||
text: String,
|
||||
angle: f64,
|
||||
asking: Option<DimAsk>,
|
||||
}
|
||||
|
||||
impl DimText {
|
||||
/// Opens the sub-prompt for Mtext, Text or Angle; `false` for any other keyword.
|
||||
fn keyword(&mut self, k: &str) -> bool {
|
||||
self.asking = match k {
|
||||
"Mtext" | "Text" => Some(DimAsk::Text),
|
||||
"Angle" => Some(DimAsk::Angle),
|
||||
_ => return false,
|
||||
};
|
||||
true
|
||||
}
|
||||
fn prompt(&self) -> Option<Prompt> {
|
||||
Some(match self.asking? {
|
||||
DimAsk::Text => Prompt::new("Enter dimension text (<> = measurement)", Accept::TEXT),
|
||||
DimAsk::Angle => Prompt::new("Specify angle of dimension text", Accept::NUMBER),
|
||||
})
|
||||
}
|
||||
/// Takes the answer to the open sub-prompt (Enter keeps the current value); `false` when none is open.
|
||||
fn answer(&mut self, s: &mut Session, i: &Input) -> bool {
|
||||
let Some(a) = self.asking else { return false };
|
||||
match (a, i) {
|
||||
(DimAsk::Text, Input::Text(t)) => self.text = t.clone(),
|
||||
(DimAsk::Angle, Input::Text(t)) => match crate::units::parse_angle(t) {
|
||||
Some(r) => self.angle = r,
|
||||
None => {
|
||||
s.echo("Requires a valid angle.");
|
||||
return true;
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
self.asking = None;
|
||||
true
|
||||
}
|
||||
/// `k` with this text and text angle.
|
||||
fn apply(&self, mut k: EntityKind) -> EntityKind {
|
||||
if let EntityKind::Dimension(d) = &mut k {
|
||||
d.text = self.text.clone();
|
||||
d.text_rotation = self.angle;
|
||||
}
|
||||
k
|
||||
}
|
||||
}
|
||||
|
||||
struct LinearM {
|
||||
aligned: bool,
|
||||
pts: Vec<Vec2>,
|
||||
rotation: Option<f64>,
|
||||
text: String,
|
||||
text: DimText,
|
||||
selecting: bool,
|
||||
asking: Option<&'static str>,
|
||||
/// Asking for the dimension line angle (Rotated).
|
||||
rotating: bool,
|
||||
}
|
||||
|
||||
impl LinearM {
|
||||
fn new(aligned: bool) -> Self {
|
||||
LinearM { aligned, pts: Vec::new(), rotation: None, text: String::new(), selecting: false, asking: None }
|
||||
LinearM { aligned, pts: Vec::new(), rotation: None, text: DimText::default(), selecting: false, rotating: false }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -808,8 +870,11 @@ impl Interactive for LinearM {
|
|||
if self.aligned { "DIMALIGNED" } else { "DIMLINEAR" }
|
||||
}
|
||||
fn prompt(&self, _s: &Session) -> Prompt {
|
||||
if let Some(a) = self.asking {
|
||||
return Prompt::new(a, Accept::TEXT);
|
||||
if self.rotating {
|
||||
return Prompt::new("Specify angle of dimension line", Accept::NUMBER);
|
||||
}
|
||||
if let Some(p) = self.text.prompt() {
|
||||
return p;
|
||||
}
|
||||
if self.selecting {
|
||||
return Prompt::new("Select object to dimension", Accept::POINT);
|
||||
|
|
@ -817,19 +882,26 @@ impl Interactive for LinearM {
|
|||
match self.pts.len() {
|
||||
0 => Prompt::new("Specify first extension line origin", Accept::POINT).default("select object"),
|
||||
1 => Prompt::new("Specify second extension line origin", Accept::POINT).base_opt(self.pts.first().copied()),
|
||||
_ if self.aligned => Prompt::new("Specify dimension line location", Accept::POINT).kw(&["Mtext", "Text", "Angle"]),
|
||||
// Once the orientation is forced, only the text options remain.
|
||||
_ if self.aligned || self.rotation.is_some() => {
|
||||
Prompt::new("Specify dimension line location", Accept::POINT).kw(&["Mtext", "Text", "Angle"])
|
||||
}
|
||||
_ => Prompt::new("Specify dimension line location", Accept::POINT).kw(&["Mtext", "Text", "Angle", "Horizontal", "Vertical", "Rotated"]),
|
||||
}
|
||||
}
|
||||
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
|
||||
if let Some(a) = self.asking.take() {
|
||||
if self.rotating {
|
||||
if let Input::Text(t) = &i {
|
||||
if a.starts_with("Enter dimension text") {
|
||||
self.text = t.clone();
|
||||
} else if let Some(r) = crate::units::parse_angle(t) {
|
||||
self.rotation = Some(r);
|
||||
}
|
||||
let Some(r) = crate::units::parse_angle(t) else {
|
||||
s.echo("Requires a valid angle.");
|
||||
return Ok(Step::Continue);
|
||||
};
|
||||
self.rotation = Some(r);
|
||||
}
|
||||
self.rotating = false;
|
||||
return Ok(Step::Continue);
|
||||
}
|
||||
if self.text.answer(s, &i) {
|
||||
return Ok(Step::Continue);
|
||||
}
|
||||
if self.selecting {
|
||||
|
|
@ -868,7 +940,7 @@ impl Interactive for LinearM {
|
|||
}
|
||||
(_, Input::Point(at)) => {
|
||||
let (Some(&a), Some(&b)) = (self.pts.first(), self.pts.get(1)) else { return Ok(Step::Continue) };
|
||||
let k = dim(s, linear_kind(a, b, at, self.rotation, self.aligned), at, a, b, Vec2::ZERO, Vec2::ZERO, &self.text);
|
||||
let k = self.text.apply(dim(s, linear_kind(a, b, at, self.rotation, self.aligned), at, a, b, Vec2::ZERO, Vec2::ZERO, ""));
|
||||
add_dim(s, k)?;
|
||||
if let Some(EntityKind::Dimension(d)) = s.last_dim.and_then(|h| s.doc().ok()?.entity(h).map(|e| e.kind.clone())) {
|
||||
let st = s.doc()?.dim_style(&d.style).cloned().unwrap_or_default();
|
||||
|
|
@ -881,9 +953,10 @@ impl Interactive for LinearM {
|
|||
match k.as_str() {
|
||||
"Horizontal" => self.rotation = Some(0.0),
|
||||
"Vertical" => self.rotation = Some(std::f64::consts::FRAC_PI_2),
|
||||
"Rotated" => self.asking = Some("Specify angle of dimension line"),
|
||||
"Text" | "Mtext" => self.asking = Some("Enter dimension text (<> = measurement)"),
|
||||
_ => self.asking = Some("Specify angle of dimension text"),
|
||||
"Rotated" => self.rotating = true,
|
||||
other => {
|
||||
self.text.keyword(other);
|
||||
}
|
||||
}
|
||||
Ok(Step::Continue)
|
||||
}
|
||||
|
|
@ -894,7 +967,7 @@ impl Interactive for LinearM {
|
|||
fn preview(&self, s: &Session, c: Vec2) -> Vec<EntityKind> {
|
||||
match self.pts.as_slice() {
|
||||
[a] => vec![super::helpers::line(*a, c)],
|
||||
[a, b] => vec![dim(s, linear_kind(*a, *b, c, self.rotation, self.aligned), c, *a, *b, Vec2::ZERO, Vec2::ZERO, &self.text)],
|
||||
[a, b] => vec![self.text.apply(dim(s, linear_kind(*a, *b, c, self.rotation, self.aligned), c, *a, *b, Vec2::ZERO, Vec2::ZERO, ""))],
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
|
@ -903,16 +976,20 @@ impl Interactive for LinearM {
|
|||
struct RadialM {
|
||||
diameter: bool,
|
||||
target: Option<(Vec2, f64)>,
|
||||
text: DimText,
|
||||
}
|
||||
|
||||
impl RadialM {
|
||||
fn new(diameter: bool) -> Self {
|
||||
RadialM { diameter, target: None, text: DimText::default() }
|
||||
}
|
||||
fn kind(&self, s: &Session, c: Vec2, r: f64, at: Vec2) -> EntityKind {
|
||||
let pt = c + (at - c).normalized() * r;
|
||||
if self.diameter {
|
||||
self.text.apply(if self.diameter {
|
||||
dim(s, DimKind::Diameter, c + (c - pt), Vec2::ZERO, Vec2::ZERO, pt, Vec2::ZERO, "")
|
||||
} else {
|
||||
dim(s, DimKind::Radius, c, Vec2::ZERO, Vec2::ZERO, pt, Vec2::ZERO, "")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -921,12 +998,18 @@ impl Interactive for RadialM {
|
|||
if self.diameter { "DIMDIAMETER" } else { "DIMRADIUS" }
|
||||
}
|
||||
fn prompt(&self, _s: &Session) -> Prompt {
|
||||
if let Some(p) = self.text.prompt() {
|
||||
return p;
|
||||
}
|
||||
match self.target {
|
||||
None => Prompt::new("Select arc or circle", Accept::POINT),
|
||||
Some((c, _)) => Prompt::new("Specify dimension line location", Accept::POINT).kw(&["Mtext", "Text", "Angle"]).base(c),
|
||||
}
|
||||
}
|
||||
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
|
||||
if self.text.answer(s, &i) {
|
||||
return Ok(Step::Continue);
|
||||
}
|
||||
match (self.target, i) {
|
||||
(None, Input::Point(p)) => {
|
||||
match pick_at(s, p).and_then(|h| circle_of(s, h)) {
|
||||
|
|
@ -940,6 +1023,10 @@ impl Interactive for RadialM {
|
|||
add_dim(s, k)?;
|
||||
Ok(Step::Done)
|
||||
}
|
||||
(Some(_), Input::Keyword(k)) => {
|
||||
self.text.keyword(&k);
|
||||
Ok(Step::Continue)
|
||||
}
|
||||
(_, Input::Enter) => Ok(Step::Cancel),
|
||||
_ => Ok(Step::Continue),
|
||||
}
|
||||
|
|
@ -957,6 +1044,10 @@ struct AngularM {
|
|||
vertex_mode: Vec<Vec2>,
|
||||
vertex: bool,
|
||||
arc: Option<(Vec2, Vec2, Vec2)>,
|
||||
text: DimText,
|
||||
/// Quadrant: a point inside the angle to measure; the arc location then only sets the radius.
|
||||
quadrant: Option<Vec2>,
|
||||
asking_quadrant: bool,
|
||||
}
|
||||
|
||||
impl AngularM {
|
||||
|
|
@ -973,6 +1064,13 @@ impl AngularM {
|
|||
let far = |p: Vec2, q: Vec2| if p.dist(v) > q.dist(v) { p } else { q };
|
||||
Some((v, far(a1, a2), far(b1, b2)))
|
||||
}
|
||||
/// The arc location `at`, turned into the locked quadrant (same radius) when one is set.
|
||||
fn located(&self, v: Vec2, at: Vec2) -> Vec2 {
|
||||
match self.quadrant {
|
||||
Some(q) if q.dist(v) > 1e-12 && at.dist(v) > 1e-12 => v + (q - v).normalized() * at.dist(v),
|
||||
_ => at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Interactive for AngularM {
|
||||
|
|
@ -980,6 +1078,12 @@ impl Interactive for AngularM {
|
|||
"DIMANGULAR"
|
||||
}
|
||||
fn prompt(&self, _s: &Session) -> Prompt {
|
||||
if let Some(p) = self.text.prompt() {
|
||||
return p;
|
||||
}
|
||||
if self.asking_quadrant {
|
||||
return Prompt::new("Specify quadrant", Accept::POINT);
|
||||
}
|
||||
if self.geometry().is_some() {
|
||||
return Prompt::new("Specify dimension arc line location", Accept::POINT).kw(&["Mtext", "Text", "Angle", "Quadrant"]);
|
||||
}
|
||||
|
|
@ -998,17 +1102,36 @@ impl Interactive for AngularM {
|
|||
}
|
||||
}
|
||||
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
|
||||
if self.text.answer(s, &i) {
|
||||
return Ok(Step::Continue);
|
||||
}
|
||||
if self.asking_quadrant {
|
||||
if let Input::Point(q) = i {
|
||||
self.quadrant = Some(q);
|
||||
}
|
||||
self.asking_quadrant = false;
|
||||
return Ok(Step::Continue);
|
||||
}
|
||||
if let Some((v, a, b)) = self.geometry() {
|
||||
return match i {
|
||||
Input::Point(at) => {
|
||||
let at = self.located(v, at);
|
||||
let k = match (self.handles.as_slice(), self.arc) {
|
||||
([h1, h2], None) => angular_from_lines(s, *h1, *h2, at),
|
||||
_ => None,
|
||||
};
|
||||
let k = k.unwrap_or_else(|| dim(s, DimKind::Angular3P, at, a, b, v, Vec2::ZERO, ""));
|
||||
add_dim(s, k)?;
|
||||
add_dim(s, self.text.apply(k))?;
|
||||
Ok(Step::Done)
|
||||
}
|
||||
Input::Keyword(k) => {
|
||||
if k == "Quadrant" {
|
||||
self.asking_quadrant = true;
|
||||
} else {
|
||||
self.text.keyword(&k);
|
||||
}
|
||||
Ok(Step::Continue)
|
||||
}
|
||||
Input::Enter => Ok(Step::Cancel),
|
||||
_ => Ok(Step::Continue),
|
||||
};
|
||||
|
|
@ -1063,13 +1186,37 @@ impl Interactive for AngularM {
|
|||
}
|
||||
}
|
||||
fn preview(&self, s: &Session, c: Vec2) -> Vec<EntityKind> {
|
||||
self.geometry().map(|(v, a, b)| vec![dim(s, DimKind::Angular3P, c, a, b, v, Vec2::ZERO, "")]).unwrap_or_default()
|
||||
self.geometry()
|
||||
.map(|(v, a, b)| vec![self.text.apply(dim(s, DimKind::Angular3P, self.located(v, c), a, b, v, Vec2::ZERO, ""))])
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ArcLenM {
|
||||
arc: Option<cadcraft_geom::Arc>,
|
||||
text: DimText,
|
||||
/// Partial: the points picked so far on the arc (`None` when not choosing a part).
|
||||
partial: Option<Vec<Vec2>>,
|
||||
}
|
||||
|
||||
/// The part of `arc` between the points nearest `p` and `q` (in the arc's direction).
|
||||
fn arc_part(arc: &cadcraft_geom::Arc, p: Vec2, q: Vec2) -> Option<cadcraft_geom::Arc> {
|
||||
let sweep = arc.sweep();
|
||||
// Offset along the arc from its start, clamped to the nearer end when the point is off the arc.
|
||||
let along = |x: Vec2| {
|
||||
let t = cadcraft_geom::ccw_sweep(arc.start, arc.center.angle_to(x));
|
||||
if t <= sweep {
|
||||
t
|
||||
} else if t - sweep < std::f64::consts::TAU - t {
|
||||
sweep
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
};
|
||||
let (t1, t2) = (along(p), along(q));
|
||||
let (lo, hi) = (t1.min(t2), t1.max(t2));
|
||||
(hi - lo > 1e-9).then(|| cadcraft_geom::Arc::new(arc.center, arc.radius, arc.start + lo, arc.start + hi))
|
||||
}
|
||||
|
||||
impl Interactive for ArcLenM {
|
||||
|
|
@ -1077,12 +1224,36 @@ impl Interactive for ArcLenM {
|
|||
"DIMARC"
|
||||
}
|
||||
fn prompt(&self, _s: &Session) -> Prompt {
|
||||
if let Some(p) = self.text.prompt() {
|
||||
return p;
|
||||
}
|
||||
if let Some(picked) = &self.partial {
|
||||
return match picked.first() {
|
||||
None => Prompt::new("Specify first point for arc length dimension", Accept::POINT),
|
||||
Some(&f) => Prompt::new("Specify second point for arc length dimension", Accept::POINT).base(f),
|
||||
};
|
||||
}
|
||||
match self.arc {
|
||||
None => Prompt::new("Select arc or polyline arc segment", Accept::POINT),
|
||||
Some(_) => Prompt::new("Specify arc length dimension location", Accept::POINT).kw(&["Mtext", "Text", "Angle", "Partial"]),
|
||||
}
|
||||
}
|
||||
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
|
||||
if self.text.answer(s, &i) {
|
||||
return Ok(Step::Continue);
|
||||
}
|
||||
if let Some(picked) = self.partial.take() {
|
||||
match (i, picked.first().copied(), self.arc) {
|
||||
(Input::Point(p), None, _) => self.partial = Some(vec![p]),
|
||||
(Input::Point(q), Some(p), Some(g)) => match arc_part(&g, p, q) {
|
||||
Some(part) => self.arc = Some(part),
|
||||
None => s.echo("The two points must be different."),
|
||||
},
|
||||
(Input::Enter, ..) => {}
|
||||
_ => self.partial = Some(picked),
|
||||
}
|
||||
return Ok(Step::Continue);
|
||||
}
|
||||
match (self.arc, i) {
|
||||
(None, Input::Point(p)) => {
|
||||
if let Some(EntityKind::Arc(a)) = pick_at(s, p).and_then(|h| s.doc().ok()?.entity(h).map(|e| e.kind.clone())) {
|
||||
|
|
@ -1093,22 +1264,44 @@ impl Interactive for ArcLenM {
|
|||
Ok(Step::Continue)
|
||||
}
|
||||
(Some(g), Input::Point(at)) => {
|
||||
add_dim(s, dim(s, DimKind::ArcLength, at, g.start_point(), g.end_point(), g.center, Vec2::ZERO, ""))?;
|
||||
add_dim(s, self.text.apply(dim(s, DimKind::ArcLength, at, g.start_point(), g.end_point(), g.center, Vec2::ZERO, "")))?;
|
||||
Ok(Step::Done)
|
||||
}
|
||||
(Some(_), Input::Keyword(k)) => {
|
||||
if k == "Partial" {
|
||||
self.partial = Some(Vec::new());
|
||||
} else {
|
||||
self.text.keyword(&k);
|
||||
}
|
||||
Ok(Step::Continue)
|
||||
}
|
||||
(_, Input::Enter) => Ok(Step::Cancel),
|
||||
_ => Ok(Step::Continue),
|
||||
}
|
||||
}
|
||||
fn preview(&self, s: &Session, c: Vec2) -> Vec<EntityKind> {
|
||||
self.arc.map(|g| vec![dim(s, DimKind::ArcLength, c, g.start_point(), g.end_point(), g.center, Vec2::ZERO, "")]).unwrap_or_default()
|
||||
if self.partial.is_some() {
|
||||
return Vec::new();
|
||||
}
|
||||
self.arc
|
||||
.map(|g| vec![self.text.apply(dim(s, DimKind::ArcLength, c, g.start_point(), g.end_point(), g.center, Vec2::ZERO, ""))])
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct OrdinateM {
|
||||
feature: Option<Vec2>,
|
||||
/// Xdatum / Ydatum: the forced ordinate type (`true` = X datum).
|
||||
xtype: Option<bool>,
|
||||
text: DimText,
|
||||
}
|
||||
|
||||
impl OrdinateM {
|
||||
fn kind(&self, s: &Session, f: Vec2, l: Vec2) -> EntityKind {
|
||||
let x = self.xtype.unwrap_or((l - f).y.abs() > (l - f).x.abs());
|
||||
self.text.apply(dim(s, DimKind::Ordinate { x_type: x }, Vec2::ZERO, f, l, Vec2::ZERO, Vec2::ZERO, ""))
|
||||
}
|
||||
}
|
||||
|
||||
impl Interactive for OrdinateM {
|
||||
|
|
@ -1116,24 +1309,36 @@ impl Interactive for OrdinateM {
|
|||
"DIMORDINATE"
|
||||
}
|
||||
fn prompt(&self, _s: &Session) -> Prompt {
|
||||
match self.feature {
|
||||
None => Prompt::new("Specify feature location", Accept::POINT),
|
||||
Some(f) => Prompt::new("Specify leader endpoint", Accept::POINT).kw(&["Xdatum", "Ydatum", "Mtext", "Text", "Angle"]).base(f),
|
||||
if let Some(p) = self.text.prompt() {
|
||||
return p;
|
||||
}
|
||||
match (self.feature, self.xtype) {
|
||||
(None, _) => Prompt::new("Specify feature location", Accept::POINT),
|
||||
(Some(f), None) => Prompt::new("Specify leader endpoint", Accept::POINT).kw(&["Xdatum", "Ydatum", "Mtext", "Text", "Angle"]).base(f),
|
||||
(Some(f), Some(_)) => Prompt::new("Specify leader endpoint", Accept::POINT).kw(&["Mtext", "Text", "Angle"]).base(f),
|
||||
}
|
||||
}
|
||||
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
|
||||
if self.text.answer(s, &i) {
|
||||
return Ok(Step::Continue);
|
||||
}
|
||||
match (self.feature, i) {
|
||||
(None, Input::Point(p)) => {
|
||||
self.feature = Some(p);
|
||||
Ok(Step::Continue)
|
||||
}
|
||||
(Some(_), Input::Keyword(k)) => {
|
||||
self.xtype = Some(k == "Xdatum");
|
||||
match k.as_str() {
|
||||
"Xdatum" => self.xtype = Some(true),
|
||||
"Ydatum" => self.xtype = Some(false),
|
||||
other => {
|
||||
self.text.keyword(other);
|
||||
}
|
||||
}
|
||||
Ok(Step::Continue)
|
||||
}
|
||||
(Some(f), Input::Point(l)) => {
|
||||
let x = self.xtype.unwrap_or((l - f).y.abs() > (l - f).x.abs());
|
||||
add_dim(s, dim(s, DimKind::Ordinate { x_type: x }, Vec2::ZERO, f, l, Vec2::ZERO, Vec2::ZERO, ""))?;
|
||||
add_dim(s, self.kind(s, f, l))?;
|
||||
Ok(Step::Done)
|
||||
}
|
||||
(_, Input::Enter) => Ok(Step::Cancel),
|
||||
|
|
@ -1141,20 +1346,7 @@ impl Interactive for OrdinateM {
|
|||
}
|
||||
}
|
||||
fn preview(&self, s: &Session, c: Vec2) -> Vec<EntityKind> {
|
||||
self.feature
|
||||
.map(|f| {
|
||||
vec![dim(
|
||||
s,
|
||||
DimKind::Ordinate { x_type: self.xtype.unwrap_or((c - f).y.abs() > (c - f).x.abs()) },
|
||||
Vec2::ZERO,
|
||||
f,
|
||||
c,
|
||||
Vec2::ZERO,
|
||||
Vec2::ZERO,
|
||||
"",
|
||||
)]
|
||||
})
|
||||
.unwrap_or_default()
|
||||
self.feature.map(|f| vec![self.kind(s, f, c)]).unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1210,10 +1402,168 @@ impl Interactive for ChainM {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
/// 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 {
|
||||
Menu,
|
||||
LeaderType,
|
||||
Landing,
|
||||
ContentType,
|
||||
MaxPoints,
|
||||
FirstAngle,
|
||||
SecondAngle,
|
||||
}
|
||||
|
||||
/// Upper bound for MLEADER Maxpoints.
|
||||
const MAX_LEADER_POINTS: usize = 64;
|
||||
|
||||
struct MLeaderM {
|
||||
order: LeaderOrder,
|
||||
/// Points in the order they were placed.
|
||||
pts: Vec<Vec2>,
|
||||
landed: bool,
|
||||
/// All leader points are placed.
|
||||
leader_done: bool,
|
||||
/// The leader text once entered (Content first enters it before the leader).
|
||||
text: Option<String>,
|
||||
opt: Option<LeaderOpt>,
|
||||
max_points: usize,
|
||||
/// First and second angle constraints (radians; 0 = free).
|
||||
angles: [f64; 2],
|
||||
landing: bool,
|
||||
content: bool,
|
||||
}
|
||||
|
||||
impl Default for MLeaderM {
|
||||
fn default() -> Self {
|
||||
MLeaderM {
|
||||
order: LeaderOrder::Arrowhead,
|
||||
pts: Vec::new(),
|
||||
leader_done: false,
|
||||
text: None,
|
||||
opt: None,
|
||||
max_points: 2,
|
||||
angles: [0.0; 2],
|
||||
landing: true,
|
||||
content: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MLeaderM {
|
||||
/// The leader from arrowhead to landing, with `extra` placed next.
|
||||
fn leader(&self, extra: Option<Vec2>) -> Vec<Vec2> {
|
||||
let mut pts = self.pts.clone();
|
||||
pts.extend(extra);
|
||||
if self.order != LeaderOrder::Arrowhead {
|
||||
pts.reverse();
|
||||
}
|
||||
pts
|
||||
}
|
||||
/// `p` with the first/second angle constraint applied to the segment it ends (counted from the
|
||||
/// first point placed).
|
||||
fn constrained(&self, p: Vec2) -> Vec2 {
|
||||
let (Some(&prev), Some(&inc)) = (self.pts.last(), self.angles.get(self.pts.len().wrapping_sub(1))) else { return p };
|
||||
let len = prev.dist(p);
|
||||
if inc <= 1e-9 || !inc.is_finite() || len <= 1e-12 {
|
||||
return p;
|
||||
}
|
||||
Vec2::polar(prev, len, ((p - prev).angle() / inc).round() * inc)
|
||||
}
|
||||
/// The text is asked now: right after the content location (Content first) or after the leader.
|
||||
fn wants_text(&self) -> bool {
|
||||
self.content && self.text.is_none() && (self.leader_done || (self.order == LeaderOrder::Content && self.pts.len() == 1))
|
||||
}
|
||||
fn finish(&self, s: &mut Session) -> Result<Step> {
|
||||
let text = self.text.clone().unwrap_or_default();
|
||||
let k = mleader_kind(s, &self.leader(None), &text, self.landing).ok_or_else(|| EngineError::Other("bad leader".into()))?;
|
||||
s.add_entity(k)?;
|
||||
Ok(Step::Done)
|
||||
}
|
||||
fn option_prompt(o: LeaderOpt) -> Prompt {
|
||||
let kw = super::curves::KW;
|
||||
match o {
|
||||
LeaderOpt::Menu => Prompt::new("Enter an option", kw)
|
||||
.kw(&["Leader type", "leader lAnding", "Content type", "Maxpoints", "First angle", "Second angle", "eXit options"])
|
||||
.default("eXit options"),
|
||||
LeaderOpt::LeaderType => Prompt::new("Select a leader type", kw).kw(&["Straight", "sPline", "None"]).default("Straight"),
|
||||
LeaderOpt::Landing => Prompt::new("Use landing", kw).kw(&["Yes", "No"]).default("Yes"),
|
||||
LeaderOpt::ContentType => Prompt::new("Select a content type", kw).kw(&["Block", "Mtext", "None"]).default("Mtext"),
|
||||
LeaderOpt::MaxPoints => Prompt::new("Enter the maximum points for leader line", Accept::NUMBER),
|
||||
LeaderOpt::FirstAngle => Prompt::new("Enter first angle constraint", Accept::NUMBER),
|
||||
LeaderOpt::SecondAngle => Prompt::new("Enter second angle constraint", Accept::NUMBER),
|
||||
}
|
||||
}
|
||||
/// One answer at the Options prompts; Enter keeps the current value.
|
||||
fn option(&mut self, s: &mut Session, o: LeaderOpt, i: Input) -> Option<LeaderOpt> {
|
||||
use LeaderOpt::*;
|
||||
match (o, i) {
|
||||
(Menu, Input::Keyword(k)) => match k.as_str() {
|
||||
"Leader type" => Some(LeaderType),
|
||||
"leader lAnding" => Some(Landing),
|
||||
"Content type" => Some(ContentType),
|
||||
"Maxpoints" => Some(MaxPoints),
|
||||
"First angle" => Some(FirstAngle),
|
||||
"Second angle" => Some(SecondAngle),
|
||||
_ => None,
|
||||
},
|
||||
(Menu, Input::Enter) => None,
|
||||
(LeaderType, Input::Keyword(k)) => {
|
||||
match k.as_str() {
|
||||
"sPline" => s.echo("Spline leaders are not available yet; the leader stays straight."),
|
||||
"None" => s.echo("Multileaders without a leader line are not available yet."),
|
||||
_ => {}
|
||||
}
|
||||
Some(Menu)
|
||||
}
|
||||
(Landing, Input::Keyword(k)) => {
|
||||
self.landing = k == "Yes";
|
||||
Some(Menu)
|
||||
}
|
||||
(ContentType, Input::Keyword(k)) => {
|
||||
match k.as_str() {
|
||||
"Block" => s.echo("Block content is not available yet; the content stays multiline text."),
|
||||
"None" => self.content = false,
|
||||
_ => self.content = true,
|
||||
}
|
||||
Some(Menu)
|
||||
}
|
||||
(MaxPoints, Input::Text(t)) => match t.trim().parse::<usize>() {
|
||||
Ok(n) if (2..=MAX_LEADER_POINTS).contains(&n) => {
|
||||
self.max_points = n;
|
||||
Some(Menu)
|
||||
}
|
||||
_ => {
|
||||
s.echo(format!("Requires an integer between 2 and {MAX_LEADER_POINTS}."));
|
||||
Some(MaxPoints)
|
||||
}
|
||||
},
|
||||
(FirstAngle | SecondAngle, Input::Text(t)) => match crate::units::parse_angle(&t).filter(|a| *a >= 0.0) {
|
||||
Some(a) => {
|
||||
self.angles[usize::from(o == SecondAngle)] = a;
|
||||
Some(Menu)
|
||||
}
|
||||
None => {
|
||||
s.echo("Requires a valid angle.");
|
||||
Some(o)
|
||||
}
|
||||
},
|
||||
(Menu | LeaderType | Landing | ContentType, Input::Text(_)) => {
|
||||
s.echo("Invalid option keyword.");
|
||||
Some(o)
|
||||
}
|
||||
(_, Input::Enter) => Some(Menu),
|
||||
_ => Some(o),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Interactive for MLeaderM {
|
||||
|
|
@ -1221,43 +1571,65 @@ impl Interactive for MLeaderM {
|
|||
"MLEADER"
|
||||
}
|
||||
fn prompt(&self, _s: &Session) -> Prompt {
|
||||
if self.landed {
|
||||
if let Some(o) = self.opt {
|
||||
return Self::option_prompt(o);
|
||||
}
|
||||
if self.wants_text() {
|
||||
return Prompt::new("Enter leader text", Accept::TEXT);
|
||||
}
|
||||
match self.pts.len() {
|
||||
0 => Prompt::new("Specify leader arrowhead location", Accept::POINT).kw(&["leader Landing first", "Content first", "Options"]),
|
||||
_ => Prompt::new("Specify leader landing location", Accept::POINT).base_opt(self.pts.last().copied()),
|
||||
let last = self.pts.len() + 1 >= self.max_points;
|
||||
match (self.pts.last(), self.order) {
|
||||
(None, LeaderOrder::Arrowhead) => Prompt::new("Specify leader arrowhead location", Accept::POINT)
|
||||
.kw(&["leader Landing first", "Content first", "Options"])
|
||||
.default("Options"),
|
||||
(None, LeaderOrder::Landing) => Prompt::new("Specify leader landing location", Accept::POINT)
|
||||
.kw(&["leader arrowHead first", "Content first", "Options"])
|
||||
.default("Options"),
|
||||
(None, LeaderOrder::Content) => Prompt::new("Specify location of the text", Accept::POINT)
|
||||
.kw(&["leader arrowHead first", "leader Landing first", "Options"])
|
||||
.default("Options"),
|
||||
(Some(&b), _) if !last => Prompt::new("Specify next point", Accept::POINT).base(b),
|
||||
(Some(&b), LeaderOrder::Arrowhead) => Prompt::new("Specify leader landing location", Accept::POINT).base(b),
|
||||
(Some(&b), _) => Prompt::new("Specify leader arrowhead location", Accept::POINT).base(b),
|
||||
}
|
||||
}
|
||||
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
|
||||
if self.landed {
|
||||
let t = match i {
|
||||
Input::Text(t) => t,
|
||||
_ => String::new(),
|
||||
};
|
||||
let k = mleader_kind(s, &self.pts, &t).ok_or_else(|| EngineError::Other("bad leader".into()))?;
|
||||
s.add_entity(k)?;
|
||||
return Ok(Step::Done);
|
||||
if let Some(o) = self.opt {
|
||||
self.opt = self.option(s, o, i);
|
||||
return Ok(Step::Continue);
|
||||
}
|
||||
if self.wants_text() {
|
||||
self.text = Some(if let Input::Text(t) = i { t } else { String::new() });
|
||||
return if self.leader_done { self.finish(s) } else { Ok(Step::Continue) };
|
||||
}
|
||||
match i {
|
||||
Input::Point(p) => {
|
||||
let p = self.constrained(p);
|
||||
self.pts.push(p);
|
||||
if self.pts.len() >= 2 {
|
||||
self.landed = true;
|
||||
if self.pts.len() >= self.max_points {
|
||||
self.leader_done = true;
|
||||
}
|
||||
Ok(Step::Continue)
|
||||
}
|
||||
Input::Enter => Ok(Step::Cancel),
|
||||
_ => Ok(Step::Continue),
|
||||
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::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,
|
||||
Input::Enter => return Ok(Step::Cancel),
|
||||
_ => {}
|
||||
}
|
||||
if self.leader_done && !self.wants_text() { self.finish(s) } else { Ok(Step::Continue) }
|
||||
}
|
||||
fn preview(&self, s: &Session, c: Vec2) -> Vec<EntityKind> {
|
||||
if self.landed || self.pts.is_empty() {
|
||||
if self.opt.is_some() || self.wants_text() || self.leader_done || self.pts.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
let mut pts = self.pts.clone();
|
||||
pts.push(c);
|
||||
mleader_kind(s, &pts, "").into_iter().collect()
|
||||
let text = self.text.as_deref().unwrap_or("");
|
||||
mleader_kind(s, &self.leader(Some(self.constrained(c))), text, self.landing).into_iter().collect()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,137 @@
|
|||
//! 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.
|
||||
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);
|
||||
|
||||
// Unmodelled choices say so and keep the following input.
|
||||
s.script("mleader o l p x 0,0 4,4 hi\n").unwrap();
|
||||
assert!(s.log.iter().any(|l| l.contains("not available yet")), "{:?}", s.log);
|
||||
assert_eq!(last_mleader(&s).text.unwrap().contents, "hi");
|
||||
assert!(s.running.is_none());
|
||||
}
|
||||
Loading…
Reference in New Issue