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.
This commit is contained in:
dotCooCoo
2026-10-24 03:24:13 -07:00
parent b9f96d3dd3
commit 6fe715e11c
5 changed files with 114 additions and 18 deletions
+4 -7
View File
@@ -1631,10 +1631,9 @@ pub(crate) fn run_rectang2(s: &mut Session, p: &Value) -> Result<Value> {
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
+41
View File
@@ -32,6 +32,11 @@ pub fn specs() -> Vec<CommandSpec> {
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<Value> {
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<Step> {
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<Value> {
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)] {
+13
View File
@@ -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)
}
+52
View File
@@ -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")
}
+4 -11
View File
@@ -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...