Files

130 lines
6.3 KiB
Rust

//! Visible art is readable by the Eyedropper without making it editable (#1251).
#![allow(clippy::unwrap_used)]
use serde_json::json;
use vectorcraft_doc::NodeId;
use vectorcraft_engine::{Session, UiRequest, ViewInfo};
use vectorcraft_tools::{Mods, PointerEvent, PointerKind};
fn fixture() -> (Session, NodeId, NodeId, NodeId) {
let mut s = Session::new();
s.execute("file.new", &json!({"width": 400, "height": 300})).unwrap();
let layer = s.doc().unwrap().doc.layers[0].id;
let source = NodeId(s.execute("shape.rectangle", &json!({"x": 20, "y": 20, "width": 60, "height": 60})).unwrap()["id"].as_u64().unwrap());
s.execute("paint.setFill", &json!({"color": "#e23355"})).unwrap();
s.execute("stroke.set", &json!({"weight": 7})).unwrap();
s.execute("layer.new", &json!({"name": "Target"})).unwrap();
let target = NodeId(s.execute("shape.rectangle", &json!({"x": 140, "y": 100, "width": 120, "height": 80})).unwrap()["id"].as_u64().unwrap());
s.execute("paint.setFill", &json!({"color": "#2255ee"})).unwrap();
s.execute("stroke.set", &json!({"weight": 1})).unwrap();
(s, source, target, layer)
}
fn click(s: &mut Session, mods: Mods) {
s.select_tool("eyedropper", ViewInfo::default()).unwrap();
for kind in [PointerKind::Down, PointerKind::Up] {
s.pointer(&PointerEvent::new(kind, 40.0, 40.0).with_mods(mods), ViewInfo::default()).unwrap();
}
}
fn assert_sample(s: &mut Session, source: NodeId, target: NodeId, mods: Mods) {
let st = s.doc().unwrap();
let before = st.doc.clone();
let selection = st.selection.clone();
let history = st.history.undo.len();
click(s, mods);
let st = s.doc().unwrap();
let source_before = before.node(source).unwrap();
let target_after = st.doc.node(target).unwrap();
assert_eq!(json!(st.doc.node(source).unwrap()), json!(source_before), "sampling must not edit its source");
assert_eq!(json!(st.selection), json!(selection));
assert_eq!(target_after.geometric_bounds(), before.node(target).unwrap().geometric_bounds());
if mods.alt {
assert_eq!(target_after.appearance.items.len(), before.node(target).unwrap().appearance.items.len() + source_before.appearance.items.len());
} else {
assert_eq!(target_after.appearance.fill_paint(), source_before.appearance.fill_paint());
assert_eq!(target_after.appearance.stroke().unwrap().width, if mods.shift { 1.0 } else { 7.0 });
}
assert_eq!(st.history.undo.len(), history + 1);
let after = st.doc.clone();
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(json!(s.doc().unwrap().doc), json!(before));
s.execute("edit.redo", &json!({})).unwrap();
assert_eq!(json!(s.doc().unwrap().doc), json!(after));
}
fn read_modes() -> [Mods; 3] {
[Mods::default(), Mods { shift: true, ..Default::default() }, Mods { shift: true, alt: true, ..Default::default() }]
}
#[test]
fn locked_objects_and_layers_are_sources_in_all_read_modes() {
for lock_layer in [false, true] {
for mods in read_modes() {
let (mut s, source, target, layer) = fixture();
s.execute("layer.setProps", &json!({"id": if lock_layer { layer } else { source }, "locked": true})).unwrap();
assert_sample(&mut s, source, target, mods);
}
}
}
#[test]
fn isolation_allows_reading_surrounding_art_without_leaving_the_group() {
for mods in read_modes() {
let (mut s, source, target, _) = fixture();
let group = NodeId(s.execute("object.group", &json!({})).unwrap()["id"].as_u64().unwrap());
s.execute("object.isolate", &json!({"id": group})).unwrap();
s.execute("select.set", &json!({"ids": [target]})).unwrap();
assert_sample(&mut s, source, target, mods);
assert_eq!(s.doc().unwrap().isolation, Some(group));
}
}
#[test]
fn alt_reverse_still_respects_locks_and_isolation_and_hidden_sources_are_skipped() {
for restriction in ["object", "layer", "isolation", "hidden"] {
let (mut s, source, target, layer) = fixture();
match restriction {
"isolation" => {
let group = s.execute("object.group", &json!({})).unwrap()["id"].clone();
s.execute("object.isolate", &json!({"id": group})).unwrap();
s.execute("select.set", &json!({"ids": [target]})).unwrap();
}
"hidden" => {
s.execute("layer.setProps", &json!({"id": layer, "visible": false})).unwrap();
}
_ => {
s.execute("layer.setProps", &json!({"id": if restriction == "layer" { layer } else { source }, "locked": true})).unwrap();
}
}
let before = json!(s.doc().unwrap().doc);
let history = s.doc().unwrap().history.undo.len();
click(&mut s, Mods { alt: restriction != "hidden", ..Default::default() });
assert_eq!(json!(s.doc().unwrap().doc), before);
assert_eq!(s.doc().unwrap().history.undo.len(), history);
s.select_tool("selection", ViewInfo::default()).unwrap();
s.pointer(&PointerEvent::new(PointerKind::Down, 40.0, 40.0), ViewInfo::default()).unwrap();
assert!(!s.doc().unwrap().selection.objects.contains(&source));
}
}
#[test]
fn a_gradient_stop_can_read_a_locked_source_and_return_to_the_gradient_tool() {
let (mut s, source, target, layer) = fixture();
s.execute("paint.editGradient", &json!({})).unwrap();
s.execute("gradient.selectStop", &json!({"index": 1})).unwrap();
s.execute("layer.setProps", &json!({"id": layer, "locked": true})).unwrap();
s.select_tool("eyedropper", ViewInfo::default()).unwrap();
s.set_tool_option("stop", &json!("gradient"));
let before = json!(s.doc().unwrap().doc.node(source).unwrap());
let requests = s
.pointer(&PointerEvent::new(PointerKind::Down, 40.0, 40.0).with_mods(Mods { alt: true, ..Default::default() }), ViewInfo::default())
.unwrap();
assert!(matches!(requests.as_slice(), [UiRequest::SwitchTool(tool)] if tool == "gradient"));
assert_eq!(s.tool_options()["stop"], json!(null));
assert_eq!(json!(s.doc().unwrap().doc.node(source).unwrap()), before);
let paint = s.doc().unwrap().doc.node(target).unwrap().appearance.fill_paint();
let vectorcraft_color::Paint::Gradient(g) = paint else { panic!("gradient retained") };
assert_eq!(g.gradient.stops[1].color.to_hex(), "#e23355");
}