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...