Files
OneCAD-Draft/src/app/commands/plotvars.rs
T
PanosandClaude Opus 5 74936063f9 feat(plot): PSETUPIN and page-setup import, page setup for new layouts
- PSETUPIN / -PSETUPIN: with no arguments a file picker offers the chosen
  drawing's named page setups in the Plot dialog; `PSETUPIN <file>` opens
  the same chooser on that file; `PSETUPIN <file> *` or
  `PSETUPIN <file> name,name` import straight away. Same-named setups are
  replaced, the import is one undo step, and the report counts both.
- The Plot dialog's page-setup list gains "Import…": the chooser shows the
  source drawing's setups with a tick each, All, Import and Cancel, and
  selects the first imported setup afterwards. The source is read off the
  UI thread with the same reader the editor uses for its own setups
  (`scene::document_page_setups`), and handles that only meant something
  in the source drawing (plot view, visual style) are dropped on import.
- Options → General → Plotting: "Show the page setup for new layouts"
  opens the Plot / Page Setup dialog whenever a layout is created
  (persisted with the plot preferences).

Tests: the document reader, the command (all / named / unknown / missing
file / undo) against a drawing saved by the app, the chooser flow, and the
new-layout option. Nine new strings in all 21 locales.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-18 14:53:09 +03:00

260 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"));
}
}