mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-09 03:48:01 +00:00
tool.setOption on another tool reloaded every saved option of the active tool. During a shape drag that reload rewrote the options Escape restores, so Escape kept a side count an arrow key had changed. The active tool now reloads only the stores it shares with the tool that was set.
327 lines
15 KiB
Rust
327 lines
15 KiB
Rust
//! Tool options that persist across tool switches, documents and launches (`Prefs::tool_settings`).
|
|
|
|
use serde_json::json;
|
|
use vectorcraft_tools::{Mods, PointerEvent, PointerKind, ToolKey};
|
|
|
|
use crate::*;
|
|
|
|
fn session() -> Session {
|
|
let mut s = Session::new();
|
|
s.execute("file.new", &json!({"width": 800, "height": 600})).unwrap();
|
|
s
|
|
}
|
|
|
|
fn select(s: &mut Session, tool: &str) {
|
|
s.select_tool(tool, ViewInfo::default()).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn blob_tip_options_report_and_persist() {
|
|
let mut s = session();
|
|
select(&mut s, "blobBrush");
|
|
assert_eq!((s.tool_options()["angle"].as_f64(), s.tool_options()["roundness"].as_f64()), (Some(0.0), Some(100.0)));
|
|
s.set_tool_option("angle", &json!(45));
|
|
s.set_tool_option("roundness", &json!(0));
|
|
select(&mut s, "selection");
|
|
let saved = serde_json::to_string(&s.prefs).unwrap();
|
|
let mut next = session();
|
|
next.apply_prefs(serde_json::from_str(&saved).unwrap());
|
|
select(&mut next, "blobBrush");
|
|
assert_eq!((next.tool_options()["angle"].as_f64(), next.tool_options()["roundness"].as_f64()), (Some(45.0), Some(0.0)));
|
|
}
|
|
|
|
#[test]
|
|
fn options_survive_a_tool_switch() {
|
|
let mut s = session();
|
|
select(&mut s, "mirrorCut");
|
|
s.set_tool_option("axis", &json!("horizontal"));
|
|
s.set_tool_option("keep", &json!("bottom"));
|
|
select(&mut s, "selection");
|
|
select(&mut s, "mirrorCut");
|
|
assert_eq!(s.tool_options(), json!({"axis": "horizontal", "keep": "bottom"}));
|
|
// Interaction state isn't kept: the Rotate tool's reference point starts afresh.
|
|
select(&mut s, "rotate");
|
|
s.set_tool_option("origin", &json!([10, 20]));
|
|
select(&mut s, "selection");
|
|
select(&mut s, "rotate");
|
|
assert_eq!(s.tool_options(), json!({"origin": null}));
|
|
}
|
|
|
|
#[test]
|
|
fn liquify_tools_share_the_global_brush_but_not_their_own_options() {
|
|
let mut s = session();
|
|
select(&mut s, "warp");
|
|
s.set_tool_option("width", &json!(60));
|
|
s.set_tool_option("intensity", &json!(80));
|
|
s.set_tool_option("detail", &json!(7));
|
|
select(&mut s, "twirl");
|
|
let o = s.tool_options();
|
|
assert_eq!((o["width"].as_f64(), o["intensity"].as_f64(), o["detail"].as_f64()), (Some(60.0), Some(0.8), Some(2.0)));
|
|
s.set_tool_option("rate", &json!(-90));
|
|
select(&mut s, "warp");
|
|
assert_eq!(s.tool_options()["detail"], json!(7.0));
|
|
select(&mut s, "twirl");
|
|
assert_eq!(s.tool_options()["rate"], json!(-90.0));
|
|
}
|
|
|
|
#[test]
|
|
fn another_tools_options_are_stored_and_shared_ones_reach_the_active_tool() {
|
|
let mut s = session();
|
|
select(&mut s, "warp");
|
|
let o = s.set_tool_option_cmd(&json!({"tool": "pucker", "values": {"width": 40, "height": 30, "detail": 5}})).unwrap();
|
|
assert_eq!((o["tool"].as_str(), o["detail"].as_f64()), (Some("pucker"), Some(5.0)));
|
|
assert_eq!((s.tool_id(), s.tool_options()["width"].as_f64(), s.tool_options()["detail"].as_f64()), ("warp", Some(40.0), Some(2.0)));
|
|
assert_eq!(s.tool_options_of("pucker")["height"], json!(30.0));
|
|
select(&mut s, "pucker");
|
|
assert_eq!(s.tool_options()["detail"], json!(5.0));
|
|
// `{}` reads the options; an unknown tool is an error.
|
|
assert_eq!(s.set_tool_option_cmd(&json!({})).unwrap(), s.tool_options());
|
|
assert!(s.set_tool_option_cmd(&json!({"tool": "nope", "key": "width", "value": 1})).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn a_gesture_keeps_what_it_changed_and_a_document_switch_keeps_options() {
|
|
let mut s = session();
|
|
select(&mut s, "spiral");
|
|
s.set_tool_option("segments", &json!(14));
|
|
s.execute("file.new", &json!({"width": 300, "height": 300})).unwrap();
|
|
assert_eq!(s.tool_id(), "spiral");
|
|
assert_eq!(s.tool_options()["segments"], json!(14));
|
|
s.set_active(0);
|
|
assert_eq!(s.tool_options()["segments"], json!(14));
|
|
}
|
|
|
|
/// Escape on a grid drag puts back the dividers its arrows changed, in the saved options too,
|
|
/// while an option set during the drag stays.
|
|
#[test]
|
|
fn escape_on_a_drag_saves_the_options_it_put_back() {
|
|
use vectorcraft_tools::{Mods, PointerEvent, PointerKind, ToolKey};
|
|
let mut s = session();
|
|
select(&mut s, "rectangularGrid");
|
|
let v = ViewInfo { smart_guides: false, ..Default::default() };
|
|
s.pointer(&PointerEvent::new(PointerKind::Down, 100.0, 100.0), v).unwrap();
|
|
s.pointer(&PointerEvent::new(PointerKind::Drag, 200.0, 180.0), v).unwrap();
|
|
s.tool_key(ToolKey::Up, Mods::default(), v).unwrap();
|
|
// Saves every option, the rows the arrow changed included.
|
|
s.set_tool_option("columns", &json!(8));
|
|
assert_eq!(s.prefs.tool_settings["rectangularGrid"]["rows"], json!(6));
|
|
s.tool_key(ToolKey::Escape, Mods::default(), v).unwrap();
|
|
let saved = &s.prefs.tool_settings["rectangularGrid"];
|
|
assert_eq!((saved["rows"].clone(), saved["columns"].clone()), (json!(5), json!(8)));
|
|
}
|
|
|
|
/// A dropped press (a long touch, a canceled touch, a second finger) keeps nothing it began: the
|
|
/// tool ends at rest with the options it had before the press, and the options the drag changed
|
|
/// are not stored. A release stores them.
|
|
#[test]
|
|
fn a_dropped_press_does_not_keep_what_its_drag_changed() {
|
|
let mut s = session();
|
|
select(&mut s, "polygon");
|
|
let v = ViewInfo::default();
|
|
let drag_with_one_more_side = |s: &mut Session| {
|
|
s.pointer(&PointerEvent::new(PointerKind::Down, 100.0, 100.0), v).unwrap();
|
|
s.pointer(&PointerEvent::new(PointerKind::Drag, 200.0, 150.0), v).unwrap();
|
|
s.tool_key(ToolKey::Up, Mods::default(), v).unwrap();
|
|
};
|
|
assert_eq!(s.tool_options()["sides"], json!(6));
|
|
drag_with_one_more_side(&mut s);
|
|
assert_eq!(s.tool_options()["sides"], json!(7));
|
|
s.drop_tool_press(v).unwrap();
|
|
assert!(!s.tool_busy(), "the tool is at rest");
|
|
let st = s.active().unwrap();
|
|
assert!(st.interaction.is_none() && st.doc.art_bounds().is_none(), "the polygon is not drawn");
|
|
assert_eq!(s.tool_options()["sides"], json!(6), "the tool has its sides from before the press");
|
|
assert_eq!(s.prefs.tool_settings.get("polygon").and_then(|o| o.get("sides")), None, "the dropped drag's sides are not stored");
|
|
drag_with_one_more_side(&mut s);
|
|
s.pointer(&PointerEvent::new(PointerKind::Up, 200.0, 150.0), v).unwrap();
|
|
assert!(s.active().unwrap().doc.art_bounds().is_some(), "a release draws the polygon");
|
|
assert_eq!(s.prefs.tool_settings["polygon"]["sides"], json!(7), "and stores its sides");
|
|
}
|
|
|
|
/// A dropped Pen press drops only the anchor it was placing: the path being drawn goes on with
|
|
/// the next click.
|
|
#[test]
|
|
fn a_dropped_pen_press_keeps_the_path_being_drawn() {
|
|
let mut s = session();
|
|
select(&mut s, "pen");
|
|
let v = ViewInfo::default();
|
|
let click = |s: &mut Session, x: f64, y: f64| {
|
|
s.pointer(&PointerEvent::new(PointerKind::Down, x, y), v).unwrap();
|
|
s.pointer(&PointerEvent::new(PointerKind::Up, x, y), v).unwrap();
|
|
};
|
|
click(&mut s, 10.0, 10.0);
|
|
click(&mut s, 100.0, 10.0);
|
|
s.pointer(&PointerEvent::new(PointerKind::Down, 100.0, 100.0), v).unwrap();
|
|
s.pointer(&PointerEvent::new(PointerKind::Drag, 140.0, 120.0), v).unwrap();
|
|
s.drop_tool_press(v).unwrap();
|
|
click(&mut s, 10.0, 100.0);
|
|
let d = &s.active().unwrap().doc;
|
|
let paths = d.layers[0].children().unwrap();
|
|
assert_eq!(paths.len(), 1, "no new path starts");
|
|
assert_eq!(paths[0].path_data().unwrap().anchor_count(), 3, "the dropped anchor is not placed");
|
|
}
|
|
|
|
/// A dropped Pen press on the first anchor does not close the path or end it.
|
|
#[test]
|
|
fn a_dropped_pen_close_keeps_the_path_open_and_being_drawn() {
|
|
let mut s = session();
|
|
select(&mut s, "pen");
|
|
let v = ViewInfo::default();
|
|
let click = |s: &mut Session, x: f64, y: f64| {
|
|
s.pointer(&PointerEvent::new(PointerKind::Down, x, y), v).unwrap();
|
|
s.pointer(&PointerEvent::new(PointerKind::Up, x, y), v).unwrap();
|
|
};
|
|
click(&mut s, 10.0, 10.0);
|
|
click(&mut s, 100.0, 10.0);
|
|
click(&mut s, 100.0, 100.0);
|
|
s.pointer(&PointerEvent::new(PointerKind::Down, 10.0, 10.0), v).unwrap();
|
|
s.pointer(&PointerEvent::new(PointerKind::Drag, 40.0, 30.0), v).unwrap();
|
|
s.drop_tool_press(v).unwrap();
|
|
click(&mut s, 10.0, 100.0);
|
|
let d = &s.active().unwrap().doc;
|
|
let paths = d.layers[0].children().unwrap();
|
|
assert_eq!(paths.len(), 1, "no new path starts");
|
|
let pd = paths[0].path_data().unwrap();
|
|
assert_eq!(pd.anchor_count(), 4, "the next click goes on the path");
|
|
assert!(!pd.subpaths[0].closed, "the path is not closed");
|
|
}
|
|
|
|
/// A dropped marquee selects nothing: the selection stays as it was before the press.
|
|
#[test]
|
|
fn a_dropped_marquee_leaves_the_selection_alone() {
|
|
let mut s = session();
|
|
s.execute("shape.rectangle", &json!({"x": 600, "y": 400, "width": 100, "height": 100})).unwrap();
|
|
let before = s.doc().unwrap().selection.objects.clone();
|
|
assert_eq!(before.len(), 1);
|
|
select(&mut s, "selection");
|
|
let v = ViewInfo::default();
|
|
s.pointer(&PointerEvent::new(PointerKind::Down, 300.0, 300.0), v).unwrap();
|
|
s.pointer(&PointerEvent::new(PointerKind::Drag, 350.0, 350.0), v).unwrap();
|
|
s.drop_tool_press(v).unwrap();
|
|
assert!(!s.tool_busy(), "the tool is at rest");
|
|
assert_eq!(s.doc().unwrap().selection.objects, before);
|
|
}
|
|
|
|
/// A dropped Shift-drag marquee of the Artboard tool toggles no artboard: the tool takes Escape,
|
|
/// as during a drag, before its release.
|
|
#[test]
|
|
fn a_dropped_artboard_marquee_toggles_nothing() {
|
|
let mut s = session();
|
|
s.execute("artboard.new", &json!({"x": 1000, "y": 0, "width": 400, "height": 400})).unwrap();
|
|
select(&mut s, "artboard");
|
|
let before = s.tool_options()["selected"].clone();
|
|
let v = ViewInfo::default();
|
|
let shift = Mods { shift: true, ..Mods::default() };
|
|
s.pointer(&PointerEvent::new(PointerKind::Down, 1200.0, 500.0).with_mods(shift), v).unwrap();
|
|
s.pointer(&PointerEvent::new(PointerKind::Drag, 1250.0, 550.0).with_mods(shift), v).unwrap();
|
|
s.drop_tool_press(v).unwrap();
|
|
assert!(!s.tool_busy(), "the tool is at rest");
|
|
assert_eq!(s.tool_options()["selected"], before, "no artboard is toggled");
|
|
}
|
|
|
|
#[test]
|
|
fn options_are_saved_with_the_preferences() {
|
|
let mut s = session();
|
|
select(&mut s, "polygon");
|
|
s.set_tool_option("sides", &json!(9));
|
|
select(&mut s, "selection");
|
|
let saved = serde_json::to_string(&s.prefs).unwrap();
|
|
assert!(saved.contains("toolSettings"), "{saved}");
|
|
// Next launch.
|
|
let mut t = session();
|
|
t.apply_prefs(serde_json::from_str(&saved).unwrap());
|
|
select(&mut t, "polygon");
|
|
assert_eq!(t.tool_options()["sides"], json!(9));
|
|
// Older preference files have none; none are written until a tool keeps something.
|
|
assert!(Prefs::default().to_json().get("toolSettings").is_none());
|
|
let old: Prefs = serde_json::from_value(json!({"keyboardIncrement": 2})).unwrap();
|
|
assert!(old.tool_settings.is_empty());
|
|
// Resetting the preferences keeps them.
|
|
t.execute("prefs.reset", &json!({})).unwrap();
|
|
assert_eq!(t.prefs.tool_settings["polygon"]["sides"], json!(9));
|
|
}
|
|
|
|
/// Escape on a polygon or rounded rectangle drag puts back the side count or corner radius its
|
|
/// arrows changed, in the saved options and the Corner Radius preference too.
|
|
#[test]
|
|
fn escape_on_a_shape_drag_saves_the_option_it_put_back() {
|
|
use vectorcraft_tools::{Mods, PointerEvent, PointerKind, ToolKey};
|
|
let mut s = session();
|
|
let v = ViewInfo { smart_guides: false, ..Default::default() };
|
|
let drag = |s: &mut Session| {
|
|
s.pointer(&PointerEvent::new(PointerKind::Down, 100.0, 100.0), v).unwrap();
|
|
s.pointer(&PointerEvent::new(PointerKind::Drag, 200.0, 180.0), v).unwrap();
|
|
s.tool_key(ToolKey::Up, Mods::default(), v).unwrap();
|
|
// Saves every option, the one the arrow changed included.
|
|
s.set_tool_option("length", &json!(50));
|
|
};
|
|
select(&mut s, "polygon");
|
|
drag(&mut s);
|
|
assert_eq!(s.prefs.tool_settings["polygon"]["sides"], json!(7));
|
|
s.tool_key(ToolKey::Escape, Mods::default(), v).unwrap();
|
|
assert_eq!(s.prefs.tool_settings["polygon"]["sides"], json!(6));
|
|
select(&mut s, "roundedRectangle");
|
|
let radius = s.prefs.corner_radius;
|
|
drag(&mut s);
|
|
assert_eq!(s.prefs.corner_radius, radius + 1.0);
|
|
s.tool_key(ToolKey::Escape, Mods::default(), v).unwrap();
|
|
assert_eq!(s.prefs.corner_radius, radius);
|
|
}
|
|
|
|
/// Setting another tool's option during a polygon drag leaves the drag's options alone: Escape
|
|
/// still puts back the side count the arrow changed.
|
|
#[test]
|
|
fn escape_puts_back_the_sides_after_another_tool_s_option_is_set() {
|
|
use vectorcraft_tools::{Mods, PointerEvent, PointerKind, ToolKey};
|
|
let mut s = session();
|
|
let v = ViewInfo { smart_guides: false, ..Default::default() };
|
|
select(&mut s, "polygon");
|
|
s.pointer(&PointerEvent::new(PointerKind::Down, 100.0, 100.0), v).unwrap();
|
|
s.pointer(&PointerEvent::new(PointerKind::Drag, 200.0, 180.0), v).unwrap();
|
|
s.tool_key(ToolKey::Up, Mods::default(), v).unwrap();
|
|
// Saves every option, the one the arrow changed included.
|
|
s.set_tool_option("length", &json!(50));
|
|
let values = json!({"points": 9});
|
|
s.set_tool_options(Some("star"), values.as_object().unwrap());
|
|
assert_eq!(s.tool_options()["sides"], json!(7), "the drag keeps its side count");
|
|
s.tool_key(ToolKey::Escape, Mods::default(), v).unwrap();
|
|
assert_eq!(s.tool_options()["sides"], json!(6));
|
|
assert_eq!(s.prefs.tool_settings["polygon"]["sides"], json!(6));
|
|
assert_eq!(s.prefs.tool_settings["star"]["points"], json!(9));
|
|
}
|
|
|
|
/// The Fidelity an Alt smoothing drag uses on a selected path.
|
|
fn alt_smooth_fidelity(s: &mut Session) -> Option<f64> {
|
|
use vectorcraft_tools::{Action, Mods, PointerEvent, PointerKind};
|
|
let mods = Mods { alt: true, ..Default::default() };
|
|
let mut last = vec![];
|
|
for (kind, x, y) in [(PointerKind::Down, 50.0, 150.0), (PointerKind::Drag, 150.0, 150.0), (PointerKind::Up, 250.0, 160.0)] {
|
|
let ev = PointerEvent { mods, ..PointerEvent::new(kind, x, y) };
|
|
last = s.with_tool_cx(ViewInfo::default(), |t, cx| t.pointer(cx, &ev));
|
|
}
|
|
last.iter().find_map(|a| match a {
|
|
Action::Exec(c, v) if c == "path.smoothRegion" => v["fidelity"].as_f64(),
|
|
_ => None,
|
|
})
|
|
}
|
|
|
|
/// Alt switches Pencil, Paintbrush and Blob Brush to Smooth, using Smooth Tool Options' stored Fidelity.
|
|
#[test]
|
|
fn alt_smoothing_takes_the_smooth_tools_fidelity() {
|
|
for id in ["pencil", "paintbrush", "blobBrush"] {
|
|
let mut s = session();
|
|
s.execute("shape.rectangle", &json!({"x": 100, "y": 100, "width": 100, "height": 100})).unwrap();
|
|
select(&mut s, "smooth");
|
|
s.set_tool_option("fidelity", &json!(6));
|
|
select(&mut s, id);
|
|
if id == "pencil" {
|
|
s.set_tool_option("altSmooth", &json!(true));
|
|
}
|
|
s.set_tool_option("fidelity", &json!(3));
|
|
assert_eq!(alt_smooth_fidelity(&mut s), Some(6.0), "{id}");
|
|
s.set_tool_option_cmd(&json!({"tool": "smooth", "key": "fidelity", "value": 9})).unwrap();
|
|
assert_eq!(alt_smooth_fidelity(&mut s), Some(9.0), "{id}");
|
|
assert_eq!(s.tool_options()["fidelity"], json!(3.0), "{id}");
|
|
}
|
|
}
|