mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-14 14:26:45 -04:00
Plot dialog: device, plot style table, Window <, and a preview of its settings (#673)
Squash of PR #673 by @aldozar. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
d365b60704
commit
c2f8782cc2
12 files changed
+388
-31
No files matched your search
@@ -11,6 +11,7 @@
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
|
||||
use cadcraft_doc::{DevicePaper, Orientation, PageSetup, PlotOutput, PlotterConfig};
|
||||
use cadcraft_geom::{Bounds2, Vec2};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::*;
|
||||
@@ -233,7 +234,7 @@ fn unknown_device(cmd: &str, name: &str) -> EngineError {
|
||||
/// paper's printable area. A device change also takes the device's default orientation unless
|
||||
/// `landscape` is given. Returns whether the paper was set here (else `paper`, if any, is a
|
||||
/// standard size for the caller to look up).
|
||||
pub(crate) fn page_device(cmd: &str, p: &Value, page: &mut PageSetup) -> Result<bool> {
|
||||
pub fn page_device(cmd: &str, p: &Value, page: &mut PageSetup) -> Result<bool> {
|
||||
let given = match p.get("device") {
|
||||
Some(v) => Some(v.as_str().ok_or_else(|| bad(cmd, "`device` must be a plotter name"))?.trim()),
|
||||
None => None,
|
||||
@@ -325,10 +326,74 @@ pub(crate) fn plot_options(s: &Session, p: &Value, cmd: &str) -> Result<Value> {
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
// ---------- PLOTWINDOW ----------
|
||||
|
||||
fn run_plotwindow(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let (a, b) = (point_req("plotwindow", p, "p1")?, point_req("plotwindow", p, "p2")?);
|
||||
set_plot_window(s, a, b)
|
||||
}
|
||||
|
||||
fn set_plot_window(s: &mut Session, a: Vec2, b: Vec2) -> Result<Value> {
|
||||
let w = Bounds2::new(a, b);
|
||||
if !(w.width() > 0.0 && w.height() > 0.0) {
|
||||
return Err(bad("plotwindow", "the window needs a width and a height"));
|
||||
}
|
||||
s.plot_window = Some(w);
|
||||
Ok(json!({ "window": [[w.min.x, w.min.y], [w.max.x, w.max.y]] }))
|
||||
}
|
||||
|
||||
/// PLOTWINDOW: two corners picked in the drawing, with the window's rectangle following the cursor.
|
||||
#[derive(Default)]
|
||||
struct PlotWindowM {
|
||||
first: Option<Vec2>,
|
||||
}
|
||||
|
||||
impl Interactive for PlotWindowM {
|
||||
fn name(&self) -> &'static str {
|
||||
"PLOTWINDOW"
|
||||
}
|
||||
fn prompt(&self, _s: &Session) -> Prompt {
|
||||
match self.first {
|
||||
None => Prompt::new("Specify first corner", Accept::POINT),
|
||||
Some(a) => Prompt::new("Specify opposite corner", Accept::POINT).base(a),
|
||||
}
|
||||
}
|
||||
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
|
||||
match (i, self.first) {
|
||||
(Input::Point(p), None) => {
|
||||
self.first = Some(p);
|
||||
Ok(Step::Continue)
|
||||
}
|
||||
(Input::Point(p), Some(a)) => {
|
||||
set_plot_window(s, a, p)?;
|
||||
Ok(Step::Done)
|
||||
}
|
||||
(Input::Cancel, _) => Ok(Step::Cancel),
|
||||
_ => Ok(Step::Continue),
|
||||
}
|
||||
}
|
||||
fn preview(&self, _s: &Session, c: Vec2) -> Vec<cadcraft_doc::EntityKind> {
|
||||
let Some(a) = self.first else { return Vec::new() };
|
||||
let corners = [a, Vec2::new(c.x, a.y), c, Vec2::new(a.x, c.y), a];
|
||||
corners
|
||||
.windows(2)
|
||||
.filter_map(|w| match w {
|
||||
[p, q] => Some(super::helpers::line(*p, *q)),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- PLOTTERMANAGER ----------
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
CommandSpec::new("plotwindow", "Plot Window", run_plotwindow)
|
||||
.params("{p1: [x,y], p2: [x,y]} (typed: two corners): the window the Plot dialog's \"Window <\" plots, kept in the session")
|
||||
.enabled(has_doc)
|
||||
.noundo()
|
||||
.interactive(|_| Ok(Box::new(PlotWindowM::default()))),
|
||||
CommandSpec::new("plottermanager", "Plotter Manager...", run_plottermanager)
|
||||
.menu(&["File", "Plotter Manager..."])
|
||||
.params(
|
||||
|
||||
@@ -584,6 +584,8 @@ pub struct Session {
|
||||
pub last_used: LastUsed,
|
||||
/// Where `clipboard` came from and the layers, styles and blocks its objects need.
|
||||
pub clipboard_source: Option<cmd::clipboard::ClipSource>,
|
||||
/// The window last picked with PLOTWINDOW (the Plot dialog's "Window <").
|
||||
pub plot_window: Option<cadcraft_geom::Bounds2>,
|
||||
}
|
||||
|
||||
impl Default for Session {
|
||||
@@ -627,6 +629,7 @@ impl Session {
|
||||
last_dim: None,
|
||||
last_used: LastUsed::default(),
|
||||
clipboard_source: None,
|
||||
plot_window: None,
|
||||
}
|
||||
}
|
||||
pub fn new_drawing(&mut self, metric: bool) -> usize {
|
||||
|
||||
@@ -172,3 +172,21 @@ fn hostile_names_and_values_are_refused() {
|
||||
assert!(s.execute("plottermanager", &p).is_err(), "{p}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plotwindow_keeps_the_picked_window_for_the_plot_dialog() {
|
||||
let mut s = Session::new();
|
||||
// Typed: two corners, with the window's rectangle following the cursor after the first.
|
||||
s.cmdline("plotwindow").unwrap();
|
||||
assert_eq!(s.current_prompt().unwrap().message, "Specify first corner");
|
||||
s.cmdline("10,20").unwrap();
|
||||
assert_eq!(s.preview(cadcraft_engine::geom::Vec2::new(40.0, 5.0)).len(), 4, "the rubber-band rectangle");
|
||||
s.cmdline("40,5").unwrap();
|
||||
assert!(s.running.is_none());
|
||||
let w = s.plot_window.expect("the picked window");
|
||||
assert_eq!((w.min.x, w.min.y, w.max.x, w.max.y), (10.0, 5.0, 40.0, 20.0));
|
||||
// JSON, and a window without area is refused.
|
||||
let r = s.execute("plotwindow", &json!({"p1": [0, 0], "p2": [3, 4]})).unwrap();
|
||||
assert_eq!(r["window"], json!([[0.0, 0.0], [3.0, 4.0]]));
|
||||
assert!(s.execute("plotwindow", &json!({"p1": [0, 0], "p2": [3, 0]})).is_err());
|
||||
}
|
||||
@@ -29,7 +29,8 @@ pub fn show(app: &mut CadApp, ctx: &egui::Context) {
|
||||
o if o == "options" || o.starts_with("options:") => crate::options::dialog(app, ctx, o, &mut open),
|
||||
other => crate::managers::dialog(app, ctx, other, &mut open),
|
||||
}
|
||||
if !open {
|
||||
// A dialog that closed by opening another (Plot Preview back to Plot) keeps the new one.
|
||||
if !open && app.ui.dialog.as_deref() == Some(d.as_str()) {
|
||||
app.ui.dialog = None;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,8 +59,34 @@ struct Preview {
|
||||
shown: Result<(TextureHandle, String), String>,
|
||||
}
|
||||
|
||||
/// Render the plot preview of the current layout (or model) as a texture.
|
||||
/// A preview the Plot dialog asked for: its PLOT parameters (PNG output) and a caption.
|
||||
#[derive(Clone)]
|
||||
struct FromPlot {
|
||||
params: Value,
|
||||
info: String,
|
||||
}
|
||||
|
||||
const FROM_PLOT: &str = "cc_plot_preview_from_plot";
|
||||
|
||||
/// Show the Plot dialog's settings as they plot (PLOT with `params`, a PNG); Print... and Close
|
||||
/// return to the dialog.
|
||||
pub(crate) fn preview_plot(app: &mut CadApp, ctx: &egui::Context, params: Value, info: String) {
|
||||
PREVIEW_GEN.fetch_add(1, Ordering::Relaxed);
|
||||
ctx.data_mut(|d| d.insert_temp(egui::Id::new(FROM_PLOT), FromPlot { params, info }));
|
||||
app.ui.dialog = Some("preview".into());
|
||||
}
|
||||
|
||||
/// Render the plot preview of the current layout (or model), or of the Plot dialog's settings,
|
||||
/// as a texture.
|
||||
fn render(app: &mut CadApp, ctx: &egui::Context) -> Result<(TextureHandle, String), String> {
|
||||
if let Some(f) = ctx.data(|d| d.get_temp::<FromPlot>(egui::Id::new(FROM_PLOT))) {
|
||||
let r = app.session.execute("plot", &f.params).map_err(|e| e.to_string())?;
|
||||
let png = r["data"].as_str().and_then(cadcraft_engine::cmd::file::base64_decode).ok_or("the preview has no image")?;
|
||||
let img = image::load_from_memory_with_format(&png, image::ImageFormat::Png).map_err(|e| e.to_string())?.to_rgba8();
|
||||
let size = [img.width() as usize, img.height() as usize];
|
||||
let tex = ctx.load_texture("plot-preview", egui::ColorImage::from_rgba_unmultiplied(size, img.as_raw()), egui::TextureOptions::LINEAR);
|
||||
return Ok((tex, f.info));
|
||||
}
|
||||
let r = app.session.execute("preview", &json!({ "width": 1600 })).map_err(|e| e.to_string())?;
|
||||
let png = r["data"].as_str().and_then(cadcraft_engine::cmd::file::base64_decode).ok_or("the preview has no image")?;
|
||||
let img = image::load_from_memory_with_format(&png, image::ImageFormat::Png).map_err(|e| e.to_string())?.to_rgba8();
|
||||
@@ -111,10 +137,18 @@ pub fn dialog(app: &mut CadApp, ctx: &egui::Context, open: &mut bool) {
|
||||
if close || print {
|
||||
*open = false;
|
||||
}
|
||||
let from_plot = ctx.data(|d| d.get_temp::<FromPlot>(egui::Id::new(FROM_PLOT))).is_some();
|
||||
if !*open {
|
||||
ctx.data_mut(|d| d.remove::<Preview>(id));
|
||||
ctx.data_mut(|d| {
|
||||
d.remove::<Preview>(id);
|
||||
d.remove::<FromPlot>(egui::Id::new(FROM_PLOT));
|
||||
});
|
||||
}
|
||||
if print {
|
||||
if from_plot && !*open {
|
||||
// Back to the Plot dialog with its settings; Print... plots them.
|
||||
crate::managers::set_flag(ctx, crate::managers::PLOT_NOW, print);
|
||||
app.ui.dialog = Some("plot".into());
|
||||
} else if print {
|
||||
app.start("plot");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -618,6 +618,7 @@
|
||||
Plot Tisk
|
||||
Plot (click: do not plot) Tisknout (kliknutím: netisknout)
|
||||
Plot Preview Náhled tisku
|
||||
Plot Window Okno tisku
|
||||
Plot style Tiskový styl
|
||||
Plot style attached to Tiskový styl přiřazený k
|
||||
Plot style table Tabulka tiskových stylů
|
||||
|
||||
|
@@ -524,6 +524,7 @@
|
||||
Plot Plotten
|
||||
Plot (click: do not plot) Plotten (Klick: nicht plotten)
|
||||
Plot Preview Plotvorschau
|
||||
Plot Window Plotfenster
|
||||
Plot style Plotstil
|
||||
Plot style attached to Plotstil zugeordnet zu
|
||||
Plot style table Plotstiltabelle
|
||||
|
||||
|
@@ -659,6 +659,7 @@
|
||||
Plot Trazar
|
||||
Plot (click: do not plot) Se trazará (clic: no trazar)
|
||||
Plot Preview Vista previa de trazado
|
||||
Plot Window Ventana de trazado
|
||||
Plot style Estilo de trazado
|
||||
Plot style attached to Estilo de trazado asociado a
|
||||
Plot style table Tabla de estilos de trazado
|
||||
|
||||
|
@@ -524,6 +524,7 @@
|
||||
Plot Plotar
|
||||
Plot (click: do not plot) Plotar (clique para não plotar)
|
||||
Plot Preview Visualizar plotagem
|
||||
Plot Window Janela de plotagem
|
||||
Plot style Estilo de plotagem
|
||||
Plot style attached to Estilo de plotagem anexado a
|
||||
Plot style table Tabela de estilos de plotagem
|
||||
|
||||
|
@@ -622,6 +622,7 @@
|
||||
Plot Друк креслення
|
||||
Plot (click: do not plot) Друкувати (натисніть, щоб вимкнути друк)
|
||||
Plot Preview Попередній перегляд друку
|
||||
Plot Window Вікно друку
|
||||
Plot style Стиль друку
|
||||
Plot style attached to Стиль друку застосовано до
|
||||
Plot style table Таблиця стилів друку
|
||||
|
||||
|
@@ -432,6 +432,7 @@ impl CadApp {
|
||||
self.collect_screenshots(ctx);
|
||||
self.issue_screenshots(ctx);
|
||||
self.gpu_fault_tick(ctx);
|
||||
managers::plot_window_resume(self, ctx);
|
||||
self.autosave_tick(ctx);
|
||||
menus::shortcuts(self, ctx);
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
|
||||
+256
-26
@@ -484,6 +484,7 @@ fn page_setup(app: &mut CadApp, ctx: &egui::Context, open: &mut bool) -> Action
|
||||
.or_else(|| layouts.first().map(|l| l.1.clone()))
|
||||
.unwrap_or_default();
|
||||
let mut action = None;
|
||||
let choices = PageChoices::new(ctx, d);
|
||||
egui::Window::new("Page Setup Manager").open(open).default_size(vec2(560.0, 300.0)).resizable(true).show(ctx, |ui| {
|
||||
if layouts.is_empty() {
|
||||
ui.label("This drawing has no layouts.");
|
||||
@@ -504,7 +505,7 @@ fn page_setup(app: &mut CadApp, ctx: &egui::Context, open: &mut bool) -> Action
|
||||
// Layout plotting always prints the whole sheet, so only that area is offered.
|
||||
let rows = PageRows { areas: &["layout"], scale: true, center: true };
|
||||
egui::Grid::new("pagesetup_grid").num_columns(2).spacing(vec2(10.0, 6.0)).show(right, |ui| {
|
||||
if let Some((k, v)) = page_rows(ui, egui::Id::new(("pagesetup", name)), page, &rows) {
|
||||
if let Some((k, v)) = page_rows(ui, egui::Id::new(("pagesetup", name)), page, &rows, &choices) {
|
||||
action = Some(("pagesetup", json!({ "layout": name, k: v })));
|
||||
}
|
||||
});
|
||||
@@ -524,6 +525,43 @@ struct PageRows {
|
||||
center: bool,
|
||||
}
|
||||
|
||||
/// The devices and plot style tables the page-setup rows offer.
|
||||
struct PageChoices {
|
||||
devices: std::sync::Arc<Vec<cadcraft_doc::PlotterConfig>>,
|
||||
styles: Vec<String>,
|
||||
}
|
||||
|
||||
impl PageChoices {
|
||||
fn new(ctx: &egui::Context, d: &cadcraft_doc::Drawing) -> Self {
|
||||
PageChoices { devices: device_choices(ctx), styles: cadcraft_doc::plot_style_table_names(d) }
|
||||
}
|
||||
}
|
||||
|
||||
/// The plotter devices (built-in, then the user's files), read again at most every 2 s: the
|
||||
/// dialogs ask every frame and user devices are files.
|
||||
fn device_choices(ctx: &egui::Context) -> std::sync::Arc<Vec<cadcraft_doc::PlotterConfig>> {
|
||||
type Cached = (f64, std::sync::Arc<Vec<cadcraft_doc::PlotterConfig>>);
|
||||
let now = crate::now_ms();
|
||||
if let Some((at, list)) = temp::<Cached>(ctx, "plot_devices")
|
||||
&& now - at < 2000.0
|
||||
{
|
||||
return list;
|
||||
}
|
||||
let list = std::sync::Arc::new(cadcraft_engine::cmd::plotters::devices());
|
||||
set_temp::<Cached>(ctx, "plot_devices", (now, list.clone()));
|
||||
list
|
||||
}
|
||||
|
||||
/// A page setup's device, or `"None"` (plot to PDF on the page's paper).
|
||||
fn device_label(page: &cadcraft_doc::PageSetup) -> &str {
|
||||
if page.device.trim().is_empty() { "None" } else { page.device.as_str() }
|
||||
}
|
||||
|
||||
/// A page setup's plot style table, or `"None"`.
|
||||
fn style_label(page: &cadcraft_doc::PageSetup) -> &str {
|
||||
if cadcraft_doc::no_plot_style_table(&page.plot_style_table) { "None" } else { page.plot_style_table.as_str() }
|
||||
}
|
||||
|
||||
fn area_label(a: &str) -> &str {
|
||||
match a {
|
||||
"layout" => "Layout",
|
||||
@@ -544,17 +582,51 @@ fn parse_ratio(t: &str) -> Option<f64> {
|
||||
(v.is_finite() && v > 0.0).then_some(v)
|
||||
}
|
||||
|
||||
/// The page-setup rows (paper, orientation, plot area, fit, scale, center, lineweights) shared by
|
||||
/// the Page Setup Manager and the Plot dialog, inside a two-column grid. Returns the setting the
|
||||
/// user changed as a `pagesetup` parameter (`paper`, `landscape`, `plotArea`, `scaleToFit`,
|
||||
/// `scale`, `center` or `lineweights`).
|
||||
fn page_rows(ui: &mut egui::Ui, salt: egui::Id, page: &cadcraft_doc::PageSetup, rows: &PageRows) -> Option<(&'static str, Value)> {
|
||||
/// The page-setup rows (device, paper, orientation, plot area, fit, scale, center, lineweights,
|
||||
/// plot style table) shared by the Page Setup Manager and the Plot dialog, inside a two-column
|
||||
/// grid. Returns the setting the user changed as a `pagesetup` parameter (`device`, `paper`,
|
||||
/// `landscape`, `plotArea`, `scaleToFit`, `scale`, `center`, `lineweights` or `plotStyleTable`).
|
||||
fn page_rows(
|
||||
ui: &mut egui::Ui,
|
||||
salt: egui::Id,
|
||||
page: &cadcraft_doc::PageSetup,
|
||||
rows: &PageRows,
|
||||
choices: &PageChoices,
|
||||
) -> Option<(&'static str, Value)> {
|
||||
let mut change = None;
|
||||
ui.label("Printer/plotter");
|
||||
egui::ComboBox::from_id_salt(salt.with("device")).selected_text(device_label(page)).width(190.0).show_ui(ui, |ui| {
|
||||
if ui.selectable_label(device_label(page) == "None", "None").on_hover_text("PDF on the paper chosen below").clicked()
|
||||
&& device_label(page) != "None"
|
||||
{
|
||||
change = Some(("device", json!("None")));
|
||||
}
|
||||
for dev in choices.devices.iter() {
|
||||
let on = dev.name.eq_ignore_ascii_case(&page.device);
|
||||
if ui.selectable_label(on, &dev.name).on_hover_text(format!("{} ({})", dev.description, dev.output.as_str().to_uppercase())).clicked()
|
||||
&& !on
|
||||
{
|
||||
change = Some(("device", json!(dev.name)));
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.end_row();
|
||||
// The device's papers, else the standard sizes.
|
||||
let device = choices.devices.iter().find(|dv| dv.name.eq_ignore_ascii_case(&page.device));
|
||||
ui.label("Paper size");
|
||||
egui::ComboBox::from_id_salt(salt.with("paper")).selected_text(page.paper.as_str()).width(190.0).show_ui(ui, |ui| {
|
||||
for p in cadcraft_render::PAPER_SIZES {
|
||||
if ui.selectable_label(p.name == page.paper, p.name).clicked() && p.name != page.paper {
|
||||
change = Some(("paper", json!(p.name)));
|
||||
egui::ComboBox::from_id_salt(salt.with("paper")).selected_text(page.paper.as_str()).width(190.0).show_ui(ui, |ui| match device {
|
||||
Some(dev) => {
|
||||
for p in &dev.papers {
|
||||
if ui.selectable_label(p.name == page.paper, &p.name).clicked() && p.name != page.paper {
|
||||
change = Some(("paper", json!(p.name)));
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
for p in cadcraft_render::PAPER_SIZES {
|
||||
if ui.selectable_label(p.name == page.paper, p.name).clicked() && p.name != page.paper {
|
||||
change = Some(("paper", json!(p.name)));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -611,19 +683,42 @@ fn page_rows(ui: &mut egui::Ui, salt: egui::Id, page: &cadcraft_doc::PageSetup,
|
||||
change = Some(("lineweights", json!(lw)));
|
||||
}
|
||||
ui.end_row();
|
||||
ui.label("Plot style table");
|
||||
egui::ComboBox::from_id_salt(salt.with("styles")).selected_text(style_label(page)).width(190.0).show_ui(ui, |ui| {
|
||||
if ui.selectable_label(style_label(page) == "None", "None").clicked() && style_label(page) != "None" {
|
||||
change = Some(("plotStyleTable", json!("None")));
|
||||
}
|
||||
for name in &choices.styles {
|
||||
let on = name.eq_ignore_ascii_case(&page.plot_style_table);
|
||||
if ui.selectable_label(on, name).clicked() && !on {
|
||||
change = Some(("plotStyleTable", json!(name)));
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.end_row();
|
||||
change
|
||||
}
|
||||
|
||||
/// Apply a [`page_rows`] change to a page setup held by a dialog.
|
||||
/// Device and paper changes follow PAGESETUP's rules (`plotters::page_device`): a device brings
|
||||
/// its papers, orientation and printable areas.
|
||||
fn apply_page_change(page: &mut cadcraft_doc::PageSetup, key: &str, v: &Value) {
|
||||
use cadcraft_engine::cmd::plotters::page_device;
|
||||
match key {
|
||||
"device" => {
|
||||
let _ = page_device("plot", &json!({ "device": v }), page);
|
||||
}
|
||||
"paper" => {
|
||||
if let Some(ps) = v.as_str().and_then(cadcraft_render::paper_size) {
|
||||
// A device's paper, else a standard size (no device).
|
||||
if !page_device("plot", &json!({ "paper": v }), page).unwrap_or(false)
|
||||
&& let Some(ps) = v.as_str().and_then(cadcraft_render::paper_size)
|
||||
{
|
||||
page.paper = ps.name.into();
|
||||
page.width_mm = ps.width_mm;
|
||||
page.height_mm = ps.height_mm;
|
||||
}
|
||||
}
|
||||
"plotStyleTable" => page.plot_style_table = v.as_str().unwrap_or_default().chars().take(260).collect(),
|
||||
"plotArea" => page.plot_area = v.as_str().unwrap_or("extents").into(),
|
||||
"scale" => page.scale = v.as_f64().filter(|s| s.is_finite() && *s > 0.0).unwrap_or(page.scale),
|
||||
_ => {
|
||||
@@ -644,6 +739,31 @@ fn apply_page_change(page: &mut cadcraft_doc::PageSetup, key: &str, v: &Value) {
|
||||
/// Bumped each time PLOT opens its dialog, so it starts again from the page setup.
|
||||
static PLOT_GEN: AtomicU32 = AtomicU32::new(0);
|
||||
|
||||
/// Set while PLOTWINDOW picks the Plot dialog's window ("Window <").
|
||||
const PICKING_WINDOW: &str = "plot_picking_window";
|
||||
/// Set by Print... in a preview opened from the Plot dialog: plot as soon as the dialog is back.
|
||||
pub(crate) const PLOT_NOW: &str = "plot_now";
|
||||
|
||||
/// Every frame: once PLOTWINDOW (started by the Plot dialog's "Window <") ends, bring the dialog
|
||||
/// back with its settings, plotting the picked window (unchanged if the pick was cancelled).
|
||||
pub fn plot_window_resume(app: &mut CadApp, ctx: &egui::Context) {
|
||||
if temp::<bool>(ctx, PICKING_WINDOW) != Some(true) || app.session.running.is_some() {
|
||||
return;
|
||||
}
|
||||
set_temp(ctx, PICKING_WINDOW, false);
|
||||
if let (Some(w), Some(mut st)) = (app.session.plot_window.take(), temp::<PlotState>(ctx, "plot_state")) {
|
||||
st.window = [w.min.x, w.min.y, w.max.x, w.max.y];
|
||||
st.page.plot_area = "window".into();
|
||||
set_temp(ctx, "plot_state", st);
|
||||
}
|
||||
app.ui.dialog = Some("plot".into());
|
||||
}
|
||||
|
||||
/// Set a flag the Plot dialog reads (see [`PLOT_NOW`]).
|
||||
pub(crate) fn set_flag(ctx: &egui::Context, id: &str, on: bool) {
|
||||
set_temp(ctx, id, on);
|
||||
}
|
||||
|
||||
/// Plot areas for model space (a layout always plots its sheet).
|
||||
const MODEL_AREAS: &[&str] = &["extents", "display", "limits", "window"];
|
||||
|
||||
@@ -677,8 +797,9 @@ fn plot_page(d: &cadcraft_doc::Drawing, target: &str) -> cadcraft_doc::PageSetup
|
||||
page
|
||||
}
|
||||
|
||||
/// The `plot` parameters for the dialog's settings (without the output path).
|
||||
fn plot_params(st: &PlotState) -> Value {
|
||||
/// The `plot` parameters for the dialog's settings (without the output path). `preview_dpi`
|
||||
/// asks for a PNG at that resolution instead of the device's output, on the same sheet.
|
||||
fn plot_params(st: &PlotState, preview_dpi: Option<f64>) -> Value {
|
||||
let page = &st.page;
|
||||
let mut p = json!({
|
||||
"layout": st.target,
|
||||
@@ -687,11 +808,33 @@ fn plot_params(st: &PlotState) -> Value {
|
||||
"lineweights": page.lineweights,
|
||||
});
|
||||
if let Some(o) = p.as_object_mut() {
|
||||
if cadcraft_render::paper_size(&page.paper).is_some() {
|
||||
o.insert("paper".into(), json!(page.paper));
|
||||
} else {
|
||||
o.insert("width".into(), json!(page.width_mm));
|
||||
o.insert("height".into(), json!(page.height_mm));
|
||||
let device = device_label(page) != "None";
|
||||
match preview_dpi {
|
||||
// The page's own sheet, so a device's paper and printable area show as they plot.
|
||||
Some(dpi) => {
|
||||
o.insert("device".into(), json!("None"));
|
||||
o.insert("output".into(), json!("png"));
|
||||
o.insert("resolution".into(), json!(dpi));
|
||||
o.insert("width".into(), json!(page.width_mm));
|
||||
o.insert("height".into(), json!(page.height_mm));
|
||||
o.insert("margins".into(), json!(page.margins_mm));
|
||||
}
|
||||
None if device => {
|
||||
o.insert("device".into(), json!(page.device));
|
||||
o.insert("paper".into(), json!(page.paper));
|
||||
}
|
||||
None if cadcraft_render::paper_size(&page.paper).is_some() => {
|
||||
o.insert("device".into(), json!("None"));
|
||||
o.insert("paper".into(), json!(page.paper));
|
||||
}
|
||||
None => {
|
||||
o.insert("device".into(), json!("None"));
|
||||
o.insert("width".into(), json!(page.width_mm));
|
||||
o.insert("height".into(), json!(page.height_mm));
|
||||
}
|
||||
}
|
||||
if style_label(page) != "None" {
|
||||
o.insert("plotStyleTable".into(), json!(page.plot_style_table));
|
||||
}
|
||||
if st.target == "Model" {
|
||||
o.insert("plotArea".into(), json!(page.plot_area));
|
||||
@@ -732,12 +875,13 @@ fn plot(app: &mut CadApp, ctx: &egui::Context, open: &mut bool) -> Action {
|
||||
PlotState { generation, page: plot_page(d, &target), target, window: [ext.min.x, ext.min.y, ext.max.x, ext.max.y] }
|
||||
});
|
||||
let model = st.target == "Model";
|
||||
let (mut ok, mut cancel, mut action) = (false, false, None);
|
||||
let choices = PageChoices::new(ctx, d);
|
||||
// Print... in a preview opened from here plots with these settings.
|
||||
let mut ok = temp::<bool>(ctx, PLOT_NOW) == Some(true);
|
||||
set_temp(ctx, PLOT_NOW, false);
|
||||
let (mut cancel, mut action, mut pick, mut preview) = (false, None, false, false);
|
||||
egui::Window::new("Plot").open(open).collapsible(false).resizable(false).show(ctx, |ui| {
|
||||
egui::Grid::new("plot_target").num_columns(2).spacing(vec2(10.0, 6.0)).show(ui, |ui| {
|
||||
ui.label("Printer/plotter");
|
||||
ui.label("PDF (vector)").on_hover_text("CADCraft plots to vector PDF files");
|
||||
ui.end_row();
|
||||
ui.label("Layout");
|
||||
egui::ComboBox::from_id_salt("plot_layout").selected_text(st.target.as_str()).width(190.0).show_ui(ui, |ui| {
|
||||
for n in &targets {
|
||||
@@ -757,7 +901,7 @@ fn plot(app: &mut CadApp, ctx: &egui::Context, open: &mut bool) -> Action {
|
||||
} else {
|
||||
PageRows { areas: &["layout"], scale: false, center: false }
|
||||
};
|
||||
if let Some((k, v)) = page_rows(ui, egui::Id::new(("plot", st.target.as_str())), &st.page, &rows) {
|
||||
if let Some((k, v)) = page_rows(ui, egui::Id::new(("plot", st.target.as_str())), &st.page, &rows, &choices) {
|
||||
apply_page_change(&mut st.page, k, &v);
|
||||
}
|
||||
if model && st.page.plot_area == "window" {
|
||||
@@ -776,6 +920,9 @@ fn plot(app: &mut CadApp, ctx: &egui::Context, open: &mut bool) -> Action {
|
||||
});
|
||||
ui.end_row();
|
||||
}
|
||||
ui.label("");
|
||||
pick = ui.button("Window <").on_hover_text("Pick the window's corners in the drawing").clicked();
|
||||
ui.end_row();
|
||||
}
|
||||
});
|
||||
sheet_sketch(ui, &st.page, &t);
|
||||
@@ -789,22 +936,42 @@ fn plot(app: &mut CadApp, ctx: &egui::Context, open: &mut bool) -> Action {
|
||||
json!({ "layout": st.target, "paper": page.paper, "landscape": page.landscape, "scaleToFit": page.scale_to_fit, "lineweights": page.lineweights }),
|
||||
));
|
||||
}
|
||||
preview = ui.button("Preview").on_hover_text("The sheet as these settings plot it").clicked();
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
cancel = ui.button("Cancel").clicked();
|
||||
ok = ui.button("OK").clicked();
|
||||
ok |= ui.button("OK").clicked();
|
||||
});
|
||||
});
|
||||
});
|
||||
// The output file type follows the device.
|
||||
let ext = choices.devices.iter().find(|dv| dv.name.eq_ignore_ascii_case(&st.page.device)).map_or("pdf", |dv| dv.output.as_str());
|
||||
set_temp(ctx, "plot_state", st.clone());
|
||||
if cancel {
|
||||
*open = false;
|
||||
}
|
||||
if pick {
|
||||
// The dialog comes back with these settings and the picked window (`plot_window_resume`).
|
||||
set_temp(ctx, PICKING_WINDOW, true);
|
||||
*open = false;
|
||||
app.start("plotwindow");
|
||||
return action;
|
||||
}
|
||||
if preview {
|
||||
// A sheet about 1600 pixels on its long side.
|
||||
let long_in = st.page.width_mm.max(st.page.height_mm).max(1.0) / 25.4;
|
||||
let mut info = format!("{} · {}", st.target, st.page.paper);
|
||||
if st.page.landscape {
|
||||
info.push_str(" · landscape");
|
||||
}
|
||||
crate::exchange::preview_plot(app, ctx, plot_params(&st, Some((1600.0 / long_in).clamp(50.0, 300.0))), info);
|
||||
return action;
|
||||
}
|
||||
if ok {
|
||||
let mut params = plot_params(&st);
|
||||
let mut params = plot_params(&st, None);
|
||||
if let Some(pick) = app.services.pick_save.as_ref() {
|
||||
let title = app.session.state().map(|s| s.title.clone()).unwrap_or_default();
|
||||
let stem = std::path::Path::new(&title).file_stem().map(|s| s.to_string_lossy().to_string()).filter(|s| !s.is_empty());
|
||||
let suggested = format!("{}-{}.pdf", stem.unwrap_or_else(|| "Drawing".into()), st.target);
|
||||
let suggested = format!("{}-{}.{ext}", stem.unwrap_or_else(|| "Drawing".into()), st.target);
|
||||
// Cancelling the file dialog keeps the Plot dialog open with its settings.
|
||||
let Some(path) = pick(&suggested) else { return action };
|
||||
if let Some(o) = params.as_object_mut() {
|
||||
@@ -910,3 +1077,66 @@ fn constraint_settings(app: &mut CadApp, ctx: &egui::Context, open: &mut bool) -
|
||||
});
|
||||
action
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod plot_dialog_tests {
|
||||
use super::*;
|
||||
use cadcraft_engine::Session;
|
||||
|
||||
fn state(target: &str) -> PlotState {
|
||||
let d = cadcraft_doc::Drawing::new_metric();
|
||||
PlotState { generation: 0, page: plot_page(&d, target), target: target.into(), window: [0.0, 0.0, 10.0, 10.0] }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_device_brings_its_papers_and_the_plot_names_them() {
|
||||
let mut st = state("Model");
|
||||
apply_page_change(&mut st.page, "device", &json!("CADCraft PNG"));
|
||||
assert_eq!(st.page.device, "CADCraft PNG");
|
||||
let dev = cadcraft_engine::cmd::plotters::find_device("CADCraft PNG").unwrap();
|
||||
assert!(dev.papers.iter().any(|p| p.name == st.page.paper), "a paper of the device: {}", st.page.paper);
|
||||
assert!(st.page.margins_mm.iter().any(|m| *m > 0.0), "the paper's printable area");
|
||||
apply_page_change(&mut st.page, "plotStyleTable", &json!("monochrome.ctb"));
|
||||
let p = plot_params(&st, None);
|
||||
assert_eq!((p["device"].as_str(), p["paper"].as_str()), (Some("CADCraft PNG"), Some(st.page.paper.as_str())));
|
||||
assert_eq!(p["plotStyleTable"], json!("monochrome.ctb"));
|
||||
// The preview plots the same sheet to PNG, whatever the device.
|
||||
let q = plot_params(&st, Some(150.0));
|
||||
assert_eq!((q["output"].as_str(), q["device"].as_str()), (Some("png"), Some("None")));
|
||||
assert_eq!(q["width"].as_f64(), Some(st.page.width_mm));
|
||||
assert_eq!(q["margins"], json!(st.page.margins_mm));
|
||||
// Back to no device: a standard paper, no device sent.
|
||||
apply_page_change(&mut st.page, "device", &json!("None"));
|
||||
apply_page_change(&mut st.page, "paper", &json!("A3"));
|
||||
let p = plot_params(&st, None);
|
||||
let a3 = cadcraft_render::paper_size("A3").unwrap().name;
|
||||
assert_eq!((p["device"].as_str(), p["paper"].as_str()), (Some("None"), Some(a3)));
|
||||
apply_page_change(&mut st.page, "plotStyleTable", &json!("None"));
|
||||
assert!(plot_params(&st, None).get("plotStyleTable").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn window_pick_brings_the_dialog_back_with_the_picked_window() {
|
||||
let ctx = egui::Context::default();
|
||||
let mut app = CadApp::new(Session::new(), crate::Services::default());
|
||||
set_temp(&ctx, "plot_state", state("Model"));
|
||||
set_temp(&ctx, PICKING_WINDOW, true);
|
||||
app.start("plotwindow");
|
||||
plot_window_resume(&mut app, &ctx);
|
||||
assert_eq!(app.ui.dialog, None, "still picking");
|
||||
app.session.cmdline("2,3").unwrap();
|
||||
app.session.cmdline("12,9").unwrap();
|
||||
plot_window_resume(&mut app, &ctx);
|
||||
assert_eq!(app.ui.dialog.as_deref(), Some("plot"));
|
||||
let st = temp::<PlotState>(&ctx, "plot_state").unwrap();
|
||||
assert_eq!((st.page.plot_area.as_str(), st.window), ("window", [2.0, 3.0, 12.0, 9.0]));
|
||||
// A cancelled pick brings the dialog back unchanged.
|
||||
app.ui.dialog = None;
|
||||
set_temp(&ctx, PICKING_WINDOW, true);
|
||||
app.start("plotwindow");
|
||||
app.session.input(cadcraft_engine::Input::Cancel).ok();
|
||||
plot_window_resume(&mut app, &ctx);
|
||||
assert_eq!(app.ui.dialog.as_deref(), Some("plot"));
|
||||
assert_eq!(temp::<PlotState>(&ctx, "plot_state").unwrap().window, [2.0, 3.0, 12.0, 9.0]);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user