.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
+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),
};