diff --git a/crates/engine/src/cmd/array.rs b/crates/engine/src/cmd/array.rs index 9d4a2fd9c..255555a28 100644 --- a/crates/engine/src/cmd/array.rs +++ b/crates/engine/src/cmd/array.rs @@ -1,4 +1,5 @@ -//! Array commands: ARRAY, ARRAYRECT and ARRAYPOLAR (ARRAYPATH lives in `modify2`). +//! Array commands: ARRAY, ARRAYRECT, ARRAYPOLAR and the classic -ARRAY (ARRAYPATH lives in +//! `modify2`). //! //! Arrays are associative by default (ARRAYASSOCIATIVITY): the source objects move into an //! anonymous source block and the array becomes one block reference to an anonymous array block @@ -21,6 +22,7 @@ use super::{CommandSpec, bad, bool_or, f64_or, find_command, point_param, point_ use crate::{Accept, EngineError, Input, Interactive, Prompt, Result, Session, Step}; +mod classic; mod edit; /// Most preview entities drawn while the array is edited. @@ -48,6 +50,9 @@ pub fn specs() -> Vec { "{handles?, center, count, angle? (degrees, default 360), rotate?: bool, rows? (1), rowSpacing?, rowElevation?, levels? (1), levelSpacing?, base? ([x, y], default the first object's centre), associative?: bool (default ARRAYASSOCIATIVITY, on)} → {handle (the array, when associative), created (copies)}", ) .interactive(|_| Ok(Box::new(PolarM::default()))), + CommandSpec::new("-array", "Array", classic::run_classic) + .params("{handles?, type?: \"rectangular\" (default) | \"polar\", rows? (1), cols? (1), rowSpacing?, colSpacing?, center, count, angle? (degrees, default 360), rotate?: bool (default true), base? ([x, y], default the DXF group 10 point of the last object)} → {created}: separate copies, as the classic -ARRAY makes them") + .interactive(|_| Ok(Box::new(classic::ClassicM::default()))), ] .into_iter() .chain(edit::specs()) diff --git a/crates/engine/src/cmd/array/classic.rs b/crates/engine/src/cmd/array/classic.rs new file mode 100644 index 000000000..5fecefa2a --- /dev/null +++ b/crates/engine/src/cmd/array/classic.rs @@ -0,0 +1,255 @@ +//! -ARRAY: the classic command-line array, as AutoCAD 2024 runs it from AutoLISP's `command` +//! (accoreconsole, every copy read back with `entget`). It makes separate copies, never an +//! associative array: +//! - "Enter the type of array [Rectangular/Polar] ": the default is the type used last; +//! - Rectangular asks "Enter the number of rows (---) <1>" and "Enter the number of columns (|||) +//! <1>", then "Enter the distance between rows or specify unit cell (---)" only for more than one +//! row (two corners give both distances) and "Specify the distance between columns (|||)" only for +//! more than one column. The copy in row r and column c moves by (c·columns, r·rows): 2 rows and +//! 2 columns at −12.57 and −18.01 put the copies below and to the left; +//! - Polar asks "Specify center point of array or [Base]", "Enter the number of items in the +//! array", "Specify the angle to fill (+=ccw, -=cw) <360>" and "Rotate arrayed objects? +//! [Yes/No] ". Items are 360/n apart around a full circle, else fill/(n − 1) apart (6 items in +//! 270°: 54°; −90° turns clockwise). Not rotated, each copy moves as the DXF group 10 point of the +//! object selected last turns about the centre (a polyline's first vertex, an arc's centre), or +//! as the Base point does. + +use cadcraft_doc::Handle; +use cadcraft_geom::{Mat3, Vec2}; +use serde_json::{Value, json}; + +use super::super::machines::{SelOutcome, SelectPhase, number}; +use super::super::{bad, bool_or, f64_or, point_param, point_req, str_param, targets}; +use super::{KEYWORDS, angle, check_size, copy_all, count, count_or, max_count, other, yes_no}; +use crate::{Accept, Input, Interactive, Prompt, Result, Session, Step}; + +#[derive(Clone, Copy, Debug, Default, PartialEq)] +enum Stage { + #[default] + Select, + Kind, + Rows, + Columns, + RowDistance, + /// The unit cell's other corner, after the first one. + Corner(Vec2), + ColumnDistance, + Center, + Base, + Items, + Fill, + Rotate, +} + +#[derive(Default)] +pub(super) struct ClassicM { + sel: SelectPhase, + objs: Vec, + stage: Stage, + rows: u64, + columns: u64, + row_distance: f64, + center: Vec2, + base: Option, + items: u64, + fill: f64, +} + +/// The moves of a rectangular array's copies (the objects themselves stay in row 0, column 0). +fn rect_mats(rows: u64, columns: u64, row_distance: f64, column_distance: f64) -> Vec { + (0..rows) + .flat_map(|r| (0..columns).map(move |c| (r, c))) + .filter(|rc| *rc != (0, 0)) + .map(|(r, c)| Mat3::translate(Vec2::new(c as f64 * column_distance, r as f64 * row_distance))) + .collect() +} + +/// The transforms of a polar array's copies: `items` around `center` filling `fill` degrees, +/// turned with it or moved as `base` turns. +fn polar_mats(center: Vec2, items: u64, fill: f64, rotate: bool, base: Vec2) -> Vec { + let full = (fill.abs() - 360.0).abs() < 1e-9; + let gaps = if full { items } else { items.saturating_sub(1) }.max(1); + let step = fill.to_radians() / gaps as f64; + (1..items) + .map(|k| { + let a = step * k as f64; + if rotate { Mat3::rotate_about(center, a) } else { Mat3::translate(base.rotate_about(center, a) - base) } + }) + .collect() +} + +/// The DXF group 10 point of the last of `hs`, which a polar array that doesn't rotate its copies +/// moves around the centre. +fn last_point(s: &Session, hs: &[Handle]) -> Option { + let d = s.doc().ok()?; + hs.last().and_then(|h| d.entity(*h)).and_then(|e| crate::session::group_10(d, e)) +} + +/// -ARRAY's JSON form. +pub(super) fn run_classic(s: &mut Session, p: &Value) -> Result { + let hs = targets(s, p)?; + let polar = match str_param(p, "type").map(str::to_ascii_lowercase).as_deref() { + None | Some("rectangular") => false, + Some("polar") => true, + Some(_) => return Err(bad("-array", "type must be \"rectangular\" or \"polar\"")), + }; + // The size is checked before the copies' transforms are made. + let mats = if polar { + let center = point_req("-array", p, "center")?; + let items = count_or("-array", p, "count", 1)?; + check_size(s, items, hs.len())?; + let base = point_param(p, "base").or_else(|| last_point(s, &hs)).unwrap_or(center); + polar_mats(center, items, f64_or(p, "angle", 360.0), bool_or(p, "rotate", true), base) + } else { + let rows = count_or("-array", p, "rows", 1)?; + let columns = count_or("-array", p, "cols", 1)?; + check_size(s, rows.saturating_mul(columns), hs.len())?; + rect_mats(rows, columns, f64_or(p, "rowSpacing", 0.0), f64_or(p, "colSpacing", 0.0)) + }; + let created = copy_all(s, &hs, mats.into_iter())?; + Ok(json!({ "created": created })) +} + +impl ClassicM { + /// Make the copies. + fn finish(&mut self, s: &mut Session, mats: Vec) -> Result { + check_size(s, mats.len() as u64 + 1, self.objs.len())?; + copy_all(s, &self.objs, mats.into_iter())?; + s.set_selection(Vec::new()); + Ok(Step::Done) + } + + /// After the rows and columns: the distances still needed, or the copies. + fn after_counts(&mut self, s: &mut Session) -> Result { + if self.rows > 1 { + self.stage = Stage::RowDistance; + } else if self.columns > 1 { + self.stage = Stage::ColumnDistance; + } else { + return self.finish(s, Vec::new()); + } + Ok(Step::Continue) + } + + fn rect(&mut self, s: &mut Session, column_distance: f64) -> Result { + let mats = rect_mats(self.rows, self.columns, self.row_distance, column_distance); + self.finish(s, mats) + } +} + +impl Interactive for ClassicM { + fn name(&self) -> &'static str { + "-ARRAY" + } + fn begin(&mut self, s: &mut Session) -> Result { + self.sel = SelectPhase::begin(s); + if self.sel.done { + self.objs = self.sel.picked.clone(); + self.stage = Stage::Kind; + } + Ok(Step::Continue) + } + fn prompt(&self, s: &Session) -> Prompt { + match self.stage { + Stage::Select => self.sel.prompt(), + Stage::Kind => Prompt::new("Enter the type of array", KEYWORDS) + .kw(&["Rectangular", "Polar"]) + .default(if s.last_used.classic_array_polar { "P" } else { "R" }), + Stage::Rows => Prompt::new("Enter the number of rows (---)", Accept::NUMBER).default("1"), + Stage::Columns => Prompt::new("Enter the number of columns (|||)", Accept::NUMBER).default("1"), + Stage::RowDistance => Prompt::new("Enter the distance between rows or specify unit cell (---)", Accept::POINT_OR_NUMBER), + Stage::Corner(a) => Prompt::new("Specify opposite corner", Accept::POINT).corner(a), + Stage::ColumnDistance => Prompt::new("Specify the distance between columns (|||)", Accept::NUMBER), + Stage::Center => Prompt::new("Specify center point of array", Accept::POINT).kw(&["Base"]), + Stage::Base => Prompt::new("Specify base point of objects", Accept::POINT), + Stage::Items => Prompt::new("Enter the number of items in the array", Accept::NUMBER), + Stage::Fill => Prompt::new("Specify the angle to fill (+=ccw, -=cw)", Accept::NUMBER).default("360"), + Stage::Rotate => Prompt::new("Rotate arrayed objects?", KEYWORDS).kw(&["Yes", "No"]).default("Y"), + } + } + fn input(&mut self, s: &mut Session, i: Input) -> Result { + let max = max_count(s, 1, self.objs.len()); + match (self.stage, i) { + (Stage::Select, i) => match self.sel.feed(s, &i)? { + SelOutcome::More => {} + SelOutcome::Done(hs) => { + self.objs = hs; + self.stage = Stage::Kind; + } + SelOutcome::Empty => return Ok(Step::Done), + }, + (Stage::Kind, i) => { + let polar = match &i { + Input::Enter => s.last_used.classic_array_polar, + Input::Keyword(k) | Input::Text(k) => match k.trim().trim_start_matches('_').to_ascii_lowercase().chars().next() { + Some('p') => true, + Some('r') => false, + _ => return Err(other("Invalid option keyword.")), + }, + _ => return Ok(Step::Continue), + }; + s.last_used.classic_array_polar = polar; + self.stage = if polar { Stage::Center } else { Stage::Rows }; + } + (Stage::Rows, i) => { + if let Some(n) = count(&i, 1, max)? { + self.rows = n; + self.stage = Stage::Columns; + } + } + // No more columns than MAXARRAY allows with the rows. + (Stage::Columns, i) => { + if let Some(n) = count(&i, 1, max_count(s, self.rows, self.objs.len()))? { + self.columns = n; + return self.after_counts(s); + } + } + (Stage::RowDistance, Input::Point(a)) => self.stage = Stage::Corner(a), + (Stage::RowDistance, Input::Text(t)) => { + self.row_distance = number(&t).filter(|v| v.is_finite()).ok_or_else(|| other("Requires a distance."))?; + if self.columns <= 1 { + return self.rect(s, 0.0); + } + self.stage = Stage::ColumnDistance; + } + // A unit cell gives both distances. + (Stage::Corner(a), Input::Point(b)) => { + self.row_distance = b.y - a.y; + return self.rect(s, b.x - a.x); + } + (Stage::ColumnDistance, Input::Text(t)) => { + let d = number(&t).filter(|v| v.is_finite()).ok_or_else(|| other("Requires a distance."))?; + return self.rect(s, d); + } + (Stage::Center, Input::Point(c)) => { + self.center = c; + self.stage = Stage::Items; + } + (Stage::Center, Input::Keyword(k)) if k == "Base" => self.stage = Stage::Base, + (Stage::Base, Input::Point(b)) => { + self.base = Some(b); + self.stage = Stage::Center; + } + (Stage::Items, i @ Input::Text(_)) => { + if let Some(n) = count(&i, 1, max)? { + self.items = n; + self.stage = Stage::Fill; + } + } + (Stage::Fill, i) => { + if let Some(a) = angle(&i, 360.0, &s.angle_settings())? { + self.fill = a; + self.stage = Stage::Rotate; + } + } + (Stage::Rotate, i) => { + let Some(rotate) = yes_no(&i, true) else { return Err(other("Invalid option keyword.")) }; + let base = self.base.or_else(|| last_point(s, &self.objs)).unwrap_or(self.center); + let mats = polar_mats(self.center, self.items, self.fill, rotate, base); + return self.finish(s, mats); + } + _ => {} + } + Ok(Step::Continue) + } +} diff --git a/crates/engine/src/last_used.rs b/crates/engine/src/last_used.rs index 88a1d4f59..e884caf1b 100644 --- a/crates/engine/src/last_used.rs +++ b/crates/engine/src/last_used.rs @@ -25,6 +25,8 @@ pub struct LastUsed { pub mleader_block: String, /// BREAKLINE's Block, Size and Extension. pub breakline: cmd::BreaklineOptions, + /// -ARRAY's type: Polar instead of Rectangular. + pub classic_array_polar: bool, /// RECTANG's chamfer, fillet, width, elevation, thickness and rotation. pub rectang: RectangModes, } @@ -41,6 +43,7 @@ impl Default for LastUsed { mleader: MLeaderOptions::default(), mleader_block: String::new(), breakline: cmd::BreaklineOptions::default(), + classic_array_polar: false, rectang: RectangModes::default(), } } diff --git a/crates/engine/src/session.rs b/crates/engine/src/session.rs index d1ae67ee1..f465b7a55 100644 --- a/crates/engine/src/session.rs +++ b/crates/engine/src/session.rs @@ -13,3 +13,5 @@ mod files; mod interactive; mod picking; mod zoom; + +pub(crate) use picking::group_10; diff --git a/crates/engine/src/session/picking.rs b/crates/engine/src/session/picking.rs index a9d146571..1ab773a00 100644 --- a/crates/engine/src/session/picking.rs +++ b/crates/engine/src/session/picking.rs @@ -162,7 +162,7 @@ impl Session { /// An object's DXF group 10 point: the first point of its entity list, or its first grip where /// entity lists are not built in. -fn group_10(d: &Drawing, e: &Entity) -> Option { +pub(crate) fn group_10(d: &Drawing, e: &Entity) -> Option { let Some(codec) = cmd::file::entity_codec() else { return e.kind.grips().first().copied() }; let (_, tags) = (codec.groups)(d, e); let i = tags.iter().position(|t| t.code == 10)?; diff --git a/crates/engine/tests/it/classic_array.rs b/crates/engine/tests/it/classic_array.rs new file mode 100644 index 000000000..8f74f8f48 --- /dev/null +++ b/crates/engine/tests/it/classic_array.rs @@ -0,0 +1,194 @@ +//! -ARRAY, the classic command-line array, as AutoCAD 2024 runs it from AutoLISP's `command` +//! (accoreconsole, every copy read back with `entget`). + +use std::f64::consts::TAU; + +use cadcraft_engine::Session; +use cadcraft_engine::cmd::file::{EntityCodec, set_entity_codec}; +use cadcraft_engine::doc::EntityKind; +use serde_json::json; + +/// A session with the object `made` (an entity list) as `a` in LISP, after `code`. +fn run(made: &str, code: &str) -> Session { + // `entmake` goes through the DXF codec, as in the app. + set_entity_codec(EntityCodec { groups: cadcraft_io::entity_groups, entity: cadcraft_io::entity_from_groups }); + let mut s = Session::new(); + s.cmdline(&format!("(progn (setq a (progn (entmake '({made})) (entlast))) {code})")).unwrap(); + s +} + +/// Where each object is: a line's start, a circle's or arc's centre, a polyline's first vertex, a +/// text's insertion point. +fn points(s: &Session) -> Vec<[f64; 2]> { + s.doc() + .unwrap() + .model + .iter() + .map(|e| match &e.kind { + EntityKind::Line(l) => [l.a.x, l.a.y], + EntityKind::Circle(c) => [c.center.x, c.center.y], + EntityKind::Arc(a) => [a.center.x, a.center.y], + EntityKind::LwPolyline(p) => [p.vertices[0].p.x, p.vertices[0].p.y], + EntityKind::Text(t) => [t.insert.x, t.insert.y], + k => panic!("unexpected {k:?}"), + }) + .collect() +} + +/// `got` holds each of `want` to `tol`, and nothing else. +fn same(got: &[[f64; 2]], want: &[[f64; 2]], tol: f64) -> bool { + got.len() == want.len() && want.iter().all(|w| got.iter().any(|g| (g[0] - w[0]).abs() <= tol && (g[1] - w[1]).abs() <= tol)) +} + +fn line(a: [f64; 2], b: [f64; 2]) -> String { + format!("(0 . \"LINE\") (10 {} {} 0.0) (11 {} {} 0.0)", a[0], a[1], b[0], b[1]) +} + +fn circle(c: [f64; 2], r: f64) -> String { + format!("(0 . \"CIRCLE\") (10 {} {} 0.0) (40 . {r})", c[0], c[1]) +} + +fn arc(c: [f64; 2], r: f64, a1: f64, a2: f64) -> String { + format!("(0 . \"ARC\") (10 {} {} 0.0) (40 . {r}) (50 . {}) (51 . {})", c[0], c[1], a1.to_radians(), a2.to_radians()) +} + +/// The measured polyline: four vertices with bulges 0.3, 0, −0.7, 0, from `at`, `scale` × the +/// drawn size, `width` wide. +fn polyline(at: [f64; 2], scale: f64, width: f64) -> String { + let v = [(0.0, 0.0, 0.3), (20.0, 5.0, 0.0), (35.0, -10.0, -0.7), (50.0, 0.0, 0.0)]; + let vs: String = v.iter().map(|(x, y, b)| format!(" (10 {} {}) (42 . {b})", at[0] + x * scale, at[1] + y * scale)).collect(); + format!("(0 . \"LWPOLYLINE\") (90 . 4) (70 . 0) (43 . {width}){vs}") +} + +#[test] +fn rectangular_copies_move_by_rows_and_columns() { + let s = run( + &line([-1.1929712440222175, 9.67888030686079], [44.51297824124873, 11.673136668914886]), + "(command \"_.-ARRAY\" a \"\" \"_R\" 2 2 -12.570757111654931 -18.0084920904546)", + ); + let want = [ + [-19.20146333447681, -2.891876804794138], + [-19.20146333447681, 9.67888030686079], + [-1.192971244022217, -2.891876804794138], + [-1.192971244022217, 9.67888030686079], + ]; + assert!(same(&points(&s), &want, 1e-9), "{:?}", points(&s)); + // One row or one column: that distance isn't asked for. + let s = run(&circle([5.801226536663419, -5.877116775641122], 23.343762414841144), "(command \"_.-ARRAY\" a \"\" \"_R\" 1 3 -26.90504352387681)"); + assert!(s.running.is_none(), "{}", s.prompt_text()); + let want = [[-48.0088605110902, -5.877116775641122], [-21.10381698721339, -5.877116775641122], [5.801226536663419, -5.877116775641122]]; + assert!(same(&points(&s), &want, 1e-9), "{:?}", points(&s)); + let s = run(&polyline([0.5, 0.25], 0.001, 0.0005), "(command \"_.-ARRAY\" a \"\" \"_R\" 3 1 -0.010095474538067209)"); + assert!(s.running.is_none(), "{}", s.prompt_text()); + assert!(same(&points(&s), &[[0.5, 0.2298090509238659], [0.5, 0.239904525461933], [0.5, 0.25]], 1e-12), "{:?}", points(&s)); + let s = run( + "(0 . \"TEXT\") (10 5.4185272544331635 4.443702697220587 0.0) (40 . 2.5) (1 . \"AB12\") (50 . 0.20972899)", + "(command \"_.-ARRAY\" a \"\" \"_R\" 1 4 26.03962020976745)", + ); + let want = [ + [83.53738788373552, 4.443702697220587], + [57.49776767396807, 4.443702697220587], + [31.45814746420061, 4.443702697220587], + [5.418527254433163, 4.443702697220587], + ]; + assert!(same(&points(&s), &want, 1e-9), "{:?}", points(&s)); +} + +#[test] +fn polar_copies_turn_about_the_centre() { + // Five items around a full circle: 72° apart. + let s = run( + &arc([8.737608057965836, 8.018370437941456], 25.76749991913238, 246.54512163341687, 4.3030133419271355), + "(command \"_.-ARRAY\" a \"\" \"_P\" '(10.631794214160564 -5.07952370466397) 5 360 \"_Y\")", + ); + let want = [ + [22.50329607545565, 0.769426262850251], + [19.86297201814329, -14.56256796884801], + [4.465473991920418, -16.78931734426366], + [-2.410379072682379, -2.833529910999876], + [8.737608057965836, 8.018370437941456], + ]; + assert!(same(&points(&s), &want, 1e-9), "{:?}", points(&s)); + let starts: Vec = + s.doc().unwrap().model.iter().filter_map(|e| if let EntityKind::Arc(a) = &e.kind { Some(a.start) } else { None }).collect(); + for want in [3.046387065797107, 1.789750004361188, 0.5331129429252733, 5.559661188668941, 4.303024127233024] { + assert!(starts.iter().any(|a| ((a - want).rem_euclid(TAU) + 1e-9).rem_euclid(TAU) < 2e-9), "{want}: {starts:?}"); + } + // Six items in 270°: 54° apart. + let s = run( + &line([14.349623930607876, -18.237839127538827], [57.5895083326729, 8.515990272720988]), + "(command \"_.-ARRAY\" a \"\" \"_P\" '(9.968618268950948 -10.513614962917108) 6 270 \"_Y\")", + ); + let want = [ + [2.244394104329227, -14.89462062457403], + [1.884125186851836, -6.839676863508278], + [8.18895082173591, -1.813636035924282], + [15.96098679213407, -3.960114445242588], + [18.79273740462331, -11.50949197954958], + [14.34962393060787, -18.23783912753882], + ]; + assert!(same(&points(&s), &want, 1e-9), "{:?}", points(&s)); +} + +#[test] +fn polar_copies_not_rotated_move_with_the_group_10_point() { + // A polyline moves with its first vertex: three items in 270°. + let s = run( + &polyline([612345.678, 2398765.432], 1.0, 0.5), + "(command \"_.-ARRAY\" a \"\" \"_P\" '(612362.9804954464 2398768.822523769) 3 270 \"_N\")", + ); + let want = [[612359.5899716774, 2398786.125019215], [612377.612669657, 2398758.985274256], [612345.678, 2398765.432]]; + assert!(same(&points(&s), &want, 1e-6), "{:?}", points(&s)); + // A circle with its centre, five items in −90° (clockwise). + let s = run( + &circle([612340.715727557, 2398755.830080357], 26.60109393014347), + "(command \"_.-ARRAY\" a \"\" \"_P\" '(612346.260090821 2398771.804712544) 5 -90 \"_N\")", + ); + let want = [ + [612330.2854586337, 2398777.349075808], + [612329.3797191395, 2398770.813809208], + [612331.0438632132, 2398764.429398659], + [612335.0245400043, 2398759.16781279], + [612340.715727557, 2398755.830080357], + ]; + assert!(same(&points(&s), &want, 1e-6), "{:?}", points(&s)); + // An arc with its centre, its angles unchanged. + let s = run( + &arc([612339.185864084, 2398766.23253733], 13.970352776505141, 238.67958380809327, 80.40871664680844), + "(command \"_.-ARRAY\" a \"\" \"_P\" '(612358.7800675274 2398749.633182222) 7 360 \"_N\")", + ); + let want = [ + [612359.541179917, 2398775.302075975], + [612379.3233625971, 2398765.042414074], + [612383.6360250826, 2398743.179286296], + [612369.2316445602, 2398726.176073787], + [612346.957013359, 2398726.836542366], + [612333.585383093, 2398744.663345723], + [612339.185864084, 2398766.23253733], + ]; + assert!(same(&points(&s), &want, 1e-6), "{:?}", points(&s)); + assert!(s.doc().unwrap().model.iter().all(|e| matches!(&e.kind, EntityKind::Arc(a) if (a.start - 4.165744594740973).abs() < 1e-9))); +} + +#[test] +fn the_type_asked_defaults_to_the_one_used_last() { + let mut s = run(&circle([0.0, 0.0], 1.0), "(command \"_.-ARRAY\" a \"\" \"_P\" '(10.0 0.0) 3 360 \"_Y\")"); + s.cmdline("(command \"_.-ARRAY\" a \"\")").unwrap(); + assert!(s.prompt_text().contains("[Rectangular/Polar]

