cadcraft/crates/engine/tests/array_commands.rs

215 lines
9.6 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! ARRAY, ARRAYRECT and ARRAYPOLAR at the command line: the type prompt and the edit-array
//! option loops. Each input is its own `cmdline` call (one typed line each).
use cadcraft_engine::Session;
use cadcraft_engine::doc::{Drawing, Entity, EntityKind};
use cadcraft_engine::geom::{Mat3, Vec2};
use serde_json::json;
fn typed(s: &mut Session, inputs: &[&str]) {
for t in inputs {
s.cmdline(t).unwrap();
}
}
/// Circle centres in model space, inside (associative) arrays too.
fn circle_centers(s: &Session) -> Vec<Vec2> {
fn walk(d: &Drawing, e: &Entity, m: Mat3, depth: usize, out: &mut Vec<Vec2>) {
match &e.kind {
EntityKind::Circle(c) => out.push(m.apply(c.center.xy())),
EntityKind::Insert(i) if depth < 8 => {
let b = d.block(&i.block).unwrap();
let im = m.then_before(i.transform(b.base.xy()));
for be in b.entities.iter() {
walk(d, be, im, depth + 1, out);
}
}
_ => {}
}
}
let d = s.doc().unwrap();
let mut out = Vec::new();
for e in d.model.iter() {
walk(d, e, Mat3::IDENTITY, 0, &mut out);
}
out
}
fn has(pts: &[Vec2], x: f64, y: f64) -> bool {
pts.iter().any(|p| p.near(Vec2::new(x, y), 1e-6))
}
fn prompt(s: &Session) -> String {
s.prompt_text()
}
#[test]
fn array_asks_for_the_type_and_continues_as_that_array() {
// ARRAY ▸ POlar.
let mut s = Session::new();
s.execute("circle", &json!({"center": [10, 0], "radius": 1})).unwrap();
typed(&mut s, &["array", "l", ""]);
assert!(prompt(&s).contains("Enter array type or [Rectangular/PAth/POlar] <Rectangular>"), "{}", prompt(&s));
typed(&mut s, &["po", "0,0", "i", "4", "x"]);
assert!(s.current_prompt().is_none(), "{}", prompt(&s));
let c = circle_centers(&s);
assert_eq!(c.len(), 4);
assert!(has(&c, 0.0, 10.0) && has(&c, -10.0, 0.0) && has(&c, 0.0, -10.0));
// AR (alias), Enter takes the default Rectangular.
let mut s = Session::new();
s.execute("circle", &json!({"center": [0, 0], "radius": 1})).unwrap();
typed(&mut s, &["ar", "l", "", ""]);
assert!(prompt(&s).contains("COUnt"), "{}", prompt(&s));
typed(&mut s, &["cou", "2", "1", "s", "5", "5", ""]);
assert!(s.current_prompt().is_none(), "{}", prompt(&s));
let c = circle_centers(&s);
assert_eq!(c.len(), 2);
assert!(has(&c, 5.0, 0.0));
// ARRAY ▸ PAth continues at the path prompt with the selected objects.
let mut s = Session::new();
s.execute("circle", &json!({"center": [0, 1], "radius": 0.25})).unwrap();
s.execute("line", &json!({"points": [[0, 0], [10, 0]]})).unwrap();
typed(&mut s, &["array", "0.25,1", "", "pa", "5,0", "3"]);
assert!(s.current_prompt().is_none(), "{}", prompt(&s));
assert_eq!(circle_centers(&s).len(), 3);
// The JSON form dispatches on `type`.
let mut s = Session::new();
let r = s.execute("circle", &json!({"center": [10, 0], "radius": 1})).unwrap();
let h = r["handle"].clone();
let r = s.execute("array", &json!({"type": "polar", "handles": [h], "center": [0, 0], "count": 3})).unwrap();
assert_eq!(r["created"], 2);
assert!(s.execute("array", &json!({"type": "spiral", "handles": [h]})).is_err());
}
#[test]
fn arrayrect_edits_the_array_in_a_loop_until_exit() {
let mut s = Session::new();
s.execute("circle", &json!({"center": [0, 0], "radius": 0.5})).unwrap();
typed(&mut s, &["arrayrect", "l", ""]);
let p = prompt(&s);
assert!(p.contains("Select grip to edit array or [ASsociative/Base point/COUnt/Spacing/COLumns/Rows/Levels/eXit] <eXit>"), "{p}");
// Nothing is created before eXit.
assert_eq!(circle_centers(&s).len(), 1);
typed(&mut s, &["cou", "2", "3", "s", "10", "20"]);
// ASsociative asks Yes/No and returns to the loop.
typed(&mut s, &["as"]);
assert!(prompt(&s).contains("Create associative array or [Yes/No] <Yes>"), "{}", prompt(&s));
typed(&mut s, &["n"]);
assert!(prompt(&s).contains("Select grip to edit array"), "{}", prompt(&s));
typed(&mut s, &["x"]);
assert!(s.current_prompt().is_none(), "{}", prompt(&s));
let c = circle_centers(&s);
assert_eq!(c.len(), 6, "{c:?}");
for (x, y) in [(10.0, 0.0), (0.0, 20.0), (10.0, 40.0)] {
assert!(has(&c, x, y), "{c:?}");
}
// COLumns with a Total distance, Rows with spacing and elevation.
let mut s = Session::new();
s.execute("circle", &json!({"center": [0, 0], "radius": 0.5})).unwrap();
typed(&mut s, &["arrayrect", "l", "", "col", "3", "t", "20", "r", "2", "5", "0", "x"]);
assert!(s.current_prompt().is_none(), "{}", prompt(&s));
let c = circle_centers(&s);
assert_eq!(c.len(), 6, "{c:?}");
assert!(has(&c, 20.0, 0.0) && has(&c, 10.0, 5.0), "{c:?}");
// Invalid counts re-prompt.
let mut s = Session::new();
s.execute("circle", &json!({"center": [0, 0], "radius": 0.5})).unwrap();
typed(&mut s, &["arrayrect", "l", "", "cou", "0"]);
assert!(prompt(&s).contains("number of columns"), "{}", prompt(&s));
typed(&mut s, &["2", "1", "x"]);
assert_eq!(circle_centers(&s).len(), 2);
// The JSON form (associative by default) places the same items.
let mut s = Session::new();
let r = s.execute("circle", &json!({"center": [0, 0], "radius": 0.5})).unwrap();
let r = s.execute("arrayrect", &json!({"handles": [r["handle"]], "rows": 2, "cols": 3, "rowSpacing": 4, "colSpacing": 2})).unwrap();
assert_eq!(r["created"], 5);
assert!(has(&circle_centers(&s), 4.0, 4.0));
}
#[test]
fn arraypolar_asks_for_the_center_then_edits_items_and_angles() {
let mut s = Session::new();
s.execute("circle", &json!({"center": [10, 0], "radius": 1})).unwrap();
typed(&mut s, &["arraypolar", "l", ""]);
assert!(prompt(&s).contains("Specify center point of array or [Base point/Axis of rotation]"), "{}", prompt(&s));
typed(&mut s, &["0,0"]);
let p = prompt(&s);
assert!(
p.contains("Select grip to edit array or [ASsociative/Base point/Items/Angle between/Fill angle/ROWs/Levels/ROTate items/eXit] <eXit>"),
"{p}"
);
typed(&mut s, &["i", "4", "x"]);
assert!(s.current_prompt().is_none(), "{}", prompt(&s));
// X ended ARRAYPOLAR; it did not start EXPLODE.
assert!(!s.log.iter().any(|l| l.contains("EXPLODE")));
let c = circle_centers(&s);
assert_eq!(c.len(), 4);
assert!(has(&c, 0.0, 10.0) && has(&c, -10.0, 0.0) && has(&c, 0.0, -10.0));
// Angle between with 4 items: 30° apart, filling 90°.
let mut s = Session::new();
s.execute("circle", &json!({"center": [10, 0], "radius": 1})).unwrap();
s.script("ARRAYPOLAR L\n\n0,0 I 4 A 30 X\n").unwrap();
assert!(s.current_prompt().is_none(), "{}", prompt(&s));
let c = circle_centers(&s);
assert_eq!(c.len(), 4);
assert!(has(&c, 0.0, 10.0) && !has(&c, -10.0, 0.0), "{c:?}");
// Fill angle 180 with 3 items, items not rotated, not associative: the copies only move.
let mut s = Session::new();
s.execute("line", &json!({"points": [[10, 0], [12, 0]]})).unwrap();
typed(&mut s, &["arraypolar", "l", "", "0,0", "i", "3", "f", "180", "rot", "n", "as", "n", "x"]);
assert!(s.current_prompt().is_none(), "{}", prompt(&s));
let lines: Vec<(Vec2, Vec2)> = s
.doc()
.unwrap()
.model
.iter()
.filter_map(|e| match &e.kind {
EntityKind::Line(l) => Some((l.a.xy(), l.b.xy())),
_ => None,
})
.collect();
assert_eq!(lines.len(), 3);
// The middle copy: the line's centre (11,0) moved to (0,11), still horizontal.
assert!(lines.iter().any(|(a, b)| a.near(Vec2::new(-1.0, 11.0), 1e-6) && b.near(Vec2::new(1.0, 11.0), 1e-6)), "{lines:?}");
}
#[test]
fn arrays_over_maxarray_are_refused_not_shrunk() {
// JSON: a million × a million rows/cols (or items) is refused up front, nothing is created.
let mut s = Session::new();
let c = s.execute("circle", &json!({"center": [0, 0], "radius": 1})).unwrap();
let h = c["handle"].clone();
let before = s.doc().unwrap().model.iter().count();
let e = s.execute("arrayrect", &json!({"handles": [h], "rows": 1_000_000, "cols": 1_000_000})).unwrap_err();
assert!(e.to_string().contains("MAXARRAY"), "{e}");
assert!(s.execute("arraypolar", &json!({"handles": [h], "center": [5, 5], "count": 200_000})).unwrap_err().to_string().contains("MAXARRAY"));
let path = s.execute("line", &json!({"points": [[0, 5], [100, 5]]})).unwrap()["handles"][0].clone();
let e = s.execute("arraypath", &json!({"handles": [h], "path": path, "spacing": 1e-6})).unwrap_err();
assert!(e.to_string().contains("MAXARRAY"), "{e}");
assert_eq!(s.doc().unwrap().model.iter().count(), before + 1);
// The limit counts every selected object, and MAXARRAY is a settable system variable.
s.execute("setvar", &json!({"name": "MAXARRAY", "value": 100})).unwrap();
let h2 = s.execute("circle", &json!({"center": [3, 0], "radius": 1})).unwrap()["handle"].clone();
assert!(s.execute("arrayrect", &json!({"handles": [h, h2], "rows": 10, "cols": 6})).is_err());
assert_eq!(s.execute("arrayrect", &json!({"handles": [h, h2], "rows": 10, "cols": 5})).unwrap()["created"], 98);
// Interactive: a count over the limit is rejected at its prompt and the loop keeps going.
let mut s = Session::new();
s.execute("circle", &json!({"center": [0, 0], "radius": 1})).unwrap();
typed(&mut s, &["arrayrect", "l", "", "cou", "1000", "1000"]);
assert!(s.log.iter().any(|l| l.contains("MAXARRAY")), "{:?}", s.log);
assert!(prompt(&s).contains("rows"), "{}", prompt(&s));
typed(&mut s, &["2", ""]);
assert!(s.current_prompt().is_none(), "{}", prompt(&s));
assert_eq!(circle_centers(&s).len(), 2000);
}