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:
PanosandClaude Opus 5 committed 2026-09-18 14:53:01 +03:00
1 parent 2b38f798c0
commit 7efb04a666
29 files changed
+893 -66

No files matched your search

+6
View File
@@ -5111,6 +5111,12 @@ plot =
.system-default-printer = طابعة النظام الافتراضية
.what-to-plot = ما المراد طباعته
.x-mm = X (مم)
.x-in = X (بوصة)
.y-in = Y (بوصة)
.hide-paperspace-objects = إخفاء كائنات مساحة الورق
.save-changes-to-layout = حفظ التغييرات في التخطيط
.printer-does-not-support-sheet-switched-to-its-default-sheet-default-paperupdate-1 = __ocs_fmt_0__ لا يدعم __ocs_fmt_1__؛ تم التبديل إلى الورقة الافتراضية __ocs_fmt_2__ (PAPERUPDATE = 1).
.printer-does-not-support-sheet-the-sheet-is-kept-paperupdate-0 = __ocs_fmt_0__ لا يدعم __ocs_fmt_1__؛ تم الاحتفاظ بالورقة (PAPERUPDATE = 0).
.y-mm = Y (مم)
manage =
+6
View File
@@ -5099,6 +5099,12 @@ plot =
.system-default-printer = Системен принтер по подразбиране
.what-to-plot = Какво да се плотира
.x-mm = X (мм)
.x-in = X (инча)
.y-in = Y (инча)
.hide-paperspace-objects = Скриване на обектите в пространството на листа
.save-changes-to-layout = Запазване на промените в оформлението
.printer-does-not-support-sheet-switched-to-its-default-sheet-default-paperupdate-1 = __ocs_fmt_0__ не поддържа __ocs_fmt_1__; превключено към листа по подразбиране __ocs_fmt_2__ (PAPERUPDATE = 1).
.printer-does-not-support-sheet-the-sheet-is-kept-paperupdate-0 = __ocs_fmt_0__ не поддържа __ocs_fmt_1__; листът се запазва (PAPERUPDATE = 0).
.y-mm = Y (мм)
manage =
+6
View File
@@ -5096,6 +5096,12 @@ plot =
.system-default-printer = Výchozí tiskárna systému
.what-to-plot = Co fabulovat
.x-mm = X (mm)
.x-in = X (palce)
.y-in = Y (palce)
.hide-paperspace-objects = Skrýt objekty výkresového prostoru
.save-changes-to-layout = Uložit změny do rozvržení
.printer-does-not-support-sheet-switched-to-its-default-sheet-default-paperupdate-1 = __ocs_fmt_0__ nepodporuje __ocs_fmt_1__; přepnuto na výchozí list __ocs_fmt_2__ (PAPERUPDATE = 1).
.printer-does-not-support-sheet-the-sheet-is-kept-paperupdate-0 = __ocs_fmt_0__ nepodporuje __ocs_fmt_1__; list byl zachován (PAPERUPDATE = 0).
.y-mm = Y (mm)
manage =
+6
View File
@@ -5097,6 +5097,12 @@ plot =
.system-default-printer = Systemstandarddrucker
.what-to-plot = Was zu plotten
.x-mm = X (mm)
.x-in = X (Zoll)
.y-in = Y (Zoll)
.hide-paperspace-objects = Papierbereichsobjekte ausblenden
.save-changes-to-layout = Änderungen im Layout speichern
.printer-does-not-support-sheet-switched-to-its-default-sheet-default-paperupdate-1 = __ocs_fmt_0__ unterstützt __ocs_fmt_1__ nicht; auf das Standardblatt __ocs_fmt_2__ gewechselt (PAPERUPDATE = 1).
.printer-does-not-support-sheet-the-sheet-is-kept-paperupdate-0 = __ocs_fmt_0__ unterstützt __ocs_fmt_1__ nicht; das Blatt wird beibehalten (PAPERUPDATE = 0).
.y-mm = Y (mm)
manage =
+6
View File
@@ -5099,6 +5099,12 @@ plot =
.system-default-printer = Προεπιλεγμένος εκτυπωτής συστήματος
.what-to-plot = Τι θα εκτυπωθεί
.x-mm = X (mm)
.x-in = X (in)
.y-in = Y (in)
.hide-paperspace-objects = Απόκρυψη αντικειμένων χώρου χαρτιού
.save-changes-to-layout = Αποθήκευση αλλαγών στο layout
.printer-does-not-support-sheet-switched-to-its-default-sheet-default-paperupdate-1 = Ο __ocs_fmt_0__ δεν υποστηρίζει το __ocs_fmt_1__· έγινε αλλαγή στο προεπιλεγμένο φύλλο του __ocs_fmt_2__ (PAPERUPDATE = 1).
.printer-does-not-support-sheet-the-sheet-is-kept-paperupdate-0 = Ο __ocs_fmt_0__ δεν υποστηρίζει το __ocs_fmt_1__· το φύλλο διατηρείται (PAPERUPDATE = 0).
.y-mm = Y (mm)
manage =
+6
View File
@@ -5100,6 +5100,12 @@ plot =
.system-default-printer = System default printer
.what-to-plot = What to plot
.x-mm = X (mm)
.x-in = X (in)
.y-in = Y (in)
.hide-paperspace-objects = Hide paperspace objects
.save-changes-to-layout = Save changes to layout
.printer-does-not-support-sheet-switched-to-its-default-sheet-default-paperupdate-1 = __ocs_fmt_0__ does not support __ocs_fmt_1__; switched to its default sheet __ocs_fmt_2__ (PAPERUPDATE = 1).
.printer-does-not-support-sheet-the-sheet-is-kept-paperupdate-0 = __ocs_fmt_0__ does not support __ocs_fmt_1__; the sheet is kept (PAPERUPDATE = 0).
.y-mm = Y (mm)
manage =
+6
View File
@@ -5099,6 +5099,12 @@ plot =
.system-default-printer = Impresora predeterminada del sistema
.what-to-plot = Qué trazar
.x-mm = X (mm)
.x-in = X (pulg)
.y-in = Y (pulg)
.hide-paperspace-objects = Ocultar objetos del espacio papel
.save-changes-to-layout = Guardar cambios en la presentación
.printer-does-not-support-sheet-switched-to-its-default-sheet-default-paperupdate-1 = __ocs_fmt_0__ no admite __ocs_fmt_1__; se cambió a su hoja predeterminada __ocs_fmt_2__ (PAPERUPDATE = 1).
.printer-does-not-support-sheet-the-sheet-is-kept-paperupdate-0 = __ocs_fmt_0__ no admite __ocs_fmt_1__; se conserva la hoja (PAPERUPDATE = 0).
.y-mm = Y (mm)
manage =
+6
View File
@@ -5098,6 +5098,12 @@ plot =
.system-default-printer = Järjestelmän oletustulostin
.what-to-plot = Mitä piirtää
.x-mm = X (mm)
.x-in = X (tuumaa)
.y-in = Y (tuumaa)
.hide-paperspace-objects = Piilota paperitilan objektit
.save-changes-to-layout = Tallenna muutokset asetteluun
.printer-does-not-support-sheet-switched-to-its-default-sheet-default-paperupdate-1 = __ocs_fmt_0__ ei tue arkkia __ocs_fmt_1__; vaihdettiin oletusarkkiin __ocs_fmt_2__ (PAPERUPDATE = 1).
.printer-does-not-support-sheet-the-sheet-is-kept-paperupdate-0 = __ocs_fmt_0__ ei tue arkkia __ocs_fmt_1__; arkki säilytetään (PAPERUPDATE = 0).
.y-mm = Y (mm)
manage =
+6
View File
@@ -5099,6 +5099,12 @@ plot =
.system-default-printer = Imprimante système par défaut
.what-to-plot = Quoi faire?
.x-mm = X (mm)
.x-in = X (po)
.y-in = Y (po)
.hide-paperspace-objects = Masquer les objets de l'espace papier
.save-changes-to-layout = Enregistrer les modifications dans la présentation
.printer-does-not-support-sheet-switched-to-its-default-sheet-default-paperupdate-1 = __ocs_fmt_0__ ne prend pas en charge __ocs_fmt_1__ ; passage à sa feuille par défaut __ocs_fmt_2__ (PAPERUPDATE = 1).
.printer-does-not-support-sheet-the-sheet-is-kept-paperupdate-0 = __ocs_fmt_0__ ne prend pas en charge __ocs_fmt_1__ ; la feuille est conservée (PAPERUPDATE = 0).
.y-mm = Y (mm)
manage =
+6
View File
@@ -5087,6 +5087,12 @@ plot =
.system-default-printer = सिस्टम डिफ़ॉल्ट प्रिंटर
.what-to-plot = क्या प्लॉट करना है
.x-mm = एक्स (मिमी)
.x-in = एक्स (इंच)
.y-in = वाई (इंच)
.hide-paperspace-objects = पेपर स्पेस ऑब्जेक्ट छिपाएँ
.save-changes-to-layout = लेआउट में परिवर्तन सहेजें
.printer-does-not-support-sheet-switched-to-its-default-sheet-default-paperupdate-1 = __ocs_fmt_0__ __ocs_fmt_1__ का समर्थन नहीं करता; इसकी डिफ़ॉल्ट शीट __ocs_fmt_2__ पर स्विच किया गया (PAPERUPDATE = 1)।
.printer-does-not-support-sheet-the-sheet-is-kept-paperupdate-0 = __ocs_fmt_0__ __ocs_fmt_1__ का समर्थन नहीं करता; शीट रखी गई है (PAPERUPDATE = 0)।
.y-mm = Y (मिमी)
manage =
+6
View File
@@ -5098,6 +5098,12 @@ plot =
.system-default-printer = Rendszer alapértelmezett nyomtató
.what-to-plot = Mit kell kitalálni
.x-mm = X (mm)
.x-in = X (hüvelyk)
.y-in = Y (hüvelyk)
.hide-paperspace-objects = Papírtér-objektumok elrejtése
.save-changes-to-layout = Módosítások mentése az elrendezésbe
.printer-does-not-support-sheet-switched-to-its-default-sheet-default-paperupdate-1 = A(z) __ocs_fmt_0__ nem támogatja a(z) __ocs_fmt_1__ lapot; átváltva az alapértelmezett __ocs_fmt_2__ lapra (PAPERUPDATE = 1).
.printer-does-not-support-sheet-the-sheet-is-kept-paperupdate-0 = A(z) __ocs_fmt_0__ nem támogatja a(z) __ocs_fmt_1__ lapot; a lap megmarad (PAPERUPDATE = 0).
.y-mm = Y (mm)
manage =
+6
View File
@@ -5098,6 +5098,12 @@ plot =
.system-default-printer = Stampante predefinita del sistema
.what-to-plot = Cosa tracciare
.x-mm = X (mm)
.x-in = X (poll.)
.y-in = Y (poll.)
.hide-paperspace-objects = Nascondi oggetti dello spazio carta
.save-changes-to-layout = Salva le modifiche nel layout
.printer-does-not-support-sheet-switched-to-its-default-sheet-default-paperupdate-1 = __ocs_fmt_0__ non supporta __ocs_fmt_1__; passato al foglio predefinito __ocs_fmt_2__ (PAPERUPDATE = 1).
.printer-does-not-support-sheet-the-sheet-is-kept-paperupdate-0 = __ocs_fmt_0__ non supporta __ocs_fmt_1__; il foglio viene mantenuto (PAPERUPDATE = 0).
.y-mm = Y (mm)
manage =
+6
View File
@@ -5087,6 +5087,12 @@ plot =
.system-default-printer = システムの既定プリンター
.what-to-plot = 印刷対象
.x-mm = X(mm)
.x-in = X(インチ)
.y-in = Y(インチ)
.hide-paperspace-objects = ペーパー空間のオブジェクトを隠す
.save-changes-to-layout = 変更をレイアウトに保存
.printer-does-not-support-sheet-switched-to-its-default-sheet-default-paperupdate-1 = __ocs_fmt_0__ は __ocs_fmt_1__ に対応していません。既定の用紙 __ocs_fmt_2__ に切り替えました(PAPERUPDATE = 1)。
.printer-does-not-support-sheet-the-sheet-is-kept-paperupdate-0 = __ocs_fmt_0__ は __ocs_fmt_1__ に対応していません。用紙はそのままです(PAPERUPDATE = 0)。
.y-mm = Y(mm)
manage =
+6
View File
@@ -5098,6 +5098,12 @@ plot =
.system-default-printer = 시스템 기본 프린터
.what-to-plot = 무엇을 플롯할 것인가
.x-mm = X (mm)
.x-in = X (인치)
.y-in = Y (인치)
.hide-paperspace-objects = 도면 공간 객체 숨기기
.save-changes-to-layout = 변경 사항을 배치에 저장
.printer-does-not-support-sheet-switched-to-its-default-sheet-default-paperupdate-1 = __ocs_fmt_0__이(가) __ocs_fmt_1__을(를) 지원하지 않습니다. 기본 용지 __ocs_fmt_2__(으)로 전환했습니다 (PAPERUPDATE = 1).
.printer-does-not-support-sheet-the-sheet-is-kept-paperupdate-0 = __ocs_fmt_0__이(가) __ocs_fmt_1__을(를) 지원하지 않습니다. 용지를 유지합니다 (PAPERUPDATE = 0).
.y-mm = Y (mm)
manage =
+6
View File
@@ -5097,6 +5097,12 @@ plot =
.system-default-printer = Standaardprinter van het systeem
.what-to-plot = Wat te plannen
.x-mm = X (mm)
.x-in = X (inch)
.y-in = Y (inch)
.hide-paperspace-objects = Papierruimte-objecten verbergen
.save-changes-to-layout = Wijzigingen in lay-out opslaan
.printer-does-not-support-sheet-switched-to-its-default-sheet-default-paperupdate-1 = __ocs_fmt_0__ ondersteunt __ocs_fmt_1__ niet; overgeschakeld naar het standaardblad __ocs_fmt_2__ (PAPERUPDATE = 1).
.printer-does-not-support-sheet-the-sheet-is-kept-paperupdate-0 = __ocs_fmt_0__ ondersteunt __ocs_fmt_1__ niet; het blad blijft behouden (PAPERUPDATE = 0).
.y-mm = Y (mm)
manage =
+6
View File
@@ -5098,6 +5098,12 @@ plot =
.system-default-printer = Domyślna drukarka systemowa
.what-to-plot = Co nakreślić
.x-mm = X (mm)
.x-in = X (cale)
.y-in = Y (cale)
.hide-paperspace-objects = Ukryj obiekty przestrzeni papieru
.save-changes-to-layout = Zapisz zmiany w arkuszu
.printer-does-not-support-sheet-switched-to-its-default-sheet-default-paperupdate-1 = __ocs_fmt_0__ nie obsługuje __ocs_fmt_1__; przełączono na domyślny arkusz __ocs_fmt_2__ (PAPERUPDATE = 1).
.printer-does-not-support-sheet-the-sheet-is-kept-paperupdate-0 = __ocs_fmt_0__ nie obsługuje __ocs_fmt_1__; arkusz zachowano (PAPERUPDATE = 0).
.y-mm = Y (mm)
manage =
+6
View File
@@ -5099,6 +5099,12 @@ plot =
.system-default-printer = Impressora padrão do sistema
.what-to-plot = O que plotar
.x-mm = X (mm)
.x-in = X (pol)
.y-in = Y (pol)
.hide-paperspace-objects = Ocultar objetos do espaço do papel
.save-changes-to-layout = Salvar alterações no layout
.printer-does-not-support-sheet-switched-to-its-default-sheet-default-paperupdate-1 = __ocs_fmt_0__ não suporta __ocs_fmt_1__; alterado para sua folha padrão __ocs_fmt_2__ (PAPERUPDATE = 1).
.printer-does-not-support-sheet-the-sheet-is-kept-paperupdate-0 = __ocs_fmt_0__ não suporta __ocs_fmt_1__; a folha foi mantida (PAPERUPDATE = 0).
.y-mm = Y (mm)
manage =
+6
View File
@@ -5100,6 +5100,12 @@ plot =
.system-default-printer = Системный принтер по умолчанию
.what-to-plot = Что спланировать
.x-mm = X (мм)
.x-in = X (дюймы)
.y-in = Y (дюймы)
.hide-paperspace-objects = Скрыть объекты пространства листа
.save-changes-to-layout = Сохранить изменения в листе
.printer-does-not-support-sheet-switched-to-its-default-sheet-default-paperupdate-1 = __ocs_fmt_0__ не поддерживает __ocs_fmt_1__; выбран формат по умолчанию __ocs_fmt_2__ (PAPERUPDATE = 1).
.printer-does-not-support-sheet-the-sheet-is-kept-paperupdate-0 = __ocs_fmt_0__ не поддерживает __ocs_fmt_1__; формат сохранён (PAPERUPDATE = 0).
.y-mm = Y (мм)
manage =
+6
View File
@@ -5039,6 +5039,12 @@ plot =
.system-default-printer = Varsayılan sistem yazıcısı
.what-to-plot = Yazdırılacak Alan
.x-mm = X (mm)
.x-in = X (inç)
.y-in = Y (inç)
.hide-paperspace-objects = Kağıt alanı nesnelerini gizle
.save-changes-to-layout = Değişiklikleri yerleşime kaydet
.printer-does-not-support-sheet-switched-to-its-default-sheet-default-paperupdate-1 = __ocs_fmt_0__, __ocs_fmt_1__ sayfasını desteklemiyor; varsayılan sayfası __ocs_fmt_2__ seçildi (PAPERUPDATE = 1).
.printer-does-not-support-sheet-the-sheet-is-kept-paperupdate-0 = __ocs_fmt_0__, __ocs_fmt_1__ sayfasını desteklemiyor; sayfa korunuyor (PAPERUPDATE = 0).
.y-mm = Y (mm)
manage =
+6
View File
@@ -5082,6 +5082,12 @@ plot =
.system-default-printer = 系统默认打印机
.what-to-plot = 打印内容
.x-mm = X (毫米)
.x-in = X(英寸)
.y-in = Y(英寸)
.hide-paperspace-objects = 隐藏图纸空间对象
.save-changes-to-layout = 将修改保存到布局
.printer-does-not-support-sheet-switched-to-its-default-sheet-default-paperupdate-1 = __ocs_fmt_0__ 不支持 __ocs_fmt_1__;已切换到其默认图纸 __ocs_fmt_2__(PAPERUPDATE = 1)。
.printer-does-not-support-sheet-the-sheet-is-kept-paperupdate-0 = __ocs_fmt_0__ 不支持 __ocs_fmt_1__;保留原图纸(PAPERUPDATE = 0)。
.y-mm = Y (毫米)
manage =
+6
View File
@@ -5094,6 +5094,12 @@ plot =
.system-default-printer = 系統預設印表機
.what-to-plot = 列印內容
.x-mm = X (毫米)
.x-in = X(英吋)
.y-in = Y(英吋)
.hide-paperspace-objects = 隱藏圖紙空間物件
.save-changes-to-layout = 將變更儲存到配置
.printer-does-not-support-sheet-switched-to-its-default-sheet-default-paperupdate-1 = __ocs_fmt_0__ 不支援 __ocs_fmt_1__;已切換至其預設圖紙 __ocs_fmt_2__(PAPERUPDATE = 1)。
.printer-does-not-support-sheet-the-sheet-is-kept-paperupdate-0 = __ocs_fmt_0__ 不支援 __ocs_fmt_1__;保留原圖紙(PAPERUPDATE = 0)。
.y-mm = Y (毫米)
manage =
+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(),
);
+390 -56
View File
@@ -227,18 +227,24 @@ fn standard_scale_for_ratio(paper: f64, drawing: f64) -> Option<acadrust::object
.map(|(_, _, scale)| *scale)
}
/// `12.5000` → `12.5`, `3.0000` → `3`: a fixed-precision number without its
/// trailing zeros, for fields the user reads back.
fn trim_decimals(text: &str) -> String {
if text.contains('.') {
text.trim_end_matches('0').trim_end_matches('.').to_string()
} else {
text.to_string()
}
}
/// A scale-list name for a plot scale the drawing has no name for, spelled as
/// the ratio it is (`1:250`, `25.4:1`) so the value survives in the picker
/// instead of collapsing to 1:1.
fn ratio_scale_name(factor: f64) -> String {
let trim = |value: f64| {
let text = format!("{value:.4}");
text.trim_end_matches('0').trim_end_matches('.').to_string()
};
if factor >= 1.0 {
format!("{}:1", trim(factor))
format!("{}:1", trim_decimals(&format!("{factor:.4}")))
} else {
format!("1:{}", trim(1.0 / factor))
format!("1:{}", trim_decimals(&format!("{:.4}", 1.0 / factor)))
}
}
@@ -3096,6 +3102,143 @@ impl OpenCADStudio {
iced::exit()
}
/// The plot scale as `paper : drawing` in the dialog's page-setup unit,
/// with the standard code it names when it names one. The dialog's
/// factor is per the drawing's scale-family unit (inches for an imperial
/// drawing, else millimetres) and already includes the drawing-unit
/// factor; the page setup counts per its own unit. A named ratio is kept
/// as written when no drawing-unit factor hides inside it — a metre
/// drawing plotting "1:100" really plots 10 mm per unit, which is what
/// the file must say — and its standard code is kept even across a
/// paper-unit conversion, the way the commercial application writes
/// "1:1" on an inch page setup over a millimetre paper space as
/// `1 in = 25.4 units`.
fn dialog_plot_scale_ratio(&self) -> ((f64, f64), Option<acadrust::objects::ScaledType>) {
let d = &self.plot_dialog;
let scene = &self.tabs[self.active_tab].scene;
let factor = plot_dialog_scale_factor(d);
let conversion = scene.scale_family_unit_mm() / d.paper_units.to_mm(1.0);
let named = scene.scale_ratio(&d.scale).filter(|(paper, drawing)| {
(paper / drawing - factor).abs() <= 1e-9 * factor.abs().max(1.0)
});
let standard = named.and_then(|(paper, drawing)| standard_scale_for_ratio(paper, drawing));
let ratio = match named {
Some((paper, drawing)) if (conversion - 1.0).abs() <= 1e-12 => (paper, drawing),
_ => {
let per_unit = (factor * conversion).max(1e-9);
if per_unit >= 1.0 {
(per_unit, 1.0)
} else {
(1.0, 1.0 / per_unit)
}
}
};
(ratio, standard)
}
/// `PAPERUPDATE`: when the printer that just answered does not list the
/// dialog's sheet, either switch to the printer's default sheet (1) or
/// keep the sheet and say so (0).
fn reconcile_sheet_with_printer(&mut self, printer: &str) {
let d = &self.plot_dialog;
let Some(caps) = d.printer_media.as_ref() else {
return;
};
if caps.media.is_empty() {
return;
}
let paper = plot_dialog_paper(d);
if caps.margins_for(&paper).is_some() {
return;
}
let sheet = paper.display();
if d.paper_update == 1 {
let Some(default) = caps
.default_paper
.clone()
.or_else(|| caps.media.first().map(|media| media.paper.clone()))
else {
return;
};
let d = &mut self.plot_dialog;
d.paper = default.canonical.to_string();
(d.paper_width_mm, d.paper_height_mm) = default.sheet_mm(plot_dialog_orientation(d));
let default = default.display();
self.command_line.push_info(
crate::tf!(
"{printer} does not support {sheet}; switched to its default sheet \
{default} (PAPERUPDATE = 1)."
)
.as_ref(),
);
} else {
self.command_line.push_info(
crate::tf!(
"{printer} does not support {sheet}; the sheet is kept (PAPERUPDATE = 0)."
)
.as_ref(),
);
}
}
/// Spell the picked scale in the custom fields.
fn refresh_custom_scale_fields(&mut self) {
let ((paper, drawing), _) = self.dialog_plot_scale_ratio();
let d = &mut self.plot_dialog;
d.custom_scale_paper = trim_decimals(&format!("{paper:.6}"));
d.custom_scale_drawing = trim_decimals(&format!("{drawing:.6}"));
}
/// Make the typed custom ratio the plot scale: the scale list's name for
/// it when it has one, else the ratio itself.
fn adopt_custom_scale(&mut self) {
let d = &self.plot_dialog;
if d.area == "Layout" {
return;
}
let (Ok(paper), Ok(drawing)) = (
d.custom_scale_paper.trim().parse::<f64>(),
d.custom_scale_drawing.trim().parse::<f64>(),
) else {
return;
};
if !(paper > 0.0 && drawing > 0.0 && paper.is_finite() && drawing.is_finite()) {
return;
}
let conversion =
self.tabs[self.active_tab].scene.scale_family_unit_mm() / d.paper_units.to_mm(1.0);
let factor = paper / drawing / conversion;
let d = &mut self.plot_dialog;
d.scale = scale_name_for_factor(&d.scales, factor).unwrap_or_else(|| {
let name = ratio_scale_name(factor);
if !d.scales.iter().any(|(n, _)| n == &name) {
d.scales.push((name.clone(), factor));
d.scales
.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
}
name
});
}
/// The plot origin in millimetres from the sheet's corner: the dialog's
/// offsets in the page-setup unit, counted from the printable area's
/// corner unless `PLOTOFFSET` says from the paper edge.
fn dialog_plot_origin_mm(&self) -> (f64, f64) {
let d = &self.plot_dialog;
let x = d.paper_units.to_mm(d.offset_x.trim().parse::<f64>().unwrap_or(0.0));
let y = d.paper_units.to_mm(d.offset_y.trim().parse::<f64>().unwrap_or(0.0));
if d.plot_offset_from_edge {
return (x, y);
}
let ps = self.plot_settings_from_dialog(self.dialog_base_settings());
let m = ps.margins;
let (left, bottom, ..) = crate::scene::rotated_margins(
(m.left, m.bottom, m.right, m.top),
ps.rotation.to_code(),
);
(x + left, y + bottom)
}
/// The settings the dialog edits on top of: the named page setup it was
/// loaded from, else the active layout's own, else a fresh metric sheet.
/// Everything the dialog has no control for (viewport-border and
@@ -3124,9 +3267,10 @@ impl OpenCADStudio {
mut ps: acadrust::objects::PlotSettings,
) -> acadrust::objects::PlotSettings {
use crate::io::paper_catalog::Orientation;
use acadrust::objects::{PlotType, ScaledType, ShadePlotMode, ShadePlotResolutionLevel};
use acadrust::objects::{
PlotPaperUnits, PlotType, ScaledType, ShadePlotMode, ShadePlotResolutionLevel,
};
let d = &self.plot_dialog;
let scene = &self.tabs[self.active_tab].scene;
// Names and margins are decided against the stored settings before
// any field is overwritten.
let paper_size = paper_name_for_settings(d, &ps);
@@ -3170,15 +3314,20 @@ impl OpenCADStudio {
}
}
ps.flags.plot_centered = d.center && !layout_area;
ps.origin_x = if layout_area {
0.0
// Offsets are typed in the page-setup unit; the file keeps millimetres.
let (origin_x, origin_y) = if layout_area {
(0.0, 0.0)
} else {
d.offset_x.parse::<f64>().unwrap_or(0.0)
(
d.paper_units.to_mm(d.offset_x.trim().parse::<f64>().unwrap_or(0.0)),
d.paper_units.to_mm(d.offset_y.trim().parse::<f64>().unwrap_or(0.0)),
)
};
ps.origin_y = if layout_area {
0.0
} else {
d.offset_y.parse::<f64>().unwrap_or(0.0)
ps.origin_x = origin_x;
ps.origin_y = origin_y;
ps.paper_units = match d.paper_units {
crate::io::paper_catalog::PaperUnits::Inches => PlotPaperUnits::Inches,
crate::io::paper_catalog::PaperUnits::Millimeters => PlotPaperUnits::Millimeters,
};
if d.fit_to_paper && !layout_area {
ps.set_scale_to_fit();
@@ -3186,36 +3335,10 @@ impl OpenCADStudio {
// "standard scale" bit and leaves the ratio it computed.
ps.flags.use_standard_scale = true;
} else {
// The dialog's factor is per the drawing's scale-family unit
// (inches for an imperial drawing, else millimetres) and already
// includes the drawing-unit factor; the file stores it per the
// layout's paper unit. A named ratio is kept as written when no
// drawing-unit factor hides inside it — a metre drawing plotting
// "1:100" really plots 10 mm per unit, which is what the file
// must say — and its standard code is kept even across a
// paper-unit conversion, the way the commercial application
// writes "1:1" on an inch page setup over a millimetre paper
// space as `1 in = 25.4 units`. A layout plot goes through the
// same path: the dialog holds it at 1:1, which the conversion
// turns into whatever keeps the paper space's unit intact.
let factor = plot_dialog_scale_factor(d);
let conversion = scene.scale_family_unit_mm() / plot_paper_unit_mm(ps.paper_units);
let named = scene.scale_ratio(&d.scale).filter(|(paper, drawing)| {
(paper / drawing - factor).abs() <= 1e-9 * factor.abs().max(1.0)
});
let standard =
named.and_then(|(paper, drawing)| standard_scale_for_ratio(paper, drawing));
let (numerator, denominator) = match named {
Some((paper, drawing)) if (conversion - 1.0).abs() <= 1e-12 => (paper, drawing),
_ => {
let per_unit = (factor * conversion).max(1e-9);
if per_unit >= 1.0 {
(per_unit, 1.0)
} else {
(1.0, 1.0 / per_unit)
}
}
};
// A layout plot goes through the same path as the other areas:
// the dialog holds it at 1:1, which the unit conversion turns
// into whatever keeps the paper space's unit intact.
let ((numerator, denominator), standard) = self.dialog_plot_scale_ratio();
// The commercial application leaves the "standard scale" bit
// clear even for a standard code and keeps the ratio in the
// numerator/denominator pair; readers that trust the bit would
@@ -3233,7 +3356,7 @@ impl OpenCADStudio {
ps.flags.plot_plot_styles = d.apply_plot_styles;
ps.flags.show_plot_styles = d.show_plot_styles;
ps.flags.draw_viewports_first = d.paperspace_last;
ps.flags.plot_hidden = d.shade == "Hidden Line";
ps.flags.plot_hidden = d.hide_paperspace;
ps.shade_plot_mode = match d.shade.as_str() {
"2D Wireframe" | "3D Wireframe" => ShadePlotMode::Wireframe,
"Hidden Line" => ShadePlotMode::Hidden,
@@ -3568,7 +3691,7 @@ impl OpenCADStudio {
})
.map_err(|error| crate::tf!("Export failed: {error}").into_owned())
};
self.run_print_all_work(dialog.background, work)
self.run_print_all_work(dialog.background_publish, work)
}
pub(super) fn on_print_all_print(&mut self) -> Task<Message> {
@@ -3686,8 +3809,7 @@ impl OpenCADStudio {
// Only the explicit upside-down choice rotates layout content here.
let rotation = dialog_rotation;
let centered = self.plot_dialog.center;
let origin_x = self.plot_dialog.offset_x.parse::<f64>().unwrap_or(0.0);
let origin_y = self.plot_dialog.offset_y.parse::<f64>().unwrap_or(0.0);
let (origin_x, origin_y) = self.dialog_plot_origin_mm();
let (scale, offset_x, offset_y) = if plot_extents {
let (min_x, min_y, max_x, max_y) =
@@ -4290,6 +4412,7 @@ impl OpenCADStudio {
let d = &mut self.plot_dialog;
if !d.to_file && d.printer.as_deref() == Some(printer.as_str()) {
d.printer_media = caps;
self.reconcile_sheet_with_printer(&printer);
}
Task::none()
}
@@ -4404,9 +4527,44 @@ impl OpenCADStudio {
M::Scale(s) => {
if self.plot_dialog.area != "Layout" {
self.plot_dialog.scale = s;
self.refresh_custom_scale_fields();
}
Task::none()
}
M::PaperUnits(s) => {
use crate::io::paper_catalog::PaperUnits;
let units = if s == "Inches" {
PaperUnits::Inches
} else {
PaperUnits::Millimeters
};
let d = &mut self.plot_dialog;
if units != d.paper_units {
// The offsets are physical distances: re-spell them in the
// new unit. The scale converts with the unit on its own.
let convert = |text: &str| {
text.parse::<f64>()
.map(|value| units.from_mm(d.paper_units.to_mm(value)))
.map(|value| trim_decimals(&format!("{value:.6}")))
.unwrap_or_else(|_| text.to_string())
};
d.offset_x = convert(&d.offset_x);
d.offset_y = convert(&d.offset_y);
d.paper_units = units;
self.refresh_custom_scale_fields();
}
Task::none()
}
M::CustomScalePaper(s) => {
self.plot_dialog.custom_scale_paper = s;
self.adopt_custom_scale();
Task::none()
}
M::CustomScaleDrawing(s) => {
self.plot_dialog.custom_scale_drawing = s;
self.adopt_custom_scale();
Task::none()
}
M::Quality(s) => {
self.plot_dialog.quality = s;
Task::none()
@@ -4455,6 +4613,8 @@ impl OpenCADStudio {
d.paperspace_last = !d.paperspace_last
}
PlotFlag::Stamp => d.stamp = !d.stamp,
PlotFlag::HidePaperspace => d.hide_paperspace = !d.hide_paperspace,
PlotFlag::SaveToLayout => d.save_to_layout = !d.save_to_layout,
_ => {}
}
Task::none()
@@ -4689,12 +4849,15 @@ impl OpenCADStudio {
d.center = true;
d.offset_x = "0.0".into();
d.offset_y = "0.0".into();
d.paper_units = crate::io::paper_catalog::PaperUnits::Millimeters;
d.upside_down = false;
d.hide_paperspace = false;
d.fit_to_paper = is_model;
d.scale_lw = false;
d.scale = scale_name_for_factor(&d.scales, 1.0)
.or_else(|| d.scales.first().map(|(name, _)| name.clone()))
.unwrap_or_else(|| "1:1".into());
self.refresh_custom_scale_fields();
} else if name == SETUP_PREV {
if let Some(prev) = self.plot_prev.clone() {
self.plot_dialog.copy_settings_from(&prev);
@@ -4733,6 +4896,7 @@ impl OpenCADStudio {
/// Load a `PlotSettings` into the dialog editor fields.
fn load_plotsettings_into_dialog(&mut self, ps: &acadrust::objects::PlotSettings) {
use crate::io::paper_catalog::PaperUnits;
use acadrust::objects::{PlotType, ShadePlotMode, ShadePlotResolutionLevel};
self.plot_setup_template = Some(ps.clone());
if matches!(ps.plot_type, PlotType::Window) && !ps.plot_window.is_empty() {
@@ -4812,8 +4976,13 @@ impl OpenCADStudio {
d.area = "Extents".into();
}
d.center = ps.flags.plot_centered;
d.offset_x = format!("{:.2}", ps.origin_x);
d.offset_y = format!("{:.2}", ps.origin_y);
d.paper_units = match ps.paper_units {
acadrust::objects::PlotPaperUnits::Inches => PaperUnits::Inches,
_ => PaperUnits::Millimeters,
};
d.offset_x = trim_decimals(&format!("{:.4}", d.paper_units.from_mm(ps.origin_x)));
d.offset_y = trim_decimals(&format!("{:.4}", d.paper_units.from_mm(ps.origin_y)));
d.hide_paperspace = ps.flags.plot_hidden;
let deg = ps.rotation.to_degrees() as i32;
if matches!(deg, 90 | 270) {
d.orientation = if d.orientation == "Portrait" {
@@ -4901,6 +5070,7 @@ impl OpenCADStudio {
d.apply_plot_styles = ps.flags.plot_plot_styles;
d.show_plot_styles = ps.flags.show_plot_styles;
d.style_missing = !d.style_name.is_empty() && !style_loaded;
self.refresh_custom_scale_fields();
}
/// Copy transient paper/scale choices out of the dialog without changing
@@ -4927,9 +5097,15 @@ impl OpenCADStudio {
}
// Remember the user's print preferences across sessions.
self.save_config();
// Preview and normal printing must not resize/mutate the live layout;
// the runtime paper/scale choices still drive this one plot operation.
// A preview never touches the layout; a sent plot writes the dialog
// into it when "Save changes to layout" is on, so the page setup
// remembers the last plot the way other applications expect. The
// runtime paper/scale choices drive this one plot operation either way.
if !preview && d.save_to_layout && d.paper_space {
self.apply_dialog_to_layout();
} else {
self.sync_dialog_plot_runtime();
}
self.active_modal = None;
self.reset_modal_geometry();
@@ -5038,7 +5214,12 @@ impl OpenCADStudio {
crate::io::pdf_export::PdfPlotOptions {
object_lineweights: d.lineweights,
scale_lineweights: d.scale_lw && !d.fit_to_paper,
transparency: d.transparency,
// PLOTTRANSPARENCYOVERRIDE: 0 never, 2 always, 1 as the dialog says.
transparency: match d.transparency_override {
0 => false,
2 => true,
_ => d.transparency,
},
stamp: d.stamp,
merge_lines: d.merge_lines,
}
@@ -5187,15 +5368,16 @@ impl OpenCADStudio {
};
let (scale, centered_x, centered_y) =
window_to_sheet((win_w, win_h), (sheet_w, sheet_h), scale_sel);
let (origin_x, origin_y) = self.dialog_plot_origin_mm();
let target_x = if self.plot_dialog.center {
centered_x
} else {
self.plot_dialog.offset_x.parse::<f64>().unwrap_or(0.0)
origin_x
};
let target_y = if self.plot_dialog.center {
centered_y
} else {
self.plot_dialog.offset_y.parse::<f64>().unwrap_or(0.0)
origin_y
};
let scene = &self.tabs[i].scene;
let (wx0, wy0, wx1, wy1) = (x0 as f32, y0 as f32, x1 as f32, y1 as f32);
@@ -5715,6 +5897,158 @@ mod plot_paper_tests {
assert!((x0 + 18.0).abs() < 1e-9 && (y0 + 5.0).abs() < 1e-9, "{x0} {y0}");
}
#[test]
fn hide_paperspace_objects_is_its_own_flag() {
use crate::ui::window::plot::PlotFlag;
let mut app = app_with_printer_named_sheet();
let _ = app.on_plot_dialog_open();
assert!(!app.plot_dialog.hide_paperspace);
// Shade plot "Hidden Line" is a shade mode, not this flag.
let _ = app.on_plot_dlg(PlotDlgMsg::Shade("Hidden Line".into()));
let _ = app.on_plot_dlg(PlotDlgMsg::SetCurrent);
assert!(!layout_settings(&app).flags.plot_hidden);
let _ = app.on_plot_dlg(PlotDlgMsg::Flag(PlotFlag::HidePaperspace));
let _ = app.on_plot_dlg(PlotDlgMsg::SetCurrent);
assert!(layout_settings(&app).flags.plot_hidden);
let _ = app.on_plot_dialog_open();
assert!(app.plot_dialog.hide_paperspace);
}
#[test]
fn switching_the_page_setup_unit_respells_offsets_and_keeps_the_output() {
use acadrust::objects::{PlotPaperUnits, ScaledType};
use crate::ui::window::plot::PlotFlag;
let mut app = app_with_printer_named_sheet();
let _ = app.on_plot_dialog_open();
let sheet_before = app.tabs[app.active_tab].scene.paper_limits().unwrap();
let _ = app.on_plot_dlg(PlotDlgMsg::Area("Extents".into()));
let _ = app.on_plot_dlg(PlotDlgMsg::Flag(PlotFlag::FitToPaper));
let _ = app.on_plot_dlg(PlotDlgMsg::Flag(PlotFlag::Center));
let _ = app.on_plot_dlg(PlotDlgMsg::OffsetX("25.4".into()));
let _ = app.on_plot_dlg(PlotDlgMsg::Scale("1:100".into()));
let _ = app.on_plot_dlg(PlotDlgMsg::PaperUnits("Inches".into()));
let d = &app.plot_dialog;
assert_eq!(d.offset_x, "1", "25.4 mm is 1 inch");
assert_eq!((d.custom_scale_paper.as_str(), d.custom_scale_drawing.as_str()), ("1", "2540"));
let _ = app.on_plot_dlg(PlotDlgMsg::SetCurrent);
let ps = layout_settings(&app);
assert_eq!(ps.paper_units, PlotPaperUnits::Inches);
assert!((ps.origin_x - 25.4).abs() < 1e-9, "the file keeps millimetres");
assert_eq!(ps.scale_type, ScaledType::OneToHundred);
assert_eq!((ps.scale_numerator, ps.scale_denominator), (1.0, 2540.0));
// Back to millimetres: the same physical setup, spelled in mm.
let _ = app.on_plot_dlg(PlotDlgMsg::PaperUnits("Millimeters".into()));
assert_eq!(app.plot_dialog.offset_x, "25.4");
assert_eq!(app.plot_dialog.custom_scale_drawing, "100");
let _ = app.on_plot_dlg(PlotDlgMsg::Area("Layout".into()));
let _ = app.on_plot_dlg(PlotDlgMsg::PaperUnits("Inches".into()));
let _ = app.on_plot_dlg(PlotDlgMsg::SetCurrent);
// An inch page setup over the millimetre paper space leaves the
// sheet where it was.
let ps = layout_settings(&app);
assert_eq!((ps.scale_numerator, ps.scale_denominator), (1.0, 25.4));
let sheet_after = app.tabs[app.active_tab].scene.paper_limits().unwrap();
let size = |((x0, y0), (x1, y1)): ((f64, f64), (f64, f64))| (x1 - x0, y1 - y0);
let (before, after) = (size(sheet_before), size(sheet_after));
assert!((before.0 - after.0).abs() < 1e-9 && (before.1 - after.1).abs() < 1e-9);
}
#[test]
fn custom_scale_fields_follow_and_drive_the_picker() {
use acadrust::objects::ScaledType;
use crate::ui::window::plot::PlotFlag;
let mut app = app_with_printer_named_sheet();
let _ = app.on_plot_dialog_open();
let _ = app.on_plot_dlg(PlotDlgMsg::Area("Extents".into()));
let _ = app.on_plot_dlg(PlotDlgMsg::Flag(PlotFlag::FitToPaper));
let _ = app.on_plot_dlg(PlotDlgMsg::Scale("1:100".into()));
let d = &app.plot_dialog;
assert_eq!((d.custom_scale_paper.as_str(), d.custom_scale_drawing.as_str()), ("1", "100"));
// A typed ratio the list knows is picked under its name…
let _ = app.on_plot_dlg(PlotDlgMsg::CustomScalePaper("2".into()));
assert_eq!(app.plot_dialog.scale, "1:50");
// …one it does not know becomes a ratio entry of its own.
let _ = app.on_plot_dlg(PlotDlgMsg::CustomScaleDrawing("6".into()));
assert_eq!(app.plot_dialog.scale, "1:3");
let _ = app.on_plot_dlg(PlotDlgMsg::SetCurrent);
let ps = layout_settings(&app);
assert_eq!(ps.scale_type, ScaledType::CustomScale);
assert_eq!((ps.scale_numerator, ps.scale_denominator), (1.0, 3.0));
// Half-typed or nonsense values leave the scale alone.
let _ = app.on_plot_dlg(PlotDlgMsg::CustomScaleDrawing("".into()));
assert_eq!(app.plot_dialog.scale, "1:3");
let _ = app.on_plot_dlg(PlotDlgMsg::CustomScaleDrawing("-4".into()));
assert_eq!(app.plot_dialog.scale, "1:3");
}
#[test]
fn the_plot_origin_counts_from_the_printable_corner_unless_plotoffset_says_edge() {
use crate::ui::window::plot::PlotFlag;
let mut app = app_with_printer_named_sheet();
let _ = app.on_plot_dialog_open();
let _ = app.on_plot_dlg(PlotDlgMsg::Printer(crate::ui::window::plot::OUT_PDF.into()));
let _ = app.on_plot_dlg(PlotDlgMsg::Area("Extents".into()));
let _ = app.on_plot_dlg(PlotDlgMsg::Flag(PlotFlag::Center));
let _ = app.on_plot_dlg(PlotDlgMsg::OffsetX("10".into()));
let _ = app.on_plot_dlg(PlotDlgMsg::OffsetY("20".into()));
// The PDF driver's margins on the drawing's landscape A4 medium:
// 5 left, 17 bottom.
assert_eq!(app.dialog_plot_origin_mm(), (15.0, 37.0));
let _ = app.run_command_line("PLOTOFFSET 1");
assert_eq!(app.dialog_plot_origin_mm(), (10.0, 20.0));
let _ = app.on_plot_dlg(PlotDlgMsg::PaperUnits("Inches".into()));
let (x, y) = app.dialog_plot_origin_mm();
assert!((x - 10.0).abs() < 1e-4, "unit change keeps the distance");
assert!((y - 20.0).abs() < 1e-4);
}
#[test]
fn sending_a_plot_saves_the_dialog_into_the_layout_when_asked() {
use crate::ui::window::plot::PlotFlag;
let mut app = app_with_printer_named_sheet();
let _ = app.on_plot_dialog_open();
assert!(app.plot_dialog.save_to_layout, "on by default, as expected of a page setup");
let _ = app.on_plot_dlg(PlotDlgMsg::Printer(crate::ui::window::plot::OUT_PDF.into()));
let _ = app.on_plot_dlg(PlotDlgMsg::Paper("ISO_A3_(297.00_x_420.00_MM)".into()));
// A preview never writes the layout.
let _ = app.on_plot_dlg(PlotDlgMsg::Preview);
assert_eq!(layout_paper(&app).0, "A4");
let _ = app.on_plot_dialog_open();
let _ = app.on_plot_dlg(PlotDlgMsg::Paper("ISO_A3_(297.00_x_420.00_MM)".into()));
let _ = app.on_plot_dlg(PlotDlgMsg::Commit);
assert_eq!(layout_paper(&app).0, "ISO_A3_(297.00_x_420.00_MM)");
// Switched off, a plot leaves the layout alone.
let _ = app.on_plot_dialog_open();
let _ = app.on_plot_dlg(PlotDlgMsg::Flag(PlotFlag::SaveToLayout));
let _ = app.on_plot_dlg(PlotDlgMsg::Paper("ISO_A2_(420.00_x_594.00_MM)".into()));
let _ = app.on_plot_dlg(PlotDlgMsg::Commit);
assert_eq!(layout_paper(&app).0, "ISO_A3_(297.00_x_420.00_MM)");
}
#[test]
fn paperupdate_decides_what_happens_to_an_unsupported_sheet() {
use crate::io::paper_catalog::{resolve, Margins};
use crate::io::plot_device::{PrinterCapabilities, PrinterMedia};
let a3 = resolve("ISO_A3_(297.00_x_420.00_MM)").unwrap();
let media = PrinterMedia { paper: a3.clone(), margins: Margins::uniform(3.0), borderless: false };
let caps = std::sync::Arc::new(PrinterCapabilities { media: vec![media], default_paper: Some(a3) });
let mut app = app_with_printer_named_sheet();
let _ = app.on_plot_dialog_open();
let _ = app.on_plot_dlg(PlotDlgMsg::Printer("OCS Test Printer".into()));
// PAPERUPDATE 0: the sheet is kept and the mismatch reported.
let media = |caps| PlotDlgMsg::PrinterMedia("OCS Test Printer".into(), Some(caps));
let _ = app.on_plot_dlg(media(caps.clone()));
assert_eq!(app.plot_dialog.paper, "ISO_A4_(210.00_x_297.00_MM)");
let last = app.command_line.history.last().map(|line| line.text.clone());
assert!(last.as_deref().unwrap_or("").contains("PAPERUPDATE = 0"), "{last:?}");
// PAPERUPDATE 1: the printer's default sheet takes over, in the
// dialog's orientation.
let _ = app.run_command_line("PAPERUPDATE 1");
let _ = app.on_plot_dlg(media(caps));
assert_eq!(app.plot_dialog.paper, "ISO_A3_(297.00_x_420.00_MM)");
assert_eq!(super::plot_dialog_sheet_mm(&app.plot_dialog), (420.0, 297.0));
}
#[test]
fn scale_names_parse_to_their_ratios() {
use crate::scene::Scene;
+1 -1
View File
@@ -67,7 +67,7 @@ impl OpenCADStudio {
sized_flow(
extra,
940,
620,
690,
|flow| {
crate::ui::window::plot::view_window(
&self.plot_dialog,
+6
View File
@@ -3144,6 +3144,12 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
"World" => Some(("common", "world")),
"Write Block" => Some(("insert", "write-block")),
"X (mm)" => Some(("plot", "x-mm")),
"X (in)" => Some(("plot", "x-in")),
"Y (in)" => Some(("plot", "y-in")),
"Hide paperspace objects" => Some(("plot", "hide-paperspace-objects")),
"Save changes to layout" => Some(("plot", "save-changes-to-layout")),
"{printer} does not support {sheet}; switched to its default sheet {default} (PAPERUPDATE = 1)." => Some(("plot", "printer-does-not-support-sheet-switched-to-its-default-sheet-default-paperupdate-1")),
"{printer} does not support {sheet}; the sheet is kept (PAPERUPDATE = 0)." => Some(("plot", "printer-does-not-support-sheet-the-sheet-is-kept-paperupdate-0")),
"X = %{x}, Y = %{y}, Z = %{z}" => Some(("inquiry", "x-x-y-y-z-z")),
"X Scale" => Some(("properties", "x-scale")),
"X-Ray" => Some(("draw", "x-ray")),
+109 -9
View File
@@ -143,6 +143,11 @@ pub enum PlotFlag {
Transparency,
PaperspaceLast,
Stamp,
/// Hidden-line removal for paper-space objects (the page setup's
/// "plot hidden" flag).
HidePaperspace,
/// Write the dialog into the layout when a plot is sent.
SaveToLayout,
}
/// Every edit the Plot dialog can emit. Wrapped in `Message::PlotDlg` so the
@@ -176,6 +181,12 @@ pub enum PlotDlgMsg {
Orientation(String),
Area(String),
Scale(String),
/// The unit the page setup counts in (offsets, custom scale).
PaperUnits(String),
/// The paper side of the custom scale, in the page-setup unit.
CustomScalePaper(String),
/// The drawing side of the custom scale.
CustomScaleDrawing(String),
Quality(String),
Shade(String),
Copies(String),
@@ -403,9 +414,21 @@ pub struct PlotDialogState {
/// keeps the same window instead of reporting an empty plot area.
pub window: Option<(f64, f64, f64, f64)>,
pub center: bool,
/// Plot offsets in the page-setup unit (`paper_units`); the file keeps
/// them in millimetres.
pub offset_x: String,
pub offset_y: String,
/// The unit the page setup counts in. Only the offsets and the custom
/// scale fields are shown in it; the paper space keeps its own unit
/// because the stored scale is converted along with it.
pub paper_units: PaperUnits,
pub scale: String,
/// The custom scale as `paper = drawing` in the page-setup unit — the
/// same ratio the picker names, or the one typed here.
#[serde(skip)]
pub custom_scale_paper: String,
#[serde(skip)]
pub custom_scale_drawing: String,
#[serde(default = "legacy_fit_to_paper_default")]
pub fit_to_paper: bool,
#[serde(skip)]
@@ -421,6 +444,26 @@ pub struct PlotDialogState {
pub transparency: bool,
pub paperspace_last: bool,
pub stamp: bool,
/// Hidden-line removal for paper-space objects.
pub hide_paperspace: bool,
/// Sending a plot also writes the dialog into the layout, the way a
/// page setup is expected to remember the last plot.
pub save_to_layout: bool,
/// `BACKGROUNDPLOT` bit 2: batch plots (PRINTALL / PUBLISH) run in the
/// background; `background` is bit 1 for single plots.
pub background_publish: bool,
/// `PLOTOFFSET`: offsets count from the paper edge instead of the
/// printable area's corner.
pub plot_offset_from_edge: bool,
/// `PAPERUPDATE`: 1 switches to the printer's default sheet when it does
/// not support the page setup's, 0 keeps the sheet and warns.
pub paper_update: u8,
/// `PLOTROTMODE` (0–2): kept for page setups that read it; every
/// rotation here turns the sheet about the page.
pub plot_rot_mode: u8,
/// `PLOTTRANSPARENCYOVERRIDE`: 0 never plots transparency, 1 follows
/// the dialog, 2 always plots it.
pub transparency_override: u8,
/// Display name of the active plot style table ("" = none).
pub style_name: String,
pub apply_plot_styles: bool,
@@ -471,7 +514,10 @@ impl Default for PlotDialogState {
center: true,
offset_x: "0.0".into(),
offset_y: "0.0".into(),
paper_units: PaperUnits::Millimeters,
scale: "1:1".into(),
custom_scale_paper: "1".into(),
custom_scale_drawing: "1".into(),
fit_to_paper: true,
scales: Vec::new(),
plot_views: Vec::new(),
@@ -484,6 +530,13 @@ impl Default for PlotDialogState {
transparency: false,
paperspace_last: false,
stamp: false,
hide_paperspace: false,
save_to_layout: true,
background_publish: false,
plot_offset_from_edge: false,
paper_update: 0,
plot_rot_mode: 2,
transparency_override: 1,
style_name: String::new(),
apply_plot_styles: true,
show_plot_styles: false,
@@ -516,7 +569,10 @@ impl PlotDialogState {
self.center = o.center;
self.offset_x = o.offset_x.clone();
self.offset_y = o.offset_y.clone();
self.paper_units = o.paper_units;
self.scale = o.scale.clone();
self.custom_scale_paper = o.custom_scale_paper.clone();
self.custom_scale_drawing = o.custom_scale_drawing.clone();
self.fit_to_paper = o.fit_to_paper;
self.scale_lw = o.scale_lw;
self.quality = o.quality.clone();
@@ -527,6 +583,7 @@ impl PlotDialogState {
self.transparency = o.transparency;
self.paperspace_last = o.paperspace_last;
self.stamp = o.stamp;
self.hide_paperspace = o.hide_paperspace;
self.style_name = o.style_name.clone();
self.apply_plot_styles = o.apply_plot_styles;
self.show_plot_styles = o.show_plot_styles;
@@ -670,6 +727,33 @@ fn check_enabled<'a>(
.into()
}
/// The custom-scale row: `Custom: [paper] mm = [drawing] units`. Editing
/// either side replaces the picked scale with the typed ratio.
fn custom_scale_row<'a>(s: &'a PlotDialogState, enabled: bool) -> Element<'a, Message> {
let field = |value: &'a str, ctor: fn(String) -> PlotDlgMsg| {
let mut input = text_input("", value).size(11).padding([3, 6]).width(58);
if enabled {
input = input.on_input(move |v| Message::PlotDlg(ctor(v)));
}
input
};
let unit = match s.paper_units {
PaperUnits::Inches => t!("Inches"),
PaperUnits::Millimeters => t!("Millimeters"),
};
row![
text(t!("Custom")).size(11).width(form::LABEL_WIDTH),
field(&s.custom_scale_paper, PlotDlgMsg::CustomScalePaper),
text(unit).size(11),
text("=").size(11),
field(&s.custom_scale_drawing, PlotDlgMsg::CustomScaleDrawing),
text(t!("units")).size(11),
]
.spacing(6)
.align_y(iced::Center)
.into()
}
fn panel<'a>(content: impl Into<Element<'a, Message>>) -> Element<'a, Message> {
container(content)
.width(Length::Fill)
@@ -1175,13 +1259,19 @@ pub fn view_window(
);
}
let common_area = s.area != "Layout";
let inches = s.paper_units == PaperUnits::Inches;
let (offset_x_label, offset_y_label) = if inches {
(t!("X (in)"), t!("Y (in)"))
} else {
(t!("X (mm)"), t!("Y (mm)"))
};
let area_panel = panel(column![
section_label(t!("Plot area")),
area_row,
section_label(t!("Plot offset")),
column![
field_row_enabled(t!("X (mm)"), &s.offset_x, PlotDlgMsg::OffsetX, 70, common_area && !s.center),
field_row_enabled(t!("Y (mm)"), &s.offset_y, PlotDlgMsg::OffsetY, 70, common_area && !s.center),
field_row_enabled(offset_x_label, &s.offset_x, PlotDlgMsg::OffsetX, 70, common_area && !s.center),
field_row_enabled(offset_y_label, &s.offset_y, PlotDlgMsg::OffsetY, 70, common_area && !s.center),
]
.spacing(7),
check_enabled(t!("Center the plot"), s.center, PlotFlag::Center, common_area),
@@ -1209,6 +1299,16 @@ pub fn view_window(
width,
common_area && !s.fit_to_paper,
),
// The custom scale spells the picked ratio in the page-setup unit
// and accepts one of its own; the unit picker decides that unit.
drop_row(
t!("Units"),
vec![PlotChoice::localized("Millimeters"), PlotChoice::localized("Inches")],
Some(PlotChoice::localized(if inches { "Inches" } else { "Millimeters" })),
PlotDlgMsg::PaperUnits,
width,
),
custom_scale_row(s, common_area && !s.fit_to_paper),
check_enabled(
t!("Scale lineweights"),
s.scale_lw && !s.fit_to_paper,
@@ -1296,14 +1396,12 @@ pub fn view_window(
].spacing(7));
// ── Output options and orientation ────────────────────────────────────
let paper_order_option: Element<'_, Message> = if s.paper_space {
check(
t!("Paper space last"),
s.paperspace_last,
PlotFlag::PaperspaceLast,
)
let paper_space_option = |on: bool, label: Cow<'static, str>, flag: PlotFlag| -> Element<'_, Message> {
if s.paper_space {
check(label, on, flag)
} else {
Space::new().height(0).into()
}
};
let options_panel = panel(column![
section_label(t!("Plot options")),
@@ -1312,13 +1410,15 @@ pub fn view_window(
check(t!("Plot in background"), s.background, PlotFlag::Background),
check(t!("Object lineweights"), s.lineweights, PlotFlag::Lineweights),
check(t!("Plot transparency"), s.transparency, PlotFlag::Transparency),
paper_space_option(s.hide_paperspace, t!("Hide paperspace objects"), PlotFlag::HidePaperspace),
]
.spacing(6)
.width(width),
column![
paper_order_option,
paper_space_option(s.paperspace_last, t!("Paper space last"), PlotFlag::PaperspaceLast),
check(t!("Merge overlapping lines"), s.merge_lines, PlotFlag::MergeLines),
check(t!("Plot stamp"), s.stamp, PlotFlag::Stamp),
paper_space_option(s.save_to_layout, t!("Save changes to layout"), PlotFlag::SaveToLayout),
]
.spacing(6)
.width(width),
+1
View File
@@ -0,0 +1 @@
Open CAD Studio pid=842697 path=/home/panos/OpenCADStudio/tests/fixtures/plot/page-setups-metric.dxf