.ai save: exports to a path and timestamped copies name the active artboard

A .ai file that File > Export As, its dialog or a control request writes
to a path now names the view's active artboard, as document.export and
document.serialize do. The UI export records the view's artboard and
takes its document from fileio::export_doc, which applies
DocState::active_artboard to the export's copy; document.export and
document.serialize use the same function.

The app now records the view's active artboard before every command,
including the commands the UI handles itself, so a .ai copy from Save a
Timestamped Copy or Export All Open Documents names it too. Before,
these files named the artboard the document opened on, or the first.
The open document is unchanged.
This commit is contained in:
dotCooCoo
2026-11-07 04:40:02 +07:00
committed by bflatastic
parent d45ebe465b
commit 5216632e6b
6 changed files with 74 additions and 13 deletions
+1 -1
View File
@@ -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;
+12 -5
View File
@@ -72,6 +72,17 @@ pub(super) fn export(s: &mut Session, p: &Value) -> Result<Value> {
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<Cow<'a, Document>> {
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<Cow<'a, Document>> {
@@ -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))
}
+26 -1
View File
@@ -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");
+3 -3
View File
@@ -93,6 +93,9 @@ impl VectorcraftApp {
if cfg!(debug_assertions) {
menus::check_params(&self.session, id, &params)?;
}
// 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, &params) {
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);
+27
View File
@@ -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<u8> {
let v = app.session.execute(cmd, &p).unwrap();
vectorcraft_format::base64_decode(v["dataBase64"].as_str().unwrap()).unwrap()
+5 -3
View File
@@ -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<Strin
// encoder (an exporter plug-in is told the file it writes).
let params = &if params.is_object() { params.clone() } else { json!({}) };
let f = fileio::writable_format(format, path.as_deref())?;
// A `.ai` file names the view's active artboard.
remember_active_artboard(app);
let st = app.session.active().ok_or("no document")?;
let doc = match fileio::export_source(st, params).map_err(|e| e.to_string())? {
let doc = match fileio::export_doc(st, f, params).map_err(|e| e.to_string())? {
std::borrow::Cow::Borrowed(_) => st.doc.clone(),
std::borrow::Cow::Owned(d) => std::sync::Arc::new(d),
};