//! 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 { fn walk(d: &Drawing, e: &Entity, m: Mat3, depth: usize, out: &mut Vec) { 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] "), "{}", 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] "), "{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] "), "{}", 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] "), "{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); }