forked from 1b-agent/opencadstudio-upstream
Merge pull request #1309 from ghost/embed-print-fonts
This commit is contained in:
commit
03f4e7cb86
29 files changed
+1833
-68
No files matched your search
@@ -4693,6 +4693,7 @@ impl OpenCADStudio {
|
||||
let x1 = p1.x.max(p2.x);
|
||||
let y1 = p1.y.max(p2.y);
|
||||
self.plot_window = Some((x0, y0, x1, y1));
|
||||
self.plot_dialog.window = self.plot_window;
|
||||
self.command_line.push_output(
|
||||
crate::tf!("Plot window: {x0:.2},{y0:.2} to {x1:.2},{y1:.2}").as_ref(),
|
||||
);
|
||||
@@ -4700,6 +4701,8 @@ impl OpenCADStudio {
|
||||
// receive the two clicks — bring the dialog back with the
|
||||
// window now active.
|
||||
self.plot_dialog.area = "Window".to_string();
|
||||
// Remember the pick immediately, like the printer choice.
|
||||
self.save_config();
|
||||
self.active_modal = Some(super::ModalKind::Plot);
|
||||
} else {
|
||||
// PLOTWINDOW always describes the plotted layout. In MSPACE
|
||||
@@ -4712,10 +4715,12 @@ impl OpenCADStudio {
|
||||
let x2 = p1.x.max(p2.x);
|
||||
let y2 = p1.y.max(p2.y);
|
||||
self.plot_window = Some((x1, y1, x2, y2));
|
||||
self.plot_dialog.window = self.plot_window;
|
||||
self.command_line.push_output(
|
||||
crate::tf!("Plot window: {x1:.2},{y1:.2} to {x2:.2},{y2:.2}").as_ref(),
|
||||
);
|
||||
self.plot_dialog.area = "Window".to_string();
|
||||
self.save_config();
|
||||
self.active_modal = Some(super::ModalKind::Plot);
|
||||
}
|
||||
self.tabs[i].active_cmd = None;
|
||||
|
||||
@@ -57,6 +57,10 @@ impl OpenCADStudio {
|
||||
return Some(Task::done(Message::ImagePick));
|
||||
}
|
||||
|
||||
cmd if cmd == "IMAGEEMBED" => {
|
||||
return Some(Task::done(Message::ImageEmbedPick));
|
||||
}
|
||||
|
||||
"REVCLOUD" | "REVCLOUD_RECTANGULAR" | "REVCLOUD_POLYGONAL" | "REVCLOUD_FREEHAND" => {
|
||||
use crate::modules::draw::draw::revcloud::RevCloudCommand;
|
||||
let view_height = self.tabs[i].scene.camera.borrow().ortho_size() as f64 * 2.0;
|
||||
|
||||
@@ -641,6 +641,7 @@ inventory::submit!(crate::command::CommandRegistration {
|
||||
"IM",
|
||||
"IMAGE",
|
||||
"IMAGEATTACH",
|
||||
"IMAGEEMBED",
|
||||
"IMPORTOBJ",
|
||||
"ISOLATEOBJECTS",
|
||||
"LA",
|
||||
|
||||
@@ -67,7 +67,54 @@ pub(super) const NAMES: &[&str] = &[
|
||||
"undo",
|
||||
"redo",
|
||||
];
|
||||
|
||||
/// Parse the `at` placement point for `embed_image`: `[x,y]` or `[x,y,z]`.
|
||||
fn embed_point(req: &Value) -> Result<acadrust::types::Vector3, Value> {
|
||||
let values = req["at"]
|
||||
.as_array()
|
||||
.filter(|v| (2..=3).contains(&v.len()))
|
||||
.ok_or_else(|| failure("invalid_point", "Expected at:[x,y] or [x,y,z]"))?;
|
||||
let mut p = [0.0_f64; 3];
|
||||
for (i, v) in values.iter().enumerate() {
|
||||
p[i] = v
|
||||
.as_f64()
|
||||
.filter(|v| v.is_finite())
|
||||
.ok_or_else(|| failure("invalid_point", "Expected finite coordinates"))?;
|
||||
}
|
||||
Ok(acadrust::types::Vector3::new(p[0], p[1], p[2]))
|
||||
}
|
||||
impl OpenCADStudio {
|
||||
/// `embed_image` — pack the picture at `path` into an OLE2FRAME placed
|
||||
/// with its lower-left corner at `at` (default width ≈ pixel_width/100,
|
||||
/// like the interactive command's Enter answer) and commit it as one
|
||||
/// undo step. The drawing stays self-contained: no external file to lose.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(super) fn control_embed_image(&mut self, req: &Value) -> Result<Task<Message>, Value> {
|
||||
let i = self.active_tab;
|
||||
let path = string(req, "path")?;
|
||||
let image = crate::io::ole_embed::EmbeddedImage::from_file(std::path::Path::new(&path))
|
||||
.map_err(|e| failure("embed_failed", e))?;
|
||||
let at = embed_point(req)?;
|
||||
let default_width = (image.pixel_width as f64 / 100.0).max(1.0);
|
||||
let width = req["width"].as_f64().filter(|w| *w > 0.0).unwrap_or(default_width);
|
||||
self.push_undo_snapshot(i, "IMAGEEMBED");
|
||||
let handle = crate::io::ole_embed::add_embedded_image(
|
||||
&mut self.tabs[i].scene.document,
|
||||
&image,
|
||||
at,
|
||||
width,
|
||||
)
|
||||
.map_err(|e| failure("embed_failed", e))?;
|
||||
self.post_ref_op(i);
|
||||
self.set_control_result(json!({
|
||||
"handle": format!("{:X}", handle.value()),
|
||||
"kind": "Ole2Frame",
|
||||
"path": path,
|
||||
"width": width,
|
||||
}));
|
||||
Ok(Task::none())
|
||||
}
|
||||
|
||||
pub(super) fn control_properties(&mut self) -> Value {
|
||||
self.refresh_properties();
|
||||
json!({"ok":true,"sections":self.tabs[self.active_tab].properties.sections.iter().map(|s|json!({"title":s.title,"properties":s.props.iter().map(|p|{
|
||||
|
||||
@@ -781,6 +781,8 @@ impl OpenCADStudio {
|
||||
"set_properties" => self.control_set_record_properties(req)?,
|
||||
"action" => self.control_ui_action(req)?,
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
"embed_image" => self.control_embed_image(req)?,
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
"save" => {
|
||||
let path = req["path"]
|
||||
.as_str()
|
||||
@@ -1387,4 +1389,39 @@ mod tests {
|
||||
);
|
||||
let _ = std::fs::remove_file(path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn embed_image_op_places_an_ole2frame_and_undoes_it() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
request(&mut app, json!({"op":"new"}));
|
||||
let img = image::RgbaImage::from_pixel(40, 30, image::Rgba([9, 9, 9, 255]));
|
||||
let path = std::env::temp_dir().join(format!("ocs-embed-{}.png", session_id()));
|
||||
img.save(&path).unwrap();
|
||||
|
||||
let response = request(
|
||||
&mut app,
|
||||
json!({"op":"embed_image","path":path.to_string_lossy(),"at":[0,0,0],"width":20}),
|
||||
);
|
||||
assert_eq!(response["status"], "completed", "{response}");
|
||||
let handle = response["result"]["handle"].as_str().unwrap().to_string();
|
||||
assert_eq!(response["result"]["kind"], "Ole2Frame");
|
||||
let ole_count = |app: &OpenCADStudio| {
|
||||
app.tabs[app.active_tab]
|
||||
.scene
|
||||
.document
|
||||
.entities()
|
||||
.filter(|e| matches!(e, acadrust::EntityType::Ole2Frame(_)))
|
||||
.count()
|
||||
};
|
||||
assert_eq!(ole_count(&app), 1);
|
||||
|
||||
// The entity must be queryable like any other and undoable as one step.
|
||||
let queried =
|
||||
app.automation_op(json!({"op":"entities","type":"OLE2FRAME"}).to_string().as_str());
|
||||
assert_eq!(queried["total"], 1, "{queried}");
|
||||
request(&mut app, json!({"op":"undo"}));
|
||||
assert_eq!(ole_count(&app), 0);
|
||||
let _ = std::fs::remove_file(path);
|
||||
let _ = handle;
|
||||
}
|
||||
}
|
||||
@@ -883,6 +883,12 @@ pub(super) struct OpenCADStudio {
|
||||
point_size_relative: bool,
|
||||
/// Whether the default-association prompt has been answered.
|
||||
default_assoc_prompted: bool,
|
||||
/// Offer to download missing `.shx` fonts on open (see `io::font_repo`).
|
||||
check_missing_fonts: bool,
|
||||
/// Custom font source base URL; empty selects the community repository.
|
||||
font_source_url: String,
|
||||
/// Editable copy of `font_source_url` shown in the missing-fonts prompt.
|
||||
font_source_input: String,
|
||||
donation_prompt_version: String,
|
||||
/// Read-only session (`--read-only`): editing is allowed but every save
|
||||
/// path is refused. Set once at boot from the CLI config.
|
||||
@@ -1171,6 +1177,9 @@ pub(super) struct OpenCADStudio {
|
||||
open_job_serial: u64,
|
||||
/// Last repair or failed-open report shown in the recovery modal.
|
||||
recovery_report: Option<crate::io::recovery::RecoveryReport>,
|
||||
/// Missing SHX fonts of the last opened drawing, offered for download
|
||||
/// from the community repository (see `crate::io::font_repo`).
|
||||
missing_fonts: Option<Vec<String>>,
|
||||
/// Drawings handed to us by other launches while `opening` was busy.
|
||||
/// `opening` is a single slot that a second `OpenPathPicked` would
|
||||
/// overwrite, and `on_file_opened` drops any result arriving once it is
|
||||
@@ -1791,6 +1800,8 @@ pub enum ModalKind {
|
||||
Unsaved,
|
||||
SaveDialog,
|
||||
Recovery,
|
||||
/// Fonts referenced by the drawing are missing on this machine.
|
||||
MissingFonts,
|
||||
RecoveryPrompt,
|
||||
Options,
|
||||
FindReplace,
|
||||
@@ -3580,6 +3591,20 @@ pub enum Message {
|
||||
ImagePick,
|
||||
/// Result of the image file picker + pixel dimension decode.
|
||||
ImagePickResult(Result<(std::path::PathBuf, u32, u32), String>),
|
||||
/// Open file-picker dialog for IMAGEEMBED command (async).
|
||||
ImageEmbedPick,
|
||||
/// Result of the embedded-image picker: bytes prepared for an OLE2FRAME.
|
||||
ImageEmbedPickResult(Result<crate::io::ole_embed::EmbeddedImage, String>),
|
||||
// ── Missing fonts ─────────────────────────────────────────────────────
|
||||
/// Download the offered missing fonts from the community repository.
|
||||
MissingFontsDownload,
|
||||
/// Edit the custom font source URL in the missing-fonts prompt.
|
||||
MissingFontsSourceChanged(String),
|
||||
/// Close the missing-fonts prompt without downloading.
|
||||
MissingFontsDismiss,
|
||||
/// Fonts fetched (or failed); payload lists (name, saved-path) pairs.
|
||||
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
|
||||
MissingFontsResult(Result<Vec<(String, std::path::PathBuf)>, String>),
|
||||
// ── PDF Underlay ──────────────────────────────────────────────────────
|
||||
/// Open file-picker dialog for PDFATTACH command (async).
|
||||
PdfAttachPick,
|
||||
@@ -3921,6 +3946,9 @@ impl OpenCADStudio {
|
||||
point_size_buf: String::new(),
|
||||
point_size_relative: true,
|
||||
default_assoc_prompted: false,
|
||||
check_missing_fonts: true,
|
||||
font_source_url: String::new(),
|
||||
font_source_input: String::new(),
|
||||
donation_prompt_version: String::new(),
|
||||
read_only: false,
|
||||
update_notice_version: None,
|
||||
@@ -3961,6 +3989,7 @@ impl OpenCADStudio {
|
||||
layout_settling: false,
|
||||
open_job_serial: 0,
|
||||
recovery_report: None,
|
||||
missing_fonts: None,
|
||||
pending_opens: std::collections::VecDeque::new(),
|
||||
active_interaction_index: None,
|
||||
queued_interaction_indices: std::collections::VecDeque::new(),
|
||||
|
||||
@@ -348,6 +348,15 @@ pub struct UserSettings {
|
||||
/// prompt has already been shown. Set once the user answers (either way),
|
||||
/// so we never nag again on subsequent launches.
|
||||
pub default_assoc_prompted: bool,
|
||||
/// Offer to download missing `.shx` fonts from the community repository
|
||||
/// when a drawing opens (see `crate::io::font_repo`).
|
||||
#[serde(default = "default_check_missing_fonts")]
|
||||
pub check_missing_fonts: bool,
|
||||
/// Custom font source base URL (empty = the OpenCADStudio community
|
||||
/// repository). Each missing font is fetched as `{base}/{file_name}`,
|
||||
/// so an intranet folder or a private GitHub raw folder both work.
|
||||
#[serde(default)]
|
||||
pub font_source_url: String,
|
||||
/// App version whose donation prompt has been displayed.
|
||||
pub donation_prompt_version: String,
|
||||
/// The graphics verdict (`GpuStatus::identity()`) whose warning popup the
|
||||
@@ -501,6 +510,10 @@ fn default_dimension_continue_mode() -> i16 {
|
||||
1
|
||||
}
|
||||
|
||||
fn default_check_missing_fonts() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn default_show_constraint_values() -> bool {
|
||||
true
|
||||
}
|
||||
@@ -553,6 +566,8 @@ impl Default for UserSettings {
|
||||
snap_angle_deg: 0.0,
|
||||
otrack: false,
|
||||
default_assoc_prompted: false,
|
||||
check_missing_fonts: true,
|
||||
font_source_url: String::new(),
|
||||
donation_prompt_version: String::new(),
|
||||
gpu_warning_silenced: String::new(),
|
||||
disabled_plugins: Vec::new(),
|
||||
|
||||
@@ -491,6 +491,8 @@ impl OpenCADStudio {
|
||||
snap_angle_deg: self.snap_angle_deg,
|
||||
otrack: self.snapper.otrack_enabled,
|
||||
default_assoc_prompted: self.default_assoc_prompted,
|
||||
check_missing_fonts: self.check_missing_fonts,
|
||||
font_source_url: self.font_source_url.clone(),
|
||||
donation_prompt_version: self.donation_prompt_version.clone(),
|
||||
gpu_warning_silenced: self.gpu_warning_silenced.clone(),
|
||||
disabled_plugins: {
|
||||
@@ -577,6 +579,9 @@ impl OpenCADStudio {
|
||||
// open / tab switch), not app-global, so they are not applied here.
|
||||
self.snapper.otrack_enabled = s.otrack;
|
||||
self.default_assoc_prompted = s.default_assoc_prompted;
|
||||
self.check_missing_fonts = s.check_missing_fonts;
|
||||
self.font_source_url = s.font_source_url.clone();
|
||||
self.font_source_input = s.font_source_url.clone();
|
||||
self.donation_prompt_version = s.donation_prompt_version.clone();
|
||||
self.gpu_warning_silenced = s.gpu_warning_silenced.clone();
|
||||
self.disabled_plugins = s.disabled_plugins.iter().cloned().collect();
|
||||
@@ -1728,6 +1733,21 @@ impl OpenCADStudio {
|
||||
self.active_modal = Some(crate::app::ModalKind::Recovery);
|
||||
Task::none()
|
||||
} else {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
// A drawing referencing fonts this machine lacks offers to
|
||||
// fetch them from the community repository before the user
|
||||
// studies garbled substitute text (unless recovery already
|
||||
// owns the modal slot).
|
||||
let missing = crate::io::font_repo::missing_shx_fonts(
|
||||
&self.tabs[i].scene.document,
|
||||
);
|
||||
if !missing.is_empty() && self.check_missing_fonts {
|
||||
self.font_source_input = self.font_source_url.clone();
|
||||
self.missing_fonts = Some(missing);
|
||||
self.active_modal = Some(crate::app::ModalKind::MissingFonts);
|
||||
}
|
||||
}
|
||||
self.drain_pending_open()
|
||||
};
|
||||
Task::batch([thumbs_task, pending_open_task, interaction_task])
|
||||
@@ -3106,9 +3126,15 @@ impl OpenCADStudio {
|
||||
.into_iter()
|
||||
.map(|(name, _, factor)| (name, factor))
|
||||
.collect();
|
||||
// This load only refreshes paper/scale context from the layout;
|
||||
// the device the user just plotted with must survive it.
|
||||
let kept_printer = self.plot_dialog.printer.clone();
|
||||
let kept_to_file = self.plot_dialog.to_file;
|
||||
if let Some(settings) = self.tabs[i].scene.effective_plot_settings() {
|
||||
self.load_plotsettings_into_dialog(&settings);
|
||||
}
|
||||
self.plot_dialog.printer = kept_printer;
|
||||
self.plot_dialog.to_file = kept_to_file;
|
||||
}
|
||||
let Some(page) = self.direct_plot_page()
|
||||
else {
|
||||
@@ -3272,6 +3298,7 @@ impl OpenCADStudio {
|
||||
} else {
|
||||
self.plot_dialog.paper_space = true;
|
||||
self.plot_window = None;
|
||||
self.plot_dialog.window = None;
|
||||
self.load_plotsettings_into_dialog(&page_setup);
|
||||
}
|
||||
let dialog = self.plot_dialog.clone();
|
||||
@@ -3799,6 +3826,13 @@ impl OpenCADStudio {
|
||||
self.plot_prev = Some(previous);
|
||||
let cur = self.tabs[self.active_tab].scene.current_layout.clone();
|
||||
let layout_entry = format!("*{cur}*");
|
||||
// The auto-applied page setup must not reset the output device: a
|
||||
// drawing rarely stores one, so honouring it would snap the dialog
|
||||
// back to the system default on every open. The user's last choice
|
||||
// (persisted across sessions with the rest of the dialog) survives
|
||||
// unless the setup names an explicit device of its own.
|
||||
let kept_printer = self.plot_dialog.printer.clone();
|
||||
let kept_to_file = self.plot_dialog.to_file;
|
||||
if self.tabs[self.active_tab]
|
||||
.scene
|
||||
.plot_settings_for(&cur)
|
||||
@@ -3808,6 +3842,19 @@ impl OpenCADStudio {
|
||||
} else {
|
||||
self.select_page_setup(crate::ui::window::plot::SETUP_PREV);
|
||||
}
|
||||
let setup_named_a_device =
|
||||
self.plot_dialog.printer.is_some() || self.plot_dialog.to_file;
|
||||
if !setup_named_a_device {
|
||||
self.plot_dialog.printer = kept_printer;
|
||||
self.plot_dialog.to_file = kept_to_file;
|
||||
}
|
||||
// A persisted plot window (saved with the dialog settings) restores
|
||||
// across sessions; a freshly loaded one wins so the two stay in sync.
|
||||
if self.plot_window.is_none() {
|
||||
self.plot_window = self.plot_dialog.window;
|
||||
} else {
|
||||
self.plot_dialog.window = self.plot_window;
|
||||
}
|
||||
if self.plot_dialog.scale.eq_ignore_ascii_case("fit") {
|
||||
self.plot_dialog.fit_to_paper = true;
|
||||
self.plot_dialog.scale = one_to_one.clone();
|
||||
@@ -4048,6 +4095,7 @@ impl OpenCADStudio {
|
||||
self.plot_dialog.to_file = false;
|
||||
self.plot_dialog.printer = Some(s);
|
||||
}
|
||||
self.save_config();
|
||||
self.request_printer_media()
|
||||
}
|
||||
M::PrinterMedia(printer, caps) => {
|
||||
@@ -4591,6 +4639,7 @@ impl OpenCADStudio {
|
||||
ps.plot_window.upper_right_x,
|
||||
ps.plot_window.upper_right_y,
|
||||
));
|
||||
self.plot_dialog.window = self.plot_window;
|
||||
}
|
||||
if !ps.current_style_sheet.is_empty()
|
||||
&& self
|
||||
@@ -5447,3 +5496,88 @@ cupsPrintQuality/Print Quality: *Normal High\n";
|
||||
assert_eq!(layout_paper(&app).0, "Roll_24_(609.60_x_1500.00_MM)");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod plot_device_persistence_tests {
|
||||
use super::*;
|
||||
|
||||
/// Reopening the Plot dialog must keep the user's printer. Layouts in
|
||||
/// real drawings carry plot settings with no device, and the auto-applied
|
||||
/// page setup used to reset the choice to the system default on every
|
||||
/// open.
|
||||
#[test]
|
||||
fn reopening_the_plot_dialog_keeps_the_chosen_printer() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
// A real drawing's layout carries plot settings with no device; the
|
||||
// auto-applied setup used to reset the choice to the default.
|
||||
let mut ps = acadrust::objects::PlotSettings::default();
|
||||
ps.printer_name = String::new();
|
||||
let layout = app.tabs[app.active_tab].scene.current_layout.clone();
|
||||
assert!(
|
||||
app.tabs[app.active_tab]
|
||||
.scene
|
||||
.set_layout_plot_settings(&layout, &ps),
|
||||
"the test layout must accept plot settings"
|
||||
);
|
||||
app.plot_dialog.printer = Some("Brother DCP-L2520D series".into());
|
||||
app.plot_dialog.to_file = false;
|
||||
let _ = app.on_plot_dialog_open();
|
||||
assert_eq!(
|
||||
app.plot_dialog.printer.as_deref(),
|
||||
Some("Brother DCP-L2520D series"),
|
||||
"the chosen printer must survive the auto-applied page setup"
|
||||
);
|
||||
assert!(!app.plot_dialog.to_file);
|
||||
}
|
||||
|
||||
/// A layout whose plot settings name an explicit device wins over the
|
||||
/// saved preference: selecting its setup is an explicit device choice.
|
||||
#[test]
|
||||
fn layout_device_overrides_the_saved_preference() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
app.plot_dialog.printer = Some("Brother DCP-L2520D series".into());
|
||||
let mut ps = acadrust::objects::PlotSettings::default();
|
||||
ps.printer_name = "Godex G500".into();
|
||||
app.load_plotsettings_into_dialog(&ps);
|
||||
assert_eq!(app.plot_dialog.printer.as_deref(), Some("Godex G500"));
|
||||
assert!(!app.plot_dialog.to_file);
|
||||
}
|
||||
|
||||
/// A persisted plot window (saved with the dialog settings) seeds the
|
||||
/// runtime window when the dialog reopens, so `area == "Window"` keeps
|
||||
/// plotting the same rectangle instead of reporting an empty area.
|
||||
#[test]
|
||||
fn persisted_window_seeds_the_dialog_on_reopen() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
assert!(app.plot_window.is_none());
|
||||
app.plot_dialog.area = "Window".into();
|
||||
app.plot_dialog.window = Some((-10.0, -5.0, 30.0, 15.0));
|
||||
let _ = app.on_plot_dialog_open();
|
||||
assert_eq!(app.plot_window, Some((-10.0, -5.0, 30.0, 15.0)));
|
||||
}
|
||||
|
||||
/// A window freshly loaded from a layout's plot settings is mirrored into
|
||||
/// the dialog so it persists with the next config save.
|
||||
#[test]
|
||||
fn layout_window_is_mirrored_into_the_dialog() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
let mut ps = acadrust::objects::PlotSettings::default();
|
||||
ps.plot_type = acadrust::objects::PlotType::Window;
|
||||
ps.set_plot_window(1.0, 2.0, 3.0, 4.0);
|
||||
app.load_plotsettings_into_dialog(&ps);
|
||||
assert_eq!(app.plot_window, Some((1.0, 2.0, 3.0, 4.0)));
|
||||
assert_eq!(app.plot_dialog.window, Some((1.0, 2.0, 3.0, 4.0)));
|
||||
}
|
||||
|
||||
/// A layout pointing at a PDF device switches the dialog to file output.
|
||||
#[test]
|
||||
fn pdf_device_switches_to_file_output() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
app.plot_dialog.printer = Some("Brother DCP-L2520D series".into());
|
||||
let mut ps = acadrust::objects::PlotSettings::default();
|
||||
ps.printer_name = "Microsoft Print to PDF".into();
|
||||
app.load_plotsettings_into_dialog(&ps);
|
||||
assert!(app.plot_dialog.to_file);
|
||||
assert!(app.plot_dialog.printer.is_none());
|
||||
}
|
||||
}
|
||||
+111
-3
@@ -1007,6 +1007,93 @@ impl OpenCADStudio {
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::ImageEmbedPick => {
|
||||
Task::perform(crate::io::pick_embedded_image_file(), Message::ImageEmbedPickResult)
|
||||
}
|
||||
|
||||
Message::ImageEmbedPickResult(Ok(image)) => {
|
||||
use crate::command::CadCommand;
|
||||
use crate::modules::draw::draw::raster_image::ImageCommand;
|
||||
self.command_line.push_output(crate::tf!(
|
||||
"IMAGEEMBED \"{name}\": {w}×{h} px (embedded)",
|
||||
name = image.name.as_str(),
|
||||
w = image.pixel_width,
|
||||
h = image.pixel_height,
|
||||
).as_ref());
|
||||
let cmd = ImageCommand::new_embedded(image);
|
||||
let i = self.active_tab;
|
||||
self.command_line.push_info(&cmd.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::ImageEmbedPickResult(Err(e)) => {
|
||||
if e != "Cancelled" {
|
||||
self.command_line
|
||||
.push_error(crate::tf!("IMAGEEMBED: {e}").as_ref());
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::MissingFontsSourceChanged(url) => {
|
||||
self.font_source_input = url;
|
||||
Task::none()
|
||||
}
|
||||
Message::MissingFontsDownload => {
|
||||
// Remember the source across sessions — and across drawings.
|
||||
let source = self.font_source_input.trim().to_string();
|
||||
if self.font_source_url != source {
|
||||
self.font_source_url = source.clone();
|
||||
self.save_config();
|
||||
}
|
||||
let fonts = self.missing_fonts.take().unwrap_or_default();
|
||||
Task::perform(
|
||||
async move {
|
||||
let source = crate::io::font_repo::FontSource::from_url(&source);
|
||||
crate::io::font_repo::download_fonts(&fonts, &source)
|
||||
},
|
||||
Message::MissingFontsResult,
|
||||
)
|
||||
}
|
||||
Message::MissingFontsDismiss => {
|
||||
self.missing_fonts = None;
|
||||
self.close_active_modal();
|
||||
Task::none()
|
||||
}
|
||||
Message::MissingFontsResult(result) => {
|
||||
self.missing_fonts = None;
|
||||
self.close_active_modal();
|
||||
match result {
|
||||
Ok(pairs) if pairs.is_empty() => {
|
||||
self.command_line.push_error(crate::t!(
|
||||
"None of the missing fonts are in the community repository yet. Contribute them at github.com/huaninstratech/OpenCADStudio/tree/main/fonts."
|
||||
).as_ref());
|
||||
}
|
||||
Ok(pairs) => {
|
||||
for (name, path) in &pairs {
|
||||
self.command_line.push_output(crate::tf!(
|
||||
"FONT Downloaded {name} → {path}",
|
||||
path = path.display()
|
||||
).as_ref());
|
||||
}
|
||||
// The downloaded files change glyph resolution for the
|
||||
// whole drawing — reload it through the standard open
|
||||
// pipeline so every wire is rebuilt with the real fonts.
|
||||
let i = self.active_tab;
|
||||
if let Some(path) = self.tabs[i].current_path.clone() {
|
||||
return Task::done(Message::OpenExternal(path));
|
||||
}
|
||||
self.command_line.push_info(crate::t!(
|
||||
"Save and reopen the drawing to apply the new fonts."
|
||||
).as_ref());
|
||||
}
|
||||
Err(e) => {
|
||||
self.command_line.push_error(crate::tf!("Font download failed: {e}").as_ref());
|
||||
}
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::PdfAttachPick => Task::perform(
|
||||
async {
|
||||
let handle = crate::sys::file_dialog()
|
||||
@@ -8446,7 +8533,7 @@ impl OpenCADStudio {
|
||||
.and_then(|path| path.file_stem())
|
||||
.map(|name| format!("{}_layouts", name.to_string_lossy()))
|
||||
.unwrap_or_else(|| "drawing_layouts".into());
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(all(not(target_arch = "wasm32"), not(target_os = "windows")))]
|
||||
{
|
||||
let Some(window_id) = self.main_window else {
|
||||
return Task::done(Message::PrintAllPdfPath(None));
|
||||
@@ -8456,6 +8543,13 @@ impl OpenCADStudio {
|
||||
})
|
||||
.map(Message::PrintAllPdfPath)
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
Task::perform(
|
||||
crate::io::pdf_export::pick_pdf_path_async(stem),
|
||||
Message::PrintAllPdfPath,
|
||||
)
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
let _ = stem;
|
||||
@@ -8491,7 +8585,7 @@ impl OpenCADStudio {
|
||||
.and_then(|p: &std::path::Path| p.file_stem())
|
||||
.map(|s: &std::ffi::OsStr| s.to_string_lossy().into_owned())
|
||||
.unwrap_or_else(|| "drawing".into());
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(all(not(target_arch = "wasm32"), not(target_os = "windows")))]
|
||||
{
|
||||
let Some(window_id) = self.main_window else {
|
||||
return Task::done(Message::PlotExportPath(None));
|
||||
@@ -8501,6 +8595,13 @@ impl OpenCADStudio {
|
||||
})
|
||||
.map(Message::PlotExportPath)
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
Task::perform(
|
||||
crate::io::pdf_export::pick_pdf_path_async(stem),
|
||||
Message::PlotExportPath,
|
||||
)
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
Task::perform(
|
||||
@@ -8520,7 +8621,7 @@ impl OpenCADStudio {
|
||||
.and_then(|p: &std::path::Path| p.file_stem())
|
||||
.map(|s: &std::ffi::OsStr| s.to_string_lossy().into_owned())
|
||||
.unwrap_or_else(|| "drawing".into());
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(all(not(target_arch = "wasm32"), not(target_os = "windows")))]
|
||||
{
|
||||
let Some(window_id) = self.main_window else {
|
||||
return Task::done(Message::PlotWindowExportPath(None));
|
||||
@@ -8530,6 +8631,13 @@ impl OpenCADStudio {
|
||||
})
|
||||
.map(Message::PlotWindowExportPath)
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
Task::perform(
|
||||
crate::io::pdf_export::pick_pdf_path_async(stem),
|
||||
Message::PlotWindowExportPath,
|
||||
)
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
Task::perform(
|
||||
|
||||
@@ -53,6 +53,7 @@ impl OpenCADStudio {
|
||||
Some(K::AttributeEditor) => crate::tr!("modal", "attribute-editor"),
|
||||
Some(K::SaveDialog) => crate::tr!("modal", "save-drawing-as"),
|
||||
Some(K::Recovery) => crate::tr!("modal", "recovery-report"),
|
||||
Some(K::MissingFonts) => crate::t!("Missing fonts").into_owned(),
|
||||
Some(K::RecoveryPrompt) => crate::tr!("modal", "recovery-prompt"),
|
||||
Some(K::GpuWarning) => crate::tr!("gpu", "title"),
|
||||
None => String::new(),
|
||||
@@ -1637,6 +1638,17 @@ impl OpenCADStudio {
|
||||
)
|
||||
})
|
||||
}
|
||||
super::super::ModalKind::MissingFonts => {
|
||||
let fonts = self.missing_fonts.as_ref()?;
|
||||
let font_source = &self.font_source_input;
|
||||
automatic_flow(ex, |flow| {
|
||||
crate::ui::window::missing_fonts::view_window(
|
||||
fonts,
|
||||
&font_source,
|
||||
flow,
|
||||
)
|
||||
})
|
||||
}
|
||||
super::super::ModalKind::Recovery => {
|
||||
let report = self.recovery_report.as_ref()?;
|
||||
sized_flow(ex, 680, 460, |flow| {
|
||||
|
||||
Reference in new issue
Block a user