mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-14 14:26:45 -04:00
Co-authored-by: Brandon Thomas <[email protected]> Co-authored-by: Claude Opus 5.5 <[email protected]>
126 lines
5.2 KiB
Rust
126 lines
5.2 KiB
Rust
//! Command-line behaviour of the SPLINE, POLYGON and CIRCLE prompt machines.
|
|
|
|
use cadcraft_doc::EntityKind;
|
|
use cadcraft_engine::Session;
|
|
use cadcraft_geom::Vec2;
|
|
|
|
fn last(s: &Session) -> EntityKind {
|
|
s.doc().unwrap().model.iter().last().map(|e| e.kind.clone()).unwrap()
|
|
}
|
|
|
|
fn logged(s: &Session, text: &str) -> bool {
|
|
s.log.iter().any(|l| l.contains(text))
|
|
}
|
|
|
|
#[test]
|
|
fn spline_close_makes_a_smooth_periodic_curve() {
|
|
let mut s = Session::new();
|
|
s.cmdline("spline 0,0 4,0 4,3 0,3 c").unwrap();
|
|
assert!(s.running.is_none());
|
|
let EntityKind::Spline(sp) = last(&s) else { panic!("no spline") };
|
|
assert!(sp.closed && sp.is_periodic());
|
|
assert_eq!(sp.fit.len(), 4, "the first point is not repeated");
|
|
let (lo, hi) = sp.domain();
|
|
assert!(sp.eval(lo).near(Vec2::ZERO, 1e-9) && sp.eval(hi).near(Vec2::ZERO, 1e-9));
|
|
let h = 1e-6;
|
|
let out = (sp.eval(lo + h) - sp.eval(lo)).normalized();
|
|
let back = (sp.eval(hi) - sp.eval(hi - h)).normalized();
|
|
assert!(out.near(back, 1e-4), "no corner at the seam");
|
|
}
|
|
|
|
#[test]
|
|
fn spline_rejects_text_and_switches_method() {
|
|
let mut s = Session::new();
|
|
s.cmdline("spline 0,0 5,5 10,0 xyz").unwrap();
|
|
assert!(logged(&s, "Point or option keyword required."));
|
|
s.cmdline("").unwrap();
|
|
let EntityKind::Spline(sp) = last(&s) else { panic!("no spline") };
|
|
assert_eq!(sp.fit, [Vec2::ZERO, Vec2::new(5.0, 5.0), Vec2::new(10.0, 0.0)], "no point was swallowed");
|
|
|
|
// Method > CV switches to drawing by control vertices.
|
|
s.cmdline("spline m cv 0,0 1,1 2,0 3,1").unwrap();
|
|
s.cmdline("").unwrap();
|
|
let EntityKind::Spline(sp) = last(&s) else { panic!("no spline") };
|
|
assert!(sp.fit.is_empty() && sp.control.len() == 4);
|
|
}
|
|
|
|
#[test]
|
|
fn circle_radius_point_on_the_centre_is_refused() {
|
|
for input in ["circle 2,3 2,3", "circle 2,3 d 2,3"] {
|
|
let mut s = Session::new();
|
|
s.cmdline(input).unwrap();
|
|
assert_eq!(s.doc().unwrap().model.len(), 0, "{input}: no circle");
|
|
assert!(logged(&s, "Value must be positive and nonzero."), "{input}: {:?}", s.log);
|
|
assert!(s.running.is_some(), "{input}: still at the radius prompt");
|
|
s.cmdline("4").unwrap();
|
|
assert!(s.running.is_none(), "{input}: a positive value finishes");
|
|
assert_eq!(s.doc().unwrap().model.len(), 1, "{input}: one circle");
|
|
let want = if input.contains(" d ") { 2.0 } else { 4.0 };
|
|
assert!(
|
|
matches!(last(&s), EntityKind::Circle(c) if c.center.xy().near(Vec2::new(2.0, 3.0), 1e-9) && (c.radius - want).abs() < 1e-9),
|
|
"{input}: the centre is kept"
|
|
);
|
|
}
|
|
}
|
|
|
|
/// The two lowest vertices of a closed polyline share their y (the bottom edge is horizontal).
|
|
fn bottom_y(k: &EntityKind) -> Option<f64> {
|
|
let EntityKind::LwPolyline(p) = k else { return None };
|
|
let mut ys: Vec<f64> = p.vertices.iter().map(|v| v.p.y).collect();
|
|
ys.sort_by(f64::total_cmp);
|
|
((ys[0] - ys[1]).abs() < 1e-9).then_some(ys[0])
|
|
}
|
|
|
|
#[test]
|
|
fn polygon_typed_radius_has_a_horizontal_bottom_edge() {
|
|
let mut s = Session::new();
|
|
let pentagon = -2.0 * (std::f64::consts::PI / 5.0).cos();
|
|
for (input, low) in
|
|
[("polygon 6 0,0 i 2", -(3f64.sqrt())), ("polygon 6 0,0 c 2", -2.0), ("polygon 4 0,0 i 2", -(2f64.sqrt())), ("polygon 5 0,0 i 2", pentagon)]
|
|
{
|
|
s.cmdline(input).unwrap();
|
|
let y = bottom_y(&last(&s));
|
|
assert!(y.is_some_and(|y| (y - low).abs() < 1e-9), "{input}: bottom edge at {y:?}, want {low}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn polygon_edge_endpoints_must_differ() {
|
|
let mut s = Session::new();
|
|
s.cmdline("polygon 4 e 1,1 1,1").unwrap();
|
|
assert_eq!(s.doc().unwrap().model.len(), 0, "no polygon");
|
|
assert!(logged(&s, "Edge endpoints must differ."), "{:?}", s.log);
|
|
assert!(s.running.is_some(), "still at the second endpoint prompt");
|
|
s.cmdline("3,1").unwrap();
|
|
assert!(s.running.is_none(), "a distinct endpoint finishes");
|
|
let EntityKind::LwPolyline(p) = last(&s) else { panic!("no polygon") };
|
|
assert!(p.closed && p.vertices.len() == 4);
|
|
assert!(p.vertices.iter().any(|v| v.p.near(Vec2::new(3.0, 1.0), 1e-9)), "the edge runs from 1,1 to 3,1");
|
|
}
|
|
|
|
#[test]
|
|
fn circle_center_prompt_rejects_garbage() {
|
|
let mut s = Session::new();
|
|
s.cmdline("circle d").unwrap();
|
|
assert!(logged(&s, "Point or option keyword required."));
|
|
assert!(s.running.is_some(), "still at the centre prompt");
|
|
s.cmdline("0,0 5").unwrap();
|
|
assert!(matches!(last(&s), EntityKind::Circle(c) if (c.radius - 5.0).abs() < 1e-9));
|
|
}
|
|
|
|
#[test]
|
|
fn polygon_radius_must_be_positive() {
|
|
for input in ["polygon 4 0,0 i 0", "polygon 4 0,0 c 0", "polygon 5 0,0 i -5", "polygon 5 0,0 c -5", "polygon 4 0,0 i 0,0", "polygon 4 0,0 c 0,0"]
|
|
{
|
|
let mut s = Session::new();
|
|
s.cmdline(input).unwrap();
|
|
assert_eq!(s.doc().unwrap().model.len(), 0, "{input}: no polygon");
|
|
assert!(logged(&s, "Value must be positive and nonzero."), "{input}: {:?}", s.log);
|
|
assert!(s.running.is_some(), "{input}: still at the radius prompt");
|
|
s.cmdline("2").unwrap();
|
|
assert!(s.running.is_none(), "{input}: a positive radius finishes");
|
|
assert_eq!(s.doc().unwrap().model.len(), 1, "{input}: one polygon");
|
|
assert!(matches!(last(&s), EntityKind::LwPolyline(p) if p.closed), "{input}");
|
|
}
|
|
}
|