Merge pull request #1343 from HakanSeven12/plot/b2-dialog-options-sysvars

feat(plot): page-setup units, custom scale, dialog options and plot sysvars
This commit is contained in:
Hakan Seven committed 2026-09-19 22:59:36 +03:00
commit 879bb70c12
29 files changed
+896 -69

No files matched your search

+12
View File
@@ -12,6 +12,7 @@ mod fileops;
mod inquiry;
mod layerprops;
mod layers;
mod plotvars;
mod styleprops;
mod view;
@@ -263,6 +264,9 @@ impl OpenCADStudio {
if let Some(t) = self.dispatch_layerprops(cmd, i) {
return Some(t);
}
if let Some(t) = self.dispatch_plotvars(cmd, i) {
return Some(t);
}
if let Some(t) = self.dispatch_styleprops(cmd, i) {
return Some(t);
}
@@ -555,6 +559,14 @@ inventory::submit!(crate::command::CommandRegistration {
"DELOBJ",
"PLINEGEN",
"PSLTSCALE",
// Plot preferences and the current-layout variables (commands/plotvars.rs).
"PLOTOFFSET",
"PAPERUPDATE",
"PLOTROTMODE",
"PLOTTRANSPARENCYOVERRIDE",
"BACKGROUNDPLOT",
"CTAB",
"TILEMODE",
"DISPSILH",
"WORLDVIEW",
"LIMCHECK",
+247
View File
@@ -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"));
}
}
+1
View File
@@ -1073,6 +1073,7 @@ impl OpenCADStudio {
let name = it.next().unwrap_or("").to_uppercase();
let value = it.next().map(|s| s.trim().to_string());
if name.is_empty() || name == "?" {
self.command_line.push_info(&super::plotvars::setvar_listing());
self.command_line.push_info(
crate::t!("SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR SHORTCUTMENU SHORTCUTMENUDURATION CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY CONSTRAINTBARMODE ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only)").as_ref(),
);