MINSERT: a block reference repeated in rows and columns, as AutoCAD does (#2698)

* MINSERT: a block reference repeated in rows and columns

MINSERT asks INSERT's questions, then the grid's as AutoCAD 2024 does
(measured with accoreconsole): the numbers of rows and columns (1 to
32767), the distance between rows or a unit cell when there is more than
one row, the distance between columns when there is more than one column,
then the attribute values once. A distance along an axis with one copy is
0, and one row by one column is a plain reference.

EXPLODE leaves a grid whole ("The object is minserted.") instead of
exploding its first copy and dropping the rest. MIRROR mirrors a block
reference as AutoCAD does, negating the X scale factor and a grid's
column distance, so a mirrored grid's rows stay on the right side. An
attribute with no default is asked without "<>".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* MINSERT JSON: refuse a distance that is not a number

`rowSpacing` and `columnSpacing` went through `f64_or`, which takes any
value that is not a number as the default, so `"rowSpacing": "15"` stored 0
and put every copy on top of the others without an error. They are now
checked as `rows` and `columns` are: absent or null is 0, a finite number is
kept, anything else is refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Khuong Doan
2026-11-03 09:06:58 -05:00
committed by GitHub
co-authored by Claude Opus 5.5
parent e10f394b28
commit 7bb6c2f06f
7 changed files with 417 additions and 17 deletions
+6 -2
View File
@@ -362,9 +362,13 @@ impl EntityKind {
i.scale.y *= s;
i.scale.z *= s;
i.rotation = cadcraft_geom::norm_angle(i.rotation + rot);
// Mirrored as AutoCAD 2024 mirrors a block reference: the X scale factor
// negated, the rotation 180 degrees plus twice the mirror line's angle less the
// old one, and a MINSERT grid's columns running the other way.
if mirror {
i.scale.y = -i.scale.y;
i.rotation = cadcraft_geom::norm_angle(rot - (i.rotation - rot));
i.scale.x = -i.scale.x;
i.rotation = cadcraft_geom::norm_angle(std::f64::consts::PI + rot - (i.rotation - rot));
i.col_spacing = -i.col_spacing;
}
for a in &mut i.attribs {
transform_text(&mut a.text, m, s, rot, false);
+171 -15
View File
@@ -43,6 +43,9 @@ pub fn specs() -> Vec<CommandSpec> {
.alias(&["i", "-insert", "ddinsert"])
.params("{name (\"*NAME\": exploded), file?: DXF/DWG path (defines or redefines block `name`, default the file name, from its model space; base point $INSBASE), at: [x,y], scale?: number | [x, y], rotation? (degrees), basePoint?: [x,y] (block coordinates), attribs?: {TAG: value}, explode?: bool}")
.interactive(|_| Ok(Box::new(InsertM::default()))),
CommandSpec::new("minsert", "Multiple Insert", run_minsert)
.params("{name, file?: DXF/DWG path (as INSERT), at: [x,y], scale?: number | [x, y], rotation? (degrees), basePoint?: [x,y], attribs?: {TAG: value}, rows?: 1..32767 (default 1), columns?: 1..32767 (default 1), rowSpacing?, columnSpacing? (along the reference's own axes, not scaled with it)}")
.interactive(|_| Ok(Box::new(InsertM { minsert: true, ..InsertM::default() }))),
CommandSpec::new("attdef", "Define Attributes...", run_attdef)
.menu(&["Draw", "Block", "Define Attributes..."])
.alias(&["att", "-attdef"])
@@ -233,6 +236,33 @@ fn insert_entity(blk: &Block, at: Vec2, scale: Vec3, rotation: f64, base: Option
ins
}
/// MINSERT's rectangular array of a block reference: rows and columns, and the distances between
/// them along the reference's rotated axes (not scaled with it). One row and one column is a plain
/// reference.
#[derive(Clone, Copy, Debug, PartialEq)]
struct Grid {
rows: u32,
cols: u32,
row_spacing: f64,
col_spacing: f64,
}
impl Grid {
const ONE: Grid = Grid { rows: 1, cols: 1, row_spacing: 0.0, col_spacing: 0.0 };
/// The most rows or columns MINSERT takes, as AutoCAD 2024 asks for them.
const MAX: u32 = 32767;
/// A distance is kept only along an axis with more than one copy: AutoCAD 2024 asks for no
/// other and stores 0 for it (one row of three columns: 44 = 20, 45 = 0).
fn new(rows: u32, cols: u32, row_spacing: f64, col_spacing: f64) -> Grid {
Grid { rows, cols, row_spacing: if rows > 1 { row_spacing } else { 0.0 }, col_spacing: if cols > 1 { col_spacing } else { 0.0 } }
}
fn apply(self, ins: &mut Insert) {
(ins.rows, ins.cols, ins.row_spacing, ins.col_spacing) = (self.rows, self.cols, self.row_spacing, self.col_spacing);
}
}
/// [`insert`] with separate X/Y scale factors (Z as X) and an optional base point (see
/// [`insert_entity`]).
pub(crate) fn insert_with(
@@ -244,10 +274,12 @@ pub(crate) fn insert_with(
base: Option<Vec2>,
values: &serde_json::Map<String, Value>,
) -> Result<Handle> {
insert_scaled(s, name, at, Vec3::new(scale.x, scale.y, scale.x), rotation, base, values)
insert_scaled(s, name, at, Vec3::new(scale.x, scale.y, scale.x), rotation, base, values, Grid::ONE)
}
/// [`insert_with`] with a Z scale factor of its own.
/// [`insert_with`] with a Z scale factor of its own, and MINSERT's grid. A grid has the one set
/// of attributes (AutoCAD 2024 asks for their values once).
#[allow(clippy::too_many_arguments)]
fn insert_scaled(
s: &mut Session,
name: &str,
@@ -256,9 +288,11 @@ fn insert_scaled(
rotation: f64,
base: Option<Vec2>,
values: &serde_json::Map<String, Value>,
grid: Grid,
) -> Result<Handle> {
let blk = s.doc()?.block(name).cloned().ok_or_else(|| EngineError::Other(format!("Block \"{name}\" not found.")))?;
let mut ins = insert_entity(&blk, at, scale, rotation, base);
grid.apply(&mut ins);
let m = ins.transform(blk.base.xy());
for e in blk.entities.iter() {
if let EntityKind::AttDef(ad) = &e.kind {
@@ -310,24 +344,52 @@ fn run_block(s: &mut Session, p: &Value) -> Result<Value> {
}
fn run_insert(s: &mut Session, p: &Value) -> Result<Value> {
insert_json(s, p, "insert", Grid::ONE)
}
/// MINSERT's JSON form: INSERT's parameters and the grid.
fn run_minsert(s: &mut Session, p: &Value) -> Result<Value> {
let count = |key: &str| -> Result<u32> {
match p.get(key) {
None | Some(Value::Null) => Ok(1),
Some(v) => v
.as_f64()
.filter(|n| n.fract() == 0.0 && *n >= 1.0 && *n <= f64::from(Grid::MAX))
.map(|n| n as u32)
.ok_or_else(|| bad("minsert", format!("`{key}` must be an integer between 1 and {}", Grid::MAX))),
}
};
// A distance given must be a number: `"15"` is refused, not taken as 0.
let spacing = |key: &str| -> Result<f64> {
match p.get(key) {
None | Some(Value::Null) => Ok(0.0),
Some(v) => v.as_f64().filter(|x| x.is_finite()).ok_or_else(|| bad("minsert", format!("`{key}` must be a number"))),
}
};
let grid = Grid::new(count("rows")?, count("columns")?, spacing("rowSpacing")?, spacing("columnSpacing")?);
insert_json(s, p, "minsert", grid)
}
/// INSERT's and MINSERT's JSON form.
fn insert_json(s: &mut Session, p: &Value, cmd: &str, grid: Grid) -> Result<Value> {
let p = &insert_file::json_params(s, p)?;
let name = str_param(p, "name").ok_or_else(|| bad("insert", "`name` is required"))?.to_string();
let at = point_req("insert", p, "at")?;
let name = str_param(p, "name").ok_or_else(|| bad(cmd, "`name` is required"))?.to_string();
let at = point_req(cmd, p, "at")?;
// `scale`: one factor, or [x, y] for different X and Y scale factors.
let scale = match p.get("scale") {
Some(v) if v.is_array() => point_value(v).ok_or_else(|| bad("insert", "`scale` must be a number or [x, y]"))?,
Some(v) if v.is_array() => point_value(v).ok_or_else(|| bad(cmd, "`scale` must be a number or [x, y]"))?,
_ => {
let f = f64_or(p, "scale", 1.0);
Vec2::new(f, f)
}
};
if scale.x == 0.0 || scale.y == 0.0 {
return Err(bad("insert", "scale cannot be 0"));
return Err(bad(cmd, "scale cannot be 0"));
}
let rot = f64_or(p, "rotation", 0.0).to_radians();
let vals = p.get("attribs").and_then(Value::as_object).cloned().unwrap_or_default();
let h = insert_with(s, &name, at, scale, rot, point_param(p, "basePoint"), &vals)?;
if bool_or(p, "explode", false) {
let h = insert_scaled(s, &name, at, Vec3::new(scale.x, scale.y, scale.x), rot, point_param(p, "basePoint"), &vals, grid)?;
if cmd == "insert" && bool_or(p, "explode", false) {
// The EXPLODE command body, not `execute`: INSERT records the one undo step.
let explode = find_command("explode").ok_or_else(|| EngineError::UnknownCommand("explode".into()))?;
return (explode.run)(s, &json!({ "handles": [h.hex()] }));
@@ -560,6 +622,15 @@ enum InsStage {
/// XYZ: the Z scale factor, after X and Y.
ZScale,
Rotation,
/// MINSERT: the number of rows, then of columns.
Rows,
Cols,
/// MINSERT: the distance between rows, or a unit cell's first corner.
RowSpacing,
/// MINSERT: the unit cell's opposite corner (its width between columns, its height between
/// rows).
Cell(Vec2),
ColSpacing,
Attribs,
/// A Verify attribute asked again.
Verify,
@@ -592,6 +663,12 @@ struct InsertM {
explode: bool,
/// The file waiting for the Redefine answer.
file: Option<insert_file::FileRef>,
/// MINSERT: after the rotation, the grid's rows and columns and the distances between them.
minsert: bool,
rows: Option<u32>,
cols: Option<u32>,
row_spacing: Option<f64>,
col_spacing: Option<f64>,
}
impl InsertM {
@@ -601,6 +678,8 @@ impl InsertM {
if self.explode { InsStage::PresetScale } else { InsStage::XScale }
} else if self.rotation.is_none() {
InsStage::Rotation
} else if let Some(g) = self.grid_stage() {
g
} else {
InsStage::Attribs
};
@@ -619,7 +698,7 @@ impl InsertM {
&& self.values.len() >= self.attdefs.len()
&& let (Some(n), Some(a), Some(sc), Some(r)) = (self.name.clone(), self.at, self.scale, self.rotation)
{
let h = insert_scaled(s, &n, a, self.scale3(sc), r, self.base, &self.values)?;
let h = insert_scaled(s, &n, a, self.scale3(sc), r, self.base, &self.values, self.grid())?;
if self.explode {
insert_file::explode_insert(s, h)?;
}
@@ -628,6 +707,30 @@ impl InsertM {
Ok(Step::Continue)
}
/// MINSERT's next grid prompt, if any is left (as AutoCAD 2024 asks them): the rows, the
/// columns, then the distance between rows when there is more than one row and between
/// columns when there is more than one column. A unit cell gives both distances.
fn grid_stage(&self) -> Option<InsStage> {
if !self.minsert {
return None;
}
let (Some(rows), Some(cols)) = (self.rows, self.cols) else {
return Some(if self.rows.is_none() { InsStage::Rows } else { InsStage::Cols });
};
if rows > 1 && self.row_spacing.is_none() {
Some(InsStage::RowSpacing)
} else if cols > 1 && self.col_spacing.is_none() {
Some(InsStage::ColSpacing)
} else {
None
}
}
/// The grid answered so far (one copy for INSERT).
fn grid(&self) -> Grid {
Grid::new(self.rows.unwrap_or(1), self.cols.unwrap_or(1), self.row_spacing.unwrap_or(0.0), self.col_spacing.unwrap_or(0.0))
}
/// The Verify attributes asked for.
fn verifying(&self) -> Vec<&(String, String, String, bool)> {
self.attdefs.iter().filter(|a| a.3).collect()
@@ -698,13 +801,18 @@ impl InsertM {
}
}
/// A distance typed at MINSERT's grid prompts: any finite number, negative or 0 too.
fn distance(t: &str) -> Result<f64> {
number(t).filter(|v| v.is_finite()).ok_or_else(|| EngineError::Other("Requires a distance.".into()))
}
fn nonzero(t: &str) -> Result<f64> {
number(t).filter(|v| *v != 0.0).ok_or_else(|| EngineError::Other("Requires a non-zero number.".into()))
}
impl Interactive for InsertM {
fn name(&self) -> &'static str {
"INSERT"
if self.minsert { "MINSERT" } else { "INSERT" }
}
fn prompt(&self, s: &Session) -> Prompt {
let at = self.at.unwrap_or_default();
@@ -731,9 +839,16 @@ impl Interactive for InsertM {
InsStage::YScale => Prompt::new("Enter Y scale factor", Accept::NUMBER).default("use X scale factor"),
InsStage::ZScale => Prompt::new("Specify Z scale factor or", Accept::NUMBER).default("use X scale factor"),
InsStage::Rotation => Prompt::new("Specify rotation angle", Accept::POINT_OR_NUMBER).default("0").base(at),
InsStage::Rows => Prompt::new("Enter number of rows (---)", Accept::NUMBER).default("1"),
InsStage::Cols => Prompt::new("Enter number of columns (|||)", Accept::NUMBER).default("1"),
InsStage::RowSpacing => Prompt::new("Enter distance between rows or specify unit cell (---)", Accept::POINT_OR_NUMBER),
InsStage::Cell(a) => Prompt::new("Specify opposite corner", Accept::POINT).corner(a),
InsStage::ColSpacing => Prompt::new("Specify distance between columns (|||)", Accept::NUMBER),
InsStage::Attribs => {
let (_, prompt, default, _) = self.attdefs.get(self.values.len()).cloned().unwrap_or_default();
Prompt::new(if prompt.is_empty() { "Enter attribute value".into() } else { prompt }, Accept::TEXT).default(default)
let p = Prompt::new(if prompt.is_empty() { "Enter attribute value".into() } else { prompt }, Accept::TEXT);
// No default shown when there is none ("Value:", AutoCAD 2024), not "<>".
if default.is_empty() { p } else { p.default(default) }
}
InsStage::Verify => {
let (tag, prompt) = self.verifying().get(self.verified).map(|a| (a.0.clone(), a.1.clone())).unwrap_or_default();
@@ -755,9 +870,9 @@ impl Interactive for InsertM {
s.echo(format!("Defined blocks: {}", names.join(", ")));
return Ok(Step::Continue);
}
// `*name`: insert exploded.
self.explode = n.starts_with('*');
let n = n.strip_prefix('*').map_or(n.as_str(), str::trim).to_string();
// `*name`: insert exploded (INSERT only: a grid has no exploded form).
self.explode = !self.minsert && n.starts_with('*');
let n = if self.explode { n.strip_prefix('*').map_or(n.as_str(), str::trim).to_string() } else { n };
match insert_file::file_ref(s, &n) {
Some(f) if f.ask_redefine(s) => {
self.file = Some(f);
@@ -897,6 +1012,45 @@ impl Interactive for InsertM {
});
return self.advance(s);
}
InsStage::Rows | InsStage::Cols => {
let n = match i {
Input::Text(t) => t
.trim()
.parse::<f64>()
.ok()
.filter(|v| v.fract() == 0.0 && *v >= 1.0 && *v <= f64::from(Grid::MAX))
.map(|v| v as u32)
.ok_or_else(|| EngineError::Other(format!("Requires an integer between 1 and {}.", Grid::MAX)))?,
Input::Enter => 1,
_ => return Ok(Step::Continue),
};
if self.stage == InsStage::Rows {
self.rows = Some(n);
} else {
self.cols = Some(n);
}
return self.advance(s);
}
InsStage::RowSpacing => match i {
Input::Point(a) => self.stage = InsStage::Cell(a),
Input::Text(t) => {
self.row_spacing = Some(distance(&t)?);
return self.advance(s);
}
_ => {}
},
InsStage::Cell(a) => {
if let Input::Point(b) = i {
(self.col_spacing, self.row_spacing) = (Some(b.x - a.x), Some(b.y - a.y));
return self.advance(s);
}
}
InsStage::ColSpacing => {
if let Input::Text(t) = i {
self.col_spacing = Some(distance(&t)?);
return self.advance(s);
}
}
InsStage::Attribs => {
let (tag, _, default, _) = self.attdefs.get(self.values.len()).cloned().unwrap_or_default();
let v = match i {
@@ -937,7 +1091,9 @@ impl Interactive for InsertM {
_ => Vec2::new(1.0, 1.0),
});
let rot = self.rotation.unwrap_or(if self.stage == InsStage::Rotation { at.angle_to(c) } else { 0.0 });
vec![EntityKind::Insert(insert_entity(b, at, self.scale3(scale), rot, self.base))]
let mut ins = insert_entity(b, at, self.scale3(scale), rot, self.base);
self.grid().apply(&mut ins);
vec![EntityKind::Insert(ins)]
}
}
+19
View File
@@ -55,6 +55,10 @@ fn explode_block_ref(d: &cadcraft_doc::Drawing, e: &Entity, ins: &cadcraft_doc::
if let Some(items) = crate::cmd::array::explode_array(d, e) {
return Some(items);
}
// A MINSERT grid stays whole, as in AutoCAD 2024 (see `explode`).
if minserted(ins) {
return None;
}
let blk = d.block(&ins.block)?;
let m = ins.transform(blk.base.xy());
let stretched = m.similarity_scale().is_none();
@@ -223,9 +227,24 @@ pub(crate) fn explode_kind(d: &cadcraft_doc::Drawing, e: &Entity) -> Option<Vec<
}
pub(super) fn explode(s: &mut Session, hs: &[Handle]) -> Result<Vec<Handle>> {
// AutoCAD 2024 refuses a MINSERT grid: "The object is minserted." (Locked ones are left
// without a word, as `explode_with` leaves them.)
let d = s.doc()?;
let grids = hs
.iter()
.filter(|h| !crate::cmd::curves::is_locked(s, **h) && d.entity(**h).is_some_and(|e| matches!(&e.kind, EntityKind::Insert(i) if minserted(i))))
.count();
for _ in 0..grids {
s.echo("The object is minserted.");
}
explode_with(s, hs, explode_kind)
}
/// A block reference with more than one row or column (MINSERT).
fn minserted(i: &cadcraft_doc::Insert) -> bool {
i.rows > 1 || i.cols > 1
}
/// Replace each of `hs` (off locked layers) by the pieces `pieces` gives for it; objects it gives
/// none for are left alone. EXPLODE, and BURST with its own pieces for block references.
pub(crate) fn explode_with(
+1
View File
@@ -81,6 +81,7 @@ mod malformed_handles;
mod matchprop;
mod measure_distance_snapping;
mod metric_linetypes;
mod minsert;
mod mirror_degenerate_line;
mod mleader_content;
mod mleader_remember;
+218
View File
@@ -0,0 +1,218 @@
//! MINSERT, one block reference repeated in rows and columns, as AutoCAD 2024 makes and edits it
//! (accoreconsole: the prompts, and `entget` of the reference after MINSERT, EXPLODE, MIRROR,
//! SCALE and ROTATE).
use std::f64::consts::PI;
use cadcraft_engine::Session;
use cadcraft_engine::doc::{EntityKind, Insert};
use serde_json::{Value, json};
fn handle(v: &Value) -> String {
v.get("handle").unwrap_or(&v["handles"][0]).as_str().unwrap().to_owned()
}
/// A session with block B (a line from (0, 0) to (10, 0) and a circle of radius 3 round (5, 5),
/// base point (0, 0)), after the AutoLISP `code`.
fn run(code: &str) -> Session {
let mut s = Session::new();
let a = handle(&s.execute("line", &json!({"points": [[0, 0], [10, 0]]})).unwrap());
let c = handle(&s.execute("circle", &json!({"center": [5, 5], "radius": 3})).unwrap());
s.execute("-block", &json!({"name": "B", "base": [0, 0], "handles": [a, c]})).unwrap();
s.cmdline(code).unwrap();
assert!(s.running.is_none(), "{}", s.prompt_text());
s
}
fn inserts(s: &Session) -> Vec<Insert> {
s.doc()
.unwrap()
.model
.iter()
.filter_map(|e| match &e.kind {
EntityKind::Insert(i) => Some(i.clone()),
_ => None,
})
.collect()
}
/// The grid as `entget` gives it: 70 columns, 71 rows, 44 between columns, 45 between rows (a
/// plain reference, one row and one column, as 1/1/0/0).
fn grid(i: &Insert) -> (u32, u32, f64, f64) {
(i.cols, i.rows, i.col_spacing, i.row_spacing)
}
fn near(a: f64, b: f64) -> bool {
(a - b).abs() < 1e-9
}
#[test]
fn the_grid_is_stored_as_autocad_2024_stores_it() {
// (MINSERT's answers after the block name and insertion point, the grid AutoCAD stored.)
let cases = [
// One row: no distance between rows asked, and 0 stored for it.
("1 \"\" 0 1 3 20.0", (3, 1, 20.0, 0.0)),
("1 \"\" 0 3 1 15.0", (1, 3, 0.0, 15.0)),
// One of each: a plain reference.
("1 \"\" 0 1 1", (1, 1, 0.0, 0.0)),
// A unit cell: its width between columns, its height between rows, signed.
("1 \"\" 0 2 3 '(0.0 0.0) '(25.0 12.0)", (3, 2, 25.0, 12.0)),
("1 \"\" 0 2 3 '(25.0 12.0) '(0.0 0.0)", (3, 2, -25.0, -12.0)),
// Its sides along the drawing's axes, the reference rotated or not.
("1 \"\" 30 2 3 '(0.0 0.0) '(25.0 12.0)", (3, 2, 25.0, 12.0)),
// Typed distances are kept as typed, negative or 0 too.
("1 \"\" 0 2 2 -15.0 -20.0", (2, 2, -20.0, -15.0)),
("1 \"\" 0 2 2 0.0 20.0", (2, 2, 20.0, 0.0)),
// The distances are not scaled with the reference.
("2 \"\" 30 2 2 15.0 20.0", (2, 2, 20.0, 15.0)),
];
for (answers, want) in cases {
let s = run(&format!("(command \"_.MINSERT\" \"B\" '(20.0 10.0) {answers})"));
let [i] = inserts(&s).try_into().unwrap();
let got = grid(&i);
assert!(got.0 == want.0 && got.1 == want.1 && near(got.2, want.2) && near(got.3, want.3), "{answers}: {got:?}");
assert!(near(i.insert.x, 20.0) && near(i.insert.y, 10.0));
}
let s = run("(command \"_.MINSERT\" \"B\" '(20.0 10.0) 2 \"\" 30 2 2 15.0 20.0)");
let [i] = inserts(&s).try_into().unwrap();
assert!(near(i.scale.x, 2.0) && near(i.scale.y, 2.0) && near(i.rotation, PI / 6.0));
}
#[test]
fn the_prompts_follow_autocad_2024() {
let mut s = Session::new();
let l = handle(&s.execute("line", &json!({"points": [[0, 0], [10, 0]]})).unwrap());
s.execute("-block", &json!({"name": "B", "base": [0, 0], "handles": [l]})).unwrap();
// Each prompt, and what was written to the command line on the way to it.
let mut asked = Vec::new();
for input in ["minsert", "B", "20,10", "1", "", "0", "0", "2", "3", "x", "15", "20"] {
let logged = s.log.len();
s.cmdline(input).unwrap();
asked.extend(s.log.iter().skip(logged).filter(|l| l.starts_with("Requires")).cloned());
if let Some(p) = s.current_prompt() {
asked.push(p.display());
}
}
// The grid's prompts and messages are AutoCAD 2024's; the ones before are INSERT's.
assert_eq!(
asked,
[
"Enter block name or [?]:",
"Specify insertion point or [Basepoint/Scale/X/Y/Z/Rotate]:",
"Enter X scale factor, specify opposite corner or [Corner/XYZ] <1>:",
"Enter Y scale factor <use X scale factor>:",
"Specify rotation angle <0>:",
"Enter number of rows (---) <1>:",
"Requires an integer between 1 and 32767.",
"Enter number of rows (---) <1>:",
"Enter number of columns (|||) <1>:",
"Enter distance between rows or specify unit cell (---):",
"Requires a distance.",
"Enter distance between rows or specify unit cell (---):",
"Specify distance between columns (|||):",
]
);
assert!(s.running.is_none());
let [i] = inserts(&s).try_into().unwrap();
assert_eq!(grid(&i), (3, 2, 20.0, 15.0));
}
#[test]
fn counts_run_from_1_to_32767() {
// AutoCAD 2024: "Requires an integer between 1 and 32767."
let mut s = Session::new();
let l = handle(&s.execute("line", &json!({"points": [[0, 0], [10, 0]]})).unwrap());
s.execute("-block", &json!({"name": "B", "base": [0, 0], "handles": [l]})).unwrap();
for input in ["minsert", "B", "0,0", "1", "", "0", "32768", "32767", "1", "2"] {
s.cmdline(input).unwrap();
}
assert!(s.running.is_none(), "{}", s.prompt_text());
assert_eq!(s.log.iter().filter(|l| l.starts_with("Requires an integer between 1 and 32767.")).count(), 1, "{:?}", s.log);
let [i] = inserts(&s).try_into().unwrap();
assert_eq!(grid(&i), (1, 32767, 0.0, 2.0));
}
#[test]
fn attribute_values_are_asked_once_after_the_grid() {
let mut s = Session::new();
let l = handle(&s.execute("line", &json!({"points": [[0, 0], [10, 0]]})).unwrap());
let a = handle(&s.execute("attdef", &json!({"tag": "TAG", "prompt": "Value", "at": [0, 2], "height": 1})).unwrap());
s.execute("-block", &json!({"name": "BA", "base": [0, 0], "handles": [l, a]})).unwrap();
for input in ["minsert", "BA", "0,700", "1", "", "0", "5", "4", "15", "20"] {
s.cmdline(input).unwrap();
}
// After the grid, with no "<>" for an empty default (AutoCAD 2024: "Value: hello").
assert_eq!(s.current_prompt().unwrap().display(), "Value:");
s.cmdline("hello").unwrap();
assert!(s.running.is_none());
let [i] = inserts(&s).try_into().unwrap();
assert_eq!(grid(&i), (4, 5, 20.0, 15.0));
assert_eq!(i.attribs.iter().map(|a| a.text.value.as_str()).collect::<Vec<_>>(), ["hello"]);
}
#[test]
fn explode_leaves_a_grid_whole() {
let s = run("(progn (command \"_.MINSERT\" \"B\" '(20.0 10.0) 1 \"\" 0 2 2 15.0 20.0) (command \"_.EXPLODE\" (entlast) \"\"))");
let [i] = inserts(&s).try_into().unwrap();
assert_eq!(grid(&i), (2, 2, 20.0, 15.0));
assert_eq!(s.doc().unwrap().model.iter().count(), 1);
assert!(s.log.iter().any(|l| l.contains("The object is minserted.")), "{:?}", s.log);
}
#[test]
fn edits_carry_the_grid_as_in_autocad_2024() {
let made = "(command \"_.MINSERT\" \"B\" '(20.0 10.0) 1 \"\" 0 2 2 15.0 20.0)";
// (the edit, the copy it leaves last: insertion point, X and Y scale, rotation, grid)
let cases = [
("(command \"_.SCALE\" (entlast) \"\" '(20.0 10.0) 2)", ([20.0, 10.0], [2.0, 2.0], 0.0, (2, 2, 40.0, 30.0))),
("(command \"_.ROTATE\" (entlast) \"\" '(20.0 10.0) 30)", ([20.0, 10.0], [1.0, 1.0], PI / 6.0, (2, 2, 20.0, 15.0))),
("(command \"_.MOVE\" (entlast) \"\" '(20.0 10.0) '(25.0 13.0))", ([25.0, 13.0], [1.0, 1.0], 0.0, (2, 2, 20.0, 15.0))),
// Mirrored about the vertical through the insertion point, and about the horizontal: X
// scale -1, and the columns run the other way.
("(command \"_.MIRROR\" (entlast) \"\" '(20.0 10.0) '(20.0 30.0) \"_N\")", ([20.0, 10.0], [-1.0, 1.0], 0.0, (2, 2, -20.0, 15.0))),
("(command \"_.MIRROR\" (entlast) \"\" '(20.0 10.0) '(40.0 10.0) \"_N\")", ([20.0, 10.0], [-1.0, 1.0], PI, (2, 2, -20.0, 15.0))),
];
for (edit, (at, scale, rot, want)) in cases {
let s = run(&format!("(progn {made} {edit})"));
let i = inserts(&s).pop().unwrap();
let got = grid(&i);
assert!(
near(i.insert.x, at[0])
&& near(i.insert.y, at[1])
&& near(i.scale.x, scale[0])
&& near(i.scale.y, scale[1])
&& near(i.rotation, rot)
&& got.0 == want.0
&& got.1 == want.1
&& near(got.2, want.2)
&& near(got.3, want.3),
"{edit}: {i:?}"
);
}
}
#[test]
fn the_json_form_makes_the_same_grid() {
let mut s = Session::new();
let l = handle(&s.execute("line", &json!({"points": [[0, 0], [10, 0]]})).unwrap());
s.execute("-block", &json!({"name": "B", "base": [0, 0], "handles": [l]})).unwrap();
s.execute("minsert", &json!({"name": "B", "at": [20, 10], "rows": 2, "columns": 3, "rowSpacing": 15, "columnSpacing": 20})).unwrap();
// One row: its distance is not kept.
s.execute("minsert", &json!({"name": "B", "at": [20, 100], "rows": 1, "columns": 3, "rowSpacing": 15, "columnSpacing": 20})).unwrap();
let got: Vec<_> = inserts(&s).iter().map(grid).collect();
assert_eq!(got, [(3, 2, 20.0, 15.0), (3, 1, 20.0, 0.0)]);
for bad in [
json!({"name": "B", "at": [0, 0], "rows": 0}),
json!({"name": "B", "at": [0, 0], "columns": 32768}),
json!({"name": "B", "at": [0, 0], "rows": 2.5}),
// A distance that is not a number is refused, not taken as 0.
json!({"name": "B", "at": [0, 0], "rows": 2, "rowSpacing": "15"}),
json!({"name": "B", "at": [0, 0], "columns": 2, "columnSpacing": true}),
] {
assert!(s.execute("minsert", &bad).is_err(), "{bad}");
}
// A whole number written as 2.0 is a count; `explode` is INSERT's and leaves the grid made.
let r = s.execute("minsert", &json!({"name": "B", "at": [0, 200], "rows": 2.0, "rowSpacing": 15, "explode": true})).unwrap();
assert!(r["handle"].is_string(), "{r}");
assert_eq!(inserts(&s).last().map(grid), Some((1, 2, 0.0, 15.0)));
}
+1
View File
@@ -533,6 +533,7 @@
Multiline Text Chữ nhiều dòng
Multiline Text... Chữ nhiều dòng...
Multiple Nhiều
Multiple Insert Chèn nhiều
Multiple Point Nhiều điểm
My Của tôi
My properties: not available yet Thuộc tính của tôi: chưa khả dụng
1 # CADCraft Vietnamese interface catalog — original translations by @2ez4gcx with Claude Code (AI-assisted).
533
534
535
536
537
538
539
+1
View File
@@ -448,6 +448,7 @@
Multiline Text 多行文字
Multiline Text... 多行文字...
Multiple 多个
Multiple Insert 多重插入
Multiple Point 多点
My 我的
My properties: not available yet 我的属性:暂不可用
1 # CADCraft Simplified Chinese interface catalog — original translations.
448
449
450
451
452
453
454