115 lines
4.5 KiB
Rust
115 lines
4.5 KiB
Rust
//! View → New View… / Edit Views…: saved views stored in the document.
|
|
|
|
use serde_json::{Value, json};
|
|
use vectorcraft_doc::SavedView;
|
|
use vectorcraft_geom::Point;
|
|
|
|
use super::*;
|
|
|
|
/// Illustrator keeps up to 25 views per document.
|
|
pub const MAX_VIEWS: usize = 25;
|
|
|
|
pub fn specs() -> Vec<CommandSpec> {
|
|
vec![
|
|
cmd!(
|
|
"view.saved.new",
|
|
"New View…",
|
|
["View"],
|
|
None,
|
|
"{name?, center: [x, y], zoom: (1 = 100%), rotation?: deg} save a view (the app fills in the current one) → {name, index}",
|
|
has_doc,
|
|
new_view
|
|
),
|
|
cmd!(
|
|
"view.saved.edit",
|
|
"Edit Views…",
|
|
["View"],
|
|
None,
|
|
"{name, newName?, delete?: bool} rename or delete a saved view; no params → {views}",
|
|
has_doc,
|
|
edit_views
|
|
),
|
|
cmd!(query "view.saved.list", "Saved Views", [], None, "{} → {views: [{name, center, zoom, rotation}]}", has_doc, list_views),
|
|
]
|
|
}
|
|
|
|
fn list_views(s: &mut Session, _: &Value) -> Result<Value> {
|
|
Ok(json!({ "views": s.doc()?.doc.views }))
|
|
}
|
|
|
|
fn new_view(s: &mut Session, p: &Value) -> Result<Value> {
|
|
const C: &str = "view.saved.new";
|
|
let center = p.get("center").and_then(Value::as_array).and_then(|a| Some(Point::new(a.first()?.as_f64()?, a.get(1)?.as_f64()?)));
|
|
let center = center.ok_or_else(|| bad(C, "missing center [x, y]"))?;
|
|
let zoom = f64_req(p, "zoom", C)?;
|
|
if !(zoom.is_finite() && zoom > 0.0) {
|
|
return Err(bad(C, "zoom must be positive"));
|
|
}
|
|
let st = s.doc()?;
|
|
if st.doc.views.len() >= MAX_VIEWS {
|
|
return Err(bad(C, format!("a document keeps at most {MAX_VIEWS} views")));
|
|
}
|
|
let name = match str_param(p, "name").map(str::trim).filter(|n| !n.is_empty()) {
|
|
Some(n) => n.to_string(),
|
|
None => (1..).map(|i| format!("View {i}")).find(|n| !st.doc.views.iter().any(|v| &v.name == n)).unwrap_or_default(),
|
|
};
|
|
if st.doc.views.iter().any(|v| v.name == name) {
|
|
return Err(bad(C, format!("a view named `{name}` exists")));
|
|
}
|
|
let view = SavedView { name: name.clone(), center, zoom, rotation: f64_or(p, "rotation", 0.0) };
|
|
let index = s.edit("New View", |d, _| {
|
|
d.views.push(view);
|
|
Ok(d.views.len() - 1)
|
|
})?;
|
|
Ok(json!({ "name": name, "index": index }))
|
|
}
|
|
|
|
fn edit_views(s: &mut Session, p: &Value) -> Result<Value> {
|
|
const C: &str = "view.saved.edit";
|
|
let Some(name) = str_param(p, "name") else { return list_views(s, p) };
|
|
let i = s.doc()?.doc.views.iter().position(|v| v.name == name).ok_or_else(|| bad(C, format!("no view named `{name}`")))?;
|
|
if bool_or(p, "delete", false) {
|
|
s.edit("Delete View", |d, _| {
|
|
d.views.remove(i);
|
|
Ok(())
|
|
})?;
|
|
return Ok(json!({ "deleted": name }));
|
|
}
|
|
let new = str_param(p, "newName").map(str::trim).filter(|n| !n.is_empty()).ok_or_else(|| bad(C, "give newName or delete"))?.to_string();
|
|
if s.doc()?.doc.views.iter().any(|v| v.name == new) {
|
|
return Err(bad(C, format!("a view named `{new}` exists")));
|
|
}
|
|
s.edit("Rename View", |d, _| {
|
|
d.views[i].name = new.clone();
|
|
Ok(())
|
|
})?;
|
|
Ok(json!({ "name": new }))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use serde_json::json;
|
|
|
|
use crate::Session;
|
|
|
|
#[test]
|
|
fn save_rename_delete_views() {
|
|
let mut s = Session::new();
|
|
s.execute("file.new", &json!({})).unwrap();
|
|
let v = s.execute("view.saved.new", &json!({"center": [100, 200], "zoom": 2.0})).unwrap();
|
|
assert_eq!(v["name"], json!("View 1"));
|
|
s.execute("view.saved.new", &json!({"name": "Logo", "center": [10, 20], "zoom": 4.0, "rotation": 15})).unwrap();
|
|
assert!(s.execute("view.saved.new", &json!({"name": "Logo", "center": [0, 0], "zoom": 1.0})).is_err());
|
|
assert!(s.execute("view.saved.new", &json!({"center": [0, 0], "zoom": 0})).is_err());
|
|
s.execute("view.saved.edit", &json!({"name": "View 1", "newName": "Overview"})).unwrap();
|
|
let l = s.execute("view.saved.list", &json!({})).unwrap();
|
|
assert_eq!(l["views"][0]["name"], json!("Overview"));
|
|
assert_eq!(l["views"][1]["rotation"], json!(15.0));
|
|
s.execute("view.saved.edit", &json!({"name": "Logo", "delete": true})).unwrap();
|
|
assert_eq!(s.doc().unwrap().doc.views.len(), 1);
|
|
// Views travel with the file.
|
|
let bytes = vectorcraft_format::save_file(&s.doc().unwrap().doc);
|
|
assert_eq!(vectorcraft_format::load(&bytes).unwrap().views[0].name, "Overview");
|
|
}
|
|
}
|