Files
cadcraft/crates/engine/tests/angle_prompts.rs
Valentin KleinandClaude Opus 5.5 45755697b6 Angle prompts read AUNITS, ANGBASE and ANGDIR (fixes #353) (#354)
ROTATE's angle and the hatch pattern angle are rotations (AUNITS, clockwise
when ANGDIR is 1). The dimension line (Rotated) and dimension text angles are
directions from ANGBASE. CHAMFER's angle and the MLEADER angle constraints are
amounts in AUNITS. The ROTATE and HATCH defaults are offered as they would be
typed.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 17:32:13 -04:00

103 lines
4.0 KiB
Rust

//! Angle prompts read typed angles with the drawing's settings (#353): AUNITS for bare numbers,
//! rotations turn clockwise when ANGDIR is 1, directions are measured from ANGBASE.
use std::f64::consts::{FRAC_PI_2, FRAC_PI_4, PI};
use cadcraft_engine::Session;
use cadcraft_engine::doc::{DimKind, EntityKind};
use cadcraft_engine::geom::Vec2;
use serde_json::json;
const QUARTER: &str = "1.5707963267948966";
const EIGHTH: &str = "0.7853981633974483";
/// Radians, clockwise angles, 0 = north.
fn session() -> Session {
let mut s = Session::new();
for (name, value) in [("AUNITS", json!(3)), ("ANGDIR", json!(1)), ("ANGBASE", json!(90))] {
s.execute("setvar", &json!({ "name": name, "value": value })).unwrap();
}
s
}
fn last(s: &Session) -> EntityKind {
s.doc().unwrap().model.last().unwrap().kind.clone()
}
/// `a - b` turned into (-π, π].
fn diff(a: f64, b: f64) -> f64 {
let d = (a - b).rem_euclid(2.0 * PI);
if d > PI { d - 2.0 * PI } else { d }
}
#[test]
fn rotate_angle_is_a_rotation_in_aunits() {
let mut s = session();
s.execute("line", &json!({ "points": [[0, 0], [10, 0]] })).unwrap();
s.script(&format!("ROTATE ALL\n\n0,0 {QUARTER}\n")).unwrap();
assert!(s.running.is_none(), "{}", s.prompt_text());
// A quarter turn clockwise (ANGDIR 1), not 1.57 degrees counterclockwise.
match last(&s) {
EntityKind::Line(l) => assert!(l.b.xy().near(Vec2::new(0.0, -10.0), 1e-9), "{:?}", l.b),
k => panic!("expected a line, got {k:?}"),
}
}
#[test]
fn hatch_angle_is_a_rotation_in_aunits() {
let mut s = session();
s.script(&format!("HATCH A {QUARTER}\n\n")).unwrap();
assert!(s.running.is_none(), "{}", s.prompt_text());
let a = s.doc().unwrap().header.f64("HPANG", 0.0);
assert!(diff(a, -FRAC_PI_2).abs() < 1e-9, "HPANG {a}");
}
#[test]
fn dimension_line_and_text_angles_are_directions() {
let mut s = session();
// Rotated 0 points at ANGBASE (north); the text angle a quarter turn clockwise from north is east.
s.script(&format!("DIMLINEAR 0,0 10,0 R 0 A {QUARTER} 5,5\n")).unwrap();
assert!(s.running.is_none(), "{}", s.prompt_text());
match last(&s) {
EntityKind::Dimension(d) => {
let DimKind::Linear { rotation } = d.kind else { panic!("expected a linear dimension, got {:?}", d.kind) };
assert!(diff(rotation, FRAC_PI_2).abs() < 1e-9, "rotation {rotation}");
assert!(diff(d.text_rotation, 0.0).abs() < 1e-9, "text rotation {}", d.text_rotation);
}
k => panic!("expected a dimension, got {k:?}"),
}
}
#[test]
fn chamfer_and_mleader_angles_are_amounts_in_aunits() {
let mut s = session();
for l in ["0,0 12,0", "10,-2 10,12"] {
s.cmdline(&format!("LINE {l}")).unwrap();
s.cmdline("").unwrap();
}
// Length 2 on the first line at an eighth turn from it, whatever ANGBASE and ANGDIR say.
s.script(&format!("CHAMFER A 2 {EIGHTH} 5,0 10,5\n")).unwrap();
assert!(s.running.is_none(), "{}", s.prompt_text());
let d = s.doc().unwrap().header.f64("CHAMFERD", 0.0);
assert!((d - FRAC_PI_4).abs() < 1e-9, "CHAMFERD {d}");
let chamfer = s.doc().unwrap().model.iter().any(|e| match &e.kind {
EntityKind::Line(l) => {
let (a, b) = (l.a.xy(), l.b.xy());
(a.near(Vec2::new(8.0, 0.0), 1e-6) && b.near(Vec2::new(10.0, 2.0), 1e-6))
|| (b.near(Vec2::new(8.0, 0.0), 1e-6) && a.near(Vec2::new(10.0, 2.0), 1e-6))
}
_ => false,
});
assert!(chamfer, "{:?}", s.doc().unwrap().model.iter().map(|e| &e.kind).collect::<Vec<_>>());
// The first angle constraint snaps the leader to eighth-turn steps.
s.script(&format!("MLEADER O F {EIGHTH} X 0,0 10,8 note\n")).unwrap();
match last(&s) {
EntityKind::MLeader(m) => {
let r = Vec2::new(10.0, 8.0).len();
assert!(m.landing.xy().near(Vec2::new(r, r) / 2f64.sqrt(), 1e-6), "{:?}", m.landing);
}
k => panic!("expected a multileader, got {k:?}"),
}
}