"), "{}", s.prompt_text()); +} + +#[test] +fn classic_array_by_json() { + let mut s = run(&circle([0.0, 0.0], 1.0), ""); + let first = s.doc().unwrap().model.iter().next().unwrap().handle.hex(); + let r = s.execute("-array", &json!({ "handles": [first], "type": "polar", "center": [10, 0], "count": 4, "rotate": false })).unwrap(); + assert_eq!(r["created"].as_u64(), Some(3), "{r}"); + assert!(same(&points(&s), &[[0.0, 0.0], [10.0, -10.0], [20.0, 0.0], [10.0, 10.0]], 1e-9), "{:?}", points(&s)); + let r = s.execute("-array", &json!({ "handles": [first], "rows": 2, "cols": 3, "rowSpacing": 5, "colSpacing": 7 })).unwrap(); + assert_eq!(r["created"].as_u64(), Some(5), "{r}"); + // An unknown type, or an array past MAXARRAY, is refused before anything is made. + assert!(s.execute("-array", &json!({ "handles": [first], "type": "path" })).is_err()); + assert!(s.execute("-array", &json!({ "handles": [first], "rows": 4294967296u64, "cols": 4294967296u64 })).is_err()); + assert!(s.execute("-array", &json!({ "handles": [first], "type": "polar", "center": [0, 0], "count": u64::MAX })).is_err()); + assert_eq!(s.doc().unwrap().model.iter().count(), 4 + 5); +} diff --git a/crates/engine/tests/it/main.rs b/crates/engine/tests/it/main.rs index 9b0907e24..0763ee6eb 100644 --- a/crates/engine/tests/it/main.rs +++ b/crates/engine/tests/it/main.rs @@ -21,6 +21,7 @@ mod break_keeps_z; mod burst; mod change_command; mod chprop_command; +mod classic_array; mod compare_export; mod constraint_geometry; mod constraint_infer;