diff --git a/crates/engine/src/cmd/fileio.rs b/crates/engine/src/cmd/fileio.rs index 7c828221b..0c888d13c 100644 --- a/crates/engine/src/cmd/fileio.rs +++ b/crates/engine/src/cmd/fileio.rs @@ -57,9 +57,9 @@ use serde_json::{Value, json}; pub use dxf::{UNSUPPORTED, Unsupported, unsupported}; pub use encode::{ARTBOARD_PARAMS, ArtboardPick, Encoded, encode, encode_all, encode_with_warnings}; pub(crate) use encode::{anti_alias, background, with_single_artboard}; -pub use export::export_source; pub(crate) use export::with_artboard_fills; pub(crate) use export::{export_crop, isolated}; +pub use export::{export_doc, export_source}; pub(crate) use load::check_not_lossy_overwrite; use load::err; pub(crate) use load::import_svg; diff --git a/crates/engine/src/cmd/fileio/export.rs b/crates/engine/src/cmd/fileio/export.rs index 77adaeac9..13564614b 100644 --- a/crates/engine/src/cmd/fileio/export.rs +++ b/crates/engine/src/cmd/fileio/export.rs @@ -72,6 +72,17 @@ pub(super) fn export(s: &mut Session, p: &Value) -> Result { write_encoded(path, &default_name(doc, f.extensions[0]), doc, &enc, json!({ "format": f.id, "warnings": enc.warnings })) } +/// The document an export of `st` in format `f` writes ([`export_source`]): a `.ai` or `.ait` +/// file names the view's active artboard ([`DocState::active_artboard`]) as its active one, as a +/// save does. The open document is unchanged. +pub fn export_doc<'a>(st: &'a DocState, f: &Format, p: &Value) -> Result> { + let mut doc = export_source(st, p)?; + if let Some(id) = super::save::moved_active_artboard(st, &doc, f) { + doc.to_mut().opened_artboard = Some(id); + } + Ok(doc) +} + /// The document an export of `st` writes: the whole document, or with `selectedOnly` just the /// selected objects, each in its layers and groups (a clipping group keeps its clipping path). pub fn export_source<'a>(st: &'a DocState, p: &Value) -> Result> { @@ -190,11 +201,7 @@ fn source<'a>(s: &'a mut Session, f: &Format, p: &Value, cmd: &str) -> Result<(C return Ok((Cow::Owned(doc), display_doc_title)); } let st = s.doc()?; - let mut doc = export_source(st, p)?; - // A `.ai` export names the view's active artboard, as a `.ai` save does. - if let Some(id) = super::save::moved_active_artboard(st, &doc, f) { - doc.to_mut().opened_artboard = Some(id); - } + let doc = export_doc(st, f, p)?; let display_doc_title = super::has_own_title(&doc); Ok((super::titled_for_export(&doc, st.path.as_deref(), f.id).map_or(doc, Cow::Owned), display_doc_title)) } diff --git a/crates/ui-egui/src/addons/document/tests.rs b/crates/ui-egui/src/addons/document/tests.rs index 69cb75091..7ef678308 100644 --- a/crates/ui-egui/src/addons/document/tests.rs +++ b/crates/ui-egui/src/addons/document/tests.rs @@ -3,7 +3,7 @@ use std::path::PathBuf; use serde_json::json; use vectorcraft_engine::Session; -use super::SAVE_COPY; +use super::{EXPORT_ALL, SAVE_COPY}; use crate::{VectorcraftApp, menus}; /// A new, empty folder of its own for test `name`. @@ -46,6 +46,31 @@ fn save_a_timestamped_copy_writes_beside_the_document() { let _ = std::fs::remove_dir_all(&dir); } +/// A `.ai` timestamped copy, and a `.ai` copy Export All Open Documents writes, name the view's +/// active artboard as their active one. +#[test] +fn ai_copies_name_the_views_active_artboard() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 60, "height": 40, "artboards": 3})).unwrap(); + app.sync_views(); + app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 20, "height": 20})).unwrap(); + let opts = vectorcraft_engine::cmd::fileio::LoadOptions { native: false, ..Default::default() }; + for (i, (cmd, artboard)) in [(SAVE_COPY, 2), (EXPORT_ALL, 1)].into_iter().enumerate() { + let dir = folder(&format!("active-artboard-{i}")); + if let Some(v) = app.view_mut() { + v.artboard = artboard; + } + app.run(cmd, json!({"folder": dir, "format": "ai"})).unwrap(); + let names = files(&dir); + assert_eq!(names.len(), 1, "{cmd}: {names:?}"); + let bytes = std::fs::read(dir.join(&names[0])).unwrap(); + let l = vectorcraft_engine::cmd::fileio::load_with("a.ai", &bytes, &opts).unwrap(); + assert!(l.editing_data && !l.restored, "{cmd}: {:?}", l.warnings); + assert_eq!(l.doc.opened_artboard.and_then(|id| l.doc.artboards.iter().position(|a| a.id == id)), Some(artboard), "{cmd}"); + let _ = std::fs::remove_dir_all(&dir); + } +} + #[test] fn an_unsaved_document_asks_for_a_folder() { let dir = folder("picked"); diff --git a/crates/ui-egui/src/app/commands.rs b/crates/ui-egui/src/app/commands.rs index a21411587..d8ef1bff8 100644 --- a/crates/ui-egui/src/app/commands.rs +++ b/crates/ui-egui/src/app/commands.rs @@ -93,6 +93,9 @@ impl VectorcraftApp { if cfg!(debug_assertions) { menus::check_params(&self.session, id, ¶ms)?; } + // A `.ai` file a command writes (a save, an export, a timestamped copy, a batch's save) names + // the view's active artboard. + io::remember_active_artboard(self); let artboards = self.artboard_tool_state(); if let Some(r) = menus::run_ui_command(self, id, ¶ms) { self.follow_artboard_tool(artboards); @@ -173,9 +176,6 @@ impl VectorcraftApp { return r; } } - // A `.ai` file a command writes (a save, an export, a batch's save) names the view's - // active artboard. - io::remember_active_artboard(self); // Native files carry the view they reopen at. if fileio::SaveMode::of(id).is_some() { io::remember_view(self); diff --git a/crates/ui-egui/src/io.rs b/crates/ui-egui/src/io.rs index d394d8e6c..d1e34e38b 100644 --- a/crates/ui-egui/src/io.rs +++ b/crates/ui-egui/src/io.rs @@ -207,6 +207,33 @@ mod tests { } } + /// A `.ai` file exported to a path (File › Export As, its dialog, a control request) names the + /// view's active artboard as its active one, and the open document is unchanged. + #[test] + fn an_ai_file_exported_to_a_path_names_the_views_active_artboard() { + let (mut app, written) = app(); + app.run("file.new", json!({"width": 60, "height": 40, "artboards": 3})).unwrap(); + app.sync_views(); + app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 20, "height": 20})).unwrap(); + let opts = fileio::LoadOptions { native: false, ..Default::default() }; + let active = |app: &VectorcraftApp| { + let bytes = written.borrow_mut().pop().map(|(_, b)| b).unwrap(); + let l = fileio::load_with("Art.ai", &bytes, &opts).unwrap(); + assert!(l.editing_data && !l.restored, "{:?}", l.warnings); + assert_eq!(app.session.doc().unwrap().doc.opened_artboard, None, "the document is unchanged"); + l.doc.opened_artboard.and_then(|id| l.doc.artboards.iter().position(|a| a.id == id)) + }; + for artboard in [2, 1] { + if let Some(v) = app.view_mut() { + v.artboard = artboard; + } + app.run("file.exportAs", json!({"format": "ai", "path": "Art.ai"})).unwrap(); + assert_eq!(active(&app), Some(artboard), "File › Export As"); + export(&mut app, Some("ai"), Some("Art.ai".into()), &json!({})).unwrap(); + assert_eq!(active(&app), Some(artboard), "the Export As dialog"); + } + } + fn bytes_of(app: &mut VectorcraftApp, cmd: &str, p: Value) -> Vec { let v = app.session.execute(cmd, &p).unwrap(); vectorcraft_format::base64_decode(v["dataBase64"].as_str().unwrap()).unwrap() diff --git a/crates/ui-egui/src/io/export.rs b/crates/ui-egui/src/io/export.rs index f32323151..1b438adf9 100644 --- a/crates/ui-egui/src/io/export.rs +++ b/crates/ui-egui/src/io/export.rs @@ -1,6 +1,6 @@ use super::save::{ - ask_before_losing, export_path, is_web, report_saved, report_write_error, saved_status, target_path, target_path_keeping, view_file, - write_encoded, write_named, write_reporting, + ask_before_losing, export_path, is_web, remember_active_artboard, report_saved, report_write_error, saved_status, target_path, + target_path_keeping, view_file, write_encoded, write_named, write_reporting, }; use super::{EngineError, Progress, Value, VectorcraftApp, Writer, background, cancel, fileio, json, progress}; @@ -33,8 +33,10 @@ pub fn export(app: &mut VectorcraftApp, format: Option<&str>, path: Option st.doc.clone(), std::borrow::Cow::Owned(d) => std::sync::Arc::new(d), };