Squash of PR #460 by @vangel76. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
61a8a5f984
commit
d42313cde2
|
|
@ -37,6 +37,23 @@ pub struct ArrayParams {
|
|||
/// Items that refer to another block than the source (ARRAYEDIT REPlace).
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub replaced: Vec<ReplacedItem>,
|
||||
/// The source objects are out for editing (ARRAYEDIT Source, until ARRAYCLOSE). Not saved:
|
||||
/// a drawing saved meanwhile keeps the objects as separate objects.
|
||||
#[serde(skip)]
|
||||
pub editing: Option<SourceEdit>,
|
||||
}
|
||||
|
||||
/// ARRAYEDIT Source in progress: the array's source objects placed in the drawing for editing.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct SourceEdit {
|
||||
/// The array (block reference) being edited.
|
||||
pub array: crate::Handle,
|
||||
/// The item the objects were placed at: (i, j, level).
|
||||
pub item: (u32, u32, u32),
|
||||
/// The objects placed for editing.
|
||||
pub objects: Vec<crate::Handle>,
|
||||
/// Objects with this handle or above were drawn while editing; they join the source too.
|
||||
pub first_new: u64,
|
||||
}
|
||||
|
||||
/// Rectangular or polar placement.
|
||||
|
|
@ -272,6 +289,7 @@ mod tests {
|
|||
levels: 2,
|
||||
level_spacing: 5.0,
|
||||
replaced: Vec::new(),
|
||||
editing: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -299,6 +317,7 @@ mod tests {
|
|||
levels: 1,
|
||||
level_spacing: 0.0,
|
||||
replaced: Vec::new(),
|
||||
editing: None,
|
||||
};
|
||||
let items = p.items().unwrap_or_default();
|
||||
assert_eq!(items.len(), 8);
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ 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).
|
||||
|
|
@ -46,13 +48,16 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
)
|
||||
.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() }
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,930 @@
|
|||
//! ARRAYEDIT and ARRAYCLOSE: change an associative array's parameters, move its base point,
|
||||
//! replace items with other objects (REPlace) and undo that (RESet), and edit the source objects
|
||||
//! (Source places them in the drawing; ARRAYCLOSE saves or discards the changes).
|
||||
//!
|
||||
//! Every change builds a new array block for the edited block reference, so copies of the array
|
||||
//! that shared the old block keep it; blocks nothing refers to any more are removed.
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::Arc;
|
||||
|
||||
use cadcraft_doc::{
|
||||
ArrayKind, ArrayParams, Block, Drawing, Entity, EntityKind, Handle, Insert, MAX_BLOCK_DEPTH, ReplacedItem, SourceEdit, entity_bounds,
|
||||
};
|
||||
use cadcraft_geom::{Bounds2, Mat3, Vec2};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::super::helpers::v3;
|
||||
use super::super::machines::{SelOutcome, SelectPhase, number};
|
||||
use super::super::*;
|
||||
use super::{KEYWORDS, angle, check_size, count, fmt, nesting, other, yes, yes_no};
|
||||
use crate::{Accept, Input, Interactive, Prompt, Result, Session, Step};
|
||||
|
||||
pub(super) fn specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
CommandSpec::new("arrayedit", "Edit Array", run_arrayedit)
|
||||
.menu(&["Modify", "Object", "Array"])
|
||||
.params(
|
||||
"{handle, cols?, rows?, colSpacing?, rowSpacing?, axisAngle? (degrees), rowElevation?, levels?, levelSpacing?, items?, angle? (fill, degrees), angleBetween?, rotate?: bool, center? ([x, y]), base? ([x, y], the new base point), reset?: bool, replace?: {handles, base? ([x, y], default the objects' centre), items: [[i, j, level]…] | \"source\"}, source?: {item? ([i, j, level], default [0, 0, 0])} (places the source objects for editing; ARRAYCLOSE ends)}",
|
||||
)
|
||||
.interactive(|_| Ok(Box::new(EditM::default()))),
|
||||
CommandSpec::new("arrayclose", "Close Array Editing", run_arrayclose)
|
||||
.params("{save?: bool (default true)}")
|
||||
.interactive(|_| Ok(Box::new(CloseM))),
|
||||
]
|
||||
}
|
||||
|
||||
// ---------------- the array ----------------
|
||||
|
||||
/// The array block reference `h`, its array block and parameters (with the block names its items
|
||||
/// use).
|
||||
fn array_of(d: &Drawing, h: Handle) -> Result<(Insert, Arc<Block>, ArrayParams)> {
|
||||
let e = d.entity(h).ok_or_else(|| other("No such object."))?;
|
||||
let p = d.array_params(e).ok_or_else(|| other("The object is not an associative array."))?;
|
||||
let EntityKind::Insert(ins) = &e.kind else { return Err(other("The object is not an associative array.")) };
|
||||
let blk = d.block(&ins.block).cloned().ok_or_else(|| other("The array's block is missing."))?;
|
||||
Ok((ins.clone(), blk, p))
|
||||
}
|
||||
|
||||
/// The open ARRAYEDIT Source, if any: the array block holding it and the state.
|
||||
fn open_edit(d: &Drawing) -> Option<(String, SourceEdit)> {
|
||||
d.blocks.iter().find_map(|(n, b)| b.array.as_ref().and_then(|a| a.editing.clone()).map(|e| (n.clone(), e)))
|
||||
}
|
||||
|
||||
fn refuse_while_editing(s: &Session) -> Result<()> {
|
||||
if open_edit(s.doc()?).is_some() {
|
||||
return Err(other("An array's source objects are being edited; enter ARRAYCLOSE first."));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Where item (i, j, level) goes in array block coordinates: the image of the block's origin
|
||||
/// and the item's rotation (a block's own base point is applied on top).
|
||||
fn item_place(p: &ArrayParams, item: (u32, u32, u32)) -> Result<Mat3> {
|
||||
let it =
|
||||
p.items().unwrap_or_default().into_iter().find(|it| (it.i, it.j, it.level) == item).ok_or_else(|| other("The array has no such item."))?;
|
||||
Ok(Mat3::translate(it.insert.xy()).then_before(Mat3::rotate(it.rotation)))
|
||||
}
|
||||
|
||||
/// Whether `e` refers to one of `names`, through nested block references.
|
||||
fn refers_to(d: &Drawing, e: &Entity, names: &[&str]) -> bool {
|
||||
let mut todo: Vec<(String, usize)> = match &e.kind {
|
||||
EntityKind::Insert(i) => vec![(i.block.clone(), 0)],
|
||||
_ => return false,
|
||||
};
|
||||
let mut seen = HashSet::new();
|
||||
while let Some((b, depth)) = todo.pop() {
|
||||
if names.iter().any(|n| n.eq_ignore_ascii_case(&b)) {
|
||||
return true;
|
||||
}
|
||||
if depth > MAX_BLOCK_DEPTH || !seen.insert(b.to_ascii_uppercase()) {
|
||||
continue;
|
||||
}
|
||||
if let Some(blk) = d.block(&b) {
|
||||
todo.extend(blk.entities.iter().filter_map(|e| match &e.kind {
|
||||
EntityKind::Insert(i) => Some((i.block.clone(), depth + 1)),
|
||||
_ => None,
|
||||
}));
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Remove the anonymous blocks among `names` that nothing refers to any more (in order, so a
|
||||
/// source block goes after the array block that used it).
|
||||
fn drop_unused(d: &mut Drawing, names: &[String]) {
|
||||
for n in names {
|
||||
if !n.starts_with('*') || d.block(n).is_none() {
|
||||
continue;
|
||||
}
|
||||
let used = |e: &Arc<Entity>| match &e.kind {
|
||||
EntityKind::Insert(i) => i.block.eq_ignore_ascii_case(n),
|
||||
EntityKind::MLeader(m) => m.block.as_ref().is_some_and(|b| b.block.eq_ignore_ascii_case(n)),
|
||||
_ => false,
|
||||
};
|
||||
let in_spaces = d.model.iter().any(used) || d.layouts.iter().any(|l| l.entities.iter().any(used));
|
||||
let in_blocks = d.blocks.values().any(|b| b.entities.iter().any(used) || b.array.as_ref().is_some_and(|a| a.source.eq_ignore_ascii_case(n)));
|
||||
if !in_spaces && !in_blocks {
|
||||
d.blocks.remove(n);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Rebuild array `h` with parameters `p` (and its block reference moved to `insert` when
|
||||
/// given): a new array block for this reference, old blocks removed when unused.
|
||||
fn rebuild(s: &mut Session, h: Handle, p: ArrayParams, insert: Option<Vec2>) -> Result<()> {
|
||||
if !p.is_valid() {
|
||||
return Err(other("The array parameters are not valid."));
|
||||
}
|
||||
let d = s.doc()?;
|
||||
let (old_ins, _, old) = array_of(d, h)?;
|
||||
let objs = d.block(&p.source).map(|b| b.entities.len()).ok_or_else(|| other("The array's source block is missing."))?;
|
||||
check_size(s, p.item_count(), objs)?;
|
||||
let d = s.doc_mut()?;
|
||||
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));
|
||||
d.modify_entity(h, |e| {
|
||||
if let EntityKind::Insert(i) = &mut e.kind {
|
||||
i.block.clone_from(&name);
|
||||
if let Some(at) = insert {
|
||||
i.insert = at.to3(i.insert.z);
|
||||
}
|
||||
}
|
||||
})
|
||||
.map_err(|e| other(e.to_string()))?;
|
||||
let mut gone = vec![old_ins.block, old.source];
|
||||
gone.extend(old.replaced.into_iter().map(|r| r.block));
|
||||
drop_unused(d, &gone);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A new anonymous block from objects `hs` in array `h`'s block coordinates, with its base point
|
||||
/// at world point `base`. The objects stay in the drawing.
|
||||
fn block_from(s: &mut Session, h: Handle, hs: &[Handle], base: Vec2) -> Result<String> {
|
||||
let d = s.doc()?;
|
||||
let (ins, blk, p) = array_of(d, h)?;
|
||||
let inv = ins.transform(blk.base.xy()).inverse().ok_or_else(|| other("The array's scale is zero."))?;
|
||||
let ents: Vec<Entity> = hs.iter().filter(|x| **x != h).filter_map(|x| d.entity(*x).map(|e| (**e).clone())).collect();
|
||||
if ents.is_empty() {
|
||||
return Err(other("Select objects other than the array."));
|
||||
}
|
||||
if ents.iter().any(|e| refers_to(d, e, &[&ins.block, &p.source])) {
|
||||
return Err(other("An array's items can't contain the array itself."));
|
||||
}
|
||||
let refs: Vec<&Entity> = ents.iter().collect();
|
||||
if nesting(d, &refs, &mut Default::default(), 0) + 2 > MAX_BLOCK_DEPTH {
|
||||
return Err(other(format!("The objects nest block references too deeply (more than {MAX_BLOCK_DEPTH} levels).")));
|
||||
}
|
||||
let d = s.doc_mut()?;
|
||||
let name = d.anonymous_block_name("*U");
|
||||
let mut b = Block::new(&name);
|
||||
b.base = v3(inv.apply(base));
|
||||
for mut e in ents {
|
||||
e.handle = d.new_handle();
|
||||
e.kind.transform(&inv);
|
||||
b.entities.push(e);
|
||||
}
|
||||
d.blocks.insert(name.clone(), Arc::new(b));
|
||||
Ok(name)
|
||||
}
|
||||
|
||||
/// The centre of objects `hs` (world).
|
||||
fn centroid(d: &Drawing, hs: &[Handle]) -> Vec2 {
|
||||
let b = hs.iter().filter_map(|h| d.entity(*h)).fold(Bounds2::EMPTY, |a, e| a.union(&entity_bounds(d, e, 0)));
|
||||
Some(b.center()).filter(|c| c.is_finite()).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// The item of array `h` under world point `at` (the lowest level when levels overlap).
|
||||
fn item_at(d: &Drawing, h: Handle, at: Vec2, tol: f64) -> Option<(u32, u32, u32)> {
|
||||
let (ins, blk, p) = array_of(d, h).ok()?;
|
||||
let w = ins.transform(blk.base.xy());
|
||||
let mut best: Option<((u32, u32, u32), f64)> = None;
|
||||
// Each block's own extents, measured once.
|
||||
let mut extents: HashMap<String, (Vec2, Bounds2)> = HashMap::new();
|
||||
for it in p.items()? {
|
||||
let name = p.block_for(it.i, it.j, it.level);
|
||||
let (base, local) = match extents.get(name) {
|
||||
Some(x) => *x,
|
||||
None => {
|
||||
let b = d.block(name)?;
|
||||
let x = (b.base.xy(), b.entities.iter().fold(Bounds2::EMPTY, |a, e| a.union(&entity_bounds(d, e, 1))));
|
||||
extents.insert(name.to_string(), x);
|
||||
x
|
||||
}
|
||||
};
|
||||
let m = w.then_before(Mat3::translate(it.insert.xy())).then_before(Mat3::rotate(it.rotation)).then_before(Mat3::translate(-base));
|
||||
if local.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let corners = [local.min, Vec2::new(local.max.x, local.min.y), local.max, Vec2::new(local.min.x, local.max.y)];
|
||||
let wb = Bounds2::from_points(corners.iter().map(|c| m.apply(*c)));
|
||||
let inside = at.x >= wb.min.x - tol && at.x <= wb.max.x + tol && at.y >= wb.min.y - tol && at.y <= wb.max.y + tol;
|
||||
let dist = wb.center().dist(at);
|
||||
if inside && best.is_none_or(|(_, bd)| dist < bd - 1e-12) {
|
||||
best = Some(((it.i, it.j, it.level), dist));
|
||||
}
|
||||
}
|
||||
best.map(|b| b.0)
|
||||
}
|
||||
|
||||
/// Move the base point of array `h` to world point `base` without moving the items.
|
||||
fn rebase(s: &mut Session, h: Handle, base: Vec2) -> Result<()> {
|
||||
let d = s.doc()?;
|
||||
let (ins, blk, mut p) = array_of(d, h)?;
|
||||
let w = ins.transform(blk.base.xy());
|
||||
let b = w.inverse().ok_or_else(|| other("The array's scale is zero."))?.apply(base);
|
||||
if !b.is_finite() {
|
||||
return Err(other("Requires a valid base point."));
|
||||
}
|
||||
// The source block keeps its objects; its base point moves with the array's.
|
||||
let src = d.block(&p.source).ok_or_else(|| other("The array's source block is missing."))?;
|
||||
let mut ns = (**src).clone();
|
||||
ns.base = (ns.base.xy() + b).to3(ns.base.z);
|
||||
let d = s.doc_mut()?;
|
||||
ns.name = d.anonymous_block_name("*U");
|
||||
p.source.clone_from(&ns.name);
|
||||
d.blocks.insert(ns.name.clone(), Arc::new(ns));
|
||||
if let ArrayKind::Polar(k) = &mut p.kind {
|
||||
k.center -= b;
|
||||
}
|
||||
rebuild(s, h, p, Some(w.apply(b)))
|
||||
}
|
||||
|
||||
/// ARRAYEDIT Source: place the source objects of array `h` at `item` for editing. Returns them.
|
||||
fn open_source(s: &mut Session, h: Handle, item: (u32, u32, u32)) -> Result<Vec<Handle>> {
|
||||
refuse_while_editing(s)?;
|
||||
let d = s.doc()?;
|
||||
let (ins, blk, p) = array_of(d, h)?;
|
||||
let src = d.block(&p.source).cloned().ok_or_else(|| other("The array's source block is missing."))?;
|
||||
let m = ins.transform(blk.base.xy()).then_before(item_place(&p, item)?).then_before(Mat3::translate(-src.base.xy()));
|
||||
let space = d.space_of(h).unwrap_or_default();
|
||||
let d = s.doc_mut()?;
|
||||
let mut out = Vec::new();
|
||||
for e in src.entities.iter() {
|
||||
let mut ne = (**e).clone();
|
||||
ne.handle = d.new_handle();
|
||||
ne.kind.transform(&m);
|
||||
out.push(ne.handle);
|
||||
d.ensure_layer(&ne.common.layer);
|
||||
if let Some(st) = d.space_mut(&space) {
|
||||
st.push(ne);
|
||||
}
|
||||
}
|
||||
let first_new = d.handseed;
|
||||
let mut b = (*blk).clone();
|
||||
if let Some(a) = &mut b.array {
|
||||
a.editing = Some(SourceEdit { array: h, item, objects: out.clone(), first_new });
|
||||
}
|
||||
d.blocks.insert(b.name.clone(), Arc::new(b));
|
||||
s.set_selection(out.clone());
|
||||
s.echo("Edit the source objects, then enter ARRAYCLOSE to save or discard the changes.");
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// ARRAYCLOSE: end the open Source editing, saving the objects as the array's new source or
|
||||
/// discarding them.
|
||||
fn close_source(s: &mut Session, save: bool) -> Result<Value> {
|
||||
let d = s.doc()?;
|
||||
let (blk_name, ed) = open_edit(d).ok_or_else(|| other("No array is being edited."))?;
|
||||
let space = d.space_of(ed.array).unwrap_or_default();
|
||||
let names: Vec<String> = {
|
||||
let b = d.block(&blk_name);
|
||||
[Some(blk_name.clone()), b.and_then(|b| b.array.as_ref()).map(|a| a.source.clone())].into_iter().flatten().collect()
|
||||
};
|
||||
let refs: Vec<&str> = names.iter().map(String::as_str).collect();
|
||||
let mut objs: Vec<Handle> = ed.objects.iter().copied().filter(|x| d.entity(*x).is_some()).collect();
|
||||
if let Some(st) = d.space(&space) {
|
||||
let fresh: Vec<Handle> = st
|
||||
.iter()
|
||||
.filter(|e| e.handle.0 >= ed.first_new && e.handle != ed.array && !objs.contains(&e.handle) && !refers_to(d, e, &refs))
|
||||
.map(|e| e.handle)
|
||||
.collect();
|
||||
objs.extend(fresh);
|
||||
}
|
||||
// The array still refers to the block being edited (else it was erased or changed).
|
||||
let live = d.entity(ed.array).is_some_and(|e| matches!(&e.kind, EntityKind::Insert(i) if i.block.eq_ignore_ascii_case(&blk_name)));
|
||||
if save && live && objs.is_empty() {
|
||||
return Err(other("The source would have no objects; enter ARRAYCLOSE No to discard the changes, or erase the array."));
|
||||
}
|
||||
let d = s.doc_mut()?;
|
||||
if let Some(b) = d.blocks.get_mut(&blk_name) {
|
||||
let b = Arc::make_mut(b);
|
||||
if let Some(a) = &mut b.array {
|
||||
a.editing = None;
|
||||
}
|
||||
}
|
||||
if !save {
|
||||
for h in &objs {
|
||||
d.remove_entity(*h);
|
||||
}
|
||||
s.set_selection(Vec::new());
|
||||
s.echo("The changes to the array were discarded.");
|
||||
return Ok(json!({ "saved": false }));
|
||||
}
|
||||
if !live {
|
||||
s.echo("The array no longer exists; the edited objects stay in the drawing.");
|
||||
return Ok(json!({ "saved": false }));
|
||||
}
|
||||
// The new source block (base point at its origin) holds the objects as seen from the item
|
||||
// they were placed at.
|
||||
let d = s.doc()?;
|
||||
let (ins, blk, mut p) = array_of(d, ed.array)?;
|
||||
let inv = ins.transform(blk.base.xy()).then_before(item_place(&p, ed.item)?).inverse().ok_or_else(|| other("The array's scale is zero."))?;
|
||||
let ents: Vec<Entity> = objs.iter().filter_map(|x| d.entity(*x).map(|e| (**e).clone())).collect();
|
||||
let er: Vec<&Entity> = ents.iter().collect();
|
||||
if nesting(d, &er, &mut Default::default(), 0) + 2 > MAX_BLOCK_DEPTH {
|
||||
return Err(other(format!("The objects nest block references too deeply (more than {MAX_BLOCK_DEPTH} levels).")));
|
||||
}
|
||||
let d = s.doc_mut()?;
|
||||
let name = d.anonymous_block_name("*U");
|
||||
let mut nb = Block::new(&name);
|
||||
for mut e in ents {
|
||||
e.handle = d.new_handle();
|
||||
e.kind.transform(&inv);
|
||||
nb.entities.push(e);
|
||||
}
|
||||
d.blocks.insert(name.clone(), Arc::new(nb));
|
||||
for h in &objs {
|
||||
d.remove_entity(*h);
|
||||
}
|
||||
p.source = name;
|
||||
rebuild(s, ed.array, p, None)?;
|
||||
s.set_selection(Vec::new());
|
||||
Ok(json!({ "saved": true }))
|
||||
}
|
||||
|
||||
// ---------------- JSON ----------------
|
||||
|
||||
fn item_param(v: &Value) -> Option<(u32, u32, u32)> {
|
||||
let a = v.as_array()?;
|
||||
let n = |k: usize| a.get(k).and_then(Value::as_u64).and_then(|x| u32::try_from(x).ok());
|
||||
Some((n(0)?, n(1).unwrap_or(0), n(2).unwrap_or(0)))
|
||||
}
|
||||
|
||||
fn count_param(p: &Value, key: &str) -> Result<Option<u32>> {
|
||||
match p.get(key) {
|
||||
None | Some(Value::Null) => Ok(None),
|
||||
Some(v) => v
|
||||
.as_u64()
|
||||
.filter(|n| *n >= 1)
|
||||
.and_then(|n| u32::try_from(n).ok())
|
||||
.map(Some)
|
||||
.ok_or_else(|| bad("arrayedit", format!("`{key}` must be a whole number of at least 1"))),
|
||||
}
|
||||
}
|
||||
|
||||
fn num_param(p: &Value, key: &str) -> Result<Option<f64>> {
|
||||
match p.get(key) {
|
||||
None | Some(Value::Null) => Ok(None),
|
||||
Some(v) => v.as_f64().filter(|x| x.is_finite()).map(Some).ok_or_else(|| bad("arrayedit", format!("`{key}` must be a number"))),
|
||||
}
|
||||
}
|
||||
|
||||
/// The parameter changes of the JSON form applied to `a`.
|
||||
fn apply_params(a: &mut ArrayParams, p: &Value, to_local: &Mat3) -> Result<()> {
|
||||
if let Some(n) = count_param(p, "levels")? {
|
||||
a.levels = n;
|
||||
}
|
||||
if let Some(x) = num_param(p, "levelSpacing")? {
|
||||
a.level_spacing = x;
|
||||
}
|
||||
match &mut a.kind {
|
||||
ArrayKind::Rect(r) => {
|
||||
if let Some(n) = count_param(p, "cols")? {
|
||||
r.cols = n;
|
||||
}
|
||||
if let Some(n) = count_param(p, "rows")? {
|
||||
r.rows = n;
|
||||
}
|
||||
if let Some(x) = num_param(p, "colSpacing")? {
|
||||
r.col_spacing = x;
|
||||
}
|
||||
if let Some(x) = num_param(p, "rowSpacing")? {
|
||||
r.row_spacing = x;
|
||||
}
|
||||
if let Some(x) = num_param(p, "axisAngle")? {
|
||||
r.axis_angle = x.to_radians();
|
||||
}
|
||||
if let Some(x) = num_param(p, "rowElevation")? {
|
||||
r.row_elevation = x;
|
||||
}
|
||||
}
|
||||
ArrayKind::Polar(k) => {
|
||||
if let Some(n) = count_param(p, "items")? {
|
||||
k.items = n;
|
||||
}
|
||||
if let Some(n) = count_param(p, "rows")? {
|
||||
k.rows = n;
|
||||
}
|
||||
if let Some(x) = num_param(p, "rowSpacing")? {
|
||||
k.row_spacing = x;
|
||||
}
|
||||
if let Some(x) = num_param(p, "rowElevation")? {
|
||||
k.row_elevation = x;
|
||||
}
|
||||
if let Some(f) = num_param(p, "angle")? {
|
||||
k.fill = fill_angle(f)?;
|
||||
}
|
||||
if let Some(b) = num_param(p, "angleBetween")? {
|
||||
k.fill = fill_from_between(b, k.items)?;
|
||||
}
|
||||
if let Some(r) = p.get("rotate").and_then(Value::as_bool) {
|
||||
k.rotate = r;
|
||||
}
|
||||
if let Some(c) = point_param(p, "center") {
|
||||
k.center = to_local.apply(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn fill_angle(f: f64) -> Result<f64> {
|
||||
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)."));
|
||||
}
|
||||
Ok(f.clamp(-360.0, 360.0))
|
||||
}
|
||||
|
||||
fn fill_from_between(b: f64, items: u32) -> Result<f64> {
|
||||
if b.abs() < 1e-9 {
|
||||
return Err(other("Requires a nonzero angle."));
|
||||
}
|
||||
let n = f64::from(items);
|
||||
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."));
|
||||
}
|
||||
Ok(fill)
|
||||
}
|
||||
|
||||
fn run_arrayedit(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let h = super::super::curves::handle_param(p, "handle").ok_or_else(|| bad("arrayedit", "`handle` (the array) is required"))?;
|
||||
refuse_while_editing(s)?;
|
||||
let (ins, blk, _) = array_of(s.doc()?, h)?;
|
||||
let w = ins.transform(blk.base.xy());
|
||||
let to_local = w.inverse().ok_or_else(|| other("The array's scale is zero."))?;
|
||||
if bool_or(p, "reset", false) {
|
||||
let (_, _, mut a) = array_of(s.doc()?, h)?;
|
||||
a.replaced.clear();
|
||||
rebuild(s, h, a, None)?;
|
||||
}
|
||||
if let Some(r) = p.get("replace").filter(|v| !v.is_null()) {
|
||||
let hs = targets(s, r)?;
|
||||
let base = point_param(r, "base").unwrap_or_else(|| s.doc().map(|d| centroid(d, &hs)).unwrap_or_default());
|
||||
let name = block_from(s, h, &hs, base)?;
|
||||
let (_, _, mut a) = array_of(s.doc()?, h)?;
|
||||
match r.get("items") {
|
||||
Some(Value::String(t)) if t.eq_ignore_ascii_case("source") => a.source = name,
|
||||
Some(Value::Array(list)) => {
|
||||
for v in list.iter().take(cadcraft_doc::MAX_ARRAY_ITEMS as usize) {
|
||||
let (i, j, level) = item_param(v).ok_or_else(|| bad("arrayedit", "`replace.items` holds [i, j, level] item numbers"))?;
|
||||
a.replaced.push(ReplacedItem { i, j, level, block: name.clone() });
|
||||
}
|
||||
}
|
||||
_ => return Err(bad("arrayedit", "`replace.items` must be a list of [i, j, level] or \"source\"")),
|
||||
}
|
||||
rebuild(s, h, a, None)?;
|
||||
}
|
||||
if let Some(b) = point_param(p, "base") {
|
||||
rebase(s, h, b)?;
|
||||
}
|
||||
let (_, _, mut a) = array_of(s.doc()?, h)?;
|
||||
let before = a.clone();
|
||||
apply_params(&mut a, p, &to_local)?;
|
||||
if a != before {
|
||||
rebuild(s, h, a, None)?;
|
||||
}
|
||||
if let Some(src) = p.get("source").filter(|v| !v.is_null()) {
|
||||
let item = src
|
||||
.get("item")
|
||||
.map(|v| item_param(v).ok_or_else(|| bad("arrayedit", "`source.item` is [i, j, level]")))
|
||||
.transpose()?
|
||||
.unwrap_or((0, 0, 0));
|
||||
let objs = open_source(s, h, item)?;
|
||||
return Ok(json!({ "handle": h.hex(), "source": objs.iter().map(|x| x.hex()).collect::<Vec<_>>() }));
|
||||
}
|
||||
Ok(json!({ "handle": h.hex() }))
|
||||
}
|
||||
|
||||
fn run_arrayclose(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
close_source(s, bool_or(p, "save", true))
|
||||
}
|
||||
|
||||
// ---------------- interactive ----------------
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
enum Stage {
|
||||
#[default]
|
||||
Select,
|
||||
Main,
|
||||
Base,
|
||||
ColsCount,
|
||||
ColsSpace,
|
||||
ColsTotal,
|
||||
RowsCount,
|
||||
RowsSpace,
|
||||
RowsTotal,
|
||||
RowsElev,
|
||||
LevelsCount,
|
||||
LevelsSpace,
|
||||
LevelsTotal,
|
||||
Items,
|
||||
Between,
|
||||
Fill,
|
||||
Rotate,
|
||||
/// REPlace: objects, their base point, then the items to replace.
|
||||
RepSelect,
|
||||
RepBase,
|
||||
RepItems,
|
||||
/// Source: the item whose objects are placed for editing.
|
||||
SourceItem,
|
||||
}
|
||||
|
||||
/// ARRAYEDIT: select an array, then change it option by option until eXit.
|
||||
#[derive(Default)]
|
||||
struct EditM {
|
||||
stage: Stage,
|
||||
array: Option<Handle>,
|
||||
sel: SelectPhase,
|
||||
rep_objs: Vec<Handle>,
|
||||
rep_block: Option<String>,
|
||||
}
|
||||
|
||||
impl EditM {
|
||||
/// The array's parameters as stored (cheap: the prompt asks for them on every redraw).
|
||||
fn params(&self, s: &Session) -> Option<ArrayParams> {
|
||||
let d = s.doc().ok()?;
|
||||
match &d.entity(self.array?)?.kind {
|
||||
EntityKind::Insert(i) => d.block(&i.block)?.array.clone(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply `f` to the array's parameters and rebuild it.
|
||||
fn change(&self, s: &mut Session, f: impl FnOnce(&mut ArrayParams) -> Result<()>) -> Result<()> {
|
||||
let h = self.array.ok_or_else(|| other("No array selected."))?;
|
||||
let (_, _, mut p) = array_of(s.doc()?, h)?;
|
||||
f(&mut p)?;
|
||||
rebuild(s, h, p, None)
|
||||
}
|
||||
|
||||
fn objs(&self, s: &Session) -> usize {
|
||||
self.params(s).and_then(|p| s.doc().ok().and_then(|d| d.block(&p.source).map(|b| b.entities.len()))).unwrap_or(1)
|
||||
}
|
||||
|
||||
/// The largest count for one direction when the others hold `others` items.
|
||||
fn max_for(&self, s: &Session, others: u64) -> u64 {
|
||||
super::max_count(s, others, self.objs(s))
|
||||
}
|
||||
|
||||
/// A distance in the array's own units: typed, or picked as the distance from the base point.
|
||||
fn distance(&self, s: &Session, i: &Input, cur: f64) -> Result<Option<f64>> {
|
||||
let d = match i {
|
||||
Input::Enter => return Ok(Some(cur)),
|
||||
Input::Text(t) => number(t),
|
||||
Input::Point(p) => {
|
||||
let h = self.array.ok_or_else(|| other("No array selected."))?;
|
||||
let (ins, blk, _) = array_of(s.doc()?, h)?;
|
||||
ins.transform(blk.base.xy()).inverse().map(|inv| inv.apply(*p).len())
|
||||
}
|
||||
_ => return Ok(None),
|
||||
};
|
||||
d.filter(|d| d.is_finite() && d.abs() > 1e-12).map(Some).ok_or_else(|| other("Requires a nonzero distance."))
|
||||
}
|
||||
|
||||
fn main(&mut self, s: &mut Session, i: Input) -> Result<Step> {
|
||||
let k = match i {
|
||||
Input::Enter => return Ok(Step::Done),
|
||||
Input::Keyword(k) => k,
|
||||
_ => return Err(other("Invalid option keyword.")),
|
||||
};
|
||||
self.stage = match k.as_str() {
|
||||
"eXit" => return Ok(Step::Done),
|
||||
"Source" => Stage::SourceItem,
|
||||
"REPlace" => {
|
||||
self.sel = SelectPhase::default();
|
||||
Stage::RepSelect
|
||||
}
|
||||
"Base point" => Stage::Base,
|
||||
"Rows" => Stage::RowsCount,
|
||||
"Columns" => Stage::ColsCount,
|
||||
"Levels" => Stage::LevelsCount,
|
||||
"Items" => Stage::Items,
|
||||
"Angle between" => Stage::Between,
|
||||
"Fill angle" => Stage::Fill,
|
||||
"ROTate items" => Stage::Rotate,
|
||||
"RESet" => {
|
||||
self.change(s, |p| {
|
||||
p.replaced.clear();
|
||||
Ok(())
|
||||
})?;
|
||||
s.echo("Replaced items were restored.");
|
||||
Stage::Main
|
||||
}
|
||||
_ => return Err(other("Invalid option keyword.")),
|
||||
};
|
||||
Ok(Step::Continue)
|
||||
}
|
||||
}
|
||||
|
||||
impl Interactive for EditM {
|
||||
fn name(&self) -> &'static str {
|
||||
"ARRAYEDIT"
|
||||
}
|
||||
fn begin(&mut self, s: &mut Session) -> Result<Step> {
|
||||
refuse_while_editing(s)?;
|
||||
let pre = s.selection();
|
||||
if let [h] = pre.as_slice()
|
||||
&& s.doc().ok().and_then(|d| d.entity(*h).and_then(|e| d.array_params(e))).is_some()
|
||||
{
|
||||
self.array = Some(*h);
|
||||
self.stage = Stage::Main;
|
||||
}
|
||||
Ok(Step::Continue)
|
||||
}
|
||||
fn prompt(&self, s: &Session) -> Prompt {
|
||||
use Stage::*;
|
||||
let p = self.params(s);
|
||||
let rect = p.as_ref().and_then(|p| match &p.kind {
|
||||
ArrayKind::Rect(r) => Some(r.clone()),
|
||||
ArrayKind::Polar(_) => None,
|
||||
});
|
||||
let polar = p.as_ref().and_then(|p| match &p.kind {
|
||||
ArrayKind::Polar(k) => Some(k.clone()),
|
||||
ArrayKind::Rect(_) => None,
|
||||
});
|
||||
let (levels, ls) = p.as_ref().map(|p| (p.levels, p.level_spacing)).unwrap_or((1, 0.0));
|
||||
let (cols, rows, cs, rs, elev) = match (&rect, &polar) {
|
||||
(Some(r), _) => (r.cols, r.rows, r.col_spacing, r.row_spacing, r.row_elevation),
|
||||
(_, Some(k)) => (k.items, k.rows, 0.0, k.row_spacing, k.row_elevation),
|
||||
_ => (1, 1, 0.0, 0.0, 0.0),
|
||||
};
|
||||
let dist = |m: &str, d: f64| Prompt::new(m, Accept::POINT_OR_NUMBER).default(fmt(d));
|
||||
let total = |n: u32, d: f64| d * f64::from(n.saturating_sub(1));
|
||||
match self.stage {
|
||||
Select => Prompt::new("Select array", Accept::SELECT),
|
||||
Main if polar.is_some() => Prompt::new("Enter an option", KEYWORDS)
|
||||
.kw(&["Source", "REPlace", "Base point", "Items", "Angle between", "Fill angle", "Rows", "Levels", "ROTate items", "RESet", "eXit"])
|
||||
.default("eXit"),
|
||||
Main => Prompt::new("Enter an option", KEYWORDS)
|
||||
.kw(&["Source", "REPlace", "Base point", "Rows", "Columns", "Levels", "RESet", "eXit"])
|
||||
.default("eXit"),
|
||||
Base => Prompt::new("Specify base point", Accept::POINT).kw(&["Key point"]).default("centroid"),
|
||||
ColsCount => Prompt::new("Enter the number of columns", Accept::NUMBER).default(cols.to_string()),
|
||||
ColsSpace => dist("Specify the distance between columns", cs).kw(&["Total"]),
|
||||
ColsTotal => dist("Specify the distance between the first and last column", total(cols, cs)),
|
||||
RowsCount => Prompt::new("Enter the number of rows", Accept::NUMBER).default(rows.to_string()),
|
||||
RowsSpace => dist("Specify the distance between rows", rs).kw(&["Total"]),
|
||||
RowsTotal => dist("Specify the distance between the first and last row", total(rows, rs)),
|
||||
RowsElev => Prompt::new("Specify the incrementing elevation between rows", Accept::NUMBER).default(fmt(elev)),
|
||||
LevelsCount => Prompt::new("Enter the number of levels", Accept::NUMBER).default(levels.to_string()),
|
||||
LevelsSpace => dist("Specify the distance between levels", ls).kw(&["Total"]),
|
||||
LevelsTotal => dist("Specify the distance between the first and last level", total(levels, ls)),
|
||||
Items => Prompt::new("Enter number of items in array", Accept::NUMBER).default(cols.to_string()),
|
||||
Between => {
|
||||
let b = polar.as_ref().map_or(0.0, |k| {
|
||||
let full = (k.fill.abs() - 360.0).abs() < 1e-9;
|
||||
if k.items <= 1 {
|
||||
k.fill
|
||||
} else if full {
|
||||
k.fill / f64::from(k.items)
|
||||
} else {
|
||||
k.fill / f64::from(k.items - 1)
|
||||
}
|
||||
});
|
||||
Prompt::new("Specify the angle between items", Accept::NUMBER).default(fmt(b))
|
||||
}
|
||||
Fill => Prompt::new("Specify the angle to fill (+=ccw, -=cw)", Accept::NUMBER).default(fmt(polar.as_ref().map_or(360.0, |k| k.fill))),
|
||||
Rotate => Prompt::new("Rotate arrayed items?", KEYWORDS).kw(&["Yes", "No"]).default(yes(polar.as_ref().is_none_or(|k| k.rotate))),
|
||||
RepSelect => Prompt::new("Select replacement objects", Accept::SELECT),
|
||||
RepBase => Prompt::new("Select base point of replacement objects", Accept::POINT).kw(&["Key point"]).default("centroid"),
|
||||
RepItems => Prompt::new("Select an item in array to replace", Accept::POINT).kw(&["Source objects"]),
|
||||
SourceItem => Prompt::new("Select item in array", Accept::POINT),
|
||||
}
|
||||
}
|
||||
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
|
||||
use Stage::*;
|
||||
let stage = self.stage;
|
||||
if stage == Select {
|
||||
return match i {
|
||||
Input::Pick(hs) => {
|
||||
let d = s.doc()?;
|
||||
match hs.iter().copied().find(|h| d.entity(*h).and_then(|e| d.array_params(e)).is_some()) {
|
||||
Some(h) => {
|
||||
self.array = Some(h);
|
||||
self.stage = Main;
|
||||
Ok(Step::Continue)
|
||||
}
|
||||
None => Err(other("The object is not an associative array.")),
|
||||
}
|
||||
}
|
||||
Input::Enter | Input::Cancel => Ok(Step::Done),
|
||||
_ => Ok(Step::Continue),
|
||||
};
|
||||
}
|
||||
if stage == RepSelect {
|
||||
return match self.sel.feed(s, &i)? {
|
||||
SelOutcome::More => Ok(Step::Continue),
|
||||
SelOutcome::Empty => {
|
||||
self.stage = Main;
|
||||
Ok(Step::Continue)
|
||||
}
|
||||
SelOutcome::Done(hs) => {
|
||||
self.rep_objs = hs;
|
||||
self.stage = RepBase;
|
||||
s.set_selection(Vec::new());
|
||||
Ok(Step::Continue)
|
||||
}
|
||||
};
|
||||
}
|
||||
let h = self.array.ok_or_else(|| other("No array selected."))?;
|
||||
let (cols, rows, levels) = self.params(s).map(|p| (p.grid().0, p.grid().1, p.levels)).unwrap_or((1, 1, 1));
|
||||
let (c64, r64, l64) = (u64::from(cols), u64::from(rows), u64::from(levels));
|
||||
let next = match (stage, &i) {
|
||||
(Main, _) => return self.main(s, i),
|
||||
(Base, Input::Point(p)) => {
|
||||
rebase(s, h, *p)?;
|
||||
Main
|
||||
}
|
||||
(Base, Input::Enter) => {
|
||||
let d = s.doc()?;
|
||||
let (ins, blk, p) = array_of(d, h)?;
|
||||
let src = d.block(&p.source).ok_or_else(|| other("The array's source block is missing."))?;
|
||||
let local = src.entities.iter().fold(Bounds2::EMPTY, |a, e| a.union(&entity_bounds(d, e, 1)));
|
||||
let at = ins.transform(blk.base.xy()).then_before(Mat3::translate(-src.base.xy())).apply(local.center());
|
||||
if at.is_finite() {
|
||||
rebase(s, h, at)?;
|
||||
}
|
||||
Main
|
||||
}
|
||||
(Base | RepBase, Input::Keyword(_)) => {
|
||||
s.echo("Key points are not available yet; specify a point.");
|
||||
stage
|
||||
}
|
||||
(ColsCount | Items, _) => match count(&i, c64, self.max_for(s, r64.saturating_mul(l64)))? {
|
||||
Some(n) => {
|
||||
let n = super::count32(n);
|
||||
self.change(s, |p| {
|
||||
match &mut p.kind {
|
||||
ArrayKind::Rect(r) => r.cols = n,
|
||||
ArrayKind::Polar(k) => k.items = n,
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
if stage == ColsCount { ColsSpace } else { Main }
|
||||
}
|
||||
None => stage,
|
||||
},
|
||||
(RowsCount, _) => match count(&i, r64, self.max_for(s, c64.saturating_mul(l64)))? {
|
||||
Some(n) => {
|
||||
let n = super::count32(n);
|
||||
self.change(s, |p| {
|
||||
match &mut p.kind {
|
||||
ArrayKind::Rect(r) => r.rows = n,
|
||||
ArrayKind::Polar(k) => k.rows = n,
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
RowsSpace
|
||||
}
|
||||
None => stage,
|
||||
},
|
||||
(LevelsCount, _) => match count(&i, l64, self.max_for(s, c64.saturating_mul(r64)))? {
|
||||
Some(n) => {
|
||||
let n = super::count32(n);
|
||||
self.change(s, |p| {
|
||||
p.levels = n;
|
||||
Ok(())
|
||||
})?;
|
||||
LevelsSpace
|
||||
}
|
||||
None => stage,
|
||||
},
|
||||
(ColsSpace, Input::Keyword(_)) => ColsTotal,
|
||||
(RowsSpace, Input::Keyword(_)) => RowsTotal,
|
||||
(LevelsSpace, Input::Keyword(_)) => LevelsTotal,
|
||||
(ColsSpace | ColsTotal | RowsSpace | RowsTotal | LevelsSpace | LevelsTotal, _) => {
|
||||
let p = self.params(s).ok_or_else(|| other("No array selected."))?;
|
||||
let (cur, n) = match (stage, &p.kind) {
|
||||
(ColsSpace | ColsTotal, ArrayKind::Rect(r)) => (r.col_spacing, r.cols),
|
||||
(RowsSpace | RowsTotal, ArrayKind::Rect(r)) => (r.row_spacing, r.rows),
|
||||
(RowsSpace | RowsTotal, ArrayKind::Polar(k)) => (k.row_spacing, k.rows),
|
||||
(LevelsSpace | LevelsTotal, _) => (p.level_spacing, p.levels),
|
||||
_ => (0.0, 1),
|
||||
};
|
||||
let is_total = matches!(stage, ColsTotal | RowsTotal | LevelsTotal);
|
||||
let gaps = f64::from(n.saturating_sub(1).max(1));
|
||||
let shown = if is_total { cur * f64::from(n.saturating_sub(1)) } else { cur };
|
||||
match self.distance(s, &i, shown)? {
|
||||
Some(d) => {
|
||||
let v = if is_total { d / gaps } else { d };
|
||||
self.change(s, |p| {
|
||||
match (stage, &mut p.kind) {
|
||||
(ColsSpace | ColsTotal, ArrayKind::Rect(r)) => r.col_spacing = v,
|
||||
(RowsSpace | RowsTotal, ArrayKind::Rect(r)) => r.row_spacing = v,
|
||||
(RowsSpace | RowsTotal, ArrayKind::Polar(k)) => k.row_spacing = v,
|
||||
_ => p.level_spacing = v,
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
if matches!(stage, RowsSpace | RowsTotal) { RowsElev } else { Main }
|
||||
}
|
||||
None => stage,
|
||||
}
|
||||
}
|
||||
(RowsElev, Input::Enter) => Main,
|
||||
(RowsElev, Input::Text(t)) => match number(t) {
|
||||
Some(z) if z.is_finite() => {
|
||||
self.change(s, |p| {
|
||||
match &mut p.kind {
|
||||
ArrayKind::Rect(r) => r.row_elevation = z,
|
||||
ArrayKind::Polar(k) => k.row_elevation = z,
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
Main
|
||||
}
|
||||
_ => return Err(other("Requires a number.")),
|
||||
},
|
||||
(Between | Fill, Input::Enter) => Main,
|
||||
(Between | Fill, _) => match angle(&i, 0.0, &s.angle_settings())? {
|
||||
Some(a) => {
|
||||
self.change(s, |p| {
|
||||
if let ArrayKind::Polar(k) = &mut p.kind {
|
||||
k.fill = if stage == Fill { fill_angle(a)? } else { fill_from_between(a, k.items)? };
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
Main
|
||||
}
|
||||
None => stage,
|
||||
},
|
||||
(Rotate, _) => {
|
||||
let cur = self.params(s).is_none_or(|p| !matches!(p.kind, ArrayKind::Polar(ref k) if !k.rotate));
|
||||
match yes_no(&i, cur) {
|
||||
Some(r) => {
|
||||
self.change(s, |p| {
|
||||
if let ArrayKind::Polar(k) = &mut p.kind {
|
||||
k.rotate = r;
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
Main
|
||||
}
|
||||
None => return Err(other("Requires Yes or No.")),
|
||||
}
|
||||
}
|
||||
(RepBase, Input::Point(_) | Input::Enter) => {
|
||||
let base = match &i {
|
||||
Input::Point(p) => *p,
|
||||
_ => centroid(s.doc()?, &self.rep_objs),
|
||||
};
|
||||
self.rep_block = Some(block_from(s, h, &self.rep_objs.clone(), base)?);
|
||||
RepItems
|
||||
}
|
||||
(RepItems, Input::Point(at)) => {
|
||||
let tol = s.pixel_size() * s.settings.pickbox;
|
||||
let item = item_at(s.doc()?, h, *at, tol).ok_or_else(|| other("No array item there; select an item of the array."))?;
|
||||
let name = self.rep_block.clone().ok_or_else(|| other("No replacement objects."))?;
|
||||
self.change(s, |p| {
|
||||
p.replaced.push(ReplacedItem { i: item.0, j: item.1, level: item.2, block: name });
|
||||
Ok(())
|
||||
})?;
|
||||
RepItems
|
||||
}
|
||||
(RepItems, Input::Keyword(_)) => {
|
||||
let name = self.rep_block.clone().ok_or_else(|| other("No replacement objects."))?;
|
||||
self.change(s, |p| {
|
||||
p.source = name;
|
||||
Ok(())
|
||||
})?;
|
||||
Main
|
||||
}
|
||||
(RepItems, Input::Enter) => Main,
|
||||
(SourceItem, Input::Point(at)) => {
|
||||
let tol = s.pixel_size() * s.settings.pickbox;
|
||||
let item = item_at(s.doc()?, h, *at, tol).ok_or_else(|| other("No array item there; select an item of the array."))?;
|
||||
open_source(s, h, item)?;
|
||||
return Ok(Step::Done);
|
||||
}
|
||||
(SourceItem, Input::Enter) => Main,
|
||||
_ => stage,
|
||||
};
|
||||
self.stage = next;
|
||||
Ok(Step::Continue)
|
||||
}
|
||||
}
|
||||
|
||||
/// ARRAYCLOSE: save or discard the source objects being edited.
|
||||
struct CloseM;
|
||||
|
||||
impl Interactive for CloseM {
|
||||
fn name(&self) -> &'static str {
|
||||
"ARRAYCLOSE"
|
||||
}
|
||||
fn begin(&mut self, s: &mut Session) -> Result<Step> {
|
||||
if open_edit(s.doc()?).is_none() {
|
||||
return Err(other("No array is being edited."));
|
||||
}
|
||||
Ok(Step::Continue)
|
||||
}
|
||||
fn prompt(&self, _s: &Session) -> Prompt {
|
||||
Prompt::new("Save changes to array?", KEYWORDS).kw(&["Yes", "No"]).default("Yes")
|
||||
}
|
||||
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
|
||||
match yes_no(&i, true) {
|
||||
Some(save) => {
|
||||
close_source(s, save)?;
|
||||
Ok(Step::Done)
|
||||
}
|
||||
None => Err(other("Requires Yes or No.")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -201,6 +201,170 @@ fn pasted_arrays_get_their_own_blocks() {
|
|||
assert!(params.iter().all(|p| d.block(&p.source).is_some()));
|
||||
}
|
||||
|
||||
/// Circle centres drawn by the array `h`, sorted.
|
||||
fn centres(s: &Session, h: &Value) -> Vec<(i64, i64)> {
|
||||
let d = s.doc().unwrap();
|
||||
let e = d.entity(Handle::parse_hex(h.as_str().unwrap()).unwrap()).unwrap().as_ref().clone();
|
||||
let mut v: Vec<(i64, i64)> = circles(d, &e).iter().map(|(p, _)| ((p.x * 1000.0).round() as i64, (p.y * 1000.0).round() as i64)).collect();
|
||||
v.sort();
|
||||
v
|
||||
}
|
||||
|
||||
fn lines_in(s: &Session, h: &Value) -> usize {
|
||||
fn walk(d: &Drawing, e: &Entity, depth: usize) -> usize {
|
||||
match &e.kind {
|
||||
EntityKind::Line(_) => 1,
|
||||
EntityKind::Insert(i) if depth < 8 => d.block(&i.block).unwrap().entities.iter().map(|b| walk(d, b, depth + 1)).sum(),
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
let d = s.doc().unwrap();
|
||||
walk(d, d.entity(Handle::parse_hex(h.as_str().unwrap()).unwrap()).unwrap(), 0)
|
||||
}
|
||||
|
||||
fn rect_array(s: &mut Session) -> Value {
|
||||
let h = circle(s, 0.0, 0.0);
|
||||
s.execute("arrayrect", &json!({"handles": [h], "rows": 1, "cols": 3, "colSpacing": 2})).unwrap()["handle"].clone()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arrayedit_changes_parameters_of_one_array() {
|
||||
let mut s = Session::new();
|
||||
let a = rect_array(&mut s);
|
||||
let b = s.execute("copy", &json!({"handles": [a], "delta": [0, 100]})).unwrap();
|
||||
let b = s.doc().unwrap().model.iter().map(|e| json!(e.handle.hex())).find(|x| *x != a).unwrap_or(b);
|
||||
s.execute("arrayedit", &json!({"handle": a, "cols": 2, "rows": 2, "rowSpacing": 5, "colSpacing": 3})).unwrap();
|
||||
assert_eq!(centres(&s, &a), vec![(0, 0), (0, 5000), (3000, 0), (3000, 5000)]);
|
||||
// The copy that shared the array's block is unchanged.
|
||||
assert_eq!(centres(&s, &b), vec![(0, 100_000), (2000, 100_000), (4000, 100_000)]);
|
||||
// Blocks nothing uses are gone: two arrays, each with an array block and a shared source.
|
||||
let anon = s.doc().unwrap().blocks.keys().filter(|k| k.starts_with('*')).count();
|
||||
assert_eq!(anon, 3);
|
||||
|
||||
// Polar: items, fill, rotation, centre.
|
||||
let mut s = Session::new();
|
||||
let h = circle(&mut s, 10.0, 0.0);
|
||||
let p = s.execute("arraypolar", &json!({"handles": [h], "center": [0, 0], "count": 4})).unwrap()["handle"].clone();
|
||||
s.execute("arrayedit", &json!({"handle": p, "items": 3, "angle": 180})).unwrap();
|
||||
assert_eq!(centres(&s, &p), vec![(-10000, 0), (0, 10000), (10000, 0)]);
|
||||
s.execute("arrayedit", &json!({"handle": p, "angleBetween": 45, "center": [0, 10]})).unwrap();
|
||||
assert_eq!(centres(&s, &p).len(), 3);
|
||||
assert!(centres(&s, &p).contains(&(10000, 0)));
|
||||
|
||||
// Bad values are refused and change nothing.
|
||||
let before = centres(&s, &p);
|
||||
assert!(s.execute("arrayedit", &json!({"handle": p, "items": 0})).is_err());
|
||||
assert!(s.execute("arrayedit", &json!({"handle": p, "items": 10_000_000})).unwrap_err().to_string().contains("MAXARRAY"));
|
||||
assert!(s.execute("arrayedit", &json!({"handle": p, "angle": 400})).is_err());
|
||||
assert_eq!(centres(&s, &p), before);
|
||||
let c = circle(&mut s, 50.0, 50.0);
|
||||
assert!(s.execute("arrayedit", &json!({"handle": c, "items": 3})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arrayedit_base_replace_and_reset() {
|
||||
let mut s = Session::new();
|
||||
let a = rect_array(&mut s);
|
||||
// A new base point moves the reference, not the items.
|
||||
s.execute("arrayedit", &json!({"handle": a, "base": [7, 7]})).unwrap();
|
||||
assert_eq!(centres(&s, &a), vec![(0, 0), (2000, 0), (4000, 0)]);
|
||||
let d = s.doc().unwrap();
|
||||
let e = d.entity(Handle::parse_hex(a.as_str().unwrap()).unwrap()).unwrap();
|
||||
assert!(matches!(&e.kind, EntityKind::Insert(i) if i.insert.xy().near(Vec2::new(7.0, 7.0), 1e-9)));
|
||||
// Spacing still counts from the items.
|
||||
s.execute("arrayedit", &json!({"handle": a, "colSpacing": 4})).unwrap();
|
||||
assert_eq!(centres(&s, &a), vec![(0, 0), (4000, 0), (8000, 0)]);
|
||||
|
||||
// REPlace item 1 with a line; the line stays in the drawing.
|
||||
let l = s.execute("line", &json!({"points": [[0, 20], [1, 20]]})).unwrap()["handles"][0].clone();
|
||||
s.execute("arrayedit", &json!({"handle": a, "replace": {"handles": [l], "base": [0, 20], "items": [[1, 0, 0]]}})).unwrap();
|
||||
assert_eq!(centres(&s, &a), vec![(0, 0), (8000, 0)]);
|
||||
assert_eq!(lines_in(&s, &a), 1);
|
||||
assert_eq!(s.doc().unwrap().model.len(), 2);
|
||||
// Replacing with the array itself is refused.
|
||||
assert!(s.execute("arrayedit", &json!({"handle": a, "replace": {"handles": [a], "items": [[0, 0, 0]]}})).is_err());
|
||||
// RESet restores the item.
|
||||
s.execute("arrayedit", &json!({"handle": a, "reset": true})).unwrap();
|
||||
assert_eq!(centres(&s, &a).len(), 3);
|
||||
assert_eq!(lines_in(&s, &a), 0);
|
||||
// REPlace Source objects: every item becomes the line.
|
||||
s.execute("arrayedit", &json!({"handle": a, "replace": {"handles": [l], "items": "source"}})).unwrap();
|
||||
assert_eq!(centres(&s, &a).len(), 0);
|
||||
assert_eq!(lines_in(&s, &a), 3);
|
||||
// EXPLODE gives three lines (plus the original one).
|
||||
s.execute("explode", &json!({"handles": [a]})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().model.iter().filter(|e| matches!(e.kind, EntityKind::Line(_))).count(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arrayedit_source_then_arrayclose() {
|
||||
let mut s = Session::new();
|
||||
let a = rect_array(&mut s);
|
||||
// Source places the circle of item 2 (at x = 4) in the drawing.
|
||||
let r = s.execute("arrayedit", &json!({"handle": a, "source": {"item": [2, 0, 0]}})).unwrap();
|
||||
let placed = r["source"].as_array().unwrap().clone();
|
||||
assert_eq!(placed.len(), 1);
|
||||
// Only one array at a time; other edits wait for ARRAYCLOSE.
|
||||
assert!(s.execute("arrayedit", &json!({"handle": a, "cols": 5})).is_err());
|
||||
// Move the circle up by 1 and draw a line next to it: both become the source.
|
||||
s.execute("move", &json!({"handles": placed, "delta": [0, 1]})).unwrap();
|
||||
s.execute("line", &json!({"points": [[4, 3], [5, 3]]})).unwrap();
|
||||
s.execute("arrayclose", &json!({})).unwrap();
|
||||
assert_eq!(centres(&s, &a), vec![(0, 1000), (2000, 1000), (4000, 1000)]);
|
||||
assert_eq!(lines_in(&s, &a), 3);
|
||||
assert_eq!(s.doc().unwrap().model.len(), 1);
|
||||
|
||||
// Discard: the placed objects go, the array stays as it was.
|
||||
s.execute("arrayedit", &json!({"handle": a, "source": {}})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().model.len(), 3);
|
||||
s.execute("arrayclose", &json!({"save": false})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().model.len(), 1);
|
||||
assert_eq!(centres(&s, &a).len(), 3);
|
||||
assert!(s.execute("arrayclose", &json!({})).is_err());
|
||||
|
||||
// Saving an empty source is refused; the edit stays open.
|
||||
let r = s.execute("arrayedit", &json!({"handle": a, "source": {}})).unwrap();
|
||||
s.execute("erase", &json!({"handles": r["source"]})).unwrap();
|
||||
assert!(s.execute("arrayclose", &json!({})).is_err());
|
||||
s.execute("arrayclose", &json!({"save": false})).unwrap();
|
||||
assert_eq!(centres(&s, &a).len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typed_arrayedit() {
|
||||
let mut s = Session::new();
|
||||
let a = rect_array(&mut s);
|
||||
typed(&mut s, &["arrayedit", "l"]);
|
||||
let p = s.prompt_text();
|
||||
assert!(p.contains("Enter an option or [Source/REPlace/Base point/Rows/Columns/Levels/RESet/eXit] <eXit>"), "{p}");
|
||||
typed(&mut s, &["c", "4", "t", "9", "r", "2", "5", ""]);
|
||||
assert!(s.prompt_text().contains("Enter an option"), "{}", s.prompt_text());
|
||||
assert_eq!(centres(&s, &a).len(), 8);
|
||||
assert!(centres(&s, &a).contains(&(9000, 5000)), "{:?}", centres(&s, &a));
|
||||
typed(&mut s, &["x"]);
|
||||
assert!(s.current_prompt().is_none(), "{}", s.prompt_text());
|
||||
// One undo step takes the whole edit back.
|
||||
s.execute("undo", &json!({})).unwrap();
|
||||
assert_eq!(centres(&s, &a).len(), 3);
|
||||
|
||||
// REPlace with a typed pick of item 2, then Source on item 0 and ARRAYCLOSE.
|
||||
s.execute("line", &json!({"points": [[0, 20], [1, 20]]})).unwrap();
|
||||
s.set_selection(Vec::new());
|
||||
typed(&mut s, &["arrayedit", "1,0"]);
|
||||
typed(&mut s, &["rep", "l", "", ""]);
|
||||
assert!(s.prompt_text().contains("Select an item in array to replace or [Source objects]"), "{}", s.prompt_text());
|
||||
typed(&mut s, &["4,0", "", "x"]);
|
||||
assert_eq!(centres(&s, &a).len(), 2);
|
||||
assert_eq!(lines_in(&s, &a), 1);
|
||||
typed(&mut s, &["arrayedit", "1,0", "s", "0,0"]);
|
||||
assert!(s.current_prompt().is_none(), "{}", s.prompt_text());
|
||||
typed(&mut s, &["arrayclose"]);
|
||||
assert!(s.prompt_text().contains("Save changes to array? [Yes/No] <Yes>"), "{}", s.prompt_text());
|
||||
typed(&mut s, &[""]);
|
||||
assert!(s.current_prompt().is_none(), "{}", s.prompt_text());
|
||||
assert_eq!(centres(&s, &a).len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn json_arrays_follow_arrayassociativity() {
|
||||
// With ARRAYASSOCIATIVITY 0 the JSON form makes copies unless `associative` asks otherwise.
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ fn params(source: &str) -> ArrayParams {
|
|||
levels: 2,
|
||||
level_spacing: 4.0,
|
||||
replaced: vec![ReplacedItem { i: 1, j: 0, level: 0, block: "Größe".into() }],
|
||||
editing: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1041,3 +1041,5 @@
|
|||
Convert to Arc Перетворити на дугу
|
||||
Convert to Line Перетворити на відрізок
|
||||
Stretch Vertex Розтягнути вершину
|
||||
Edit Array Редагувати масив
|
||||
Close Array Editing Завершити редагування масиву
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 7.
|
Loading…
Reference in New Issue