Files
OneCAD-Draft/src/app/commands/plotvars.rs
T

263 lines
11 KiB
Rust

//! Plot system variables and the page-setup import command. `PLOTOFFSET`,
//! `PAPERUPDATE`, `PLOTROTMODE`, `PLOTTRANSPARENCYOVERRIDE` and
//! `BACKGROUNDPLOT` are plot preferences of the application, kept with the
//! Plot dialog's persisted settings; `CTAB` and `TILEMODE` say which layout
//! of the drawing is current. Each is typeable on its own (`PLOTOFFSET 1`,
//! `CTAB Layout1`) and through `SETVAR`, and a bare name prompts for the
//! value like the other variables. `PSETUPIN` brings named page setups in
//! from another drawing (see `app::update::page_setup_import`).
use crate::app::{Message, OpenCADStudio};
use iced::Task;
/// The variables this family answers for, for the command registry and the
/// `SETVAR ?` listing.
pub(super) const PLOT_SYSVARS: &[&str] = &[
"PLOTOFFSET",
"PAPERUPDATE",
"PLOTROTMODE",
"PLOTTRANSPARENCYOVERRIDE",
"BACKGROUNDPLOT",
"CTAB",
"TILEMODE",
];
/// The `SETVAR ?` line for this family.
pub(super) fn setvar_listing() -> String {
format!("SETVAR (plot): {}", PLOT_SYSVARS.join(" "))
}
/// What setting a variable did.
enum Outcome {
/// The value was set (or was already that); the variable is part of the
/// application's settings and persists with them.
Preference(String),
/// The value was set and the drawing changed accordingly, possibly with
/// a follow-up task (a layout switch).
Drawing(String, Task<Message>),
/// No value given: the current one, for the prompt.
Current(String),
}
impl OpenCADStudio {
pub(super) fn dispatch_plotvars(&mut self, cmd: &str, i: usize) -> Option<Task<Message>> {
// PSETUPIN [file [* | name,name…]] — the dash form is the same
// command; both read the file and report on the command line.
for name in ["PSETUPIN", "-PSETUPIN"] {
if cmd == name {
return Some(self.on_psetupin(""));
}
if let Some(args) = cmd.strip_prefix(name).filter(|rest| rest.starts_with(' ')) {
return Some(self.on_psetupin(args));
}
}
let rest = cmd.strip_prefix("SETVAR ").map(str::trim).unwrap_or(cmd);
let mut parts = rest.splitn(2, char::is_whitespace);
let name = parts.next().unwrap_or("").to_ascii_uppercase();
if !PLOT_SYSVARS.contains(&name.as_str()) {
return None;
}
let value = parts.next().map(str::trim).filter(|v| !v.is_empty());
let task = match self.plot_sysvar(i, &name, value) {
Ok(Outcome::Preference(shown)) => {
self.persist_settings_if_changed();
self.command_line.push_output(&format!("{name} = {shown}"));
Task::none()
}
Ok(Outcome::Drawing(shown, task)) => {
self.command_line.push_output(&format!("{name} = {shown}"));
task
}
Ok(Outcome::Current(current)) => {
self.command_line
.push_output(crate::tf!("Enter new value for {name} <{current}>:").as_ref());
self.pending_setvar = Some(name);
Task::none()
}
Err(error) => {
self.command_line.push_error(&error);
Task::none()
}
};
Some(task)
}
fn plot_sysvar(
&mut self,
i: usize,
name: &str,
value: Option<&str>,
) -> Result<Outcome, String> {
let parse_range = |value: &str, max: u8| -> Result<u8, String> {
value
.parse::<u8>()
.ok()
.filter(|v| *v <= max)
.ok_or_else(|| format!("SETVAR: integer from 0 to {max} required."))
};
let d = &mut self.plot_dialog;
match name {
"PLOTOFFSET" => Ok(match value {
Some(v) => {
d.plot_offset_from_edge = parse_range(v, 1)? == 1;
Outcome::Preference((d.plot_offset_from_edge as u8).to_string())
}
None => Outcome::Current((d.plot_offset_from_edge as u8).to_string()),
}),
"PAPERUPDATE" => Ok(match value {
Some(v) => {
d.paper_update = parse_range(v, 1)?;
Outcome::Preference(d.paper_update.to_string())
}
None => Outcome::Current(d.paper_update.to_string()),
}),
"PLOTROTMODE" => Ok(match value {
Some(v) => {
d.plot_rot_mode = parse_range(v, 2)?;
Outcome::Preference(d.plot_rot_mode.to_string())
}
None => Outcome::Current(d.plot_rot_mode.to_string()),
}),
"PLOTTRANSPARENCYOVERRIDE" => Ok(match value {
Some(v) => {
d.transparency_override = parse_range(v, 2)?;
Outcome::Preference(d.transparency_override.to_string())
}
None => Outcome::Current(d.transparency_override.to_string()),
}),
// Bit 1: single plots run in the background; bit 2: batch plots.
"BACKGROUNDPLOT" => {
let current = d.background as u8 | (d.background_publish as u8) << 1;
Ok(match value {
Some(v) => {
let bits = parse_range(v, 3)?;
d.background = bits & 1 != 0;
d.background_publish = bits & 2 != 0;
Outcome::Preference(bits.to_string())
}
None => Outcome::Current(current.to_string()),
})
}
"CTAB" => {
let current = self.tabs[i].scene.current_layout.clone();
let Some(v) = value else {
return Ok(Outcome::Current(current));
};
let Some(target) = self.tabs[i]
.scene
.layout_names()
.into_iter()
.find(|layout| layout.eq_ignore_ascii_case(v))
else {
return Err(format!("CTAB: no layout named \"{v}\"."));
};
let task = if target == current {
Task::none()
} else {
self.on_layout_switch(target.clone())
};
Ok(Outcome::Drawing(target, task))
}
// 1: model space; 0: the paper layout that was last current (the
// drawing's own `CTAB`), else the first one.
"TILEMODE" => {
let in_model = self.tabs[i].scene.current_layout == "Model";
let Some(v) = value else {
return Ok(Outcome::Current((in_model as u8).to_string()));
};
let model = parse_range(v, 1)? == 1;
if model == in_model {
return Ok(Outcome::Drawing((model as u8).to_string(), Task::none()));
}
let target = if model {
"Model".to_string()
} else {
let scene = &self.tabs[i].scene;
let layouts = scene.layout_names();
crate::io::saved_active_layout(&scene.document)
.filter(|saved| saved != "Model" && layouts.contains(saved))
.or_else(|| layouts.into_iter().find(|layout| layout != "Model"))
.ok_or_else(|| "TILEMODE: the drawing has no paper layout.".to_string())?
};
let task = self.on_layout_switch(target);
Ok(Outcome::Drawing((model as u8).to_string(), task))
}
_ => Err(format!("SETVAR: unknown variable \"{name}\".")),
}
}
}
#[cfg(test)]
mod tests {
use crate::app::OpenCADStudio;
fn app() -> OpenCADStudio {
let mut app = OpenCADStudio::new_for_test();
app.automation_op(r#"{"op":"new"}"#);
app
}
fn last_line(app: &OpenCADStudio) -> String {
app.command_line
.history
.last()
.map(|line| line.text.clone())
.unwrap_or_default()
}
#[test]
fn plot_preferences_are_typeable_directly_and_through_setvar() {
let mut app = app();
let _ = app.run_command_line("PLOTOFFSET 1");
assert!(app.plot_dialog.plot_offset_from_edge);
assert_eq!(last_line(&app), "PLOTOFFSET = 1");
let _ = app.run_command_line("SETVAR PAPERUPDATE 1");
assert_eq!(app.plot_dialog.paper_update, 1);
let _ = app.run_command_line("PLOTROTMODE 0");
assert_eq!(app.plot_dialog.plot_rot_mode, 0);
let _ = app.run_command_line("PLOTTRANSPARENCYOVERRIDE 2");
assert_eq!(app.plot_dialog.transparency_override, 2);
// Out-of-range values are refused and leave the setting alone.
let _ = app.run_command_line("PLOTROTMODE 3");
assert_eq!(app.plot_dialog.plot_rot_mode, 0);
assert!(last_line(&app).contains("0 to 2"));
// The preferences reach the persisted plot config.
let config = app.current_config();
assert!(config.plot.plot_offset_from_edge);
assert_eq!(config.plot.transparency_override, 2);
}
#[test]
fn backgroundplot_maps_to_the_two_background_switches() {
let mut app = app();
let _ = app.run_command_line("BACKGROUNDPLOT 2");
assert!(!app.plot_dialog.background);
assert!(app.plot_dialog.background_publish);
let _ = app.run_command_line("BACKGROUNDPLOT 3");
assert!(app.plot_dialog.background && app.plot_dialog.background_publish);
let _ = app.run_command_line("BACKGROUNDPLOT");
assert_eq!(app.pending_setvar.as_deref(), Some("BACKGROUNDPLOT"));
assert!(last_line(&app).contains("<3>"));
}
#[test]
fn ctab_and_tilemode_switch_the_current_layout() {
let mut app = app();
assert_eq!(app.tabs[app.active_tab].scene.current_layout, "Model");
let _ = app.run_command_line("CTAB layout1");
assert_eq!(app.tabs[app.active_tab].scene.current_layout, "Layout1");
assert_eq!(last_line(&app), "CTAB = Layout1");
let _ = app.run_command_line("TILEMODE 1");
assert_eq!(app.tabs[app.active_tab].scene.current_layout, "Model");
// Back to paper space: the layout that was current last.
let _ = app.run_command_line("SETVAR TILEMODE 0");
assert_eq!(app.tabs[app.active_tab].scene.current_layout, "Layout1");
let _ = app.run_command_line("CTAB Nowhere");
assert!(last_line(&app).contains("no layout named"));
assert_eq!(app.tabs[app.active_tab].scene.current_layout, "Layout1");
let _ = app.run_command_line("CTAB");
assert!(last_line(&app).contains("<Layout1>"));
assert_eq!(app.pending_setvar.as_deref(), Some("CTAB"));
}
}