//! Array commands: ARRAY, ARRAYRECT and ARRAYPOLAR (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 //! holding a reference per item, with the array parameters kept on that block //! ([`cadcraft_doc::ArrayParams`]). With ASsociative = No the items are separate copies. The //! interactive forms select objects, then edit the array in an option loop with a live preview, //! and create the array on eXit (or Enter). use std::collections::HashMap; use std::sync::Arc; use cadcraft_doc::{ArrayKind, ArrayParams, Block, Drawing, Entity, EntityKind, Handle, Insert, MAX_BLOCK_DEPTH, PolarArray, RectArray}; use cadcraft_geom::{Bounds2, Mat3, Vec2, Vec3}; use serde_json::{Value, json}; use super::helpers::{line, v3}; use super::machines::{SelOutcome, SelectPhase, number}; use super::modify::transform_entities; use super::*; use crate::{Accept, EngineError, Input, Interactive, Prompt, Result, Session, Step}; mod edit; /// Most preview entities drawn while the array is edited. const PREVIEW_LIMIT: usize = 2000; /// Keyword-only prompts: each typed token is matched on its own (no free text). const KEYWORDS: Accept = Accept { point: false, number: false, text: false, select: false, enter: true }; const FLAT_LEVELS: &str = "Levels need an associative array; the copies are made on one level."; pub fn specs() -> Vec { vec![ CommandSpec::new("array", "Array", run_array) .alias(&["ar"]) .params("{type?: \"rectangular\" (default) | \"polar\" | \"path\", ...the parameters of arrayrect, arraypolar or arraypath}") .interactive(|_| Ok(Box::new(ArrayM::default()))), CommandSpec::new("arrayrect", "Rectangular Array", run_arrayrect) .menu(&["Modify", "Array", "Rectangular Array"]) .params( "{handles?, rows, cols, rowSpacing, colSpacing, levels? (1), levelSpacing?, rowElevation?, axisAngle? (degrees), base? ([x, y], default the objects' centre), associative?: bool (default ARRAYASSOCIATIVITY, on)} → {handle (the array, when associative), created (copies)}", ) .interactive(|_| Ok(Box::new(RectM::default()))), CommandSpec::new("arraypolar", "Polar Array", run_arraypolar) .menu(&["Modify", "Array", "Polar Array"]) .params( "{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()))), ] .into_iter() .chain(edit::specs()) .collect() } // ---------------- geometry ---------------- /// Array parameters around `kind` (the source block is named when the array is made). fn params(kind: ArrayKind, levels: u64, level_spacing: f64) -> ArrayParams { ArrayParams { source: "*".into(), kind, levels: u32::try_from(levels).unwrap_or(u32::MAX), level_spacing, replaced: Vec::new(), editing: None } } fn count32(n: u64) -> u32 { u32::try_from(n).unwrap_or(u32::MAX) } /// World transforms of the items of `p` with its base point at `base`, except the first item /// (the source objects themselves) and items above the first level (copies are flat). fn item_mats(p: &ArrayParams, base: Vec2) -> Vec { p.items() .unwrap_or_default() .into_iter() .filter(|it| it.level == 0 && (it.i, it.j) != (0, 0)) .map(|it| Mat3::translate(base + it.insert.xy()).then_before(Mat3::rotate(it.rotation)).then_before(Mat3::translate(-base))) .collect() } /// How deeply block references nest in `ents` (0 without any), at most `MAX_BLOCK_DEPTH + 1`. fn nesting(d: &Drawing, ents: &[&Entity], memo: &mut HashMap, depth: usize) -> usize { let mut most = 0; for e in ents { let EntityKind::Insert(i) = &e.kind else { continue }; let key = i.block.to_ascii_uppercase(); let n = match memo.get(&key) { Some(n) => *n, None if depth > MAX_BLOCK_DEPTH => MAX_BLOCK_DEPTH + 1, None => { // Cycles end here: a block being measured counts as too deep. memo.insert(key.clone(), MAX_BLOCK_DEPTH + 1); let inner: Vec<&Entity> = d.block(&i.block).map(|b| b.entities.iter().map(|e| e.as_ref()).collect()).unwrap_or_default(); let n = (1 + nesting(d, &inner, memo, depth + 1)).min(MAX_BLOCK_DEPTH + 1); memo.insert(key, n); n } }; most = most.max(n); } most } /// ARRAYASSOCIATIVITY: whether new arrays start associative (on unless set to 0). fn assoc_default(s: &Session) -> bool { s.doc().map(|d| d.header.i64("ARRAYASSOCIATIVITY", 1) != 0).unwrap_or(true) } /// Make `objs` an associative array with its base point at `base`: the objects move into a new /// source block, the items into a new array block, and one block reference to the array block /// takes their place. Returns that reference. pub(crate) fn make_assoc(s: &mut Session, objs: &[Handle], base: Vec2, mut p: ArrayParams) -> Result { let d = s.doc()?; let objs: Vec = objs.iter().copied().filter(|h| !super::curves::is_locked(s, *h)).collect(); let ents: Vec = objs.iter().filter_map(|h| d.entity(*h).map(|e| (**e).clone())).collect(); if ents.is_empty() { return Err(other("No objects to array (objects on locked layers are left out).")); } // The array adds two levels of block references (array block, then source block). let refs: Vec<&Entity> = ents.iter().collect(); if nesting(d, &refs, &mut HashMap::new(), 0) + 2 > MAX_BLOCK_DEPTH { return Err(other(format!("The objects nest block references too deeply to array (more than {MAX_BLOCK_DEPTH} levels)."))); } if !base.is_finite() { return Err(other("The array base point is not a valid point.")); } let d = s.doc_mut()?; let src_name = d.anonymous_block_name("*U"); let mut src = Block::new(&src_name); let to_local = Mat3::translate(-base); for mut e in ents { e.handle = d.new_handle(); e.kind.transform(&to_local); src.entities.push(e); } d.blocks.insert(src_name.clone(), Arc::new(src)); p.source = src_name; let items = p.item_entities(d).ok_or_else(|| other("The array parameters are not valid."))?; let name = d.anonymous_block_name("*U"); let mut blk = Block::new(&name); for e in items { blk.entities.push(e); } blk.array = Some(p); d.blocks.insert(name.clone(), Arc::new(blk)); for h in &objs { d.remove_entity(*h); } s.add_entity(EntityKind::Insert(Insert { block: name, insert: v3(base), scale: Vec3::new(1.0, 1.0, 1.0), rotation: 0.0, attribs: Vec::new(), cols: 1, rows: 1, col_spacing: 0.0, row_spacing: 0.0, })) } /// Create the array: associative (one block reference) or separate copies. Returns the JSON /// result: the array's handle when associative, and the number of copies the array adds. fn create(s: &mut Session, objs: &[Handle], base: Vec2, p: ArrayParams, assoc: bool) -> Result { let copies = p.item_count().saturating_sub(1).saturating_mul(objs.len() as u64); if assoc { let h = make_assoc(s, objs, base, p)?; return Ok(json!({ "handle": h.hex(), "created": copies, "associative": true })); } if p.levels > 1 { s.echo(FLAT_LEVELS); } let n = copy_all(s, objs, item_mats(&p, base).into_iter())?; Ok(json!({ "created": n })) } /// EXPLODE of an associative array: every item becomes the objects of its block (the source /// objects, or a replacement block's reference), in world coordinates. `None` for other objects. pub(crate) fn explode_array(d: &Drawing, e: &Entity) -> Option> { let EntityKind::Insert(ins) = &e.kind else { return None }; let p = d.array_params(e)?; let blk = d.block(&ins.block)?; let m = ins.transform(blk.base.xy()); let mut out = Vec::new(); for item in blk.entities.iter() { let item = placed(e, item, &m); let source = match &item.kind { EntityKind::Insert(i) if i.block.eq_ignore_ascii_case(&p.source) => d.block(&i.block).map(|b| (i.transform(b.base.xy()), b)), _ => None, }; match source { Some((im, b)) => out.extend(b.entities.iter().map(|se| placed(&item, se, &im))), None => out.push(item), } } Some(out) } /// `child` of a block referenced by `parent`, moved by `m`, with layer 0 and ByBlock properties /// taken from the reference (as EXPLODE does). fn placed(parent: &Entity, child: &Entity, m: &Mat3) -> Entity { let mut ne = child.clone(); ne.kind.transform(m); if ne.common.layer == "0" { ne.common.layer = parent.common.layer.clone(); } if ne.common.color == cadcraft_color::Color::ByBlock { ne.common.color = parent.common.color; } if ne.common.linetype.eq_ignore_ascii_case("ByBlock") { ne.common.linetype = parent.common.linetype.clone(); } if ne.common.lineweight == cadcraft_doc::Lineweight::ByBlock { ne.common.lineweight = parent.common.lineweight; } ne } fn bounds(d: &Drawing, hs: &[Handle]) -> Bounds2 { hs.iter().filter_map(|h| d.entity(*h).map(|e| cadcraft_doc::entity_bounds(d, e, 0))).fold(Bounds2::EMPTY, |a, b| a.union(&b)) } /// The centre of the first object (the point a non-rotated polar copy is moved by). fn first_center(d: &Drawing, hs: &[Handle]) -> Option { hs.first().and_then(|h| d.entity(*h)).map(|e| cadcraft_doc::entity_bounds(d, e, 0).center()).filter(|c| c.is_finite()) } /// MAXARRAY: refuse an array of `items` copies of `objs` objects that would exceed the limit /// (instead of creating fewer copies than asked for). pub(crate) fn check_size(s: &Session, items: u64, objs: usize) -> Result<()> { let max = s.settings.maxarray; let total = items.saturating_mul(objs.max(1) as u64); if total > max { return Err(other(format!( "The array would create {total} objects; the limit is {max} (MAXARRAY). Use fewer items or select fewer objects." ))); } Ok(()) } /// The most items a count prompt accepts when the rest of the array is `others` items of `objs` objects. fn max_count(s: &Session, others: u64, objs: usize) -> u64 { (s.settings.maxarray / others.max(1).saturating_mul(objs.max(1) as u64)).max(1) } fn copy_all(s: &mut Session, hs: &[Handle], mats: impl Iterator) -> Result { let mut n = 0; for m in mats { n += transform_entities(s, hs, &m, true)?.len(); } Ok(n) } /// The objects transformed by each matrix, for the preview. fn preview_copies(s: &Session, hs: &[Handle], mats: impl Iterator) -> Vec { let Ok(d) = s.doc() else { return Vec::new() }; let src: Vec = hs.iter().take(PREVIEW_LIMIT).filter_map(|h| d.entity(*h)).map(|e| e.kind.clone()).collect(); if src.is_empty() { return Vec::new(); } let mut out = Vec::new(); for m in mats { for k in &src { if out.len() >= PREVIEW_LIMIT { return out; } let mut k = k.clone(); k.transform(&m); out.push(k); } } out } fn other(m: impl Into) -> EngineError { EngineError::Other(m.into()) } fn fmt(v: f64) -> String { format!("{v:.4}") } /// A count typed at a prompt (Enter keeps `cur`); `None` for inputs the prompt ignores. fn count(i: &Input, cur: u64, max: u64) -> Result> { match i { Input::Enter => Ok(Some(cur)), Input::Text(t) => t .trim() .parse::() .ok() .filter(|v| v.fract() == 0.0 && *v >= 1.0 && *v <= max as f64) .map(|v| Some(v as u64)) .ok_or_else(|| other(format!("Requires an integer between 1 and {max} (MAXARRAY limits the size of the array)."))), _ => Ok(None), } } /// An angle in degrees typed at a prompt in AUNITS (Enter keeps `cur`); `None` for inputs the /// prompt ignores. fn angle(i: &Input, cur: f64, au: &crate::units::AngleSettings) -> Result> { match i { Input::Enter => Ok(Some(cur)), Input::Text(t) => au.amount(t).map(|a| Some(a.to_degrees())).ok_or_else(|| other("Requires a valid angle in degrees.")), _ => Ok(None), } } fn yes_no(i: &Input, cur: bool) -> Option { match i { Input::Enter => Some(cur), Input::Keyword(k) | Input::Text(k) => match k.trim().to_ascii_lowercase().chars().next() { Some('y') => Some(true), Some('n') => Some(false), _ => None, }, _ => None, } } // ---------------- JSON ---------------- fn run_array(s: &mut Session, p: &Value) -> Result { let id = match str_param(p, "type").unwrap_or("rectangular").to_ascii_lowercase().as_str() { "rectangular" | "rect" | "r" => "arrayrect", "polar" | "po" => "arraypolar", "path" | "pa" => "arraypath", _ => return Err(bad("array", "`type` must be \"rectangular\", \"polar\" or \"path\"")), }; let spec = find_command(id).ok_or_else(|| EngineError::UnknownCommand(id.into()))?; (spec.run)(s, p) } /// A count parameter (at least 1). fn count_or(p: &Value, key: &str, default: u64) -> u64 { p.get(key).and_then(Value::as_u64).unwrap_or(default).max(1) } fn run_arrayrect(s: &mut Session, p: &Value) -> Result { let hs = targets(s, p)?; let rows = count_or(p, "rows", 3); let cols = count_or(p, "cols", 4); let levels = count_or(p, "levels", 1); check_size(s, rows.saturating_mul(cols).saturating_mul(levels), hs.len())?; let kind = ArrayKind::Rect(RectArray { cols: count32(cols), rows: count32(rows), col_spacing: f64_or(p, "colSpacing", 1.0), row_spacing: f64_or(p, "rowSpacing", 1.0), axis_angle: f64_or(p, "axisAngle", 0.0).to_radians(), row_elevation: f64_or(p, "rowElevation", 0.0), }); let rs = f64_or(p, "rowSpacing", 1.0); let base = match point_param(p, "base") { Some(b) => b, None => Some(bounds(s.doc()?, &hs).center()).filter(|c| c.is_finite()).unwrap_or_default(), }; let p2 = params(kind, levels, f64_or(p, "levelSpacing", rs)); let associative = bool_or(p, "associative", assoc_default(s)); create(s, &hs, base, p2, associative) } fn run_arraypolar(s: &mut Session, p: &Value) -> Result { let hs = targets(s, p)?; let c = point_req("arraypolar", p, "center")?; let n = count_or(p, "count", 6); let rows = count_or(p, "rows", 1); let levels = count_or(p, "levels", 1); check_size(s, n.saturating_mul(rows).saturating_mul(levels), hs.len())?; let base = point_param(p, "base").or_else(|| s.doc().ok().and_then(|d| first_center(d, &hs))).unwrap_or(c); let row_spacing = f64_or(p, "rowSpacing", 1.0); let kind = ArrayKind::Polar(PolarArray { center: c - base, items: count32(n), fill: f64_or(p, "angle", 360.0), rotate: bool_or(p, "rotate", true), rows: count32(rows), row_spacing, row_elevation: f64_or(p, "rowElevation", 0.0), }); let p2 = params(kind, levels, f64_or(p, "levelSpacing", row_spacing)); let associative = bool_or(p, "associative", assoc_default(s)); create(s, &hs, base, p2, associative) } // ---------------- ARRAY ---------------- /// ARRAY: select objects, choose the array type, then continue as ARRAYRECT, ARRAYPATH or /// ARRAYPOLAR with those objects. #[derive(Default)] struct ArrayM { sel: SelectPhase, objs: Vec, inner: Option>, } impl Interactive for ArrayM { 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(); } Ok(Step::Continue) } fn prompt(&self, s: &Session) -> Prompt { match &self.inner { Some(m) => m.prompt(s), None if !self.sel.done => self.sel.prompt(), None => Prompt::new("Enter array type", KEYWORDS).kw(&["Rectangular", "PAth", "POlar"]).default("Rectangular"), } } fn input(&mut self, s: &mut Session, i: Input) -> Result { if let Some(m) = &mut self.inner { return m.input(s, i); } if !self.sel.done { return match self.sel.feed(s, &i)? { SelOutcome::More => Ok(Step::Continue), SelOutcome::Empty => Ok(Step::Done), SelOutcome::Done(hs) => { self.objs = hs; Ok(Step::Continue) } }; } let kind = match &i { Input::Enter => "Rectangular", Input::Keyword(k) => k.as_str(), _ => return Err(other("Invalid option keyword.")), }; let objs = self.objs.clone(); let (m, step): (Box, Step) = match kind { "PAth" => (Box::new(super::modify2::PathArrayM::with_objects(objs)), Step::Continue), "POlar" => { let mut m = PolarM::with_objects(objs); let step = m.ready(s); (Box::new(m), step) } _ => { let mut m = RectM::with_objects(objs); let step = m.ready(s)?; (Box::new(m), step) } }; self.inner = Some(m); Ok(step) } fn preview(&self, s: &Session, cursor: Vec2) -> Vec { self.inner.as_ref().map(|m| m.preview(s, cursor)).unwrap_or_default() } fn drag(&self, s: &Session, cursor: Vec2) -> Option { self.inner.as_ref().and_then(|m| m.drag(s, cursor)) } } // ---------------- ARRAYRECT ---------------- #[derive(Clone, Copy, Debug, Default, PartialEq)] enum RStage { #[default] Main, Assoc, Base, /// COUnt: columns, then rows. CountCols, CountRows, /// Spacing: between columns (or a unit cell), then between rows. SpaceCols, SpaceRows, UnitCell, /// COLumns: count, then spacing (or the total). ColsCount, ColsSpace, ColsTotal, /// Rows: count, then spacing (or the total), then the elevation increment. RowsCount, RowsSpace, RowsTotal, RowsElev, /// Levels: count, then spacing (or the total). LevelsCount, LevelsSpace, LevelsTotal, } /// ARRAYRECT: select objects, then edit columns/rows/levels/spacing in a loop; eXit creates the /// array. #[derive(Default)] struct RectM { sel: SelectPhase, objs: Vec, stage: RStage, assoc: bool, cols: u64, rows: u64, levels: u64, cs: f64, rs: f64, /// Distance between levels and the elevation added per row. ls: f64, elev: f64, /// Array base point (distances picked with a point are measured from it). base: Vec2, centroid: Vec2, /// Unit cell: the first corner, once picked. corner: Option, } impl RectM { fn with_objects(objs: Vec) -> Self { RectM { sel: SelectPhase { picked: objs.clone(), done: true, ..SelectPhase::default() }, objs, ..RectM::default() } } /// Objects are selected: set the defaults (4 columns × 3 rows, 1.5 × the selection size apart). fn ready(&mut self, s: &mut Session) -> Result { let b = bounds(s.doc()?, &self.objs); let (w, h) = (b.width(), b.height()); let size = |v: f64, o: f64| match (v.is_finite() && v > 1e-9, o.is_finite() && o > 1e-9) { (true, _) => v * 1.5, (false, true) => o * 1.5, _ => 1.0, }; (self.cols, self.rows, self.levels, self.cs, self.rs) = (4, 3, 1, size(w, h), size(h, w)); (self.ls, self.elev) = (self.rs, 0.0); self.assoc = assoc_default(s); self.centroid = if b.center().is_finite() { b.center() } else { Vec2::ZERO }; self.base = self.centroid; self.stage = RStage::Main; s.echo(format!("Type = Rectangular Associative = {}", yes(self.assoc))); Ok(Step::Continue) } fn params(&self) -> ArrayParams { let kind = ArrayKind::Rect(RectArray { cols: count32(self.cols), rows: count32(self.rows), col_spacing: self.cs, row_spacing: self.rs, axis_angle: 0.0, row_elevation: self.elev, }); params(kind, if self.assoc { self.levels } else { 1 }, self.ls) } /// A distance typed or picked (from the base point); Enter keeps `cur`. fn distance(&self, i: &Input, cur: f64) -> Result> { let d = match i { Input::Enter => return Ok(Some(cur)), Input::Text(t) => number(t), Input::Point(p) => Some(self.base.dist(*p)), _ => return Ok(None), }; d.filter(|d| d.is_finite() && d.abs() > 1e-12).map(Some).ok_or_else(|| other("Requires a nonzero distance.")) } fn finish(&mut self, s: &mut Session) -> Result { let levels = if self.assoc { self.levels } else { 1 }; check_size(s, self.rows.saturating_mul(self.cols).saturating_mul(levels), self.objs.len())?; if !self.assoc && self.levels > 1 { s.echo(FLAT_LEVELS); } create(s, &self.objs.clone(), self.base, self.params(), self.assoc)?; s.set_selection(Vec::new()); Ok(Step::Done) } fn main(&mut self, s: &mut Session, i: Input) -> Result { let k = match i { Input::Enter => return self.finish(s), Input::Keyword(k) => k, Input::Point(_) => return Err(other("Editing the array with grips is not available yet; choose an option.")), _ => return Err(other("Invalid option keyword.")), }; self.stage = match k.as_str() { "eXit" => return self.finish(s), "ASsociative" => RStage::Assoc, "Base point" => RStage::Base, "COUnt" => RStage::CountCols, "Spacing" => RStage::SpaceCols, "COLumns" => RStage::ColsCount, "Rows" => RStage::RowsCount, "Levels" => RStage::LevelsCount, _ => return Err(other("Invalid option keyword.")), }; Ok(Step::Continue) } } fn yes(b: bool) -> &'static str { if b { "Yes" } else { "No" } } /// The ASsociative prompt. fn assoc_prompt(cur: bool) -> Prompt { Prompt::new("Create associative array", KEYWORDS).kw(&["Yes", "No"]).default(yes(cur)) } impl Interactive for RectM { fn name(&self) -> &'static str { "ARRAYRECT" } fn begin(&mut self, s: &mut Session) -> Result { self.sel = SelectPhase::begin(s); if self.sel.done { self.objs = self.sel.picked.clone(); return self.ready(s); } Ok(Step::Continue) } fn prompt(&self, _s: &Session) -> Prompt { use RStage::*; if !self.sel.done { return self.sel.prompt(); } let cols = || Prompt::new("Enter the number of columns", Accept::NUMBER).default(self.cols.to_string()); let rows = || Prompt::new("Enter the number of rows", Accept::NUMBER).default(self.rows.to_string()); let dist = |m: &str, d: f64| Prompt::new(m, Accept::POINT_OR_NUMBER).default(fmt(d)).base(self.base); match self.stage { Main => Prompt::new("Select grip to edit array", Accept::POINT) .kw(&["ASsociative", "Base point", "COUnt", "Spacing", "COLumns", "Rows", "Levels", "eXit"]) .default("eXit"), Assoc => assoc_prompt(self.assoc), Base => Prompt::new("Specify base point", Accept::POINT).kw(&["Key point"]).default("centroid"), CountCols | ColsCount => cols(), CountRows | RowsCount => rows(), SpaceCols => dist("Specify the distance between columns", self.cs).kw(&["Unit cell"]), SpaceRows => dist("Specify the distance between rows", self.rs), UnitCell => match self.corner { None => Prompt::new("Specify first corner of the unit cell", Accept::POINT).base(self.base), Some(c) => Prompt::new("Specify other corner", Accept::POINT).corner(c), }, ColsSpace => dist("Specify the distance between columns", self.cs).kw(&["Total"]), ColsTotal => dist("Specify the distance between the first and last column", self.cs * self.cols.saturating_sub(1) as f64), RowsSpace => dist("Specify the distance between rows", self.rs).kw(&["Total"]), RowsTotal => dist("Specify the distance between the first and last row", self.rs * self.rows.saturating_sub(1) as f64), RowsElev => Prompt::new("Specify the incrementing elevation between rows", Accept::NUMBER).default(fmt(self.elev)), LevelsCount => Prompt::new("Enter the number of levels", Accept::NUMBER).default(self.levels.to_string()), LevelsSpace => dist("Specify the distance between levels", self.ls).kw(&["Total"]), LevelsTotal => dist("Specify the distance between the first and last level", self.ls * self.levels.saturating_sub(1) as f64), } } fn input(&mut self, s: &mut Session, i: Input) -> Result { use RStage::*; if !self.sel.done { return match self.sel.feed(s, &i)? { SelOutcome::More => Ok(Step::Continue), SelOutcome::Empty => Ok(Step::Done), SelOutcome::Done(hs) => { self.objs = hs; self.ready(s) } }; } let stage = self.stage; let n = self.objs.len(); let next = match (stage, &i) { (Main, _) => return self.main(s, i), (Assoc, _) => match yes_no(&i, self.assoc) { Some(a) => { self.assoc = a; Main } None => return Err(other("Requires Yes or No.")), }, (Base, Input::Point(p)) => { self.base = *p; Main } (Base, Input::Enter) => { self.base = self.centroid; Main } (Base, Input::Keyword(_)) => { s.echo("Key points are not available yet; specify a point."); Base } (CountCols | ColsCount, _) => match count(&i, self.cols, max_count(s, self.rows.saturating_mul(self.levels), n))? { Some(c) => { self.cols = c; if stage == CountCols { CountRows } else { ColsSpace } } None => stage, }, (CountRows | RowsCount, _) => match count(&i, self.rows, max_count(s, self.cols.saturating_mul(self.levels), n))? { Some(r) => { self.rows = r; if stage == CountRows { Main } else { RowsSpace } } None => stage, }, (LevelsCount, _) => match count(&i, self.levels, max_count(s, self.cols.saturating_mul(self.rows), n))? { Some(l) => { self.levels = l; LevelsSpace } None => stage, }, (SpaceCols, Input::Keyword(_)) => { self.corner = None; UnitCell } (ColsSpace, Input::Keyword(_)) => ColsTotal, (RowsSpace, Input::Keyword(_)) => RowsTotal, (LevelsSpace, Input::Keyword(_)) => LevelsTotal, (SpaceCols | ColsSpace, _) => match self.distance(&i, self.cs)? { Some(d) => { self.cs = d; if stage == SpaceCols { SpaceRows } else { Main } } None => stage, }, (SpaceRows | RowsSpace, _) => match self.distance(&i, self.rs)? { Some(d) => { self.rs = d; if stage == SpaceRows { Main } else { RowsElev } } None => stage, }, (LevelsSpace, _) => match self.distance(&i, self.ls)? { Some(d) => { self.ls = d; if !self.assoc && self.levels > 1 { s.echo(FLAT_LEVELS); } Main } None => stage, }, (ColsTotal, _) => match self.distance(&i, self.cs * self.cols.saturating_sub(1) as f64)? { Some(t) => { self.cs = t / self.cols.saturating_sub(1).max(1) as f64; Main } None => stage, }, (RowsTotal, _) => match self.distance(&i, self.rs * self.rows.saturating_sub(1) as f64)? { Some(t) => { self.rs = t / self.rows.saturating_sub(1).max(1) as f64; RowsElev } None => stage, }, (LevelsTotal, _) => match self.distance(&i, self.ls * self.levels.saturating_sub(1) as f64)? { Some(t) => { self.ls = t / self.levels.saturating_sub(1).max(1) as f64; Main } None => stage, }, (RowsElev, Input::Enter) => Main, (RowsElev, Input::Text(t)) => match number(t) { Some(z) if z.is_finite() => { self.elev = z; if !self.assoc && z.abs() > 1e-12 { s.echo("Row elevations need an associative array; the copies stay at the same elevation."); } Main } _ => return Err(other("Requires a number.")), }, (UnitCell, Input::Point(p)) => match self.corner { None => { self.corner = Some(*p); UnitCell } Some(c) => { let (dx, dy) = (p.x - c.x, p.y - c.y); if dx.abs() <= 1e-12 || dy.abs() <= 1e-12 { return Err(other("The unit cell must have a width and a height.")); } (self.cs, self.rs, self.corner) = (dx, dy, None); Main } }, (UnitCell, Input::Enter) => { self.corner = None; Main } _ => stage, }; self.stage = next; Ok(Step::Continue) } fn preview(&self, s: &Session, cursor: Vec2) -> Vec { if !self.sel.done { return Vec::new(); } let mut out = preview_copies(s, &self.objs, item_mats(&self.params(), self.base).into_iter()); match (self.stage, self.corner) { (RStage::UnitCell, Some(c)) => out.push(line(c, cursor)), ( RStage::SpaceCols | RStage::SpaceRows | RStage::ColsSpace | RStage::ColsTotal | RStage::RowsSpace | RStage::RowsTotal | RStage::LevelsSpace | RStage::LevelsTotal, _, ) => out.push(line(self.base, cursor)), _ => {} } out } } // ---------------- ARRAYPOLAR ---------------- #[derive(Clone, Copy, Debug, Default, PartialEq)] enum PStage { #[default] Center, /// Base point asked from the center prompt (returns there). CenterBase, Main, Assoc, Base, Items, Between, Fill, Rotate, /// ROWs: count, then spacing (or the total), then the elevation increment. RowsCount, RowsSpace, RowsTotal, RowsElev, /// Levels: count, then spacing (or the total). LevelsCount, LevelsSpace, LevelsTotal, } /// ARRAYPOLAR: select objects, specify the center, then edit items/angles/rows/levels in a loop; /// eXit creates the array. struct PolarM { sel: SelectPhase, objs: Vec, stage: PStage, assoc: bool, center: Vec2, items: u64, /// Fill angle in degrees (positive = counterclockwise). fill: f64, rotate: bool, rows: u64, /// Distance between rows (rings), the elevation added per row, and the distance between levels. rs: f64, elev: f64, levels: u64, ls: f64, /// Base point: the point non-rotated copies are moved by (default: the first object's centre). base: Option, } impl Default for PolarM { fn default() -> Self { PolarM { sel: SelectPhase::default(), objs: Vec::new(), stage: PStage::Center, assoc: true, center: Vec2::ZERO, items: 6, fill: 360.0, rotate: true, rows: 1, rs: 1.0, elev: 0.0, levels: 1, ls: 1.0, base: None, } } } impl PolarM { fn with_objects(objs: Vec) -> Self { PolarM { sel: SelectPhase { picked: objs.clone(), done: true, ..SelectPhase::default() }, objs, ..PolarM::default() } } fn ready(&mut self, s: &mut Session) -> Step { self.stage = PStage::Center; self.assoc = assoc_default(s); // Rows and levels default to 1.5 × the selection's size apart. if let Ok(d) = s.doc() { let b = bounds(d, &self.objs); let size = b.width().max(b.height()); if size.is_finite() && size > 1e-9 { (self.rs, self.ls) = (size * 1.5, size * 1.5); } } s.echo(format!("Type = Polar Associative = {}", yes(self.assoc))); Step::Continue } fn full(&self) -> bool { (self.fill.abs() - 360.0).abs() < 1e-9 } /// The angle between items, derived from the item count and the fill angle. fn between(&self) -> f64 { match self.items { 0 | 1 => self.fill, n if self.full() => self.fill / n as f64, n => self.fill / (n - 1) as f64, } } /// The base point: picked, else the first object's centre, else the centre of the array. fn base_point(&self, s: &Session, center: Vec2) -> Vec2 { self.base.or_else(|| s.doc().ok().and_then(|d| first_center(d, &self.objs))).unwrap_or(center) } fn params(&self, base: Vec2, center: Vec2) -> ArrayParams { let kind = ArrayKind::Polar(PolarArray { center: center - base, items: count32(self.items), fill: self.fill, rotate: self.rotate, rows: count32(self.rows), row_spacing: self.rs, row_elevation: self.elev, }); params(kind, if self.assoc { self.levels } else { 1 }, self.ls) } fn finish(&mut self, s: &mut Session) -> Result { let levels = if self.assoc { self.levels } else { 1 }; check_size(s, self.items.saturating_mul(self.rows).saturating_mul(levels), self.objs.len())?; if !self.assoc && self.levels > 1 { s.echo(FLAT_LEVELS); } let base = self.base_point(s, self.center); create(s, &self.objs.clone(), base, self.params(base, self.center), self.assoc)?; s.set_selection(Vec::new()); Ok(Step::Done) } fn main(&mut self, s: &mut Session, i: Input) -> Result { let k = match i { Input::Enter => return self.finish(s), Input::Keyword(k) => k, Input::Point(_) => return Err(other("Editing the array with grips is not available yet; choose an option.")), _ => return Err(other("Invalid option keyword.")), }; self.stage = match k.as_str() { "eXit" => return self.finish(s), "ASsociative" => PStage::Assoc, "Base point" => PStage::Base, "Items" => PStage::Items, "Angle between" => PStage::Between, "Fill angle" => PStage::Fill, "ROTate items" => PStage::Rotate, "ROWs" => PStage::RowsCount, "Levels" => PStage::LevelsCount, _ => return Err(other("Invalid option keyword.")), }; Ok(Step::Continue) } /// A distance typed or picked (from the centre); Enter keeps `cur`. fn distance(&self, i: &Input, cur: f64) -> Result> { let d = match i { Input::Enter => return Ok(Some(cur)), Input::Text(t) => number(t), Input::Point(p) => Some(self.center.dist(*p)), _ => return Ok(None), }; d.filter(|d| d.is_finite() && d.abs() > 1e-12).map(Some).ok_or_else(|| other("Requires a nonzero distance.")) } } impl Interactive for PolarM { fn name(&self) -> &'static str { "ARRAYPOLAR" } fn begin(&mut self, s: &mut Session) -> Result { self.sel = SelectPhase::begin(s); if self.sel.done { self.objs = self.sel.picked.clone(); return Ok(self.ready(s)); } Ok(Step::Continue) } fn prompt(&self, _s: &Session) -> Prompt { use PStage::*; if !self.sel.done { return self.sel.prompt(); } let dist = |m: &str, d: f64| Prompt::new(m, Accept::POINT_OR_NUMBER).default(fmt(d)).base(self.center); match self.stage { Center => Prompt::new("Specify center point of array", Accept::POINT).kw(&["Base point", "Axis of rotation"]), CenterBase | Base => Prompt::new("Specify base point", Accept::POINT).kw(&["Key point"]).default("centroid"), Main => Prompt::new("Select grip to edit array", Accept::POINT) .kw(&["ASsociative", "Base point", "Items", "Angle between", "Fill angle", "ROWs", "Levels", "ROTate items", "eXit"]) .default("eXit"), Assoc => assoc_prompt(self.assoc), Items => Prompt::new("Enter number of items in array", Accept::NUMBER).default(self.items.to_string()), Between => Prompt::new("Specify the angle between items", Accept::NUMBER).default(fmt(self.between())), Fill => Prompt::new("Specify the angle to fill (+=ccw, -=cw)", Accept::NUMBER).default(fmt(self.fill)), Rotate => Prompt::new("Rotate arrayed items?", KEYWORDS).kw(&["Yes", "No"]).default(yes(self.rotate)), RowsCount => Prompt::new("Enter the number of rows", Accept::NUMBER).default(self.rows.to_string()), RowsSpace => dist("Specify the distance between rows", self.rs).kw(&["Total"]), RowsTotal => dist("Specify the distance between the first and last row", self.rs * self.rows.saturating_sub(1) as f64), RowsElev => Prompt::new("Specify the incrementing elevation between rows", Accept::NUMBER).default(fmt(self.elev)), LevelsCount => Prompt::new("Enter the number of levels", Accept::NUMBER).default(self.levels.to_string()), LevelsSpace => dist("Specify the distance between levels", self.ls).kw(&["Total"]), LevelsTotal => dist("Specify the distance between the first and last level", self.ls * self.levels.saturating_sub(1) as f64), } } fn input(&mut self, s: &mut Session, i: Input) -> Result { use PStage::*; if !self.sel.done { return match self.sel.feed(s, &i)? { SelOutcome::More => Ok(Step::Continue), SelOutcome::Empty => Ok(Step::Done), SelOutcome::Done(hs) => { self.objs = hs; Ok(self.ready(s)) } }; } let stage = self.stage; let n = self.objs.len(); let next = match (stage, &i) { (Main, _) => return self.main(s, i), (Assoc, _) => match yes_no(&i, self.assoc) { Some(a) => { self.assoc = a; Main } None => return Err(other("Requires Yes or No.")), }, (Center, Input::Point(p)) => { self.center = *p; Main } (Center, Input::Keyword(k)) if k == "Base point" => CenterBase, (Center, Input::Keyword(_)) => { s.echo("A rotation axis (3D) is not available yet; specify the center point."); Center } (Center, _) => return Err(other("Requires a point or an option keyword.")), (CenterBase | Base, Input::Point(p)) => { self.base = Some(*p); if stage == CenterBase { Center } else { Main } } (CenterBase | Base, Input::Enter) => { self.base = None; if stage == CenterBase { Center } else { Main } } (CenterBase | Base, Input::Keyword(_)) => { s.echo("Key points are not available yet; specify a point."); stage } (Items, _) => match count(&i, self.items, max_count(s, self.rows.saturating_mul(self.levels), n))? { Some(c) => { self.items = c; Main } None => stage, }, (RowsCount, _) => match count(&i, self.rows, max_count(s, self.items.saturating_mul(self.levels), n))? { Some(c) => { self.rows = c; RowsSpace } None => stage, }, (LevelsCount, _) => match count(&i, self.levels, max_count(s, self.items.saturating_mul(self.rows), n))? { Some(c) => { self.levels = c; LevelsSpace } None => stage, }, (RowsSpace, Input::Keyword(_)) => RowsTotal, (LevelsSpace, Input::Keyword(_)) => LevelsTotal, (RowsSpace, _) => match self.distance(&i, self.rs)? { Some(d) => { self.rs = d; RowsElev } None => stage, }, (RowsTotal, _) => match self.distance(&i, self.rs * self.rows.saturating_sub(1) as f64)? { Some(t) => { self.rs = t / self.rows.saturating_sub(1).max(1) as f64; RowsElev } None => stage, }, (RowsElev, Input::Enter) => Main, (RowsElev, Input::Text(t)) => match number(t) { Some(z) if z.is_finite() => { self.elev = z; if !self.assoc && z.abs() > 1e-12 { s.echo("Row elevations need an associative array; the copies stay at the same elevation."); } Main } _ => return Err(other("Requires a number.")), }, (LevelsSpace, _) => match self.distance(&i, self.ls)? { Some(d) => { self.ls = d; if !self.assoc && self.levels > 1 { s.echo(FLAT_LEVELS); } Main } None => stage, }, (LevelsTotal, _) => match self.distance(&i, self.ls * self.levels.saturating_sub(1) as f64)? { Some(t) => { self.ls = t / self.levels.saturating_sub(1).max(1) as f64; Main } None => stage, }, (Between, Input::Enter) => Main, (Between, _) => match angle(&i, self.between(), &s.angle_settings())? { Some(b) => { if b.abs() < 1e-9 { return Err(other("Requires a nonzero angle.")); } let n = self.items as f64; let fill = if (b.abs() * n - 360.0).abs() < 1e-9 { 360.0 * b.signum() } else { b * (n - 1.0).max(1.0) }; if fill.abs() > 360.0 + 1e-9 { return Err(other("The items would span more than 360 degrees; enter a smaller angle or fewer items.")); } self.fill = fill; Main } None => stage, }, (Fill, _) => match angle(&i, self.fill, &s.angle_settings())? { Some(f) => { if f.abs() < 1e-9 || f.abs() > 360.0 + 1e-9 { return Err(other("Requires a fill angle between -360 and 360 degrees (not 0).")); } self.fill = f.clamp(-360.0, 360.0); Main } None => stage, }, (Rotate, _) => match yes_no(&i, self.rotate) { Some(r) => { self.rotate = r; Main } None => return Err(other("Requires Yes or No.")), }, _ => stage, }; self.stage = next; Ok(Step::Continue) } fn preview(&self, s: &Session, cursor: Vec2) -> Vec { if !self.sel.done { return Vec::new(); } let center = match self.stage { PStage::Center | PStage::CenterBase => cursor, _ => self.center, }; let base = self.base_point(s, center); preview_copies(s, &self.objs, item_mats(&self.params(base, center), base).into_iter()) } } #[cfg(test)] mod tests { use super::*; fn polar(n: u32, fill: f64) -> ArrayParams { let kind = ArrayKind::Polar(PolarArray { center: -Vec2::X, items: n, fill, rotate: true, rows: 1, row_spacing: 0.0, row_elevation: 0.0 }); params(kind, 1, 0.0) } #[test] fn polar_angles() { // Base point (1,0) around the origin. Full circle: 4 items 90° apart; partial: the last // item sits at the fill angle. let at = |p: &ArrayParams| -> Vec { item_mats(p, Vec2::X).iter().map(|m| m.apply(Vec2::X)).collect() }; let a = at(&polar(4, 360.0)); assert_eq!(a.len(), 3); assert!(a[0].near(Vec2::new(0.0, 1.0), 1e-9) && a[2].near(Vec2::new(0.0, -1.0), 1e-9)); let b = at(&polar(3, -180.0)); assert!(b[0].near(Vec2::new(0.0, -1.0), 1e-9) && b[1].near(Vec2::new(-1.0, 0.0), 1e-9)); // A full clockwise circle does not put the last item on top of the source. let c = at(&polar(4, -360.0)); assert!(c[2].near(Vec2::new(0.0, 1.0), 1e-9)); } }