mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-28 03:37:00 -06:00
Artboards: the Artboards panel's Delete Empty Artboards deletes every artboard with no artwork in one undo step
Squashed from #4142 onto main.
This commit is contained in:
committed by
bflatastic
parent
0b5affef5e
commit
a34055beec
@@ -1144,3 +1144,122 @@ fn stroke_advanced_scale_swap_flip() {
|
||||
assert!(s.execute("stroke.setAdvanced", &json!({})).is_err());
|
||||
assert!(s.execute("stroke.setAdvanced", &json!({"flipProfile": "sideways"})).is_err());
|
||||
}
|
||||
|
||||
/// A document of `n` 200 × 150 artboards in a row, 400 pt apart (`x` = 400·i), each with a
|
||||
/// guide of its own.
|
||||
fn row_of_artboards(n: usize) -> Session {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 200, "height": 150})).unwrap();
|
||||
for i in 1..n {
|
||||
s.execute("artboard.new", &json!({"x": 400 * i, "y": 0, "width": 200, "height": 150})).unwrap();
|
||||
}
|
||||
for i in 0..n {
|
||||
s.execute("guide.add", &json!({"vertical": true, "pos": 400 * i + 50, "artboard": i})).unwrap();
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
fn box_at(s: &mut Session, x: f64, w: f64) -> NodeId {
|
||||
NodeId(s.execute("shape.rectangle", &json!({"x": x, "y": 10, "width": w, "height": 20})).unwrap()["id"].as_u64().unwrap())
|
||||
}
|
||||
|
||||
fn artboard_names(s: &Session) -> Vec<String> {
|
||||
s.doc().unwrap().doc.artboards.iter().map(|a| a.name.clone()).collect()
|
||||
}
|
||||
|
||||
/// Artboards panel › Delete Empty Artboards: with art only on artboard 2, artboards 1 and 3 go
|
||||
/// with their guides in one undo step, the art and its ids untouched; Undo brings them back.
|
||||
#[test]
|
||||
fn delete_empty_artboards_deletes_those_with_no_art_in_one_step() {
|
||||
let mut s = row_of_artboards(3);
|
||||
let art = box_at(&mut s, 450.0, 20.0);
|
||||
let before = s.doc().unwrap().doc.clone();
|
||||
let steps = s.doc().unwrap().history.undo.len();
|
||||
assert_eq!(s.execute("artboard.deleteEmpty", &json!({})).unwrap(), json!({"deleted": [0, 2]}));
|
||||
let st = s.doc().unwrap();
|
||||
assert_eq!(artboard_names(&s), ["Artboard 2"]);
|
||||
assert_eq!(st.doc.guides.len(), 1, "the deleted artboards' guides go with them");
|
||||
assert_eq!(st.doc.node(art).map(|n| n.geometric_bounds()), before.node(art).map(|n| n.geometric_bounds()), "the art stays");
|
||||
assert_eq!(st.history.undo.len(), steps + 1);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(artboard_names(&s), ["Artboard 1", "Artboard 2", "Artboard 3"]);
|
||||
assert_eq!(s.doc().unwrap().doc.guides.len(), 3);
|
||||
}
|
||||
|
||||
/// What counts as art: hidden and locked objects, an object straddling two artboards and one
|
||||
/// that only touches an artboard's edge; a group counts by its members, not by its box.
|
||||
#[test]
|
||||
fn delete_empty_artboards_keeps_artboards_with_hidden_locked_or_shared_art() {
|
||||
let mut s = row_of_artboards(5);
|
||||
// Hidden art on 1, locked art on 2.
|
||||
box_at(&mut s, 10.0, 20.0);
|
||||
s.execute("object.hide", &json!({})).unwrap();
|
||||
box_at(&mut s, 410.0, 20.0);
|
||||
s.execute("object.lock", &json!({})).unwrap();
|
||||
// Straddling 3 (x 800–1000) and 4 (x 1200–1400).
|
||||
box_at(&mut s, 900.0, 350.0);
|
||||
// A group of a box on 3 and one whose right edge touches 6's left edge (x 2000): its box spans
|
||||
// 5 (x 1600–1800), which stays empty, and 6 has art only by the touch.
|
||||
s.execute("artboard.new", &json!({"x": 2000, "y": 0, "width": 200, "height": 150})).unwrap();
|
||||
let a = box_at(&mut s, 850.0, 20.0);
|
||||
let b = box_at(&mut s, 1980.0, 20.0);
|
||||
// No stroke, so its visual box ends at x 2000.
|
||||
s.execute("paint.setStroke", &json!({"none": true})).unwrap();
|
||||
s.execute("select.set", &json!({"ids": [a.0, b.0]})).unwrap();
|
||||
s.execute("object.group", &json!({})).unwrap();
|
||||
assert_eq!(s.execute("artboard.deleteEmpty", &json!({})).unwrap(), json!({"deleted": [4]}));
|
||||
assert_eq!(artboard_names(&s), ["Artboard 1", "Artboard 2", "Artboard 3", "Artboard 4", "Artboard 6"]);
|
||||
}
|
||||
|
||||
/// A locked empty artboard stays; with every artboard empty artboard 1 stays; with none empty
|
||||
/// the command is disabled, refuses and adds no undo step.
|
||||
#[test]
|
||||
fn delete_empty_artboards_leaves_locked_ones_and_artboard_1() {
|
||||
let mut s = row_of_artboards(3);
|
||||
s.execute("artboard.setProps", &json!({"index": 1, "locked": true})).unwrap();
|
||||
assert_eq!(s.execute("artboard.deleteEmpty", &json!({})).unwrap(), json!({"deleted": [0, 2]}), "all empty, one locked");
|
||||
assert_eq!(artboard_names(&s), ["Artboard 2"]);
|
||||
|
||||
let mut s = row_of_artboards(3);
|
||||
assert_eq!(s.execute("artboard.deleteEmpty", &json!({})).unwrap(), json!({"deleted": [1, 2]}), "all empty: artboard 1 stays");
|
||||
assert_eq!(artboard_names(&s), ["Artboard 1"]);
|
||||
let steps = s.doc().unwrap().history.undo.len();
|
||||
let spec = crate::cmd::command_specs().iter().find(|c| c.id == "artboard.deleteEmpty").unwrap();
|
||||
assert!((spec.enabled)(&s).is_err(), "one artboard left: nothing to delete");
|
||||
assert!(s.execute("artboard.deleteEmpty", &json!({})).is_err());
|
||||
box_at(&mut s, 10.0, 20.0);
|
||||
let steps_with_art = s.doc().unwrap().history.undo.len();
|
||||
assert!(s.execute("artboard.deleteEmpty", &json!({})).is_err(), "nothing empty");
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), steps_with_art);
|
||||
assert!(steps_with_art > steps);
|
||||
}
|
||||
|
||||
/// Art outside a clip shows nowhere: a clip group or a clipping layer has art only where its
|
||||
/// members show inside the clip, so an artboard that only the clipped-away part of a member
|
||||
/// covers is empty.
|
||||
#[test]
|
||||
fn delete_empty_artboards_measures_clipped_art_through_the_clip() {
|
||||
// Clip group: the clip on 1, a member running on to 2. Clipping layer: the clip on 3, a member
|
||||
// running on to 4. Nothing else.
|
||||
let mut s = row_of_artboards(4);
|
||||
let long = box_at(&mut s, 50.0, 400.0);
|
||||
let clip = box_at(&mut s, 10.0, 100.0);
|
||||
// The front object clips (drawn after the member).
|
||||
s.execute("select.set", &json!({"ids": [long.0, clip.0]})).unwrap();
|
||||
s.execute("object.clippingMask.make", &json!({})).unwrap();
|
||||
s.execute("layer.new", &json!({"name": "Clipped"})).unwrap();
|
||||
box_at(&mut s, 850.0, 400.0);
|
||||
// The layer's top object becomes its clipping path.
|
||||
let layer_clip = box_at(&mut s, 810.0, 100.0);
|
||||
let layer_of =
|
||||
|s: &Session| s.doc().unwrap().doc.layers.iter().find(|l| l.children().unwrap().iter().any(|c| c.id == layer_clip)).unwrap().clone();
|
||||
let layer = layer_of(&s);
|
||||
assert_eq!(layer.children().unwrap().len(), 2, "both in their own layer");
|
||||
s.execute("layer.clippingMask.toggle", &json!({"id": layer.id.0})).unwrap();
|
||||
let layer = layer_of(&s);
|
||||
assert!(matches!(layer.kind, vectorcraft_doc::NodeKind::Layer { clip: true, .. }));
|
||||
let first = layer.children().unwrap()[0].id;
|
||||
assert_eq!(first, layer_clip, "the layer's first object clips the rest");
|
||||
assert_eq!(s.execute("artboard.deleteEmpty", &json!({})).unwrap(), json!({"deleted": [1, 3]}));
|
||||
assert_eq!(artboard_names(&s), ["Artboard 1", "Artboard 3"]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user