Files
cadcraft/crates/engine/src/sysvars/table.rs
T

230 lines
9.5 KiB
Rust

//! 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)),
// 1: HATCH and GRADIENT typed or from a menu open the Hatch and Gradient dialog; 0: the command line.
("HPDLGMODE", Kind::Choice(&[0, 1]), Def::Int(1)),
("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)),
// XCLIP boundaries: 0 hidden, 1 shown and plotted, 2 shown but not plotted.
("XCLIPFRAME", Kind::Choice(&[0, 1, 2]), Def::Int(2)),
];
/// 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] = &[
("AUDITCTL", |s| s.auditctl, |s, v| s.auditctl = v),
("ATTREQ", |s| s.attreq, |s, v| s.attreq = v),
("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);
}
}