Files
vectorcraft/crates/engine/src/tests_newart.rs
T
Taha-Mahmoodi 7c8840b93a Tools: double-clicking the Line Segment, Arc, Rectangular Grid and Polar Grid tools opens their Tool Options
Step 3 of the Rectangular Grid tool and Arc tool help pages
(https://helpx.adobe.com/illustrator/using/tool-techniques/rectangular-grid-tool.html,
https://helpx.adobe.com/illustrator/using/tool-techniques/arc-tool.html)
and the Tools page (https://helpx.adobe.com/illustrator/using/tools.html)
say a double-click on the tool opens its Tool Options. For these four tools
the double-click (tool.options, and Enter once #2891 lands) said the tool
had no options (#2691).

The Tool Options are the tool's click dialog in dialogs/shapes.rs, opened
without a place to draw (shapes::open_options; a `__toolOptions` field
marks it), as the review of #2979 asked: no copied form code. Its OK runs
tool.setOption and draws nothing; Cancel changes nothing. The grids' dialogs
are titled Rectangular Grid / Polar Grid Tool Options there. The options
are what the next drag draws with and what a click's dialog opens at:
- Line Segment: Length and Angle (now kept by the tool and used by the
  click dialog) and Fill Line (new: shape.line {fill: true} gives the line
  the current fill; the click dialog has it too);
- Arc: lengths, Reference Point, Type, Base Along, Slope, Fill Arc;
- the grids: dividers, skews and switches, plus the Default Size and
  Reference Point, now kept by the tool (width, height, reference) for the
  click dialog; a drag leaves them out of its command.
The grids' Reference Point is drawn only as the 9-point locator (it also
showed as a plain number field).

New strings (Rectangular Grid Tool Options, Polar Grid Tool Options, Fill
Line) have rows in all 24 catalogs.

Verification:
toolbar::tests::double_clicking_the_line_arc_and_grid_tools_opens_their_tool_options
double-clicks each tool's toolbar button: the dialog and its title, Cancel
leaves the next drag as it was, OK draws nothing, the next drag uses the
option and a click's dialog opens at it (a grid at its Default Size);
line_segment_options_and_fill_line, grid_default_size_is_an_option_the_drag_leaves_out,
fill_line_gives_a_new_line_the_current_fill. Grid dialog tests now expect
the stored Default Size. Live: double-clicking the Rectangular Grid tool in
the release build opens Rectangular Grid Tool Options; OK with 2 and 3
dividers, then a drag, draws a 3 × 4 grid.
2026-10-24 03:09:07 +07:00

158 lines
8.0 KiB
Rust

//! The new-art template (M3.65): Stroke panel options and graphic styles chosen with nothing
//! selected, New Art Has Basic Appearance and `paint.default`.
use serde_json::{Value, json};
use vectorcraft_color::{BlendMode, Paint};
use vectorcraft_doc::{Appearance, Dash, LineCap, Node};
use super::*;
fn session() -> Session {
let mut s = Session::new();
s.execute("file.new", &json!({"width": 400, "height": 300})).unwrap();
s
}
fn run(s: &mut Session, cmd: &str, p: Value) -> Value {
s.execute(cmd, &p).unwrap_or_else(|e| panic!("{cmd}: {e}"))
}
fn rect(s: &mut Session, x: f64) -> NodeId {
NodeId(run(s, "shape.rectangle", json!({"x": x, "y": 20, "width": 60, "height": 40}))["id"].as_u64().unwrap())
}
fn node(s: &Session, id: NodeId) -> Node {
s.doc().unwrap().doc.node(id).unwrap().clone()
}
fn undo_len(s: &Session) -> usize {
s.doc().unwrap().history.undo.len()
}
#[test]
fn stroke_options_set_with_nothing_selected_carry_to_the_next_object() {
let mut s = session();
let before = rect(&mut s, 20.0);
run(&mut s, "select.none", json!({}));
let undo = undo_len(&s);
run(&mut s, "stroke.set", json!({"cap": "round", "dash": [4, 2], "weight": 3, "endArrow": "Arrow"}));
assert_eq!(undo_len(&s), undo, "no document change, no undo step");
// The Stroke panel shows them with nothing selected.
let shown = s.shown_stroke().unwrap();
assert_eq!((shown.cap, shown.width, shown.dash.as_ref().map(|d| d.pattern.clone())), (LineCap::Round, 3.0, Some(vec![4.0, 2.0])));
let id = rect(&mut s, 120.0);
let st = node(&s, id).appearance.stroke().unwrap().clone();
assert_eq!((st.cap, st.width, st.end_arrow.is_some()), (LineCap::Round, 3.0, true));
assert_eq!(st.dash, Some(Dash { pattern: vec![4.0, 2.0], ..Default::default() }));
assert_eq!(node(&s, id).appearance.fill_paint(), Paint::solid(vectorcraft_color::Color::WHITE), "the fill stays the proxy's");
assert_eq!(node(&s, before).appearance.stroke().unwrap().cap, LineCap::Butt, "art drawn before keeps its stroke");
// Editing a selected object's stroke leaves the template alone.
run(&mut s, "stroke.set", json!({"cap": "square"}));
run(&mut s, "select.none", json!({}));
assert_eq!(s.shown_stroke().unwrap().cap, LineCap::Round);
// Other new art takes it too (a line still without a fill).
let line = NodeId(run(&mut s, "shape.line", json!({"x1": 10, "y1": 200, "x2": 200, "y2": 200}))["id"].as_u64().unwrap());
let l = node(&s, line).appearance;
assert_eq!((l.fill_paint(), l.stroke().unwrap().cap), (Paint::None, LineCap::Round));
// Default Fill and Stroke resets the template.
run(&mut s, "paint.default", json!({}));
let plain = rect(&mut s, 200.0);
assert_eq!(node(&s, plain).appearance, Appearance::default_art());
assert!(s.shown_stroke().is_some_and(|st| st.dash.is_none() && st.cap == LineCap::Butt));
}
/// A rectangle with two fills, a drop shadow and 50% opacity, selected.
fn fancy(s: &mut Session) -> NodeId {
let id = rect(s, 20.0);
run(s, "paint.setFill", json!({"color": "#ff0000"}));
run(s, "appearance.addFill", json!({}));
run(s, "paint.setFill", json!({"color": "#0000ff"}));
run(s, "effect.apply", json!({"effect": "stylize.dropShadow"}));
run(s, "transparency.set", json!({"opacity": 50}));
id
}
#[test]
fn with_basic_appearance_off_new_art_takes_the_last_selections_whole_appearance() {
let mut s = session();
assert!(s.prefs.new_art_basic, "on by default");
let src = fancy(&mut s);
run(&mut s, "select.none", json!({}));
let basic = rect(&mut s, 120.0);
let b = node(&s, basic);
assert_eq!((b.appearance.items.len(), b.appearance.effects.len(), b.opacity), (2, 0, 1.0), "on: one fill and stroke");
assert_eq!(run(&mut s, "appearance.setNewArtBasic", json!({})), json!({"on": false}), "no param toggles");
run(&mut s, "select.set", json!({"ids": [src.0]}));
run(&mut s, "select.none", json!({}));
let id = rect(&mut s, 220.0);
let (n, from) = (node(&s, id), node(&s, src));
assert_eq!(n.appearance.items.iter().filter(|i| i.is_fill()).count(), 2, "both fills");
assert_eq!(n.appearance.effects.len(), 1, "the drop shadow");
assert!(n.appearance.approx_eq(&from.appearance));
assert_eq!((n.opacity, n.blend), (0.5, BlendMode::Normal));
let q = run(&mut s, "appearance.newArt", json!({}));
assert_eq!((q["basic"].clone(), q["inherited"].clone(), q["opacity"].clone()), (json!(false), json!(true), json!(50.0)));
// Back on, new art takes only the basic fill and stroke again.
run(&mut s, "appearance.setNewArtBasic", json!({"on": true}));
run(&mut s, "select.none", json!({}));
let again = rect(&mut s, 300.0);
let again = node(&s, again);
assert_eq!((again.appearance.items.len(), again.appearance.effects.len(), again.opacity), (2, 0, 1.0));
}
#[test]
fn a_graphic_style_clicked_with_nothing_selected_styles_the_next_object() {
let mut s = session();
fancy(&mut s);
// A placed gradient on the top fill: the style keeps it relative to the object's bounds.
let stops = json!([{"offset": 0, "color": "#ffffff"}, {"offset": 1, "color": "#000000"}]);
run(&mut s, "paint.setFill", json!({"gradient": {"stops": stops, "start": [20, 40], "end": [50, 40]}}));
let name = run(&mut s, "graphicStyle.new", json!({"name": "Fancy"}))["name"].as_str().unwrap().to_string();
run(&mut s, "select.none", json!({}));
let undo = undo_len(&s);
assert_eq!(run(&mut s, "graphicStyle.apply", json!({"name": name})), json!({"newArt": true}));
assert_eq!(undo_len(&s), undo);
assert_eq!(run(&mut s, "appearance.newArt", json!({}))["graphicStyle"], json!("Fancy"));
let id = NodeId(run(&mut s, "shape.ellipse", json!({"x": 200, "y": 100, "width": 100, "height": 50}))["id"].as_u64().unwrap());
let n = node(&s, id);
assert_eq!((n.appearance.items.len(), n.appearance.effects.len(), n.opacity), (3, 1, 0.5));
// Linked to the style (its look, gradient placed on the ellipse's own bounds).
let (g, linked) = s.selection_graphic_style().unwrap();
assert_eq!((g.name.as_str(), linked), ("Fancy", true));
// Alt-click (add) with nothing selected stacks a style on the template, unlinked.
run(&mut s, "select.none", json!({}));
run(&mut s, "graphicStyle.apply", json!({"name": name, "add": true}));
let q = run(&mut s, "appearance.newArt", json!({}));
assert_eq!((q["appearance"]["items"].as_array().unwrap().len(), q["graphicStyle"].clone()), (6, Value::Null));
assert!(s.execute("graphicStyle.apply", &json!({"name": name, "ids": []})).is_err(), "explicit ids that match nothing");
}
#[test]
fn the_option_round_trips_with_the_preferences() {
let mut s = session();
run(&mut s, "prefs.set", json!({"key": "newArtBasic", "value": false}));
assert!(!s.prefs.new_art_basic);
assert_eq!(run(&mut s, "prefs.get", json!({"key": "newArtBasic"})), json!(false));
let json = s.prefs.to_json();
assert_eq!(json["newArtBasic"], json!(false));
let back: Prefs = serde_json::from_value(json).unwrap();
assert_eq!(back, s.prefs);
// Older preference files load with it on.
let old: Prefs = serde_json::from_value(json!({"keyboardIncrement": 2})).unwrap();
assert!(old.new_art_basic);
}
/// Line Segment Tool Options' Fill Line: `shape.line {fill: true}` gives the line the current
/// fill; without it a new line has none.
#[test]
fn fill_line_gives_a_new_line_the_current_fill() {
let mut s = session();
run(&mut s, "paint.setFill", json!({"color": "#ff0000"}));
let plain = NodeId(run(&mut s, "shape.line", json!({"x1": 10, "y1": 20, "x2": 200, "y2": 80}))["id"].as_u64().unwrap());
let filled = NodeId(run(&mut s, "shape.line", json!({"x1": 10, "y1": 20, "x2": 200, "y2": 80, "fill": true}))["id"].as_u64().unwrap());
assert_eq!(node(&s, plain).appearance.fill_paint(), Paint::None);
assert_eq!(node(&s, filled).appearance.fill_paint(), s.paint.fill);
assert_ne!(s.paint.fill, Paint::None);
assert!(s.execute("shape.line", &json!({"x1": 0, "y1": 0, "x2": 1, "y2": 1, "fill": "yes"})).is_err());
}