mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-28 19:57:05 -06:00
Save: a remembered artboard pick follows the artboards it named
A save without options reuses the options the document was last saved
with in that format. Their artboard, artboards and range name artboards
by number, so once artboards were deleted or moved, the save wrote
other artboards or was refused: with three artboards, a PDF saved with
range "2" wrote the third artboard after the first was deleted, and one
saved with range "1, 3" was refused after the third was deleted. The
same held for SVG saves, a legacy .ai, and .ai and native files saved
with separateArtboards. file.formatOptions and SVG Options showed the
same numbers.
The document now keeps the ids of the artboards it had when it was
saved, and the remembered pick follows them: an artboard deleted since
is left out, one that moved is named by its new number, and the rest of
the pick is kept. A range open at its end ("2-") is written out as the
artboards it named once artboards are added. When none of the artboards
the save used is left, the pick is left out, and the save writes the
artboards it writes when none is named. A range of "all" stays as it
is. An artboard given to the save is still refused when the document
does not have it.
A new artboard no longer takes the id of an artboard deleted earlier in
the same document (Document::retire_artboard_id), or of one an undo took
away: the highest id given out is shared with the undo history, so a
new artboard is not taken for an earlier one.
file.revert keeps the pick only when the file read back has the
artboards the document had at the save, with the same ids in the same
order. A PDF saved with a range holds only those pages, and one saved
without preserveEditing reads back with new ids in page order, so their
picks are left out.
This commit is contained in:
+40
-1
@@ -959,6 +959,12 @@ pub struct Document {
|
||||
/// engine sets it from the selection's ([`Selection::new_art`]). Editing state, never saved.
|
||||
#[serde(skip)]
|
||||
pub new_art: NewArtIds,
|
||||
/// The highest artboard id given out ([`Document::next_artboard_id`]) or deleted from the
|
||||
/// document ([`Document::retire_artboard_id`]): a new artboard takes a higher one. Shared by
|
||||
/// every copy of the document, the undo history's included, so an undo does not lower it.
|
||||
/// Never saved.
|
||||
#[serde(skip)]
|
||||
pub retired_artboard_id: RetiredArtboardId,
|
||||
/// Some object is a dimension (the Dimension tool): edits refresh dimensions only while this
|
||||
/// is set, so documents without any pay nothing for them.
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
@@ -968,6 +974,27 @@ pub struct Document {
|
||||
index: id_index::IdIndex,
|
||||
}
|
||||
|
||||
/// [`Document::retired_artboard_id`]: not document content, so two documents are equal whatever it
|
||||
/// holds. A clone shares the value with the document it was cloned from.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct RetiredArtboardId(Arc<std::sync::atomic::AtomicU32>);
|
||||
|
||||
impl RetiredArtboardId {
|
||||
pub fn get(&self) -> u32 {
|
||||
self.0.load(std::sync::atomic::Ordering::Relaxed)
|
||||
}
|
||||
/// Raise the value to `id` when `id` is higher.
|
||||
pub fn raise(&self, id: u32) {
|
||||
self.0.fetch_max(id, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for RetiredArtboardId {
|
||||
fn eq(&self, _: &Self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fn ppi72() -> f64 {
|
||||
72.0
|
||||
}
|
||||
@@ -1052,6 +1079,7 @@ impl Document {
|
||||
puppet,
|
||||
new_art,
|
||||
dimensions,
|
||||
retired_artboard_id: _,
|
||||
index: _,
|
||||
} = self;
|
||||
*version == other.version
|
||||
@@ -1225,6 +1253,7 @@ impl Document {
|
||||
puppet: None,
|
||||
new_art: Default::default(),
|
||||
dimensions: false,
|
||||
retired_artboard_id: Default::default(),
|
||||
index: Default::default(),
|
||||
};
|
||||
let id = d.alloc_id();
|
||||
@@ -1940,8 +1969,18 @@ impl Document {
|
||||
self.guides.iter().filter(|g| g.artboard == Some(from)).map(|g| Guide { artboard: Some(to), ..g.moved(d) }).collect();
|
||||
self.guides.extend(copies);
|
||||
}
|
||||
/// An id for a new artboard: one no artboard has, above the ids given out before and the ids of
|
||||
/// the artboards deleted from the document ([`Document::retired_artboard_id`]). The id counts
|
||||
/// as given out, so an artboard made after an undo does not take the id of one the undo took
|
||||
/// away.
|
||||
pub fn next_artboard_id(&self) -> u32 {
|
||||
free_id(self.artboards.iter().map(|a| a.id))
|
||||
let id = free_id(self.artboards.iter().map(|a| a.id).chain([self.retired_artboard_id.get()]));
|
||||
self.retired_artboard_id.raise(id);
|
||||
id
|
||||
}
|
||||
/// Record that the artboard with `id` was deleted: no later artboard takes its id.
|
||||
pub fn retire_artboard_id(&self, id: u32) {
|
||||
self.retired_artboard_id.raise(id);
|
||||
}
|
||||
pub fn pattern(&self, name: &str) -> Option<&PatternDef> {
|
||||
self.patterns.iter().find(|p| p.name == name)
|
||||
|
||||
@@ -74,8 +74,8 @@ pub use native::with_compression_pref;
|
||||
pub use pdfimport::{LoadOptions, page_document};
|
||||
pub(crate) use save::job_for;
|
||||
pub use save::{
|
||||
SAVE_FORMATS, SaveJob, SaveMode, SavePlan, StreamWrite, TEMPLATE_FORMATS, export_folder, save_filters, save_format, save_job, save_plan,
|
||||
save_with, stamp_save_dates, template_filters, templates_dialog_folder, templates_folder,
|
||||
SAVE_FORMATS, SaveJob, SaveMode, SavePlan, StreamWrite, TEMPLATE_FORMATS, export_folder, remembered_save_options, save_filters, save_format,
|
||||
save_job, save_plan, save_with, stamp_save_dates, template_filters, templates_dialog_folder, templates_folder,
|
||||
};
|
||||
pub use screens::{
|
||||
PRESET_KEYS as SCREEN_EXPORT_PRESET_KEYS, PRESETS as SCREEN_PRESETS, ScreenExport, ScreenExportPreset, ScreenSize,
|
||||
|
||||
@@ -14,7 +14,8 @@ use vectorcraft_doc::Document;
|
||||
|
||||
use super::super::*;
|
||||
use super::{
|
||||
ArtboardPick, Encoded, Format, Loaded, encode_all, file_stem, format, format_for_name, load, read_file, with_compression_pref, write_encoded,
|
||||
ARTBOARD_PARAMS, ArtboardPick, Encoded, Format, Loaded, encode_all, file_stem, format, format_for_name, load, read_file, with_compression_pref,
|
||||
write_encoded,
|
||||
};
|
||||
use crate::{DocState, Prefs};
|
||||
|
||||
@@ -253,7 +254,7 @@ pub fn save_plan(s: &Session, mode: SaveMode, p: &Value) -> Result<SavePlan> {
|
||||
}
|
||||
}
|
||||
// None given: the ones the document was last saved with in this format.
|
||||
let options = if given.is_empty() && format.id == st.format { st.save_options.clone() } else { given };
|
||||
let options = if given.is_empty() && format.id == st.format { remembered_save_options(st) } else { given };
|
||||
// Save writes the document's own file; the other modes only where they are told to.
|
||||
let path =
|
||||
path.or_else(|| (mode == SaveMode::Save && !st.converted && !st.recovered && format.id == st.format).then(|| st.path.clone()).flatten());
|
||||
@@ -275,6 +276,99 @@ pub fn save_plan(s: &Session, mode: SaveMode, p: &Value) -> Result<SavePlan> {
|
||||
Ok(SavePlan { mode, path, format, options, name, folder, modified })
|
||||
}
|
||||
|
||||
/// The options `st` was last saved with, their artboards (`artboard`, `artboards` and `range`)
|
||||
/// following the artboards they named ([`DocState::save_artboards`]): an artboard deleted since is
|
||||
/// left out, and one that moved is named by its new number. When none of the artboards the save
|
||||
/// used is left, the pick is left out, and a save without options writes the artboards it writes
|
||||
/// when none is named. `range` `"all"` stays as it is. Artboards given to a save are still checked.
|
||||
pub fn remembered_save_options(st: &DocState) -> Map<String, Value> {
|
||||
let mut options = st.save_options.clone();
|
||||
let ids: Vec<u32> = st.doc.artboards.iter().map(|a| a.id).collect();
|
||||
// The encoders refuse a document without artboards themselves.
|
||||
if ids.is_empty() {
|
||||
return options;
|
||||
}
|
||||
// Artboard `i` of the save, where it is now (by number when the save's artboards are unknown).
|
||||
let then = &st.save_artboards;
|
||||
let count = if then.is_empty() { ids.len() } else { then.len() };
|
||||
let now = |i: usize| match then.get(i) {
|
||||
Some(id) => ids.iter().position(|x| x == id),
|
||||
None => (then.is_empty() && i < ids.len()).then_some(i),
|
||||
};
|
||||
// Each pick that names artboards: `Some` of where they are now (empty: all gone).
|
||||
let mut moved: Vec<(&str, Option<Vec<usize>>)> = vec![];
|
||||
for k in ARTBOARD_PARAMS {
|
||||
let named = match (k, options.get(k)) {
|
||||
("range", Some(Value::String(r))) if r.trim().is_empty() || r.trim().eq_ignore_ascii_case("all") => continue,
|
||||
("range", Some(Value::String(r))) => match vectorcraft_doc::range::parse_range(r, count) {
|
||||
Ok(v) => v,
|
||||
Err(_) => {
|
||||
moved.push((k, Some(vec![])));
|
||||
continue;
|
||||
}
|
||||
},
|
||||
("artboards", Some(v @ Value::Array(_))) => match serde_json::from_value::<Vec<usize>>(v.clone()) {
|
||||
Ok(v) => v,
|
||||
Err(_) => continue,
|
||||
},
|
||||
("artboard", Some(v)) => match v.as_u64().and_then(|i| usize::try_from(i).ok()) {
|
||||
Some(i) => vec![i],
|
||||
None => continue,
|
||||
},
|
||||
_ => continue,
|
||||
};
|
||||
let there: Vec<usize> = named.iter().filter_map(|i| now(*i)).collect();
|
||||
// A range that reads differently against the artboards there are now (`"2-"` once one
|
||||
// was added) is written out as the artboards it named.
|
||||
let reads = match (k, options.get(k)) {
|
||||
("range", Some(Value::String(r))) => vectorcraft_doc::range::parse_range(r, ids.len()).ok(),
|
||||
_ => Some(named),
|
||||
};
|
||||
moved.push((k, (reads.as_ref() != Some(&there)).then_some(there)));
|
||||
}
|
||||
// The pick a save uses (as `ArtboardPick::resolve` reads it: `range`, then `artboards`, then
|
||||
// `artboard`) is gone: so is the rest.
|
||||
let used = ["range", "artboards", "artboard"].into_iter().find(|k| match options.get(*k) {
|
||||
Some(Value::String(r)) => !r.trim().is_empty(),
|
||||
Some(v) => !v.is_null(),
|
||||
None => false,
|
||||
});
|
||||
let gone = |k: &str| moved.iter().any(|(m, there)| *m == k && there.as_ref().is_some_and(Vec::is_empty));
|
||||
if used.is_some_and(gone) {
|
||||
for k in ARTBOARD_PARAMS {
|
||||
options.remove(k);
|
||||
}
|
||||
return options;
|
||||
}
|
||||
for (k, there) in moved {
|
||||
let Some(there) = there else { continue };
|
||||
let value = match (k, there.first()) {
|
||||
(_, None) => None,
|
||||
("range", Some(_)) => Some(json!(range_text(&there))),
|
||||
("artboards", Some(_)) => Some(json!(there)),
|
||||
(_, Some(i)) => Some(json!(i)),
|
||||
};
|
||||
match value {
|
||||
Some(v) => _ = options.insert(k.into(), v),
|
||||
None => _ = options.remove(k),
|
||||
}
|
||||
}
|
||||
options
|
||||
}
|
||||
|
||||
/// 0-based artboard indices as a 1-based range in the same order, runs joined (`"1-2, 5"`).
|
||||
fn range_text(picked: &[usize]) -> String {
|
||||
let mut runs: Vec<(usize, usize)> = vec![];
|
||||
for &i in picked {
|
||||
match runs.last_mut() {
|
||||
Some((_, end)) if *end + 1 == i => *end = i,
|
||||
_ => runs.push((i, i)),
|
||||
}
|
||||
}
|
||||
let run = |&(a, b): &(usize, usize)| if a == b { format!("{}", a + 1) } else { format!("{}-{}", a + 1, b + 1) };
|
||||
runs.iter().map(run).collect::<Vec<_>>().join(", ")
|
||||
}
|
||||
|
||||
/// Does a save in `f` read option `key`? PDF (and a .ai file, a PDF) also reads its General
|
||||
/// settings (`createLayers`…) and presets.
|
||||
fn reads_option(f: &Format, key: &str) -> bool {
|
||||
@@ -664,6 +758,7 @@ impl SaveJob {
|
||||
st.path = Some(path);
|
||||
st.format = self.plan.format.id;
|
||||
st.save_options = self.plan.options;
|
||||
st.save_artboards = self.snapshot.artboards.iter().map(|a| a.id).collect();
|
||||
st.converted = false;
|
||||
st.recovered = false;
|
||||
if let Some(at) = self.stamped {
|
||||
@@ -756,14 +851,14 @@ fn format_options(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let f = format(id)
|
||||
.filter(|f| f.write || SAVE_FORMATS.contains(&f.id) || TEMPLATE_FORMATS.contains(&f.id))
|
||||
.ok_or_else(|| bad(C, format!("`{id}` is no writable format (see document.formats)")))?;
|
||||
let saved = st.filter(|d| d.format == f.id).map(|d| &d.save_options);
|
||||
let saved = st.filter(|d| d.format == f.id).map(remembered_save_options);
|
||||
// A native save compresses as Use Compression says, unless told otherwise.
|
||||
let prefs = with_compression_pref(&s.prefs, &json!({}));
|
||||
let mut v = f.to_json();
|
||||
if let Some(options) = v["options"].as_object_mut() {
|
||||
for (name, o) in options.iter_mut() {
|
||||
let default = prefs.get(name).filter(|_| matches!(f.id, "vectorcraft" | "template")).unwrap_or(&o["default"]);
|
||||
o["value"] = saved.and_then(|m| m.get(name)).unwrap_or(default).clone();
|
||||
o["value"] = saved.as_ref().and_then(|m| m.get(name)).unwrap_or(default).clone();
|
||||
}
|
||||
}
|
||||
v["saveFormats"] =
|
||||
|
||||
@@ -866,6 +866,7 @@ fn artboard_delete(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
// The last first, so the others keep their places; their guides go with them.
|
||||
for &i in indices.iter().rev() {
|
||||
let id = d.artboards.remove(i).id;
|
||||
d.retire_artboard_id(id);
|
||||
d.retain_guides(sel, |_, g| g.artboard != Some(id));
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -969,10 +969,12 @@ fn convert_to_artboards(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
if b.width() <= 0.0 || b.height() <= 0.0 {
|
||||
continue;
|
||||
}
|
||||
// Named one past the highest artboard id the document has, whatever id it is given.
|
||||
let number = d.artboards.iter().map(|a| a.id).max().map_or(1, |m| m.saturating_add(1));
|
||||
let aid = d.next_artboard_id();
|
||||
d.artboards.push(vectorcraft_doc::Artboard {
|
||||
id: aid,
|
||||
name: format!("Artboard {aid}"),
|
||||
name: format!("Artboard {number}"),
|
||||
rect: b,
|
||||
locked: false,
|
||||
show_center_mark: false,
|
||||
|
||||
@@ -208,6 +208,7 @@ pub(crate) fn artboard_copy(s: &mut Session, p: &Value, cut: bool) -> Result<Val
|
||||
// The last first, so the others keep their places; their guides go with them.
|
||||
for &i in indices.iter().rev() {
|
||||
let gone = d.artboards.remove(i).id;
|
||||
d.retire_artboard_id(gone);
|
||||
d.retain_guides(sel, |_, g| g.artboard != Some(gone));
|
||||
}
|
||||
d.remove_many(&art)?;
|
||||
|
||||
@@ -378,6 +378,10 @@ pub struct DocState {
|
||||
/// That format's options as last saved (SVG options for SVG, the Save PDF settings for PDF;
|
||||
/// empty for native files): Save reuses them and `file.formatOptions` reads them back.
|
||||
pub save_options: serde_json::Map<String, Value>,
|
||||
/// The ids of the document's artboards, in order, when `save_options` were stored: the
|
||||
/// artboards their `artboard`, `artboards` and `range` named by number then
|
||||
/// ([`cmd::fileio::remembered_save_options`]).
|
||||
pub save_artboards: Vec<u32>,
|
||||
/// Opened from an older native file (former name or format version): the title says
|
||||
/// "[Converted]" and Save asks for a new name instead of overwriting it.
|
||||
pub converted: bool,
|
||||
@@ -569,6 +573,7 @@ impl DocState {
|
||||
transparency_grid,
|
||||
format: "vectorcraft",
|
||||
save_options: Default::default(),
|
||||
save_artboards: vec![],
|
||||
converted: false,
|
||||
view,
|
||||
recovered: false,
|
||||
@@ -1682,6 +1687,17 @@ impl Session {
|
||||
st.revision = old.revision + 1;
|
||||
st.format = old.format;
|
||||
st.save_options = old.save_options.clone();
|
||||
// The remembered artboard pick follows the artboards it named when the file read back has
|
||||
// the artboards the document had at the save, in the same order and with the same ids (a
|
||||
// native file keeps them; a PDF's pages get new ids in page order); otherwise it is left
|
||||
// out.
|
||||
if old.save_artboards.is_empty() || st.doc.artboards.iter().map(|a| a.id).eq(old.save_artboards.iter().copied()) {
|
||||
st.save_artboards = old.save_artboards.clone();
|
||||
} else {
|
||||
for k in cmd::fileio::ARTBOARD_PARAMS {
|
||||
st.save_options.remove(k);
|
||||
}
|
||||
}
|
||||
st.converted = old.converted;
|
||||
st.imported_from = old.imported_from.clone();
|
||||
st.import_losses = old.import_losses.clone();
|
||||
|
||||
@@ -977,6 +977,24 @@ fn convert_to_artboards_and_rearrange() {
|
||||
assert_eq!(bounds(&s, b), Rect::new(50.0, 675.0, 60.0, 685.0));
|
||||
}
|
||||
|
||||
/// Convert to Artboards names an artboard one past the highest artboard id the document has, also
|
||||
/// after a delete of the last artboard or an undo of a new one.
|
||||
#[test]
|
||||
fn convert_to_artboards_names_by_the_artboards_the_document_has() {
|
||||
let name_after = |change: &dyn Fn(&mut Session)| {
|
||||
let mut s = session();
|
||||
s.execute("artboard.new", &json!({})).unwrap();
|
||||
s.execute("artboard.new", &json!({})).unwrap();
|
||||
change(&mut s);
|
||||
let a = rect(&mut s, 3000.0, 0.0, 200.0, 100.0);
|
||||
sel(&mut s, &[a]);
|
||||
s.execute("artboard.convertToArtboards", &json!({})).unwrap();
|
||||
s.doc().unwrap().doc.artboards.last().map(|b| b.name.clone()).unwrap()
|
||||
};
|
||||
assert_eq!(name_after(&|s| _ = s.execute("artboard.delete", &json!({"index": 2})).unwrap()), "Artboard 3");
|
||||
assert_eq!(name_after(&|s| _ = s.execute("edit.undo", &json!({})).unwrap()), "Artboard 3");
|
||||
}
|
||||
|
||||
/// Rearrange All Artboards refuses `columns` that are not a whole number (a fraction, a negative number or a string),
|
||||
/// and the artboards stay where they were; 1.0, as the Rearrange All Artboards dialog sends a count, is one column.
|
||||
#[test]
|
||||
|
||||
@@ -188,6 +188,130 @@ fn ai_without_pdf_content_and_compression() {
|
||||
assert!(loose.len() > packed.len(), "{} vs {}", loose.len(), packed.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_remembered_artboard_the_document_no_longer_has_is_dropped() {
|
||||
let dir = Folder::new("remembered-artboards");
|
||||
for (name, p) in [
|
||||
("legacy.ai", json!({"aiVersion": "7", "range": "3"})),
|
||||
("separate.ai", json!({"separateArtboards": true, "range": "3"})),
|
||||
("separate.vectorcraft", json!({"separateArtboards": true, "range": "1, 3"})),
|
||||
("doc.pdf", json!({"range": "3"})),
|
||||
("doc.svg", json!({"artboard": 2})),
|
||||
] {
|
||||
let mut s = three_boards();
|
||||
let mut first = p.clone();
|
||||
first["path"] = json!(dir.file(name));
|
||||
s.execute("file.saveAs", &first).unwrap_or_else(|e| panic!("{name}: {e}"));
|
||||
s.execute("artboard.delete", &json!({"index": 2})).unwrap();
|
||||
let v = s.execute("file.formatOptions", &json!({})).unwrap();
|
||||
for k in ["artboard", "range"].iter().filter(|k| p.get(**k).is_some()) {
|
||||
assert_ne!(v["options"][*k]["value"], p[*k], "{name}: Save no longer uses {k}");
|
||||
}
|
||||
s.execute("document.save", &json!({})).unwrap_or_else(|e| panic!("{name}: {e}"));
|
||||
// Given to the save, it is still refused.
|
||||
let e = s.execute("document.save", &p).unwrap_err().to_string();
|
||||
assert!(e.contains("from 1 to 2") || e.contains("no artboard 2"), "{name}: {e}");
|
||||
}
|
||||
// A remembered artboard that is still there is kept.
|
||||
let mut s = three_boards();
|
||||
s.execute("file.saveAs", &json!({"path": dir.file("kept.ai"), "aiVersion": "7", "range": "2"})).unwrap();
|
||||
s.execute("artboard.delete", &json!({"index": 2})).unwrap();
|
||||
let v = s.execute("file.formatOptions", &json!({})).unwrap();
|
||||
assert_eq!(v["options"]["range"]["value"], json!("2"));
|
||||
let r = s.execute("document.save", &json!({})).unwrap();
|
||||
assert!(r["path"].as_str().is_some_and(|p| p.ends_with("kept.ai")), "{r}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_remembered_artboard_pick_follows_the_artboards_it_named() {
|
||||
let dir = Folder::new("remembered-pick");
|
||||
// Save each artboard of A, B and C to a file with `range`, change the artboards, then save
|
||||
// without options: the range Save now uses and the artboards it writes, by file name.
|
||||
let save = |range: &str, change: &[(&str, Value)]| -> (Value, Vec<String>) {
|
||||
let mut s = three_boards();
|
||||
let path = dir.file("x.ai");
|
||||
s.execute("file.saveAs", &json!({"path": path, "separateArtboards": true, "range": range})).unwrap();
|
||||
for (cmd, p) in change {
|
||||
s.execute(cmd, p).unwrap();
|
||||
}
|
||||
let range = s.execute("file.formatOptions", &json!({})).unwrap()["options"]["range"]["value"].clone();
|
||||
let r = s.execute("document.save", &json!({})).unwrap();
|
||||
let files = r["files"].as_array().unwrap().iter().skip(1).map(|f| f.as_str().unwrap().rsplit('/').next().unwrap().to_string());
|
||||
(range, files.collect())
|
||||
};
|
||||
let delete = |i: usize| ("artboard.delete", json!({"index": i}));
|
||||
assert_eq!(save("1, 3", &[delete(2)]), (json!("1"), vec!["x-A.ai".to_string()]));
|
||||
assert_eq!(save("1, 3", &[delete(0)]), (json!("2"), vec!["x-C.ai".to_string()]));
|
||||
assert_eq!(save("1-3", &[delete(1)]), (json!("1-2"), vec!["x-A.ai".to_string(), "x-C.ai".to_string()]));
|
||||
let (range, mut files) = save("1, 3", &[("artboard.reorder", json!({"index": 2, "to": 0}))]);
|
||||
files.sort();
|
||||
assert_eq!((range, files), (json!("2, 1"), vec!["x-A.ai".to_string(), "x-C.ai".to_string()]));
|
||||
// None of them left: every artboard, as when none is named.
|
||||
assert_eq!(save("1, 3", &[delete(2), delete(0)]), (Value::Null, vec!["x-B.ai".to_string()]));
|
||||
assert_eq!(save("all", &[delete(0)]), (json!("all"), vec!["x-B.ai".to_string(), "x-C.ai".to_string()]));
|
||||
// An artboard made after one was deleted is not taken for it.
|
||||
let new = ("artboard.new", json!({"x": 600, "y": 0, "width": 100, "height": 100, "name": "D"}));
|
||||
let all = ["x-A.ai", "x-B.ai", "x-D.ai"].map(String::from).to_vec();
|
||||
assert_eq!(save("3", &[delete(2), new.clone()]), (Value::Null, all));
|
||||
assert_eq!(save("1, 3", &[delete(2), new.clone()]), (json!("1"), vec!["x-A.ai".to_string()]));
|
||||
// A range open at its end keeps to the artboards it named.
|
||||
assert_eq!(save("2-", std::slice::from_ref(&new)), (json!("2-3"), vec!["x-B.ai".to_string(), "x-C.ai".to_string()]));
|
||||
// Nor is one made after undoing the artboard (and the three rectangles made after it).
|
||||
let undo = ("edit.undo", json!({}));
|
||||
let undone = [undo.clone(), undo.clone(), undo.clone(), undo, new];
|
||||
assert_eq!(save("1, 3", &undone), (json!("1"), vec!["x-A.ai".to_string()]));
|
||||
|
||||
// The other saves that remember a pick: a PDF's range, an SVG's artboard, a legacy .ai's
|
||||
// range and a native file's separate files.
|
||||
for (name, p, gone, after) in [
|
||||
("doc.pdf", json!({"range": "1, 3"}), 0, json!({"range": "2"})),
|
||||
// `range` wins over `artboard`, which a save then does not use.
|
||||
("unused.pdf", json!({"artboard": 0, "range": "3"}), 0, json!({"range": "2"})),
|
||||
("doc.svg", json!({"artboard": 2}), 0, json!({"artboard": 1})),
|
||||
("legacy.ai", json!({"aiVersion": "7", "range": "1, 3"}), 2, json!({"range": "1"})),
|
||||
("doc.vectorcraft", json!({"separateArtboards": true, "range": "1, 3"}), 0, json!({"range": "2"})),
|
||||
] {
|
||||
let mut s = three_boards();
|
||||
let mut first = p.clone();
|
||||
first["path"] = json!(dir.file(name));
|
||||
s.execute("file.saveAs", &first).unwrap();
|
||||
s.execute("artboard.delete", &json!({"index": gone})).unwrap();
|
||||
let v = s.execute("file.formatOptions", &json!({})).unwrap();
|
||||
for (k, value) in after.as_object().unwrap() {
|
||||
assert_eq!(&v["options"][k]["value"], value, "{name}: {k}");
|
||||
}
|
||||
s.execute("document.save", &json!({})).unwrap_or_else(|e| panic!("{name}: {e}"));
|
||||
if name.ends_with(".pdf") {
|
||||
let bytes = std::fs::read(dir.file(name)).unwrap();
|
||||
let info = s.execute("document.pdfInfo", &json!({"dataBase64": vectorcraft_format::base64_encode(&bytes)})).unwrap();
|
||||
assert_eq!(info["pages"], 1, "only the artboard that was 3");
|
||||
}
|
||||
}
|
||||
|
||||
// File > Revert: a PDF holds only the pages it was saved with, so its pick is left out; a
|
||||
// native file keeps every artboard, and the pick stays.
|
||||
let mut s = three_boards();
|
||||
s.execute("file.saveAs", &json!({"path": dir.file("revert.pdf"), "range": "1, 3"})).unwrap();
|
||||
s.execute("shape.rectangle", &json!({"x": 10, "y": 50, "width": 30, "height": 30})).unwrap();
|
||||
s.execute("file.revert", &json!({})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc.artboards.len(), 2);
|
||||
assert_eq!(s.execute("file.formatOptions", &json!({})).unwrap()["options"]["range"]["value"], Value::Null);
|
||||
// A PDF without the editable document reads back with new ids in page order: after a reorder,
|
||||
// the pick is left out, not taken for another reorder.
|
||||
let mut s = three_boards();
|
||||
s.execute("artboard.reorder", &json!({"index": 2, "to": 0})).unwrap();
|
||||
s.execute("file.saveAs", &json!({"path": dir.file("reordered.pdf"), "range": "1-3", "preserveEditing": false})).unwrap();
|
||||
s.execute("shape.rectangle", &json!({"x": 10, "y": 50, "width": 30, "height": 30})).unwrap();
|
||||
s.execute("file.revert", &json!({})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc.artboards.iter().map(|a| a.id).collect::<Vec<_>>(), [1, 2, 3]);
|
||||
assert_eq!(s.execute("file.formatOptions", &json!({})).unwrap()["options"]["range"]["value"], Value::Null);
|
||||
let mut s = three_boards();
|
||||
s.execute("file.saveAs", &json!({"path": dir.file("revert.vectorcraft"), "separateArtboards": true, "range": "1, 3"})).unwrap();
|
||||
s.execute("artboard.delete", &json!({"index": 0})).unwrap();
|
||||
s.execute("file.revert", &json!({})).unwrap();
|
||||
assert_eq!(s.execute("file.formatOptions", &json!({})).unwrap()["options"]["range"]["value"], json!("1, 3"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_options_list_the_save_options() {
|
||||
let mut s = three_boards();
|
||||
|
||||
@@ -92,7 +92,7 @@ pub fn open(app: &mut VectorcraftApp, mode: Mode, path: Option<&str>) {
|
||||
.session
|
||||
.active()
|
||||
.filter(|st| mode != Mode::Export && matches!(st.format, "svg" | "svgz"))
|
||||
.map(|st| Value::Object(st.save_options.clone()));
|
||||
.map(|st| Value::Object(vectorcraft_engine::cmd::fileio::remembered_save_options(st)));
|
||||
for last in [&app.ui.svg_options].into_iter().chain(saved.as_ref()) {
|
||||
if let Some(o) = last.as_object() {
|
||||
fields.extend(o.iter().map(|(k, v)| (k.clone(), v.clone())));
|
||||
@@ -416,6 +416,18 @@ mod tests {
|
||||
assert!(vectorcraft_format::sniff(&written.borrow()[3].1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_as_svg_leaves_out_a_remembered_artboard_the_document_no_longer_has() {
|
||||
let (mut app, written) = app("/tmp/doc.svg");
|
||||
app.run("file.saveAs", json!({"path": "/tmp/doc.svg", "svg": {"artboard": 1}})).unwrap();
|
||||
assert_eq!(app.session.active().unwrap().save_options["artboard"], 1);
|
||||
app.run("artboard.delete", json!({"index": 1})).unwrap();
|
||||
app.run("file.saveAs", Value::Null).unwrap();
|
||||
assert!(app.ui.dialog.as_ref().unwrap().fields.get("artboard").is_none());
|
||||
super::super::confirm(&mut app).unwrap();
|
||||
assert_eq!((written.borrow().len(), written.borrow()[1].0.as_str()), (2, "/tmp/doc.svg"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profile_encoding_and_embedded_fonts_reach_the_file() {
|
||||
let (mut app, written) = app("/tmp/tiny.svg");
|
||||
|
||||
Reference in New Issue
Block a user