mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-11 02:19:56 -06:00
Merge pull request #5951 from dotCooCoo/fix/ai-active-artboard
.ai save: the editing copy names the active artboard
This commit is contained in:
@@ -326,8 +326,8 @@ pub struct Document {
|
||||
#[serde(skip)]
|
||||
pub retired_artboard_id: RetiredArtboardId,
|
||||
/// The id of the artboard an imported file names as its active one (a `.ai` file's
|
||||
/// `CropAreaActive`): the document opens with that artboard active. `None`: the first. Never
|
||||
/// saved.
|
||||
/// `CropAreaActive`): the document opens with that artboard active. A `.ai` save or export
|
||||
/// names it as the file's active artboard. `None`: the first. Native files do not keep it.
|
||||
#[serde(skip)]
|
||||
pub opened_artboard: Option<u32>,
|
||||
/// Some object is a dimension (the Dimension tool): edits refresh dimensions only while this
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,7 +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 doc = export_source(st, p)?;
|
||||
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))
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ pub const AI_OPTIONS: &[FormatOption] = &[
|
||||
name: "editingData",
|
||||
ty: "boolean",
|
||||
default: "true",
|
||||
description: "also carry an editing copy of the artwork in the form other apps that open .ai files read: layers and sublayers, groups, compound paths, paths, fills and strokes, colors, linear and radial gradients, patterns on a grid of tiles, swatches, artboards, the transparency of objects and layers, objects' opacity masks, clipping groups and layers clipped by a path, embedded images with their pixels, and linked images as placed files with a preview, with type, other patterns, symbols, other clipping objects and live objects as plain filled and stroked paths, freeform gradients, gradients along or across a stroke and gradients of more than 256 stops as their average color, gradients that dither or mix perceptually as classic ones, and gradient stops linked to global process colors as their colors (written only with every artboard, as Preserve Editing is)",
|
||||
description: "also carry an editing copy of the artwork in the form other apps that open .ai files read: layers and sublayers, groups, compound paths, paths, fills and strokes, colors, linear and radial gradients, patterns on a grid of tiles, swatches, artboards and which one is active, the transparency of objects and layers, objects' opacity masks, clipping groups and layers clipped by a path, embedded images with their pixels, and linked images as placed files with a preview, with type, other patterns, symbols, other clipping objects and live objects as plain filled and stroked paths, freeform gradients, gradients along or across a stroke and gradients of more than 256 stops as their average color, gradients that dither or mix perceptually as classic ones, and gradient stops linked to global process colors as their colors (written only with every artboard, as Preserve Editing is)",
|
||||
},
|
||||
FormatOption {
|
||||
name: "aiVersion",
|
||||
|
||||
@@ -447,22 +447,34 @@ fn is_svg(f: &Format) -> bool {
|
||||
}
|
||||
|
||||
/// The document as written: native files carry the view to reopen at and the Layers panel's open
|
||||
/// rows and transparency grid visibility.
|
||||
/// rows and transparency grid visibility; a `.ai` file names the view's active artboard
|
||||
/// ([`DocState::active_artboard`]) as its active one.
|
||||
fn doc_to_save(st: &DocState, f: &Format) -> Arc<Document> {
|
||||
if !is_native(f) {
|
||||
return st.doc.clone();
|
||||
}
|
||||
let open = st.layers_open.saved(&st.doc);
|
||||
if st.doc.last_view == st.view && st.doc.layers_open == open && st.doc.transparency_grid == st.transparency_grid {
|
||||
let active = moved_active_artboard(st, &st.doc, f);
|
||||
if st.doc.last_view == st.view && st.doc.layers_open == open && st.doc.transparency_grid == st.transparency_grid && active.is_none() {
|
||||
return st.doc.clone();
|
||||
}
|
||||
let mut d = (*st.doc).clone();
|
||||
if active.is_some() {
|
||||
d.opened_artboard = active;
|
||||
}
|
||||
d.last_view = st.view.clone();
|
||||
d.transparency_grid = st.transparency_grid;
|
||||
d.layers_open = open;
|
||||
Arc::new(d)
|
||||
}
|
||||
|
||||
/// The artboard a `.ai` or `.ait` file written from `st` (`doc`, the document it writes) names as
|
||||
/// its active one, when that is not already the one `doc` opened on: the view's active artboard
|
||||
/// ([`DocState::active_artboard`]), if `doc` has it.
|
||||
pub(super) fn moved_active_artboard(st: &DocState, doc: &Document, f: &Format) -> Option<u32> {
|
||||
st.active_artboard.filter(|id| matches!(f.id, "ai" | "ait") && doc.artboards.iter().any(|a| a.id == *id) && doc.opened_artboard != Some(*id))
|
||||
}
|
||||
|
||||
/// What a format loses against a native file (reported whenever a save writes it).
|
||||
fn fidelity_warning(f: &Format) -> Option<String> {
|
||||
(!is_native(f)).then(|| {
|
||||
|
||||
@@ -148,6 +148,61 @@ fn an_ai_file_opens_with_its_active_artboard() {
|
||||
assert_eq!(d.opened_artboard, Some(d.artboards[1].id), "{r}");
|
||||
}
|
||||
|
||||
/// A `.ai` save names the view's active artboard in its editing copy, else the one the document
|
||||
/// opened on, else the first; the file read from that copy opens on it.
|
||||
#[test]
|
||||
fn an_ai_save_names_its_active_artboard() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 60, "height": 40, "artboards": 3})).unwrap();
|
||||
s.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 20, "height": 20})).unwrap();
|
||||
let ids: Vec<u32> = s.doc().unwrap().doc.artboards.iter().map(|a| a.id).collect();
|
||||
let opts = LoadOptions { native: false, ..Default::default() };
|
||||
let active = |s: &mut Session, format: &str| {
|
||||
let cmd = if format == "ait" { "file.saveAsTemplate" } else { "file.saveCopy" };
|
||||
let saved = s.execute(cmd, &json!({"format": format})).unwrap();
|
||||
let bytes = vectorcraft_format::base64_decode(saved["dataBase64"].as_str().unwrap()).unwrap();
|
||||
let l = load::load_with(&format!("a.{format}"), &bytes, &opts).unwrap();
|
||||
assert!(l.editing_data && !l.restored, "{:?}", l.warnings);
|
||||
assert_eq!(l.doc.artboards.len(), 3);
|
||||
l.doc.opened_artboard.and_then(|id| l.doc.artboards.iter().position(|a| a.id == id)).unwrap_or(0)
|
||||
};
|
||||
assert_eq!(active(&mut s, "ai"), 0);
|
||||
// The view's.
|
||||
s.doc_mut().unwrap().active_artboard = Some(ids[2]);
|
||||
assert_eq!((active(&mut s, "ai"), active(&mut s, "ait")), (2, 2));
|
||||
// The one the document opened on, when the view names none or an artboard it doesn't have.
|
||||
Arc::make_mut(&mut s.doc_mut().unwrap().doc).opened_artboard = Some(ids[1]);
|
||||
for view in [None, Some(99)] {
|
||||
s.doc_mut().unwrap().active_artboard = view;
|
||||
assert_eq!(active(&mut s, "ai"), 1, "{view:?}");
|
||||
}
|
||||
// The view's first artboard over the one the document opened on.
|
||||
s.doc_mut().unwrap().active_artboard = Some(ids[0]);
|
||||
assert_eq!(active(&mut s, "ai"), 0);
|
||||
assert_eq!(s.doc().unwrap().doc.opened_artboard, Some(ids[1]), "the document is unchanged");
|
||||
}
|
||||
|
||||
/// A `.ai` export (`document.serialize`, `document.export`, with or without `selectedOnly`) names
|
||||
/// the view's active artboard in its editing copy, as a save does.
|
||||
#[test]
|
||||
fn an_ai_export_names_its_active_artboard() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 60, "height": 40, "artboards": 3})).unwrap();
|
||||
s.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 20, "height": 20})).unwrap();
|
||||
let ids: Vec<u32> = s.doc().unwrap().doc.artboards.iter().map(|a| a.id).collect();
|
||||
s.doc_mut().unwrap().active_artboard = Some(ids[2]);
|
||||
let opts = LoadOptions { native: false, ..Default::default() };
|
||||
for (cmd, selected) in [("document.serialize", false), ("document.serialize", true), ("document.export", false)] {
|
||||
let out = s.execute(cmd, &json!({"format": "ai", "selectedOnly": selected})).unwrap();
|
||||
let bytes = vectorcraft_format::base64_decode(out["dataBase64"].as_str().unwrap()).unwrap();
|
||||
let l = load::load_with("a.ai", &bytes, &opts).unwrap();
|
||||
assert!(l.editing_data && !l.restored, "{cmd}: {:?}", l.warnings);
|
||||
let active = l.doc.opened_artboard.and_then(|id| l.doc.artboards.iter().position(|a| a.id == id));
|
||||
assert_eq!(active, Some(2), "{cmd} {selected}");
|
||||
}
|
||||
assert_eq!(s.doc().unwrap().doc.opened_artboard, None, "the document is unchanged");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_ai_unread_group_keeps_its_layers_and_live_neighbor() {
|
||||
let live = format!("0 0 1 0 k\n{}f", ai::rect(10.0, 10.0, 14.0, 14.0));
|
||||
|
||||
@@ -76,6 +76,11 @@ pub struct DocState {
|
||||
/// The view saved into native files (`Document::last_view`); the UI keeps it current before a
|
||||
/// save and restores it when the document opens.
|
||||
pub view: Option<vectorcraft_doc::SavedView>,
|
||||
/// The id of the view's active artboard, which the UI keeps current before each command it
|
||||
/// runs: a `.ai` save or export names that artboard as the file's active one. `None`, or an
|
||||
/// artboard the document no longer has: the artboard the document opened on
|
||||
/// ([`vectorcraft_doc::Document::opened_artboard`]), else the first.
|
||||
pub active_artboard: Option<u32>,
|
||||
/// Restored by Data Recovery: the title says "[Recovered]" and Save asks where to save it
|
||||
/// (suggesting `path`, the file it was copied from) instead of overwriting that file.
|
||||
pub recovered: bool,
|
||||
@@ -266,6 +271,7 @@ impl DocState {
|
||||
save_artboards: vec![],
|
||||
converted: false,
|
||||
view,
|
||||
active_artboard: None,
|
||||
recovered: false,
|
||||
recovery: None,
|
||||
print_tiling: false,
|
||||
|
||||
@@ -431,10 +431,12 @@ fn ruler_units(u: Unit) -> u8 {
|
||||
/// bottom-right corners in art space (`PositionPoint1`, `PositionPoint2`), its top-left corner on
|
||||
/// the canvas (`RulerOrigin`, `origin` being art space's origin there), its pixel aspect ratio
|
||||
/// (`PAR`), `DisplayMark` 0 (as in the files users sent), whether it is the selected one (the
|
||||
/// first) and its name; then the canvas's size. Every record holds all of these entries. The
|
||||
/// artboards and the canvas size are written in the `/Recorded` part of the `/Document`
|
||||
/// dictionary, followed by an empty `/NotRecorded` part.
|
||||
/// active one) and its name; then the index of the active artboard (`CropAreaActive`: the one
|
||||
/// [`Document::opened_artboard`] names, else the first) and the canvas's size. Every record holds
|
||||
/// all of these entries. The artboards, the active artboard and the canvas size are written in the
|
||||
/// `/Recorded` part of the `/Document` dictionary, followed by an empty `/NotRecorded` part.
|
||||
fn artboards(s: &mut String, doc: &Document, to_art: Affine, origin: (f64, f64)) {
|
||||
let active = doc.opened_artboard.and_then(|id| doc.artboards.iter().position(|a| a.id == id)).unwrap_or(0);
|
||||
s.push_str("%AI9_BeginDocumentData\n%_/Document :\n%_/Dictionary :\n%_/Array :\n");
|
||||
for (i, ab) in doc.artboards.iter().enumerate() {
|
||||
let r = to_art.transform_rect_bbox(ab.rect);
|
||||
@@ -449,10 +451,11 @@ fn artboards(s: &mut String, doc: &Document, to_art: Affine, origin: (f64, f64))
|
||||
ps::push_num(s, if par.is_finite() && par > 0.0 { par } else { 1.0 });
|
||||
// Display marks: 1 the center mark, 2 cross hairs, 4 video safe areas.
|
||||
let mark = u8::from(ab.show_center_mark) | u8::from(ab.show_cross_hairs) << 1 | u8::from(ab.show_video_safe_areas) << 2;
|
||||
let _ = writeln!(s, " /Real (PAR) ,\n%_{mark} /Int (DisplayMark) ,\n%_{} /Bool (IsArtboardSelected) ,", u8::from(i == 0));
|
||||
let _ = writeln!(s, " /Real (PAR) ,\n%_{mark} /Int (DisplayMark) ,\n%_{} /Bool (IsArtboardSelected) ,", u8::from(i == active));
|
||||
let _ = writeln!(s, "%_{} /UnicodeString (Name) ,\n%_; ,", ps::bytes(ab.name.as_bytes()));
|
||||
}
|
||||
s.push_str("%_; (ArtboardArray) ,\n");
|
||||
let _ = writeln!(s, "%_{active} /Int (CropAreaActive) ,");
|
||||
let _ = writeln!(s, "%_{CANVAS_SIZE} /Int (AIDocumentCanvasSize) ,");
|
||||
s.push_str("%_; /Recorded ,\n%_/Dictionary : /NotRecorded ,\n%_; /NotRecorded ,\n%_;\n%AI9_EndDocumentData\n");
|
||||
}
|
||||
|
||||
@@ -367,9 +367,9 @@ fn add_rect(doc: &mut Document, r: Rect) {
|
||||
doc.insert(Some(layer), usize::MAX, rect).unwrap();
|
||||
}
|
||||
|
||||
/// The end of the document data: the artboards and the canvas size in the `/Recorded` part of the
|
||||
/// `/Document` dictionary, then an empty `/NotRecorded` part.
|
||||
const RECORDED: &str = "%_; (ArtboardArray) ,\n%_16383 /Int (AIDocumentCanvasSize) ,\n%_; /Recorded ,\n%_/Dictionary : /NotRecorded ,\n%_; /NotRecorded ,\n%_;\n%AI9_EndDocumentData\n";
|
||||
/// The end of the document data: the artboards, the active artboard (the first) and the canvas
|
||||
/// size in the `/Recorded` part of the `/Document` dictionary, then an empty `/NotRecorded` part.
|
||||
const RECORDED: &str = "%_; (ArtboardArray) ,\n%_0 /Int (CropAreaActive) ,\n%_16383 /Int (AIDocumentCanvasSize) ,\n%_; /Recorded ,\n%_/Dictionary : /NotRecorded ,\n%_; /NotRecorded ,\n%_;\n%AI9_EndDocumentData\n";
|
||||
|
||||
/// One letter-size artboard: the art 36 points in from its top-left corner, the artboard's record
|
||||
/// and the boxes, with the artboards in the `/Recorded` part of the `/Document` dictionary.
|
||||
@@ -984,6 +984,29 @@ fn artboard_display_marks_are_written() {
|
||||
assert!(ab.show_center_mark && !ab.show_cross_hairs && ab.show_video_safe_areas);
|
||||
}
|
||||
|
||||
/// The active artboard, the one `opened_artboard` names (else the first), is the selected one and
|
||||
/// its index follows the artboards in the recorded part; the editing data reads back with that
|
||||
/// artboard active.
|
||||
#[test]
|
||||
fn the_active_artboard_follows_the_artboards() {
|
||||
let mut doc = Document::new(200.0, 100.0);
|
||||
for (id, x0) in [(2, 250.0), (3, 500.0)] {
|
||||
doc.artboards.push(Artboard { id, rect: Rect::new(x0, 0.0, x0 + 200.0, 100.0), ..doc.artboards[0].clone() });
|
||||
}
|
||||
let first = doc.artboards[0].id;
|
||||
// None, the first, the third, and an artboard the document doesn't have (the first).
|
||||
for (opened, index) in [(None, 0), (Some(first), 0), (Some(3), 2), (Some(9), 0)] {
|
||||
doc.opened_artboard = opened;
|
||||
let p = program(&doc);
|
||||
let entry = format!("%_; (ArtboardArray) ,\n%_{index} /Int (CropAreaActive) ,\n%_16383 /Int (AIDocumentCanvasSize) ,\n%_; /Recorded ,\n");
|
||||
assert!(p.contains(&entry), "{opened:?}: {p}");
|
||||
assert_eq!(p.matches("%_1 /Bool (IsArtboardSelected) ,\n").count(), 1, "{opened:?}");
|
||||
let back = read_back(&doc);
|
||||
let active = back.opened_artboard.and_then(|id| back.artboards.iter().position(|a| a.id == id)).unwrap_or(0);
|
||||
assert_eq!((back.artboards.len(), active), (3, index), "{opened:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A guide is its path followed by `(n) *` (closed) or `(N) *` (open), with no `(N) *` before
|
||||
/// it, and reads back as the same guide.
|
||||
#[test]
|
||||
|
||||
@@ -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,64 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
/// Export All Open Documents as `.ai` names each document's own active artboard, also for a
|
||||
/// document whose artboard changed in its view after the last command and that is not active.
|
||||
#[test]
|
||||
fn export_all_names_each_documents_active_artboard() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
for name in ["first", "second"] {
|
||||
app.run("file.new", json!({"width": 60, "height": 40, "artboards": 3, "name": name})).unwrap();
|
||||
app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 20, "height": 20})).unwrap();
|
||||
}
|
||||
app.sync_views();
|
||||
// Choosing a tab and an artboard runs no command.
|
||||
assert!(app.session.set_active(0));
|
||||
if let Some(v) = app.view_mut() {
|
||||
v.artboard = 2;
|
||||
}
|
||||
assert!(app.session.set_active(1));
|
||||
if let Some(v) = app.view_mut() {
|
||||
v.artboard = 1;
|
||||
}
|
||||
let dir = folder("export-all-active-artboards");
|
||||
let r = app.run(EXPORT_ALL, json!({"folder": dir, "format": "ai"})).unwrap();
|
||||
let opts = vectorcraft_engine::cmd::fileio::LoadOptions { native: false, ..Default::default() };
|
||||
let written = r["files"].as_array().unwrap();
|
||||
assert_eq!(written.len(), 2, "{r}");
|
||||
for (f, artboard) in written.iter().zip([2, 1]) {
|
||||
let bytes = std::fs::read(f["path"].as_str().unwrap()).unwrap();
|
||||
let l = vectorcraft_engine::cmd::fileio::load_with("a.ai", &bytes, &opts).unwrap();
|
||||
assert!(l.editing_data && !l.restored, "{:?}", l.warnings);
|
||||
assert_eq!(l.doc.opened_artboard.and_then(|id| l.doc.artboards.iter().position(|a| a.id == id)), Some(artboard), "{}", f["title"]);
|
||||
}
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_unsaved_document_asks_for_a_folder() {
|
||||
let dir = folder("picked");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -28,7 +28,7 @@ pub use open::{
|
||||
};
|
||||
#[cfg(test)]
|
||||
pub(crate) use open::{open_file, opened_with_notes};
|
||||
pub use save::{ask_format_options, ask_pdf_options, remember_view, save, save_pdf};
|
||||
pub use save::{ask_format_options, ask_pdf_options, remember_active_artboard, remember_view, save, save_pdf};
|
||||
pub(crate) use save::{export_path, is_web, report_saved, target_path, write_named, write_reporting, write_to};
|
||||
#[cfg(test)]
|
||||
pub(crate) use save::{report_write_error, saved_status};
|
||||
@@ -164,6 +164,76 @@ mod tests {
|
||||
assert!(!app.session.doc().unwrap().is_dirty());
|
||||
}
|
||||
|
||||
/// A `.ai` file saved from the app names the view's active artboard as its active one, in
|
||||
/// the editing copy other apps read.
|
||||
#[test]
|
||||
fn a_saved_ai_file_names_the_views_active_artboard() {
|
||||
let (mut app, written) = app();
|
||||
app.run("file.new", json!({"width": 60, "height": 40, "artboards": 3})).unwrap();
|
||||
app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 20, "height": 20})).unwrap();
|
||||
app.sync_views();
|
||||
let opts = fileio::LoadOptions { native: false, ..Default::default() };
|
||||
for artboard in [2, 0] {
|
||||
if let Some(v) = app.view_mut() {
|
||||
v.artboard = artboard;
|
||||
}
|
||||
save(&mut app, SaveMode::Copy, &json!({"path": "Art.ai"}), false).unwrap();
|
||||
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);
|
||||
let active = l.doc.opened_artboard.and_then(|id| l.doc.artboards.iter().position(|a| a.id == id)).unwrap_or(0);
|
||||
assert_eq!(active, artboard);
|
||||
}
|
||||
}
|
||||
|
||||
/// A `.ai` file a command exports names the view's active artboard as its active one: an
|
||||
/// index past the last artboard names the last, as the Artboards panel shows it.
|
||||
#[test]
|
||||
fn an_exported_ai_file_names_the_views_active_artboard() {
|
||||
let (mut app, _) = app();
|
||||
app.run("file.new", json!({"width": 60, "height": 40, "artboards": 3})).unwrap();
|
||||
app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 20, "height": 20})).unwrap();
|
||||
app.sync_views();
|
||||
let opts = fileio::LoadOptions { native: false, ..Default::default() };
|
||||
for (artboard, active) in [(1, 1), (99, 2)] {
|
||||
if let Some(v) = app.view_mut() {
|
||||
v.artboard = artboard;
|
||||
}
|
||||
let out = app.run("document.serialize", json!({"format": "ai"})).unwrap();
|
||||
let bytes = vectorcraft_format::base64_decode(out["dataBase64"].as_str().unwrap()).unwrap();
|
||||
let l = fileio::load_with("Art.ai", &bytes, &opts).unwrap();
|
||||
assert!(l.editing_data && !l.restored, "{:?}", l.warnings);
|
||||
assert_eq!(l.doc.opened_artboard.and_then(|id| l.doc.artboards.iter().position(|a| a.id == id)), Some(active), "{artboard}");
|
||||
}
|
||||
}
|
||||
|
||||
/// 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()
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
@@ -171,14 +171,30 @@ pub(super) fn target_path_keeping(
|
||||
pick_path(app, &FilePick { name, folder, filters }, keeps)
|
||||
}
|
||||
|
||||
/// Keep the active document's saved view current before a save (native files reopen at it).
|
||||
/// Keep the active document's saved view current before a save (native files reopen at it), and
|
||||
/// each document's active artboard ([`remember_active_artboard`]).
|
||||
pub fn remember_view(app: &mut VectorcraftApp) {
|
||||
remember_active_artboard(app);
|
||||
let Some(v) = app.view().copied().filter(|v| v.fitted) else { return };
|
||||
if let Some(st) = app.session.active_mut() {
|
||||
st.view = Some(SavedView { name: String::new(), center: v.center, zoom: v.zoom, rotation: v.rotation });
|
||||
}
|
||||
}
|
||||
|
||||
/// Keep every open document's active artboard current (a `.ai` file saved or exported names it as
|
||||
/// its active one, also in Export All Open Documents): its own view's, an index past the last
|
||||
/// artboard being the last one as in the Artboards panel; `None` without a view.
|
||||
pub fn remember_active_artboard(app: &mut VectorcraftApp) {
|
||||
let uids: Vec<u64> = app.session.documents().iter().map(|d| d.uid).collect();
|
||||
for uid in uids {
|
||||
let index = app.view_uids.iter().position(|u| *u == uid).and_then(|k| app.views.get(k)).map(|v| v.artboard);
|
||||
if let Some(st) = app.session.document_mut(uid) {
|
||||
let last = st.doc.artboards.len().saturating_sub(1);
|
||||
st.active_artboard = index.and_then(|i| st.doc.artboards.get(i.min(last))).map(|a| a.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A path typed in a save panel: kept when its extension names one of the panel's file types (a
|
||||
/// save format, or a template format for Save as Template, `mode`). An extension that names
|
||||
/// another format (`card.png`, or `card.ait` in Save As) is replaced with `f`'s, and `f`'s is
|
||||
|
||||
@@ -62,7 +62,9 @@ private editing data. VectorCraft:
|
||||
Editing Data for Other Apps) every page's `PieceInfo` also carries an editing copy of the art in
|
||||
`AIPrivateData` streams, in the form VectorCraft's `.ai` reader reads: layers and sublayers, groups, paths with
|
||||
their fills and strokes (a compound path as one path of all its subpaths), process, spot and global colors, swatches and color groups, guides,
|
||||
hidden and locked objects, object names, every artboard with its name, the transparency of objects and layers
|
||||
hidden and locked objects, object names, every artboard with its name, the active artboard (`CropAreaActive`: the
|
||||
view's active artboard when the app saves or exports the file, else the one the document opened on, else the first;
|
||||
the reader opens the file on it), the transparency of objects and layers
|
||||
(opacity, blending mode, isolation, knockout and knockout shape), objects' opacity masks, and clipping groups and
|
||||
clipping layers clipped by a path or compound path, linear and radial gradients on fills and strokes (with their
|
||||
stops' opacity, midpoints and spot colors, elliptical radials and focal points) and gradient swatches, and
|
||||
|
||||
@@ -4684,6 +4684,11 @@ or in `options` (`file.formatOptions {format}` lists them with their values):
|
||||
- `pdfCompatible`: native files (default false) also carry a PDF of every artboard (`pdf` in the file); `.ai` files
|
||||
(default true) with false write blank pages around the native document (smaller; other apps show empty pages).
|
||||
- `.ai` only: `compress` (default true) is the PDF option `compression.compressText`.
|
||||
- The editing copy of a `.ai` or `.ait` file (`editingData`) names its active artboard (`CropAreaActive`): the active
|
||||
artboard of the app's view when the app saves or exports the file, otherwise the artboard the document opened on (the
|
||||
active artboard of the `.ai` file it was opened from), else the first. A `.ai` file read from its editing copy
|
||||
opens on that artboard. VectorCraft opens a `.ai` file it saved from the native document the file carries, on the
|
||||
first artboard.
|
||||
|
||||
File → Save As asks for these in the save options dialog (VectorCraft Options) after the save panel, and Save a Copy
|
||||
does too for a `.ai` file; Save, Save a Copy to a native file and Save as Template reuse the options as last saved (or Use Compression).
|
||||
|
||||
Reference in New Issue
Block a user