SETVAR/GETVAR: header variables the commands keep, TRANSPARENCYDISPLAY, dimension variables in SETVAR ? (fixes #461) (#467)

Squash of PR #467 by @vangel76.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Valentin KleinandClaude Opus 5.5 authored and GitHub committed 2026-10-11 11:36:59 -04:00
1 parent 0371c448f6
commit 10f7a9b2a3
3 files changed
+331 -1

No files matched your search

+10 -1
View File
@@ -4,6 +4,8 @@ use serde_json::{Value, json};
use crate::{EngineError, Result, Session};
mod table;
const SESSION_VARS: &[&str] = &[
"OSMODE",
"OSNAPHATCH",
@@ -113,6 +115,9 @@ pub fn get(s: &Session, name: &str) -> Option<Value> {
json!([p.x, p.y, 0.0])
}
_ => {
if let Some(v) = table::get(s, &n) {
return Some(v);
}
let d = s.doc().ok()?;
match d.header.get(&n) {
Some(h) => serde_json::to_value(h).ok()?,
@@ -146,7 +151,7 @@ fn as_pt(v: &Value) -> Option<cadcraft_geom::Vec2> {
const READ_ONLY: &[&str] = &["CMDNAMES", "DWGNAME", "DBMOD", "CTAB", "LASTPOINT", "VIEWCTR", "VIEWSIZE", "EXTMIN", "EXTMAX", "UNDOCTL", "UNDOMARKS"];
pub fn is_read_only(name: &str) -> bool {
READ_ONLY.iter().any(|r| r.eq_ignore_ascii_case(name.trim()))
READ_ONLY.iter().any(|r| r.eq_ignore_ascii_case(name.trim())) || table::is_read_only(&name.trim().to_ascii_uppercase())
}
/// POLARADDANG as typed: up to ten angles in degrees separated by `;` (radians out); `None` when
@@ -226,6 +231,9 @@ pub fn set(s: &mut Session, name: &str, v: &Value) -> Result<()> {
s.doc_mut()?.header.set_f64("ANGBASE", cadcraft_geom::norm_angle(a));
}
_ => {
if let Some(r) = table::set(s, &n, v) {
return r;
}
let d = s.doc_mut()?;
let val = match v {
Value::Number(x) if x.is_i64() => cadcraft_doc::HVal::Int(x.as_i64().unwrap_or(0)),
@@ -285,5 +293,6 @@ pub fn list(s: &Session) -> Value {
}
}
}
table::list(s, &mut m);
Value::Object(m)
}
+221
View File
@@ -0,0 +1,221 @@
//! System variables the engine already keeps in the drawing header (or in [`crate::Settings`]) and
//! reads with a default, described once in a table: their type, allowed values and default. GETVAR
//! reports the default until a value is set, SETVAR checks the value against the table, and
//! `SETVAR ?` lists them. Variables CADCraft doesn't model are not in the table.
use serde_json::{Value, json};
use crate::{EngineError, Result, Session, Settings};
/// What a header variable holds and which values it takes.
#[derive(Clone, Copy, Debug)]
enum Kind {
/// An integer from the list.
Choice(&'static [i64]),
/// An integer in `min..=max`.
Int(i64, i64),
/// A real number of at least `min` (`strict`: greater than `min`).
Real { min: f64, strict: bool },
/// Any finite real number.
AnyReal,
/// An angle: kept in radians, shown and typed in degrees (a typed text follows AUNITS).
Angle,
/// A hatch pattern name from the library.
Pattern,
/// A block name; `.` clears it.
Name,
/// Text set by its own command, not by SETVAR.
ReadOnlyStr,
}
/// A default that may depend on the drawing's units (MEASUREMENT 1 = metric).
#[derive(Clone, Copy, Debug)]
enum Def {
Int(i64),
Real(f64),
/// Imperial, metric.
Units(f64, f64),
Str(&'static str),
}
/// Header variables: name, kind, the default the commands use while the header has none.
const HEADER: &[(&str, Kind, Def)] = &[
("CENTEREXE", Kind::Real { min: 0.0, strict: false }, Def::Units(0.12, 3.5)),
("CHAMFERA", Kind::Real { min: 0.0, strict: false }, Def::Real(0.0)),
("CHAMFERB", Kind::Real { min: 0.0, strict: false }, Def::Real(0.0)),
("CHAMFERC", Kind::Real { min: 0.0, strict: false }, Def::Real(0.0)),
("CHAMFERD", Kind::Angle, Def::Real(0.0)),
("CHAMMODE", Kind::Choice(&[0, 1]), Def::Int(0)),
("CIRCLERAD", Kind::Real { min: 0.0, strict: false }, Def::Real(0.0)),
("CMLJUST", Kind::Choice(&[0, 1, 2]), Def::Int(0)),
("CMLSCALE", Kind::AnyReal, Def::Units(1.0, 20.0)),
("CPLOTSTYLE", Kind::ReadOnlyStr, Def::Str("ByLayer")),
("DIMASSOC", Kind::Choice(&[0, 1, 2]), Def::Int(2)),
("DONUTID", Kind::Real { min: 0.0, strict: false }, Def::Real(0.5)),
("DONUTOD", Kind::Real { min: 0.0, strict: true }, Def::Real(1.0)),
("EDGEMODE", Kind::Choice(&[0, 1]), Def::Int(0)),
("HPANG", Kind::Angle, Def::Real(0.0)),
("HPNAME", Kind::Pattern, Def::Str("ANSI31")),
("HPSCALE", Kind::Real { min: 0.0, strict: true }, Def::Real(1.0)),
("INSNAME", Kind::Name, Def::Str("")),
("LWDEFAULT", Kind::Choice(&LINEWEIGHTS), Def::Int(25)),
// Negative: Through (OFFSET picks a point).
("OFFSETDIST", Kind::AnyReal, Def::Real(-1.0)),
("OFFSETERASE", Kind::Choice(&[0, 1]), Def::Int(0)),
("OFFSETLAYER", Kind::Choice(&[0, 1]), Def::Int(0)),
("POLYSIDES", Kind::Int(3, 1024), Def::Int(4)),
("PROJMODE", Kind::Choice(&[0, 1, 2]), Def::Int(1)),
("REVCLOUDARCLEN", Kind::Real { min: 0.0, strict: true }, Def::Units(0.5, 15.0)),
("SPLINESEGS", Kind::Int(-32768, 32767), Def::Int(8)),
("SPLKNOTS", Kind::Choice(&[0, 1, 2]), Def::Int(0)),
("TEXTFILL", Kind::Choice(&[0, 1]), Def::Int(1)),
("TRIMEXTENDMODE", Kind::Choice(&[0, 1]), Def::Int(1)),
("TRIMMODE", Kind::Choice(&[0, 1]), Def::Int(1)),
("WIPEOUTFRAME", Kind::Choice(&[0, 1, 2]), Def::Int(1)),
];
/// The standard lineweights in hundredths of a millimetre.
const LINEWEIGHTS: [i64; 24] = [0, 5, 9, 13, 15, 18, 20, 25, 30, 35, 40, 50, 53, 60, 70, 80, 90, 100, 106, 120, 140, 158, 200, 211];
/// On/off session settings: name, read, write.
type Toggle = (&'static str, fn(&Settings) -> bool, fn(&mut Settings, bool));
const SETTINGS: &[Toggle] = &[("TRANSPARENCYDISPLAY", |s| s.transparency_display, |s, v| s.transparency_display = v)];
fn header_var(name: &str) -> Option<&'static (&'static str, Kind, Def)> {
HEADER.iter().find(|(n, _, _)| *n == name)
}
fn toggle(name: &str) -> Option<&'static Toggle> {
SETTINGS.iter().find(|(n, _, _)| *n == name)
}
/// Whether `name` (upper case) is a table variable SETVAR can't change.
pub(super) fn is_read_only(name: &str) -> bool {
header_var(name).is_some_and(|(_, k, _)| matches!(k, Kind::ReadOnlyStr))
}
/// The value of a table variable (upper-case `name`), or None when it isn't one.
pub(super) fn get(s: &Session, name: &str) -> Option<Value> {
if let Some((_, read, _)) = toggle(name) {
return Some(json!(i32::from(read(&s.settings))));
}
let (_, kind, def) = header_var(name)?;
let d = s.doc().ok()?;
let h = &d.header;
let real_def = match *def {
Def::Real(f) => f,
Def::Units(imperial, metric) => {
if h.i64("MEASUREMENT", 0) == 1 {
metric
} else {
imperial
}
}
Def::Int(i) => i as f64,
Def::Str(_) => 0.0,
};
Some(match (kind, def) {
(Kind::Choice(_) | Kind::Int(..), Def::Int(i)) => json!(h.i64(name, *i)),
(Kind::Angle, _) => json!(h.f64(name, real_def).to_degrees()),
(_, Def::Str(t)) => json!(h.str(name, t)),
_ => json!(h.f64(name, real_def)),
})
}
fn int_value(v: &Value) -> Option<i64> {
match v {
Value::Number(n) => n.as_i64().or_else(|| n.as_f64().filter(|f| f.fract() == 0.0 && f.abs() < 1e15).map(|f| f as i64)),
Value::Bool(b) => Some(i64::from(*b)),
Value::String(t) => t.trim().parse().ok(),
_ => None,
}
}
/// Set a table variable (upper-case `name`); None when it isn't one.
pub(super) fn set(s: &mut Session, name: &str, v: &Value) -> Option<Result<()>> {
let bad = |allowed: String| EngineError::BadParams { cmd: "setvar".into(), msg: format!("invalid value for {name} ({allowed})") };
if let Some((_, _, write)) = toggle(name) {
let r = match int_value(v) {
Some(i @ (0 | 1)) => {
write(&mut s.settings, i == 1);
Ok(())
}
_ => Err(bad("0 or 1".into())),
};
return Some(r);
}
let (_, kind, _) = header_var(name)?;
Some(set_header(s, name, *kind, v, bad))
}
fn set_header(s: &mut Session, name: &str, kind: Kind, v: &Value, bad: impl Fn(String) -> EngineError) -> Result<()> {
let real = || super::as_f64(v);
match kind {
Kind::Choice(allowed) => {
let i = int_value(v).filter(|i| allowed.contains(i));
let list = || allowed.iter().map(i64::to_string).collect::<Vec<_>>().join(", ");
let i = i.ok_or_else(|| bad(list()))?;
s.doc_mut()?.header.set_i64(name, i);
}
Kind::Int(min, max) => {
let i = int_value(v).filter(|i| (min..=max).contains(i)).ok_or_else(|| bad(format!("{min} to {max}")))?;
s.doc_mut()?.header.set_i64(name, i);
}
Kind::Real { min, strict } => {
let f = real().filter(|f| if strict { *f > min } else { *f >= min });
let f = f.ok_or_else(|| bad(format!("a number {} {min}", if strict { "above" } else { "of at least" })))?;
s.doc_mut()?.header.set_f64(name, f);
}
Kind::AnyReal => {
let f = real().ok_or_else(|| bad("a number".into()))?;
s.doc_mut()?.header.set_f64(name, f);
}
Kind::Angle => {
let a = match v {
Value::String(t) => s.angle_settings().amount(t),
_ => v.as_f64().map(f64::to_radians),
};
let a = a.filter(|a| a.is_finite()).ok_or_else(|| bad("an angle".into()))?;
s.doc_mut()?.header.set_f64(name, cadcraft_geom::norm_angle(a));
}
Kind::Pattern => {
let p = v.as_str().map(|t| t.trim().to_ascii_uppercase()).filter(|p| cadcraft_doc::library::pattern(p).is_some());
let p = p.ok_or_else(|| bad("a hatch pattern name".into()))?;
s.doc_mut()?.header.set_str(name, &p);
}
Kind::Name => {
let t = v.as_str().map(str::trim).filter(|t| t.chars().count() <= 255).ok_or_else(|| bad("a name, or . for none".into()))?;
let t = if t == "." { "" } else { t };
s.doc_mut()?.header.set_str(name, t);
}
Kind::ReadOnlyStr => return Err(EngineError::BadParams { cmd: "setvar".into(), msg: format!("{name} is read-only") }),
}
Ok(())
}
/// Add the table variables and the dimension variables of the current style to a `SETVAR ?` list.
pub(super) fn list(s: &Session, m: &mut serde_json::Map<String, Value>) {
let names = SETTINGS.iter().map(|(n, _, _)| *n).chain(HEADER.iter().map(|(n, _, _)| *n));
for n in names.chain(cadcraft_doc::DIMVARS.iter().map(|(n, _)| *n)) {
if !m.contains_key(n)
&& let Some(v) = super::get(s, n)
{
m.insert(n.to_string(), v);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn table_names_are_upper_case_and_unique() {
let mut names: Vec<&str> = HEADER.iter().map(|(n, _, _)| *n).chain(SETTINGS.iter().map(|(n, _, _)| *n)).collect();
assert!(names.iter().all(|n| n.chars().all(|c| c.is_ascii_uppercase() || c.is_ascii_digit())));
let len = names.len();
names.sort_unstable();
names.dedup();
assert_eq!(names.len(), len);
}
}
+100
View File
@@ -0,0 +1,100 @@
//! SETVAR/GETVAR cover the variables the commands keep in the drawing header with a default
//! (CIRCLERAD, OFFSETDIST, POLYSIDES, HPNAME…), TRANSPARENCYDISPLAY and the dimension variables in
//! `SETVAR ?`; values are checked; unknown variables still give an error.
use cadcraft_engine::Session;
use serde_json::{Value, json};
fn getvar(s: &mut Session, name: &str) -> Value {
s.execute("getvar", &json!({ "name": name })).unwrap()["value"].clone()
}
fn setvar(s: &mut Session, name: &str, value: Value) -> bool {
s.execute("setvar", &json!({ "name": name, "value": value })).is_ok()
}
#[test]
fn defaults_are_the_ones_the_commands_use() {
let mut s = Session::new();
for (name, v) in [
("POLYSIDES", json!(4)),
("OFFSETDIST", json!(-1.0)),
("TRIMMODE", json!(1)),
("PROJMODE", json!(1)),
("EDGEMODE", json!(0)),
("HPNAME", json!("ANSI31")),
("HPSCALE", json!(1.0)),
("HPANG", json!(0.0)),
("DONUTID", json!(0.5)),
("DONUTOD", json!(1.0)),
("WIPEOUTFRAME", json!(1)),
("LWDEFAULT", json!(25)),
("DIMASSOC", json!(2)),
("CPLOTSTYLE", json!("ByLayer")),
("TRANSPARENCYDISPLAY", json!(0)),
] {
assert_eq!(getvar(&mut s, name), v, "{name}");
}
// Lowercase names work; the list shows the new variables and the dimension variables.
assert_eq!(getvar(&mut s, "circlerad"), json!(0.0));
let all = cadcraft_engine::sysvars::list(&s);
for n in ["POLYSIDES", "SPLINESEGS", "CHAMFERD", "TRANSPARENCYDISPLAY", "DIMASZ", "OSMODE", "CLAYER"] {
assert!(all.get(n).is_some(), "{n} listed");
}
}
#[test]
fn values_reach_the_commands() {
let mut s = Session::new();
assert!(setvar(&mut s, "POLYSIDES", json!(6)));
s.cmdline("polygon").unwrap();
assert!(s.current_prompt().unwrap().display().contains("<6>"), "{}", s.current_prompt().unwrap().display());
s.cancel();
assert!(setvar(&mut s, "HPANG", json!(30)));
assert_eq!(getvar(&mut s, "HPANG").as_f64().unwrap().round(), 30.0, "degrees in, degrees out");
assert!((s.doc().unwrap().header.f64("HPANG", 0.0) - 30f64.to_radians()).abs() < 1e-9, "kept in radians");
// Typed at the command line.
s.cmdline("setvar hpname net").unwrap();
assert_eq!(getvar(&mut s, "HPNAME"), json!("NET"));
s.cmdline("setvar transparencydisplay 1").unwrap();
assert!(s.settings.transparency_display);
assert!(setvar(&mut s, "INSNAME", json!("Valve")));
assert!(setvar(&mut s, "INSNAME", json!(".")));
assert_eq!(getvar(&mut s, "INSNAME"), json!(""));
}
#[test]
fn bad_values_and_unknown_names_are_refused() {
let mut s = Session::new();
for (name, v) in [
("POLYSIDES", json!(2)),
("POLYSIDES", json!(5000)),
("TRIMMODE", json!(3)),
("LWDEFAULT", json!(26)),
("HPSCALE", json!(0)),
("HPNAME", json!("NO-SUCH-PATTERN")),
("DONUTOD", json!(0)),
("CIRCLERAD", json!(-1)),
("SPLINESEGS", json!(1e9)),
("TRANSPARENCYDISPLAY", json!(5)),
("CHAMFERD", json!("abc")),
("CPLOTSTYLE", json!("Style 1")),
("OFFSETDIST", json!(f64::MAX.to_string() + "0")),
] {
assert!(!setvar(&mut s, name, v.clone()), "{name} = {v} refused");
}
assert_eq!(getvar(&mut s, "POLYSIDES"), json!(4), "unchanged");
assert!(cadcraft_engine::sysvars::is_read_only("cplotstyle"));
assert!(s.execute("getvar", &json!({ "name": "NOSUCHVAR" })).is_err());
s.cmdline("setvar nosuchvar").unwrap();
assert!(s.log.iter().any(|l| l.contains("Unknown variable NOSUCHVAR")), "{:?}", s.log);
}
#[test]
fn metric_defaults() {
let mut s = Session::new();
s.execute("setvar", &json!({ "name": "MEASUREMENT", "value": 1 })).unwrap();
assert_eq!(getvar(&mut s, "REVCLOUDARCLEN"), json!(15.0));
assert_eq!(getvar(&mut s, "CMLSCALE"), json!(20.0));
assert_eq!(getvar(&mut s, "CENTEREXE"), json!(3.5));
}