Files
cadcraft/crates/engine/tests/it/array_commands.rs
T
aiongg 9cf115e808 CI: one test binary per crate, nextest, line tables only (#1989)
* Build: line tables only in dev, no debug info for dependencies

* Tests: one integration-test binary per crate

Each crate's tests/*.rs becomes a module of tests/it/main.rs. Files that
install process-wide state (the file io hooks, user aliases, SAVETIME and
SAVEFILEPATH, user linetype and pattern files, registered fonts) stay their
own test binaries.

* xtask ci: nextest, tests without examples, one wasm check call
2026-10-23 13:34:26 -04:00

215 lines
9.6 KiB
Rust
Raw 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);
}