Files
vectorcraft/crates/engine/src/tests_newart.rs
T
bflatasticandClaude Opus 5.5 faf006e135 M3.65: new-art appearance template (stroke options and graphic styles with nothing selected, New Art Has Basic Appearance)
With nothing selected, Stroke panel edits (stroke.set / stroke.setAdvanced: cap, join, dashes,
arrowheads, profile...) and a graphic style clicked in the Graphic Styles panel
(graphicStyle.apply, add too) set up the next object drawn: its stroke options, fills, strokes,
effects, opacity and blend mode, linked to the style. The Appearance panel's New Art Has Basic
Appearance (appearance.setNewArtBasic, preference newArtBasic, on by default) now works: off,
new art takes the whole appearance of the last selection. paint.default resets the template,
appearance.newArt reports it, and with nothing selected the Stroke panel shows its stroke and
the Appearance panel its rows. PaintDefaults gains the template appearance, opacity, blend
mode and style.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 04:38:09 +07:00

144 lines
7.2 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);
}