mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 17:56:50 -06:00
Merge pull request #3979 from dotCooCoo/fix/remembered-artboard-pick
Save: a remembered artboard pick follows the artboards it named
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