From 6fe715e11c62156367a03ea16e1a6717d00fdaa0 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 24 Oct 2026 03:14:34 -0700 Subject: [PATCH] THICKNESS: Format > Thickness, and new 2D objects take the current thickness New lines, arcs, circles, points and lightweight polylines (for example PLINE, POLYGON, DONUT and BOUNDARY) now take the THICKNESS system variable as their 3D thickness. THICKNESS could already be set with SETVAR and with the Properties palette when nothing is selected, but new objects were always created with a thickness of 0, apart from RECTANG's own Thickness option. RECTANG keeps that option, and single-line text does not take THICKNESS, as in AutoCAD. The new THICKNESS command is on Format > Thickness. Typed or picked from the menu, it asks "Enter new value for THICKNESS <0.0000>:"; Enter keeps the current value, and a negative value is accepted. docs/parity-checklist.md is regenerated. --- crates/engine/src/cmd/draw2.rs | 11 +++---- crates/engine/src/cmd/props.rs | 41 +++++++++++++++++++++++++++ crates/engine/src/lib.rs | 13 +++++++++ crates/engine/src/tests.rs | 52 ++++++++++++++++++++++++++++++++++ docs/parity-checklist.md | 15 +++------- 5 files changed, 114 insertions(+), 18 deletions(-) diff --git a/crates/engine/src/cmd/draw2.rs b/crates/engine/src/cmd/draw2.rs index 68dfd5dbc..527f66624 100644 --- a/crates/engine/src/cmd/draw2.rs +++ b/crates/engine/src/cmd/draw2.rs @@ -1631,10 +1631,9 @@ pub(crate) fn run_rectang2(s: &mut Session, p: &Value) -> Result { let first = if centered { a - Vec2::new(w / 2.0, h / 2.0).rotate(rot) } else { a }; let k = rect_kind(first, w, h, rot, fillet, ch, width, f64_or(p, "elevation", 0.0)); let hd = s.add_entity(k)?; + // A rectangle has its own thickness option and does not take THICKNESS. let th = f64_or(p, "thickness", 0.0); - if th != 0.0 { - s.doc_mut()?.modify_entity(hd, |e| e.common.thickness = th)?; - } + s.doc_mut()?.modify_entity(hd, |e| e.common.thickness = th)?; added(hd) } @@ -1683,10 +1682,8 @@ impl RectM2 { } let first = self.origin(w, h).unwrap_or(a); let hd = s.add_entity(rect_kind(first, w, h, self.rotation, self.fillet, self.chamfer, self.width, self.elevation))?; - if self.thickness != 0.0 { - let t = self.thickness; - s.doc_mut()?.modify_entity(hd, |e| e.common.thickness = t)?; - } + let t = self.thickness; + s.doc_mut()?.modify_entity(hd, |e| e.common.thickness = t)?; Ok(Step::Done) } /// The rectangle's local origin (the corner `rect_kind` draws from) for a size: the first diff --git a/crates/engine/src/cmd/props.rs b/crates/engine/src/cmd/props.rs index 7ef3aa687..75dc0b046 100644 --- a/crates/engine/src/cmd/props.rs +++ b/crates/engine/src/cmd/props.rs @@ -32,6 +32,11 @@ pub fn specs() -> Vec { CommandSpec::new("lweight", "Lineweight...", run_lweight).menu(&["Format", "Lineweight..."]).alias(&["lw", "lineweight"]).params("{lineweight: mm | ByLayer}"), CommandSpec::new("ltscale", "Linetype Scale", run_ltscale).alias(&["lts"]).params("{scale}").interactive(|_| Ok(Box::new(LtscaleM))), CommandSpec::new("units", "Units...", run_units).menu(&["Format", "Units..."]).alias(&["un"]).params("{lunits?: 1..5, luprec?: 0..8, aunits?: 0..4, auprec?: 0..8, insunits?}"), + CommandSpec::new("thickness", "Thickness", run_thickness) + .menu(&["Format", "Thickness"]) + .params("{thickness} (THICKNESS: the 3D thickness given to new lines, arcs, circles, points and polylines; may be negative) → {thickness}") + .transparent() + .interactive(|_| Ok(Box::new(ThicknessM))), CommandSpec::new("limits", "Drawing Limits", run_limits).menu(&["Format", "Drawing Limits"]).params("{min: [x,y], max: [x,y]}"), CommandSpec::new("style", "Text Style...", run_style) .menu(&["Format", "Text Style..."]) @@ -534,6 +539,42 @@ impl Interactive for LtscaleM { } } +fn run_thickness(s: &mut Session, p: &Value) -> Result { + let t = f64_req("thickness", p, "thickness")?; + if !t.is_finite() || t.abs() > 1e12 { + return Err(bad("thickness", "thickness is out of range")); + } + s.doc_mut()?.header.set_f64("THICKNESS", t); + Ok(json!({ "thickness": t })) +} + +/// Format > Thickness and typed THICKNESS: "Enter new value for THICKNESS <0.0000>:". +struct ThicknessM; + +impl Interactive for ThicknessM { + fn name(&self) -> &'static str { + "THICKNESS" + } + fn prompt(&self, s: &Session) -> Prompt { + let cur = s.doc().map(|d| d.header.f64("THICKNESS", 0.0)).unwrap_or(0.0); + Prompt::new("Enter new value for THICKNESS", Accept::NUMBER).default(format!("{cur:.4}")) + } + fn input(&mut self, s: &mut Session, i: Input) -> Result { + let text = match i { + Input::Enter | Input::Cancel => return Ok(Step::Done), + Input::Text(t) => t, + _ => return Ok(Step::Continue), + }; + match super::machines::number(&text).map(|t| run_thickness(s, &json!({ "thickness": t }))) { + Some(Ok(_)) => Ok(Step::Done), + _ => { + s.echo("Requires a number."); + Ok(Step::Continue) + } + } + } +} + fn run_units(s: &mut Session, p: &Value) -> Result { let d = s.doc_mut()?; for (k, lo, hi) in [("lunits", 1, 5), ("luprec", 0, 8), ("aunits", 0, 4), ("auprec", 0, 8), ("insunits", 0, 24)] { diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index 117db5813..e37aadbdd 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -885,6 +885,19 @@ impl Session { if d.annotative_by_style(&kind) { common.anno_scales = vec![d.current_anno_scale()]; } + // Lines, arcs, circles, points, polylines and 2D solids take the current 3D thickness + // (THICKNESS). RECTANG sets its own, and text never takes it. + if matches!( + kind, + EntityKind::Line(_) + | EntityKind::Arc(_) + | EntityKind::Circle(_) + | EntityKind::Point(_) + | EntityKind::LwPolyline(_) + | EntityKind::Solid(_) + ) { + common.thickness = Some(d.header.f64("THICKNESS", 0.0)).filter(|t| t.is_finite()).unwrap_or(0.0); + } let h = self.doc_mut()?.add(&space, common, kind)?; Ok(h) } diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index a6c4c41d2..731256107 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -1309,6 +1309,58 @@ fn typed_ltscale_asks_for_the_scale() { assert_eq!(s.doc().unwrap().header.f64("LTSCALE", 0.0), 0.1); } +#[test] +fn format_thickness_asks_for_the_thickness() { + let mut s = Session::new(); + s.start("thickness").unwrap(); + assert!(s.prompt_text().contains("Enter new value for THICKNESS <0.0000>"), "{}", s.prompt_text()); + // After text that is not a number, the prompt asks again. + s.cmdline("thick").unwrap(); + assert!(s.current_prompt().is_some(), "asked again"); + // A negative thickness is accepted. + s.cmdline("-1.5").unwrap(); + assert!(s.current_prompt().is_none()); + assert_eq!(s.doc().unwrap().header.f64("THICKNESS", 0.0), -1.5); + // Typed, Enter keeps the current thickness. + s.cmdline("thickness").unwrap(); + assert!(s.prompt_text().contains("<-1.5000>"), "{}", s.prompt_text()); + s.cmdline("").unwrap(); + assert!(s.current_prompt().is_none()); + assert_eq!(s.doc().unwrap().header.f64("THICKNESS", 0.0), -1.5); + assert!(s.execute("thickness", &json!({"thickness": "thick"})).is_err()); + assert!(s.execute("thickness", &json!({"thickness": 1e13})).is_err()); +} + +#[test] +fn new_objects_take_the_current_thickness() { + let mut s = Session::new(); + s.execute("thickness", &json!({"thickness": 2.5})).unwrap(); + s.execute("line", &json!({"points": [[0, 0], [10, 0]]})).unwrap(); + s.execute("arc", &json!({"center": [0, 0], "radius": 3, "start": 0, "end": 90})).unwrap(); + s.execute("circle", &json!({"center": [0, 0], "radius": 5})).unwrap(); + s.execute("point", &json!({"at": [1, 1]})).unwrap(); + s.execute("pline", &json!({"vertices": [[0, 0], [5, 0], [5, 5]]})).unwrap(); + s.execute("polygon", &json!({"sides": 6, "center": [20, 20], "radius": 2})).unwrap(); + s.execute("donut", &json!({"center": [30, 30], "inside": 1, "outside": 2})).unwrap(); + let thick: Vec<(&str, f64)> = s.doc().unwrap().model.iter().map(|e| (e.kind.type_name(), e.common.thickness)).collect(); + assert_eq!(thick.len(), 7, "{thick:?}"); + assert!(thick.iter().all(|(_, t)| *t == 2.5), "{thick:?}"); + // RECTANG keeps its own thickness option, and text does not take THICKNESS. + s.execute("rectang", &json!({"p1": [0, 0], "p2": [4, 4]})).unwrap(); + assert_eq!(s.doc().unwrap().model.last().unwrap().common.thickness, 0.0); + s.execute("rectang", &json!({"p1": [0, 0], "p2": [4, 4], "thickness": 1})).unwrap(); + assert_eq!(s.doc().unwrap().model.last().unwrap().common.thickness, 1.0); + let before = s.doc().unwrap().model.len(); + s.cmdline("rectang 0,0 4,4").unwrap(); + assert_eq!(s.doc().unwrap().model.len(), before + 1); + assert_eq!(s.doc().unwrap().model.last().unwrap().common.thickness, 0.0); + s.execute("text", &json!({"at": [0, 0], "text": "A"})).unwrap(); + assert_eq!(s.doc().unwrap().model.last().unwrap().common.thickness, 0.0); + // An ellipse has no thickness. + s.execute("ellipse", &json!({"center": [0, 0], "major": [4, 0], "ratio": 0.5})).unwrap(); + assert_eq!(s.doc().unwrap().model.last().unwrap().common.thickness, 0.0); +} + fn setvar_prompt_open(s: &Session) -> bool { s.running.as_ref().is_some_and(|r| r.id == "setvar") } diff --git a/docs/parity-checklist.md b/docs/parity-checklist.md index 5e1c7cf7f..a25961050 100644 --- a/docs/parity-checklist.md +++ b/docs/parity-checklist.md @@ -1,11 +1,10 @@ # Parity checklist -> **Generated:** 2026-10-15 by `cargo xtask parity`. Do not edit by hand. -> **Generated:** 2026-10-16 by `cargo xtask parity`. Do not edit by hand. +> **Generated:** 2026-10-24 by `cargo xtask parity`. Do not edit by hand. Menu-breadth parity: a reference menu item counts as covered when a CADCraft command is registered under the same menu label. It measures breadth, not depth; see [target-app-parity.md](target-app-parity.md) for the weighted assessment. -**Menu breadth: 283 / 491 items (58%)** · 427 registered commands. +**Menu breadth: 288 / 491 items (59%)** · 434 registered commands. | Menu | Items | Covered | % | |---|---|---|---| @@ -13,8 +12,8 @@ Menu-breadth parity: a reference menu item counts as covered when a CADCraft com | Edit | 13 | 11 | 85% | | View | 75 | 31 | 41% | | Insert | 11 | 5 | 45% | -| Format | 27 | 25 | 93% | -| Tools | 85 | 60 | 71% | +| Format | 27 | 26 | 96% | +| Tools | 85 | 64 | 75% | | Draw | 87 | 55 | 63% | | Dimension | 28 | 28 | 100% | | Modify | 113 | 52 | 46% | @@ -85,7 +84,6 @@ Menu-breadth parity: a reference menu item counts as covered when a CADCraft com - Insert > ACIS File... - Insert > Drawing Exchange Binary... - Format > Transparency -- Format > Thickness - Tools > Spelling - Tools > Inquiry > Volume - Tools > Update Fields @@ -101,13 +99,8 @@ Menu-breadth parity: a reference menu item counts as covered when a CADCraft com - Tools > New UCS > Z - Tools > Named UCS... - Tools > Customize > Interface (CUI)... -- Tools > Customize > Edit Command Aliases (PGP)... - Tools > Import Settings - Tools > Export Settings -- Tools > Express Tools > Convert Text to Mtext -- Tools > Express Tools > Break-line Symbol -- Tools > Express Tools > Move/Copy/Rotate -- Tools > Express Tools > Explode Attributes to Text - Tools > Express Tools > Replace Block... - Tools > Express Tools > Super Hatch > Image... - Tools > Express Tools > Super Hatch > Xref...