forked from 1b-agent/opencadstudio-upstream
feat(plot): page-setup units, custom scale, hide-paperspace, save-to-layout and the plot sysvars
Plot dialog: - "Units" (millimetres / inches) is the page setup's own unit: the offsets and the custom scale are shown in it, the stored ratio converts with it so the plotted result and the paper space's unit stay the same - "Custom: [paper] mm = [drawing] units" spells the picked scale in that unit and accepts a typed ratio, which selects the list's name for it or becomes a ratio entry of its own - "Hide paperspace objects" is the page setup's plot-hidden flag; it is no longer derived from the "Hidden Line" shade mode - "Save changes to layout" (paper layouts, on by default) writes the dialog into the layout when a plot is sent; previews never touch it - offsets are labelled in the page-setup unit and kept in millimetres in the file System variables (typeable, through SETVAR, listed by SETVAR ?): - PLOTOFFSET, PAPERUPDATE, PLOTROTMODE, PLOTTRANSPARENCYOVERRIDE and BACKGROUNDPLOT live with the persisted plot preferences: the plot origin counts from the printable corner unless PLOTOFFSET says the paper edge; a printer that does not list the sheet either warns or switches to its default sheet; transparency can be forced on or off; BACKGROUNDPLOT bit 1 is the dialog's background switch and bit 2 drives batch plots - CTAB and TILEMODE read and switch the current layout Tests cover every new control and variable; six new strings in all 21 locales; the Plot dialog grows to 940×690 for the two extra rows. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2b38f798c0
commit
7efb04a666
29 files changed
+896
-69
No files matched your search
@@ -0,0 +1,247 @@
|
||||
//! Plot system variables. `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.
|
||||
|
||||
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>> {
|
||||
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"));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user