diff --git a/crates/engine/src/cmd/brushlib.rs b/crates/engine/src/cmd/brushlib.rs index 988aa89572..16ab1c76d4 100644 --- a/crates/engine/src/cmd/brushlib.rs +++ b/crates/engine/src/cmd/brushlib.rs @@ -170,6 +170,7 @@ pub struct BuiltinBrushLibrary { /// The built-in brush libraries, in menu order: original brushes generated in code. pub const BUILTIN: &[BuiltinBrushLibrary] = &[ BuiltinBrushLibrary { id: "art-ribbons", name: "Art Ribbons", make: art_ribbons }, + BuiltinBrushLibrary { id: "ink-and-wash", name: "Ink and Wash", make: ink_and_wash }, BuiltinBrushLibrary { id: "calligraphic-nibs", name: "Calligraphic Nibs", make: calligraphic_nibs }, BuiltinBrushLibrary { id: "bristle-brushes", name: "Bristle Brushes", make: bristle_brushes }, BuiltinBrushLibrary { id: "pattern-borders", name: "Pattern Borders", make: pattern_borders }, @@ -268,6 +269,78 @@ fn art_ribbons() -> BrushLibrary { built("Art Ribbons", doc, brushes) } +/// A band between the edges y = `top(x)` and y = `bottom(x)` for x in `from`..`to`, in `n` steps. +fn between(from: f64, to: f64, n: usize, top: impl Fn(f64) -> f64, bottom: impl Fn(f64) -> f64) -> PathData { + let xs: Vec = (0..=n).map(|i| from + (to - from) * i as f64 / n as f64).collect(); + let mut pts: Vec = xs.iter().map(|&x| Point::new(x, top(x))).collect(); + pts.extend(xs.iter().rev().map(|&x| Point::new(x, bottom(x)))); + PathData::single(SubPath::polyline(&pts, true)) +} + +/// Path `p` filled with gray `hex` at `opacity`: one layer of a wash. +fn wash(p: PathData, hex: &str, opacity: f32) -> Node { + let mut n = filled(p, Paint::solid(Color::from_hex(hex).unwrap_or(Color::BLACK))); + n.opacity = opacity; + n +} + +/// How much of a wash's width is left at x: rounded ends, full width from 8 pt in. +fn wash_ends(x: f64) -> f64 { + let ends = ((x / 8.0).min(1.0) * ((100.0 - x) / 8.0).min(1.0)).max(0.0); + 0.4 + 0.6 * ends.sqrt() +} + +/// Ink and Wash: Tapered Ink, broad near its start and thinning to a fine tip; Brush Pen, a +/// rounded nib whose size follows the pen pressure; and two washes of translucent grays, Soft Wash +/// with faint edges and Pooling Wash with a darker rim and two light breaks. Black and grays take +/// the stroke color as tints of it. +fn ink_and_wash() -> BrushLibrary { + let tapered = { + let half = |x: f64| 3.0 * (x / 22.0).min(1.0).powf(0.6) * ((100.0 - x) / 78.0).min(1.0).powf(1.4); + black(between(0.0, 100.0, 48, |x| -0.85 * half(x), |x| 1.15 * half(x))) + }; + let pen = Brush { + name: "Brush Pen".into(), + kind: BrushKind::Calligraphic(Calligraphic { + angle: 40.0, + roundness: 75.0, + size: 5.0, + variation: [0.0, 0.0, 2.5], + modes: Some([Variation::Fixed, Variation::Fixed, Variation::Pressure]), + }), + }; + let soft = { + let top = |x: f64| -(4.5 + 0.5 * (x * 0.21).sin() + 0.3 * (x * 0.57 + 1.3).sin()) * wash_ends(x); + let bottom = |x: f64| (4.5 + 0.45 * (x * 0.26 + 2.1).sin() + 0.3 * (x * 0.69 + 0.4).sin()) * wash_ends(x); + group(vec![ + wash(between(0.0, 100.0, 40, top, bottom), "#404040", 0.4), + wash(between(2.0, 98.0, 32, |x| top(x) - 0.7 - 0.3 * (x * 0.43).sin().abs(), |x| top(x) + 0.5), "#404040", 0.2), + wash(between(3.0, 97.0, 32, |x| bottom(x) - 0.5, |x| bottom(x) + 0.6 + 0.35 * (x * 0.37 + 1.0).sin().abs()), "#404040", 0.18), + wash(shapes::ellipse(Rect::new(21.0, -2.4, 39.0, 0.8)), "#e6e6e6", 0.22), + wash(shapes::ellipse(Rect::new(61.0, -0.3, 75.0, 2.3)), "#e6e6e6", 0.18), + ]) + }; + let pooling = { + let top = |x: f64| -(4.0 + 0.4 * (x * 0.18 + 0.7).sin() + 0.2 * (x * 0.53).sin()) * wash_ends(x); + let bottom = |x: f64| (4.0 + 0.35 * (x * 0.23 + 1.9).sin() + 0.25 * (x * 0.61 + 0.2).sin()) * wash_ends(x); + let rim = |x: f64, k: f64, phase: f64| 0.35 + k * (0.5 + 0.5 * (x * 0.29 + phase).sin()); + group(vec![ + wash(between(0.0, 100.0, 40, top, bottom), "#4d4d4d", 0.35), + wash(between(1.0, 99.0, 40, top, |x| top(x) + rim(x, 0.45, 0.0)), "#262626", 0.55), + wash(between(1.0, 99.0, 40, |x| bottom(x) - rim(x, 0.5, 1.1), bottom), "#262626", 0.55), + wash(polygon(&[(34.0, -2.6), (39.0, -2.8), (36.0, 2.7), (31.0, 2.5)]), "#e6e6e6", 0.2), + wash(polygon(&[(70.0, -2.4), (74.0, -2.5), (72.0, 2.6), (68.0, 2.4)]), "#e6e6e6", 0.18), + ]) + }; + let brushes = vec![ + art("Tapered Ink", tapered, Colorization::Tints), + pen, + art("Soft Wash", soft, Colorization::Tints), + art("Pooling Wash", pooling, Colorization::Tints), + ]; + built("Ink and Wash", Document::new(100.0, 100.0), brushes) +} + fn nib(name: &str, angle: f64, roundness: f64, size: f64) -> Brush { Brush { name: name.into(), kind: BrushKind::Calligraphic(Calligraphic { angle, roundness, size, ..Default::default() }) } } diff --git a/crates/engine/src/tests/brushlib.rs b/crates/engine/src/tests/brushlib.rs index 9a3a18ed91..2d363d18cf 100644 --- a/crates/engine/src/tests/brushlib.rs +++ b/crates/engine/src/tests/brushlib.rs @@ -106,7 +106,17 @@ fn the_builtin_libraries_are_original_brushes_generated_in_code() { let ids: Vec<&str> = libs.iter().map(|l| l["id"].as_str().unwrap()).collect(); assert_eq!( ids, - ["art-ribbons", "calligraphic-nibs", "bristle-brushes", "pattern-borders", "scatter-shapes", "game-trails", "energy-beams", "dry-media"] + [ + "art-ribbons", + "ink-and-wash", + "calligraphic-nibs", + "bristle-brushes", + "pattern-borders", + "scatter-shapes", + "game-trails", + "energy-beams", + "dry-media" + ] ); assert!(libs.iter().all(|l| l["category"] == "builtIn")); let lib = run(&mut s, "brush.library", json!({"library": "art ribbons"})); @@ -120,26 +130,28 @@ fn the_builtin_libraries_are_original_brushes_generated_in_code() { trails["brushes"].as_array().unwrap().iter().map(|b| b["name"].as_str().unwrap()).collect::>(), ["Speed Line", "Dash Trail", "Burst Ray", "Zigzag"] ); - assert_eq!(libs[5]["count"].as_u64(), Some(4)); + assert_eq!(libs[6]["count"].as_u64(), Some(4)); let beams = run(&mut s, "brush.library", json!({"library": "energy-beams"})); assert_eq!( beams["brushes"].as_array().unwrap().iter().map(|b| b["name"].as_str().unwrap()).collect::>(), ["Laser Bolt", "Plasma Arc", "Charge Wave", "Spark Trail"] ); - assert_eq!(libs[6]["count"].as_u64(), Some(4)); + assert_eq!(libs[7]["count"].as_u64(), Some(4)); let dry = run(&mut s, "brush.library", json!({"library": "Dry Media"})); assert_eq!( dry["brushes"].as_array().unwrap().iter().map(|b| (b["name"].as_str().unwrap(), b["type"].as_str().unwrap())).collect::>(), [("Chalk Grain", "art"), ("Graphite Pencil", "art"), ("Wax Crayon", "art"), ("Charcoal Dust", "art")] ); - assert_eq!((libs[7]["name"].as_str(), libs[7]["count"].as_u64()), (Some("Dry Media"), Some(4))); + assert_eq!((libs[8]["name"].as_str(), libs[8]["count"].as_u64()), (Some("Dry Media"), Some(4))); run(&mut s, "file.new", json!({"width": 200, "height": 200})); let id = run(&mut s, "shape.rectangle", json!({"x": 10, "y": 20, "width": 40, "height": 8}))["id"].as_u64().unwrap(); let added = run(&mut s, "brush.addFromLibrary", json!({"library": "energy-beams", "name": "Laser Bolt", "apply": true, "ids": [id]})); assert_eq!(strs(&added["added"]), ["Laser Bolt"]); - // One kind of brush per library, every brush drawing something. + // One kind of brush per library (Ink and Wash: art brushes and a calligraphic one), every + // brush drawing something. for (id, kind) in [ ("art-ribbons", "art"), + ("ink-and-wash", "art"), ("calligraphic-nibs", "calligraphic"), ("bristle-brushes", "bristle"), ("pattern-borders", "pattern"), @@ -151,6 +163,7 @@ fn the_builtin_libraries_are_original_brushes_generated_in_code() { let (_, lib) = cmd::brushlib::library(&s, id).unwrap(); assert!(lib.len() >= 4, "{id}"); for b in &lib.brushes { + let kind = if b.name == "Brush Pen" { "calligraphic" } else { kind }; assert_eq!(b.kind.type_id(), kind, "{}", b.name); let mut line = vectorcraft_geom::BezPath::new(); line.move_to((0.0, 0.0)); @@ -176,6 +189,153 @@ fn the_builtin_libraries_are_original_brushes_generated_in_code() { assert!(s.execute("brush.library", &json!({"library": "nope"})).is_err()); } +/// Every path of brush art `n`: (anchors, opacity). +fn art_paths(n: &vectorcraft_doc::Node) -> Vec<(usize, f32)> { + let mut out = vec![]; + n.walk(&mut |c| { + if let Some(p) = c.path_data() { + out.push((p.anchor_count(), c.opacity)); + } + }); + out +} + +/// The Ink and Wash library, made once. +fn inks(s: &Session) -> std::sync::Arc { + cmd::brushlib::library(s, "ink-and-wash").unwrap().1 +} + +/// A straight line from the origin to (`x`, 0). +fn line_to(x: f64) -> vectorcraft_geom::BezPath { + let mut line = vectorcraft_geom::BezPath::new(); + line.move_to((0.0, 0.0)); + line.line_to((x, 0.0)); + line +} + +fn black_stroke() -> vectorcraft_doc::StrokeLayer { + vectorcraft_doc::StrokeLayer::new(vectorcraft_color::Paint::solid(vectorcraft_color::Color::BLACK), 1.0) +} + +/// Ink and Wash: a tapered ink art brush, a brush pen whose size follows the pen pressure and two +/// translucent washes, with names of their own and a few paths of art each. +#[test] +fn the_ink_and_wash_library_has_tapered_ink_a_brush_pen_and_two_washes() { + let mut s = session(); + let lib = run(&mut s, "brush.library", json!({"library": "ink-and-wash"})); + assert_eq!((lib["name"].as_str(), lib["category"].as_str()), (Some("Ink and Wash"), Some("builtIn"))); + let listed: Vec<(&str, &str)> = + lib["brushes"].as_array().unwrap().iter().map(|b| (b["name"].as_str().unwrap(), b["type"].as_str().unwrap())).collect(); + assert_eq!(listed, [("Tapered Ink", "art"), ("Brush Pen", "calligraphic"), ("Soft Wash", "art"), ("Pooling Wash", "art")]); + assert_eq!(run(&mut s, "brush.library", json!({"library": "Ink and Wash"}))["id"], "ink-and-wash"); + let inks = inks(&s); + // Names no other built-in library uses. + for b in cmd::brushlib::BUILTIN.iter().filter(|b| b.id != "ink-and-wash") { + let (_, other) = cmd::brushlib::library(&s, b.id).unwrap(); + assert!(inks.brushes.iter().all(|x| other.brush(&x.name).is_none()), "{}", b.id); + } + // The art: within its 100 pt length, a few paths each, opaque ink and translucent washes. + let mut anchors = 0; + for name in ["Tapered Ink", "Soft Wash", "Pooling Wash"] { + let BrushKind::Art(a) = &inks.brush(name).unwrap().kind else { panic!("{name} is not an art brush") }; + assert_eq!((a.scale, a.colorization), (vectorcraft_brush::ArtScale::Stretch, vectorcraft_brush::Colorization::Tints), "{name}"); + let b = a.art.geometric_bounds().unwrap(); + assert!([b.x0, b.y0, b.x1, b.y1].iter().all(|v| v.is_finite()), "{name}"); + assert!(b.x0 >= 0.0 && b.x1 <= 100.0 && b.x1 - b.x0 > 99.0, "{name}: {b:?}"); + assert!(b.y0 >= -8.0 && b.y1 <= 8.0 && b.y1 - b.y0 > 5.0, "{name}: {b:?}"); + let paths = art_paths(&a.art); + let count: usize = paths.iter().map(|p| p.0).sum(); + assert!(paths.len() <= 80 && count <= 500, "{name}: {} paths, {count} anchors", paths.len()); + anchors += count; + let opacities: Vec = paths.iter().map(|p| p.1).collect(); + if name == "Tapered Ink" { + assert_eq!(opacities, [1.0], "opaque ink"); + } else { + assert!(paths.len() >= 3, "{name}: a wash of several layers"); + assert!(opacities.iter().all(|o| (0.15..=0.55).contains(o)), "{name}: {opacities:?}"); + } + } + assert!(anchors <= 1200, "{anchors} anchors"); + let BrushKind::Calligraphic(pen) = &inks.brush("Brush Pen").unwrap().kind else { panic!("Brush Pen is not calligraphic") }; + use vectorcraft_brush::Variation::{Fixed, Pressure}; + assert_eq!((pen.size, pen.modes(), pen.variation), (5.0, [Fixed, Fixed, Pressure], [0.0, 0.0, 2.5]), "no random variation"); +} + +/// Every Ink and Wash brush draws along straight, curved, short and tight paths within finite +/// bounds, and Tapered Ink is broad near its start and fine at its end. +#[test] +fn ink_and_wash_brushes_draw_along_straight_curved_short_and_tight_paths() { + let inks = inks(&Session::new()); + let mut curve = vectorcraft_geom::BezPath::new(); + curve.move_to((0.0, 0.0)); + curve.curve_to((60.0, -40.0), (140.0, 40.0), (200.0, 0.0)); + let tight = vectorcraft_geom::shapes::ellipse(vectorcraft_geom::Rect::new(0.0, 0.0, 6.0, 6.0)).to_bezpath(); + let paths = [line_to(200.0), line_to(3.0), curve, tight]; + for b in &inks.brushes { + for line in &paths { + let pieces = vectorcraft_brush::stroke_pieces(&inks.doc, b, line, &black_stroke()); + assert!(!pieces.is_empty(), "{} draws nothing", b.name); + let bounds: Vec<_> = pieces.iter().map(|p| p.geometric_bounds()).collect(); + assert!(bounds.iter().all(|r| r.is_some_and(|r| [r.x0, r.y0, r.x1, r.y1].iter().all(|v| v.is_finite()))), "{}", b.name); + } + } + let pieces = vectorcraft_brush::stroke_pieces(&inks.doc, inks.brush("Tapered Ink").unwrap(), &line_to(100.0), &black_stroke()); + let widest = |from: f64, to: f64| { + pieces + .iter() + .filter_map(|n| n.path_data()) + .flat_map(|p| p.anchors().map(|(_, _, a)| a.p).collect::>()) + .filter(|p| p.x >= from && p.x <= to) + .map(|p| p.y.abs()) + .fold(0.0, f64::max) + }; + assert!(widest(15.0, 30.0) > 4.0 * widest(95.0, 100.0), "broad start, fine tip"); +} + +/// Ink and Wash brushes are added and applied in one step, undone and redone, not added twice, +/// and kept with the brushed stroke by a saved and reopened document. +#[test] +fn ink_and_wash_brushes_add_apply_undo_and_survive_a_saved_document() { + let mut s = session(); + let inks = inks(&s); + let path = run(&mut s, "shape.line", json!({"x1": 10, "y1": 10, "x2": 150, "y2": 60}))["id"].as_u64().unwrap(); + let (defaults, before) = (brushes(&s), undo_len(&s)); + let names = ["Tapered Ink", "Brush Pen", "Soft Wash", "Pooling Wash"]; + let r = run(&mut s, "brush.addFromLibrary", json!({"library": "ink-and-wash", "name": "Soft Wash", "apply": true, "ids": [path]})); + assert_eq!((strs(&r["added"]), r["applied"].clone(), undo_len(&s)), (vec!["Soft Wash"], json!([path]), before + 1)); + let stroke_brush = |s: &Session| doc(s).node(NodeId(path)).unwrap().appearance.stroke().and_then(|st| st.brush.clone()); + assert_eq!(stroke_brush(&s).as_deref(), Some("Soft Wash")); + run(&mut s, "edit.undo", json!({})); + assert_eq!((brushes(&s), stroke_brush(&s)), (defaults.clone(), None)); + run(&mut s, "edit.redo", json!({})); + assert_eq!(stroke_brush(&s).as_deref(), Some("Soft Wash")); + let r = run(&mut s, "brush.addFromLibrary", json!({"library": "ink-and-wash"})); + assert_eq!( + (strs(&r["names"]), strs(&r["existing"]), strs(&r["added"])), + (names.to_vec(), vec!["Soft Wash"], vec!["Tapered Ink", "Brush Pen", "Pooling Wash"]) + ); + let before = undo_len(&s); + let r = run(&mut s, "brush.addFromLibrary", json!({"library": "ink-and-wash"})); + assert_eq!((strs(&r["existing"]), undo_len(&s)), (names.to_vec(), before)); + assert_eq!(brushes(&s).len(), defaults.len() + 4); + for n in names { + assert_eq!(&brush(&s, n), inks.brush(n).unwrap(), "{n}"); + } + + // Saved and opened again, the document keeps the definitions and the brushed stroke. + let b64 = run(&mut s, "document.serialize", json!({}))["dataBase64"].as_str().unwrap().to_string(); + let reopened = vectorcraft_format::load(&vectorcraft_format::base64_decode(&b64).unwrap()).unwrap(); + for n in names { + assert_eq!(vectorcraft_brush::find(&reopened, n).as_ref(), inks.brush(n), "{n}"); + } + let n = reopened.node(NodeId(path)).unwrap(); + assert_eq!(n.appearance.stroke().and_then(|st| st.brush.clone()).as_deref(), Some("Soft Wash")); + assert_eq!( + vectorcraft_brush::node_pieces(&reopened, n).map(|p| p.len()), + vectorcraft_brush::node_pieces(doc(&s), doc(&s).node(NodeId(path)).unwrap()).map(|p| p.len()) + ); +} + /// Whether `p` lies inside the polygon through `pts` (even-odd ray cast). fn inside(p: vectorcraft_geom::Point, pts: &[vectorcraft_geom::Point]) -> bool { let mut odd = false; diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index 4ca1e345f9..abf371b274 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -6069,6 +6069,11 @@ axis Y Axis Y ос User Brush Library 9 Потребителска библиотека с четки 9 User Brush Library 10 Потребителска библиотека с четки 10 Art Ribbons Арт ленти + Ink and Wash Туш и лавиране + Tapered Ink Изтъняващ туш + Brush Pen Писалка-четка + Soft Wash Меко лавиране + Pooling Wash Лавиране с ръб Zigzag Зигзаг Burst Ray Лъч на удар Dash Trail Пунктирна следа diff --git a/crates/ui-egui/src/i18n/bs.tsv b/crates/ui-egui/src/i18n/bs.tsv index 972414922b..e11d2c21e4 100644 --- a/crates/ui-egui/src/i18n/bs.tsv +++ b/crates/ui-egui/src/i18n/bs.tsv @@ -6122,6 +6122,11 @@ axis Y Axis Y osa User Brush Library 9 Korisnička biblioteka četkica 9 User Brush Library 10 Korisnička biblioteka četkica 10 Art Ribbons Umjetničke trake + Ink and Wash Tuš i lavir + Tapered Ink Suženi tuš + Brush Pen Kistasti flomaster + Soft Wash Meki lavir + Pooling Wash Lavir s rubom Zigzag Cik-cak Burst Ray Zraka udara Dash Trail Isprekidani trag diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index ebfbcd26db..91812ded4e 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -363,6 +363,11 @@ antiAlias Sharp Nítid Art Brush Options Opcions del pinzell artístic Art Optimized Optimitzat per a l’art Art Ribbons Cintes d’art + Ink and Wash Tinta i aiguada + Tapered Ink Tinta afuada + Brush Pen Retolador de pinzell + Soft Wash Aiguada suau + Pooling Wash Aiguada amb vora Zigzag Zigzag Burst Ray Raig d’impacte Dash Trail Estela discontínua diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index f5ea30ac2b..ddc33a08ae 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -6337,6 +6337,11 @@ axis Y Axis osa Y User Brush Library 9 Uživatelská knihovna štětců 9 User Brush Library 10 Uživatelská knihovna štětců 10 Art Ribbons Obrazové stuhy + Ink and Wash Tuš a lavírování + Tapered Ink Zužující se tuš + Brush Pen Štětcový fix + Soft Wash Jemná lavírka + Pooling Wash Lavírka s okrajem Zigzag Cikcak Burst Ray Paprsek nárazu Dash Trail Čárkovaná stopa diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index fbc690cf7f..5478fb4f98 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -6086,6 +6086,11 @@ axis Y Axis Y-akse User Brush Library 9 Brugerpenselbibliotek 9 User Brush Library 10 Brugerpenselbibliotek 10 Art Ribbons Kunstbånd + Ink and Wash Blæk og lavering + Tapered Ink Tilspidset blæk + Brush Pen Penselpen + Soft Wash Blød lavering + Pooling Wash Lavering med rand Zigzag Zigzag Burst Ray Burststråle Dash Trail Streget spor diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index e776bc1264..7621e4c4c0 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -5928,6 +5928,11 @@ axis Y Axis Y-Achse User Brush Library 9 Benutzer-Pinsel-Bibliothek 9 User Brush Library 10 Benutzer-Pinsel-Bibliothek 10 Art Ribbons Bildbänder + Ink and Wash Tusche und Lavur + Tapered Ink Spitz zulaufende Tusche + Brush Pen Pinselstift + Soft Wash Weiche Lavur + Pooling Wash Lavur mit Farbrand Zigzag Zickzack Burst Ray Stoßstrahl Dash Trail Strichelspur diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 711bdeb099..557be7dce9 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -6288,6 +6288,11 @@ axis Y Axis Άξονας Y User Brush Library 9 Βιβλιοθήκη πινέλων χρήστη 9 User Brush Library 10 Βιβλιοθήκη πινέλων χρήστη 10 Art Ribbons Γραφικές κορδέλες + Ink and Wash Μελάνι και ακουαρέλα + Tapered Ink Λεπτυνόμενο μελάνι + Brush Pen Πένα πινέλου + Soft Wash Απαλή ακουαρέλα + Pooling Wash Ακουαρέλα με περίγραμμα Zigzag Ζιγκζάγκ Burst Ray Ακτίνα κρούσης Dash Trail Διακεκομμένο ίχνος diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 2ab24e227f..4a35ed82c6 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -6279,6 +6279,11 @@ axis Y Axis Eje Y User Brush Library 9 Biblioteca de pinceles del usuario 9 User Brush Library 10 Biblioteca de pinceles del usuario 10 Art Ribbons Cintas de ilustración + Ink and Wash Tinta y aguada + Tapered Ink Tinta afilada + Brush Pen Rotulador de pincel + Soft Wash Aguada suave + Pooling Wash Aguada con borde Zigzag Zigzag Burst Ray Rayo de impacto Dash Trail Estela discontinua diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index 650898ae92..2b1a420e90 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -6367,6 +6367,11 @@ axis Y Axis Y-akseli User Brush Library 9 Käyttäjän sivellinkirjasto 9 User Brush Library 10 Käyttäjän sivellinkirjasto 10 Art Ribbons Kuvitusnauhat + Ink and Wash Muste ja laveeraus + Tapered Ink Suippeneva muste + Brush Pen Pensselikynä + Soft Wash Pehmeä laveeraus + Pooling Wash Reunustettu laveeraus Zigzag Siksak Burst Ray Iskusäde Dash Trail Katkoviiva diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 95e9c9e4c2..892d576345 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -6332,6 +6332,11 @@ axis Y Axis Axe Y User Brush Library 9 Bibliothèque de formes utilisateur 9 User Brush Library 10 Bibliothèque de formes utilisateur 10 Art Ribbons Rubans d'illustration + Ink and Wash Encre et lavis + Tapered Ink Encre effilée + Brush Pen Feutre pinceau + Soft Wash Lavis doux + Pooling Wash Lavis à auréole Zigzag Zigzag Burst Ray Rayon d’impact Dash Trail Traînée tiretée diff --git a/crates/ui-egui/src/i18n/hi.tsv b/crates/ui-egui/src/i18n/hi.tsv index 3d9850d4f5..a73be6f0df 100644 --- a/crates/ui-egui/src/i18n/hi.tsv +++ b/crates/ui-egui/src/i18n/hi.tsv @@ -424,6 +424,11 @@ antiAlias Sharp शार्प Art Brush Options आर्ट ब्रश ऑप्शन्स Art Optimized आर्टवर्क के लिए अनुकूलित Art Ribbons आर्ट रिबन + Ink and Wash स्याही और वॉश + Tapered Ink पतली होती स्याही + Brush Pen ब्रश पेन + Soft Wash सॉफ्ट वॉश + Pooling Wash पूलिंग वॉश Zigzag ज़िगज़ैग Burst Ray बर्स्ट रे Dash Trail डैश ट्रेल diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 948931ac13..8ca8717178 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -6320,6 +6320,11 @@ axis Y Axis Y tengely User Brush Library 9 Felhasználói ecsetkönyvtár 9 User Brush Library 10 Felhasználói ecsetkönyvtár 10 Art Ribbons Grafikus szalagok + Ink and Wash Tus és lavírozás + Tapered Ink Elvékonyodó tus + Brush Pen Ecsetfilc + Soft Wash Lágy lavírozás + Pooling Wash Szegélyes lavírozás Zigzag Cikkcakk Burst Ray Ütéssugár Dash Trail Szaggatott nyom diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index 55e2e73bf1..c233d02ec9 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -6086,6 +6086,11 @@ axis Y Axis Sumbu Y User Brush Library 9 Pustaka kuas pengguna 9 User Brush Library 10 Pustaka kuas pengguna 10 Art Ribbons Pita seni + Ink and Wash Tinta dan sapuan + Tapered Ink Tinta meruncing + Brush Pen Pena kuas + Soft Wash Sapuan lembut + Pooling Wash Sapuan menggenang Zigzag Zigzag Burst Ray Sinar ledakan Dash Trail Jejak putus-putus diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 81dffc495f..6e4179989f 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -5962,6 +5962,11 @@ axis Y Axis Asse Y User Brush Library 9 Libreria pennelli utente 9 User Brush Library 10 Libreria pennelli utente 10 Art Ribbons Nastri grafici + Ink and Wash Inchiostro e acquerellato + Tapered Ink Inchiostro affusolato + Brush Pen Pennarello a pennello + Soft Wash Velatura morbida + Pooling Wash Velatura con bordo Zigzag Zigzag Burst Ray Raggio d’impatto Dash Trail Scia tratteggiata diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 9d8ec83078..ff5e05f97a 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -6027,6 +6027,11 @@ axis Y Axis Y 軸 User Brush Library 9 ユーザーブラシライブラリ 9 User Brush Library 10 ユーザーブラシライブラリ 10 Art Ribbons アートリボン + Ink and Wash 墨と淡彩 + Tapered Ink 先細りの墨 + Brush Pen 筆ペン + Soft Wash 柔らかい淡彩 + Pooling Wash たまりのある淡彩 Zigzag ジグザグ Burst Ray バーストレイ Dash Trail ダッシュトレイル diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index 2630b64269..4694025797 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -6288,6 +6288,11 @@ axis Y Axis Y ღერძი User Brush Library 9 მომხმარებლის ფუნჯების ბიბლიოთეკა 9 User Brush Library 10 მომხმარებლის ფუნჯების ბიბლიოთეკა 10 Art Ribbons ნახატის ლენტები + Ink and Wash მელანი და ლავირება + Tapered Ink წვეტიანი მელანი + Brush Pen ფუნჯ-კალამი + Soft Wash რბილი ლავირება + Pooling Wash კიდიანი ლავირება Zigzag ზიგზაგი Burst Ray დარტყმის სხივი Dash Trail წყვეტილი კვალი diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index 30ac7dfb76..48d92bd719 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -5923,6 +5923,11 @@ axis Y Axis Y축 User Brush Library 9 사용자 브러시 라이브러리 9 User Brush Library 10 사용자 브러시 라이브러리 10 Art Ribbons 아트 리본 + Ink and Wash 먹과 담채 + Tapered Ink 가늘어지는 먹선 + Brush Pen 붓펜 + Soft Wash 부드러운 담채 + Pooling Wash 고임 담채 Zigzag 지그재그 Burst Ray 버스트 레이 Dash Trail 대시 트레일 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index 57ca80dc20..fa9aca3df9 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -6137,6 +6137,11 @@ axis Y Axis Y-as User Brush Library 9 Penseelbibliotheek van gebruiker 9 User Brush Library 10 Penseelbibliotheek van gebruiker 10 Art Ribbons Illustratielinten + Ink and Wash Inkt en wassing + Tapered Ink Taps toelopende inkt + Brush Pen Penseelstift + Soft Wash Zachte wassing + Pooling Wash Wassing met rand Zigzag Zigzag Burst Ray Inslagstraal Dash Trail Streepspoor diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 4094d29879..0e6e2e82a2 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -6191,6 +6191,11 @@ axis Y Axis Oś Y User Brush Library 9 Biblioteka pędzli użytkownika 9 User Brush Library 10 Biblioteka pędzli użytkownika 10 Art Ribbons Wstęgi artystyczne + Ink and Wash Tusz i laworowanie + Tapered Ink Zwężający się tusz + Brush Pen Pisak pędzelkowy + Soft Wash Miękkie laworowanie + Pooling Wash Laworowanie z obwódką Zigzag Zygzak Burst Ray Promień uderzenia Dash Trail Ślad kreskowany diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index a367880be2..4b0a3cd7da 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -6252,6 +6252,11 @@ axis Y Axis Eixo Y User Brush Library 9 Biblioteca de Pincéis do Usuário 9 User Brush Library 10 Biblioteca de Pincéis do Usuário 10 Art Ribbons Fitas de Arte + Ink and Wash Nanquim e Aguada + Tapered Ink Nanquim Afilado + Brush Pen Caneta Pincel + Soft Wash Aguada Suave + Pooling Wash Aguada com Borda Zigzag Zigue-zague Burst Ray Raio de impacto Dash Trail Rastro tracejado diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index e03cc555b1..06875888cf 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -5948,6 +5948,11 @@ axis Y Axis Ось Y User Brush Library 9 Пользовательская библиотека кистей 9 User Brush Library 10 Пользовательская библиотека кистей 10 Art Ribbons Художественные ленты + Ink and Wash Тушь и размывка + Tapered Ink Сужающаяся тушь + Brush Pen Ручка-кисть + Soft Wash Мягкая размывка + Pooling Wash Размывка с ободком Zigzag Зигзаг Burst Ray Ударный луч Dash Trail Пунктирный след diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 83a29c8531..b3e3062143 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -6354,6 +6354,11 @@ axis Y Axis Y-axel User Brush Library 9 Användarbibliotek för penslar 9 User Brush Library 10 Användarbibliotek för penslar 10 Art Ribbons Konstband + Ink and Wash Tusch och lavering + Tapered Ink Avsmalnande tusch + Brush Pen Penselpenna + Soft Wash Mjuk lavering + Pooling Wash Lavering med kant Zigzag Sicksack Burst Ray Stötstråle Dash Trail Streckat spår diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index c781b80ebb..799ebb6b40 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -6293,6 +6293,11 @@ axis Y Axis Y ekseni User Brush Library 9 Kullanıcı Fırça Kitaplığı 9 User Brush Library 10 Kullanıcı Fırça Kitaplığı 10 Art Ribbons Sanat Şeritleri + Ink and Wash Mürekkep ve Lavi + Tapered Ink Sivrilen Mürekkep + Brush Pen Fırça Kalem + Soft Wash Yumuşak Lavi + Pooling Wash Kenarlı Lavi Zigzag Zigzag Burst Ray Darbe ışını Dash Trail Kesik iz diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 75e062627c..c7e77a3f17 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -5942,6 +5942,11 @@ axis Y Axis Вісь Y User Brush Library 9 Користувацька бібліотека пензлів 9 User Brush Library 10 Користувацька бібліотека пензлів 10 Art Ribbons Художні стрічки + Ink and Wash Туш і розмивка + Tapered Ink Звужена туш + Brush Pen Пензлева ручка + Soft Wash М'яка розмивка + Pooling Wash Розмивка з облямівкою Zigzag Зигзаг Burst Ray Ударний промінь Dash Trail Пунктирний слід diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 7df0386f9e..13669ca858 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -5943,6 +5943,11 @@ axis Y Axis Trục Y User Brush Library 9 Thư viện cọ người dùng 9 User Brush Library 10 Thư viện cọ người dùng 10 Art Ribbons Dải băng nghệ thuật + Ink and Wash Mực và màu loang + Tapered Ink Mực vuốt nhọn + Brush Pen Bút lông + Soft Wash Màu loang mềm + Pooling Wash Màu loang đọng viền Zigzag Zigzag Burst Ray Tia va chạm Dash Trail Vệt đứt đoạn diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 9b593188b9..ae00ce8a34 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -6175,6 +6175,11 @@ axis Y Axis Y 轴 User Brush Library 9 用户画笔库 9 User Brush Library 10 用户画笔库 10 Art Ribbons 艺术丝带 + Ink and Wash 水墨淡彩 + Tapered Ink 渐细墨线 + Brush Pen 软头笔 + Soft Wash 柔和淡彩 + Pooling Wash 积色淡彩 Zigzag 锯齿 Burst Ray 冲击射线 Dash Trail 虚线轨迹 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index d2245176dd..4970f1d88f 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -5980,6 +5980,11 @@ axis Y Axis Y 軸 User Brush Library 9 使用者筆刷資料庫 9 User Brush Library 10 使用者筆刷資料庫 10 Art Ribbons 藝術緞帶 + Ink and Wash 水墨淡彩 + Tapered Ink 漸細墨線 + Brush Pen 軟頭筆 + Soft Wash 柔和淡彩 + Pooling Wash 積色淡彩 Zigzag 鋸齒 Burst Ray 衝擊射線 Dash Trail 虛線軌跡 diff --git a/crates/ui-egui/src/panels/tests_brush_libraries.rs b/crates/ui-egui/src/panels/tests_brush_libraries.rs index 157b58d3cb..263342741a 100644 --- a/crates/ui-egui/src/panels/tests_brush_libraries.rs +++ b/crates/ui-egui/src/panels/tests_brush_libraries.rs @@ -120,6 +120,26 @@ fn clicking_a_library_brush_adds_it_to_the_documents_brushes_in_one_step() { assert!(last_open(&app.ui).is_none()); } +#[test] +fn the_ink_and_wash_library_opens_and_a_click_adds_and_applies_its_brush() { + let mut app = app(); + let ctx = context(); + let r = app.run("window.brushLibrary", json!({"library": "ink-and-wash"})).unwrap(); + assert_eq!((r["open"].as_str(), r["count"].as_u64()), (Some("ink-and-wash"), Some(4))); + let lib = BrushLibraries::get(&app, "ink-and-wash").unwrap().1; + let rows = BrushLibraries::rows(&lib, ""); + assert_eq!(rows.iter().map(|r| r.name).collect::>(), ["Tapered Ink", "Brush Pen", "Soft Wash", "Pooling Wash"]); + let path = app.run("shape.line", json!({"x1": 10, "y1": 10, "x2": 150, "y2": 60})).unwrap()["id"].as_u64().unwrap(); + let (undo, brushes) = (undo_len(&app), count(&app)); + click(&mut app, &ctx, "Pooling Wash", Modifiers::NONE, 0.0); + assert_eq!((count(&app), undo_len(&app)), (brushes + 1, undo + 1), "one brush, one undo step"); + let n = app.session.doc().unwrap().doc.node(vectorcraft_doc::NodeId(path)).unwrap().clone(); + assert_eq!(n.appearance.stroke().and_then(|s| s.brush.clone()).as_deref(), Some("Pooling Wash")); + click(&mut app, &ctx, "Brush Pen", Modifiers::NONE, 2.0); + assert!(has(&app, "Brush Pen")); + assert_eq!(app.run("brush.list", json!({})).unwrap()["current"], "Brush Pen"); +} + #[test] fn the_window_menu_and_the_brushes_panel_open_brush_libraries() { let mut app = app(); @@ -131,6 +151,7 @@ fn the_window_menu_and_the_brushes_panel_open_brush_libraries() { opened, [ json!("art-ribbons"), + json!("ink-and-wash"), json!("calligraphic-nibs"), json!("bristle-brushes"), json!("pattern-borders"), diff --git a/docs/gaps.md b/docs/gaps.md index 8a71326662..2766e8aa2f 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -635,7 +635,7 @@ macOS cursor delivery (#2292) is narrowed to a native host adapter using the exi libraries ([public source](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/apply-and-edit-strokes/create-new-brush-libraries.html), [import](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/apply-and-edit-strokes/import-brushes-from-brush-libraries-or-other-files.html)): Window › Brush Libraries and the Brushes panel's Brush Libraries Menu open each library in a library panel of - its own (eight original libraries generated in code: Art Ribbons, Calligraphic Nibs, Bristle Brushes (one + its own (nine original libraries generated in code: Art Ribbons, Ink and Wash, Calligraphic Nibs, Bristle Brushes (one bristle brush for each tip shape), Pattern Borders, Scatter Shapes, Game Trails, Energy Beams and Dry Media (grainy chalk, pencil, crayon and charcoal art brushes); User Defined; Other Library… reads the brushes of any document VectorCraft opens); a click adds a brush to the document's Brushes with the patterns, symbols and other resources its art uses, as one undo diff --git a/docs/mcp.md b/docs/mcp.md index 62a4212472..8ff1ed185b 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -3682,7 +3682,7 @@ error. ## Brush libraries -Brush libraries are read-only sets of brushes: eight generated in code (Art Ribbons, Calligraphic Nibs, Bristle +Brush libraries are read-only sets of brushes: nine generated in code (Art Ribbons, Ink and Wash, Calligraphic Nibs, Bristle Brushes with one bristle brush for each tip shape, Pattern Borders, Scatter Shapes, Game Trails, Energy Beams, and Dry Media with the Chalk Grain, Graphite Pencil, Wax Crayon and Charcoal Dust art brushes, id `dry-media`), the `.vectorcraft` files in the user library folder of the desktop app (User Defined) and the brushes of any document loaded with `brush.loadLibrary` (a document without brushes of its own has diff --git a/docs/roadmap.md b/docs/roadmap.md index 175b532997..2360cb7bb0 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -153,7 +153,7 @@ native wording review, layout checks and a web Devanagari font remain. [Notes](l | M7 | Type (point/area/path, editing, styles, OpenType, threading, glyphs) | Spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof and the custom dictionary and Ignore All commands and Auto Spell Check with its right-click suggestions, Ignore All and Add to Dictionary, and Spelling › Auto Spell Check in the context menu of type, and the Edit Custom Dictionary dialog (#4936; the Check Spelling dialog remains); Right-aligned area type sets the text after a line's last tab against the right edge, the text before it at its stops (#6147); A line a soft return ends is justified as a wrapped line is (#6137); Indian-language pattern hyphenation preserves syllables in both composers (#4947); 🟡 Hanging punctuation keeps its published amounts under Glyph Scaling, and Standard burasagari counts a hung line start as room; Fullwidth `!` / `?` keep an upright fallback in vertical type; Hyphenation… and Justification… per paragraph (#2310, #2755), Auto Leading also driving the Character panel's Leading, the Alt+↑/↓ leading nudge and Fill with Placeholder Text; Document-point character controls after object/group scaling (#1034; optional font-size preservation remains separate), Character/Paragraph Styles (explicit Auto-leading resets and local leading overrides), per-paragraph paragraph attributes, mixed paragraph alignment indication (headless regressions, full integration CI and visual review pass), Area Type Options (with Width/Height, Auto Size and Shrink Text to Fit; overflow reported to agents and vertical Align), type areas resized by their handles and reshaped with Direct Selection, threaded text, Fit Headline, Glyphs (feature-valued alternates and unencoded glyphs applied in one Undo step while keeping Arabic joining, #5514), OpenType panel (including font-defined Stylistic Sets), Find Font (Document, Recent and System replacement sources, #5217), installed system fonts in every font menu (fonts registered with Windows outside its font folders, the XDG and Flatpak host font folders on Linux, and fonts installed while the app runs, listed when it comes back to the front; searchable; the Character and Properties menus show each family's sample of the selected text, preview it live under the pointer or ↑/↓, show recently applied families first (#3515), filter by script and kind, list a family's styles, and star favourites; the Glyphs panel and Find Font's Replace With use the same menu without the live preview) and found by name on open/import (full-name aliases and Missing Fonts searches verified, #1258; Windows font-service QA pending), variable fonts' named instances as styles, CJK names in the UI, input methods (marked text in place, candidate window at the caret; Japanese IME), Text Wrap (offset, invert, both sides of an object; follows edits), Type on a Path effects and Options with draggable start/centre/end brackets (slide, flip), tab stops + Tabs panel (Above Text floats it over the selected text, its ruler at the canvas zoom so stops line up with the columns, #729), inline symbols in text (beyond parity), Hebrew/Arabic bidirectional text, Paragraph Direction, Every-line/Single-line Composer per object (ragged text too) done; paint-only Arabic harakat keep joining with GDEF-mark ownership and Insert Kashidas tatweels on the base letter's paint (`tests::harakat`); tab leaders, spell check, vertical type pending | 45–60 | | M8 | Transform & distort (Puppet Warp, Liquify tools, Envelopes, Blends, Perspective Grid) | ✅ done (M8.1–M8.20 on 2026-10-06, fidelity pass): Liquify Tool Options with one shared brush, pen pressure, hold-to-apply, a safe scope and incremental strokes; tool options that last; Puppet Warp rotation, multi-select, Control bar and a rest-shape session that follows Undo; Width tool on compound paths; Envelope dialogs with preview, Reset with Warp/Mesh, full Envelope Options, type/images/symbols/appearance/gradients/patterns distorted on canvas and in every export, warps that keep their frame, Edit Contents, mesh handles and Control bar; Blend Options, anchor-targeted Blend tool, editable spine, keys picked on the canvas, faithful interpolation and knockout; Anchor Point spine conversion/handle editing and selection priority (#2544), with #2425 Shift reshape preserved; Perspective Grid with Define Grid, presets, lock/snap/rulers, widgets, the 1–4 plane switch, movable planes, Perspective Selection scale/copy/perpendicular/Transform Again, and type and symbols truly projected with Edit Text. Left: Anti-Alias and Preserve Shape output, warp-envelope point editing, brush interpolation in blends, Free Transform/Scale/Reflect along a rotated box | 2–4 | | M9 | Live effects (+ 3D & Materials) | 🟡 2D effects done incl. Effect → Pathfinder, Color Adjustments and Document Raster Effects Settings; PDF export warns when the pixel cap lowers raster-effect resolution; Artistic › Colored Pencil, Cutout, Dry Brush, Film Grain, Fresco, Neon Glow, Paint Daubs, Palette Knife, Poster Edges, Rough Pastels, Smudge Stick, Sponge, Underpainting and Watercolor and Sketch › Halftone Pattern, Bas Relief, Charcoal, Conté Crayon, Photocopy, Stamp, Torn Edges and Water Paper done (57/57 Photoshop filters); Revolve with automatic smooth vector shading, default-on circular caps and continuous rims, Extrude & Bevel, Inflate and Rotate with one to ten colored directional lights and four original lighting presets; SVG Filters, remaining raster effects and 3D materials and shadows pending | 81–130 | -| M10 | Brushes, symbols, patterns, Repeat | 🟡 pattern swatches (5 tile types, Pattern Options, editing mode, SVG ``/PDF export) and live Repeat (radial/grid/mirror) done; interactive selected mirror axis implemented (focused and native checks passed; full CI blocked by an unrelated text test); symbol libraries done (Window › Symbol Libraries, five generated libraries (Basic Shapes, UI Frames, Map Markers, Inventory Icons, Skill Icons), User Defined, Other Library…, Save Symbols); brush libraries done (Window › Brush Libraries, eight generated libraries including Bristle Brushes and Dry Media, User Defined, Other Library…, Save Brushes); symbol editing shows draggable 9-slice guides (#5280), with guides outside the artwork still unsupported; brushes/symbols in progress | 23–37 | +| M10 | Brushes, symbols, patterns, Repeat | 🟡 pattern swatches (5 tile types, Pattern Options, editing mode, SVG ``/PDF export) and live Repeat (radial/grid/mirror) done; interactive selected mirror axis implemented (focused and native checks passed; full CI blocked by an unrelated text test); symbol libraries done (Window › Symbol Libraries, five generated libraries (Basic Shapes, UI Frames, Map Markers, Inventory Icons, Skill Icons), User Defined, Other Library…, Save Symbols); brush libraries done (Window › Brush Libraries, nine generated libraries including Ink and Wash, Bristle Brushes and Dry Media, User Defined, Other Library…, Save Brushes); symbol editing shows draggable 9-slice guides (#5280), with guides outside the artwork still unsupported; brushes/symbols in progress | 23–37 | | M11 | Artboards & views (selected-set deletion shared by Artboards menu, trash and Properties, retaining artboard 1 when all are selected; Artboard deletion keys keep the first of an all-selected set and protect artwork; history prunes invalid indices while preserving valid multiselection (history tests and full workspace CI pass); click-to-fit objects with the Artboard tool (#1227), artboard escape routing and document setup modal handoff done; artboard panel/tool with Move Artwork for X/Y fields and Artboard Options, Trim View, middle-button pan, print tiling and the status bar's artboard navigator with synchronized Artboard tool selection done; Window › Arrange › Tile and Consolidate All Windows done; multiple windows, presentation polish) (cached canvas reprojection preserves scale during resize, rotation and DPI changes; regression-tested and contributor-confirmed 2026-10-19; estimates unchanged) (cached canvas dimensions match the renderer limit before cache-key creation, view construction and worker submission; regression-tested; estimates unchanged); status-bar pointer coordinates follow the active ruler origin and General units | 🟡; Artboard Options shares existing presets and orientation controls and can fit artwork without moving it; artwork fitting includes all drawn document art across artboards and off-board; artwork bounds are cached by active document and revision, and selection uses the shared bounds cache across artboards | 15–25 | | M12 | Advanced color & art (CMYK/ICC, separations, Gradient Mesh, Live Paint, Image Trace, Graphs) | 🟡 Gradient Mesh, Live Paint, Image Trace (junction-returning pieces retain their positioned boundaries; a simpler contour walk with identical output; an edge's sub-pixel shift is the same from either side; notches read only their own piece; collapsed loops keep few segments; large images trace with a warning; Auto Color keeps rare distinct colours; quick presets trace at once and the Image Trace button opens the adjustment controls; Properties reopens them; tiny fitted contours retain islands and holes; Create Strokes: exact crop EDT, Guo–Hall thinning and maximal-disc graph simplification; 15 presets, the 14 documented ones each with an enhanced version; Ignore Color, Shapes, Auto Grouping and Gradients options; measured in `image-trace-quality.md`; closed outlines retain their interior after straightening; the Flat Logo mode places edges to a fraction of a pixel, restores sharp corners, snaps lines and circles, and refuses gradients; live trace result bounds and selection highlights exclude the retained bitmap except in source-image views), Recolor Artwork, colour management (ICC, soft proofing, separations preview), Graphs (all 9 tools, Graph Data/Type, group-selected series colours persist through regenerate/save/open) done; Graph Design and Marker designs, Transpose and Switch x/y, `\|` label line breaks, the pie options, drop shadows, the legend across the top, overlapping widths, First Row/Column in Front, filled lines, kept label formatting and the Graph Data worksheet done; graph Column designs pending; Vector Halftone Preview keeps the selection box without drawing dot selection marks (#6369); Live Paint Bucket fills faces crossed in one drag (#6373) | 6–10 | | M13 | Automation (Actions ✅ record/playback, persisted; variables, scripting, batch) | 🟡 MCP `add_text` exposes existing vertical point, area and path type; `script.run` and File › Scripts › Other Script… run JavaScript files, and Window › Script Console shows their output; the script document model and batch work remain | 18–27 | diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 0f942f7ddb..8cdb182a3f 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -224,7 +224,7 @@ dominate; 3D, graphs and automation are specialist). They sum to 105. Feature de | Drawing tools (Pen, Curvature, Pencil, shapes, Shaper) | 7 | 85% | Shaper construction widget, Pencil/Pen nuances, Blob Brush options, Touch Type, Dimension tool; see [ui-parity.md](ui-parity.md) | 8–13 | estimated; Curvature point display regression-tested (#1236, in part); Polygon/Star exact-size count persistence regression-covered; Live Corners' Relative rounding done (#3091), score unchanged; Copy and Cut with the Pen or Curvature tool take the anchors a Cmd/Ctrl-lent Direct Selection picked (#6123), score and hours unchanged | | Path operations, Pathfinder, Shape Builder, Live Paint | 5 | 80% | Shape Builder on the reporter's file (#937: art past 4,096 segments is cut by lines again; unconfirmed on the file itself); gap detection shares Live Paint’s finder, with a 72 pt custom cap and the existing options; Live Paint Gap Options | 4–8 | estimated; Shape Builder Gap Detection shares Live Paint’s spatial gap finder and temporary merge bridges (#2526); Custom is capped at 72 pt in the existing options dialog (lowered from 86%: open issues); compound attributes regression-tested (#1197); single-result source names survive SVG export/import (#1293), with [SVG ID semantics](https://www.w3.org/TR/SVG2/struct.html#IDAttribute) and original-shape tests; Simplify component selection and preview regression-tested (#1272); Simplify of a Direct Selection part of a path regression-tested (`tests/simplify_part.rs`); Divide open-cutting-path geometry and appearance regression-tested (#1307); the Shape Builder's edge-source grid is bounded in memory and files a segment once per cell (`planar.rs` test `rect_index_holds_a_bounded_number_of_references`); live compound shapes (Make, Alt-click, member Shape Modes, Release, Expand, saved and exported) regression-tested (#4522); Live Paint Bucket face drags, including narrow faces and one Undo, regression-tested (#6373) | | Colour, swatches, gradients, patterns, mesh, recolor | 7 | 92% | Eyedropper copies effective character dimensions across text transforms (#6404); 30.0's gradient additions are done (dither, perceptual blending, presets); freeform gradients in SVG/PDF, mixed spot/process gradients in SVG, PDF gradients mixing a spot color with RGB or Lab stops, and dithered spot gradient fills and Perceptual spot gradients with RGB or Lab stops other than white or more than 4,096 stops on their curve, which PDF export writes as images in process colors| 4–7 | estimated; Recolor model selection is available through the engine and MCP (#2520), and the dialog's color sliders (HSB, RGB, CMYK, Lab, Tint for a spot color) edit the selected row's new color in its mode, and the Edit tab's Global Adjust sliders (Saturation, Brightness, Temperature, Luminosity, -100% to 100%) change every recolored color at once (lowered from 96%: target moved); unpainted zero-width stroke restoration fixed (#1253), score and hours unchanged; Eyedropper source reads regression-tested (#1251); screen-bounded Recolor Edit chip list (#3416), score and hours unchanged | -| Strokes, brushes, width profiles | 5 | 88% | Shift-double-click adjoining adjustment is implemented; brush libraries are done (eight generated libraries including Bristle Brushes, Game Trails, Energy Beams and Dry Media, User Defined, Other Library…, Save Brushes); Art brush Overlap and Pattern brush auto corners are done; remaining: pen pressure on macOS, Stylus Wheel, more built-in brush libraries, tilt, bearing and rotation for Art, Pattern and Bristle brushes and on native Wayland and the web | 8–15 | estimated; width-point creation and adjoining adjustment regression-tested; dialog layout reviewed; evidence below; finite-arithmetic follow-up regressions and full CI passed (evidence below) | +| Strokes, brushes, width profiles | 5 | 88% | Shift-double-click adjoining adjustment is implemented; brush libraries are done (nine generated libraries including Ink and Wash, Bristle Brushes, Game Trails, Energy Beams and Dry Media, User Defined, Other Library…, Save Brushes); Art brush Overlap and Pattern brush auto corners are done; remaining: pen pressure on macOS, Stylus Wheel, more built-in brush libraries, tilt, bearing and rotation for Art, Pattern and Bristle brushes and on native Wayland and the web | 8–15 | estimated; width-point creation and adjoining adjustment regression-tested; dialog layout reviewed; evidence below; finite-arithmetic follow-up regressions and full CI passed (evidence below) | | Appearance, transparency, graphic styles, masks | 5 | 97% | raster effect reach in preview bounds | 1–2 | estimated; Transform typing, sliders, preview, and undo are covered in `crates/ui-egui/src/panels/tests_appearance.rs` | | 3D and Materials (and Turntable) | 4 | ~25% | live Revolve with automatic smooth vector shading and default-on continuous circular caps, Extrude & Bevel (twist, taper, caps, five bevel shapes), Inflate and Rotate with Position presets (colored Inflate uses smooth membrane lighting; white-light Inflate retains its existing shading; Extrude & Bevel and Rotate retain planar shading; one to ten colored lights, shared neutral ambient, painter visibility); materials, light presets, shadows, ray tracing, the rest of 3D Classic (Rotate, Map Art), Turntable views, textures, map art and shading in 3D export | 30–55 | estimated (Extrude & Bevel, Inflate and Rotate added 2026-10-12) | | Type core | 9 | 80% | Auto-leading style resets are implemented; Hyphenation… and Justification… options are done (#2310, #2755), and a paragraph's Auto Leading also sets the Character panel's Leading, the Alt+↑/↓ leading nudge and Fill with Placeholder Text; remaining: Optical Margin Alignment, Middle Eastern features beyond bidi layout, optional group font-size preservation; transformed character controls fixed (#1034), see [type-parity.md](type-parity.md) | 14–19 | estimated; eleven-language pattern hyphenation in both composers, with grapheme and shaping-cluster protection (#4947, `hyphen::tests` and `layout::wrapping_tests`); Sanskrit patterns pending an allowlisted source; recent-font groups in font pickers (#3515); full-name font matches regression-tested (#1258); mixed paragraph alignment indication implemented; headless regressions, full integration CI and visual review pass; localized legacy family-name fallback with loaded/catalog regressions (#2208); Fractional Widths per type object regression-tested; color emoji fonts with bitmap glyphs (`sbix`, `CBDT`) draw and export; threading ports: every selected area type and type on a path shows its in and out ports (empty, arrow, red "+"), and the Selection tool threads from either, the in port into a frame before it, and a double-click on a threaded port breaks the thread there (`threads::tests::break_splits_the_thread_and_the_story_flows_into_the_first_part`, `select::tests::a_double_click_on_a_threaded_port_breaks_the_thread`, `canvas::tests::a_double_click_on_a_threaded_out_port_breaks_the_thread`) (#2572; `select::tests::a_click_on_the_in_port_threads_into_a_frame_before_it`, `text::tests::selected_type_shows_its_ports_in_each_state`, `threads::tests::add_before_threads_the_story_from_the_new_frame_into_the_clicked_one`, live screenshot); the Glyphs panel highlights and scrolls to the character selected with the Type tool (#4908, `panels::glyphs` tests); the OpenType panel lists only `ss01`–`ss20` sets present in the selected font and uses its custom names when supplied; Type-range desktop Copy publishes styled art and keeps font family, size and color on canvas Paste after editing (#5636); paint-only Arabic harakat keep joining with conditional GDEF-mark ownership and Insert Kashidas base-letter paint (`tests::harakat`) | @@ -493,7 +493,7 @@ about 1–1.5 agent-hours each, so the other rows (estimated on the older scale) | Drawing tools | 7 | 86% | rough-square Shaper recognition and orientation steps corrected; editable Shaper Groups landed; Pencil: Alt key toggles to Smooth Tool (#2001); remaining: Shaper construction widget/gesture refinements, pencil and the remaining pen modifier nuances, Blob Brush Merge Only with Selection, Touch Type, Live Corners' Relative rounding | 7–12 | | Path operations, Pathfinder, Shape Builder, Live Paint | 5 | 86% | Live Paint gap options, Shape Builder gap detection, options (Tool Options dialog, clicking a stroke in merge mode) and Shift-drag marquee | 3–6 | | Colour, swatches, gradients, patterns, mesh, recolor | 7 | 96% | freeform gradients in SVG/PDF and mixed spot/process gradients in SVG (exported as stops; PDF export keeps the spot inks in a DeviceN shading, except next to RGB or Lab stops; dithered fills and Perceptual gradients with RGB or Lab stops other than white or more than 4,096 stops on their curve are written as images in process colors), pattern fills in Expand | 2–4; Eyedropper source reads regression-tested (#1251) | -| Strokes, brushes, width profiles | 5 | 90% | more built-in brush libraries (eight generated in code including Bristle Brushes, Game Trails, Energy Beams and Dry Media are done; Art brush Overlap and Pattern brush auto corners are done), pen pressure on macOS (the windowing layer reports none there), Stylus Wheel, tilt, bearing and rotation for Art, Pattern and Bristle brushes and on native Wayland and the web | 8–15 | +| Strokes, brushes, width profiles | 5 | 90% | more built-in brush libraries (nine generated in code including Ink and Wash, Bristle Brushes, Game Trails, Energy Beams and Dry Media are done; Art brush Overlap and Pattern brush auto corners are done), pen pressure on macOS (the windowing layer reports none there), Stylus Wheel, tilt, bearing and rotation for Art, Pattern and Bristle brushes and on native Wayland and the web | 8–15 | | Appearance, transparency, graphic styles, masks | 5 | 98% | raster effect reach in preview bounds (flattener presets apply in PDF, EPS and print) | 1–2 | | Live vector effects | 5 | 85% | Outline Object, Pathfinder Hard/Soft Mix and Trap (since done: #3061, #3063, #3785), SVG Filters, Transform Patterns and Scale Strokes & Effects on a fill's or stroke's own Transform effect | 6–10 | | 3D and Materials | 4 | ~25% | Initial Revolve, Extrude & Bevel, Inflate and Rotate, with simple lighting; materials, light presets, shadows, ray tracing, Rotate (Classic), Map Art and more complete visibility remain | 25–45 | @@ -545,7 +545,7 @@ since 2026-10-01. | **Interaction fidelity:** modifiers, cursors, small behaviours | ~30–40% | The dedicated pass hasn't started, and there has been no side-by-side session with Illustrator yet. A power user notices this first | | **Look & feel** | ~75–80% | Measured against Illustrator 2026 screenshots (2026-10-02); the panels added since haven't been re-measured | | **File interop** | ~85% | SVG/SVGZ, PDF and PDF-compatible `.ai` (and legacy version 7 `.ai` out, keeping layers, groups and type as text), EPS, DXF, EMF/WMF, raster formats, PSD export and PSD/PSB placement (merged image), Place and Links, Package, Print, clipboard flavours. Affinity documents (Affinity 1–3) open natively as editable art, checked against 51 pinned public files (33 current `.af`), with selected saved-thumbnail comparisons and every-board native/SVG/PDF/PSD checks for current files; Affinity export is missing and effects, adjustments and brushes don't import. Affinity isn't an Illustrator feature, so this doesn't raise the parity score. The editing copy inside an Illustrator EPS or `.ai` is read for its structure: layers, groups, names, hidden objects and type, colours, gradients, transparency, images and every artboard (see Out of scope for what it doesn't read yet); DWG has no open spec | -| **Bundled content:** brush, symbol, style and swatch libraries | ~46% | Ours are original and generated in code, and far fewer than Illustrator ships; symbol libraries have five (Basic Shapes, UI Frames with default 9-slice, Map Markers, Inventory Icons, Skill Icons), graphic style libraries eight (including Game HUD and Status Effects) and brush libraries eight (Art Ribbons, Calligraphic Nibs, Bristle Brushes, Pattern Borders, Scatter Shapes, Game Trails, Energy Beams, Dry Media) beside User Defined and Other Library… | +| **Bundled content:** brush, symbol, style and swatch libraries | ~46% | Ours are original and generated in code, and far fewer than Illustrator ships; symbol libraries have five (Basic Shapes, UI Frames with default 9-slice, Map Markers, Inventory Icons, Skill Icons), graphic style libraries eight (including Game HUD and Status Effects) and brush libraries nine (Art Ribbons, Ink and Wash, Calligraphic Nibs, Bristle Brushes, Pattern Borders, Scatter Shapes, Game Trails, Energy Beams, Dry Media) beside User Defined and Other Library… | | **Performance** | unverified | Multithreaded, off-thread rendering and caches are in place. The last budget run (2026-10-01, loaded machine) measured 290 ms for a 50k-path fit against a 16 ms budget. Re-run `vectorcraft-cli perf` on an idle machine | | **Robustness** | good, new | ~2,840 tests, property tests, no panics in shipped code (lints, the `guard` safety net, import fuzzing), Data Recovery. Missing: a corpus of real-world files, Windows/Linux/browser QA | | **Platforms and 1.0 polish** | ~60% | The macOS app and the web build work, and Finder opens every readable format with the macOS app (#295, #354); on macOS the menus are in the system menu bar, laid out as a Mac app's, with the in-window menus as a fallback (`--in-window-menus`; verified on Windows and by cross-compilation, not yet by hand on a Mac); hybrid-graphics laptops render on the power-saving GPU by default on Windows and macOS (#306), and Windows starts on Direct3D 12, falling back to OpenGL, and loads no Vulkan driver unless `WGPU_BACKEND` asks for it (#545: Vulkan flickered black on an Intel GPU, DX12 and OpenGL didn't; #806: a faulty Vulkan driver crashed the app before its window appeared; verified by unit tests and the reporter's runs with `WGPU_BACKEND`), and Linux on the GPU the desktop runs on, since a Wayland compositor may refuse frames from the other one; a window that fails on its GPU while starting up starts again on the next one instead of crashing (#502; the dual-GPU Wayland case is verified by unit tests and cross-compilation, not on such hardware); web drops land in the document they were dropped on (#359); desktop drops carry no pointer position (winit 0.30), so a dropped document opens as a tab of its own and a picture or text is placed in the middle of the view, while the web places anything dropped on the canvas and opens what is dropped off it, as Illustrator does (#472); a web page that loses its WebGPU device or WebGL context restarts its graphics with the same documents (#369); on Linux/Wayland the clipboard reads the compositor's own (data-control) and copied image files paste, but files can't be dropped on the window under native Wayland until eframe moves to winit 0.31 (#398; the Wayland side is verified by compilation only), so Preferences › Performance › Linux Display Server › X11 (XWayland) opens the window through XWayland, where drops work (#1151; verified by unit tests and cross-compilation, not on a Wayland desktop); on Linux the Paste menu checks the system clipboard on a background thread, so a clipboard owner that never answers no longer freezes the window, while Windows and macOS keep their quick in-line check (#510; the X11 hang verified by the contributor, here by headless tests); on KDE Plasma 6.3+ under Wayland, pens and pen displays reach the app only under XWayland (`WAYLAND_DISPLAY=`) or with KWin's `KWIN_WAYLAND_EMULATE_TABLET=1`, since winit 0.30 has no Wayland tablet input (#491, not verified on hardware); Windows/Linux packaging and accessibility are pending | @@ -863,7 +863,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - **Expand of symbol instances:** Object › Expand turns selected symbol instances (without effects, not in perspective, in groups or placed documents too) into their art as Break Link to Symbol places it, one level deep, as one undo step; the Expand dialog's Fill and Stroke reach that art (`object.expand`, `crates/engine/src/tests/expand_symbols.rs`; estimated, score unchanged). - **Reset Transformation** ([Transform symbols](https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/transform-symbols.html), [Symbols panel options](https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/symbols-panel-options.html)): the Symbols panel menu's Reset Transformation and the Control bar's Reset, shown while symbol instances are selected, give the selected instances their symbol's own scale again, without rotation or shear, as one undo step (`symbol.instance.resetTransform`); an instance in perspective is left as it is. VectorCraft's own rules: each instance keeps the center of its bounding box in place, reflection is removed too, and the menu item and the button are enabled only while a selected instance would change. Tests: `tests::brushsym::reset_transformation_restores_the_symbols_scale_and_keeps_the_center`, `reset_transformation_keeps_the_center_of_art_away_from_the_origin`, `reset_transformation_ignores_a_stored_size_that_is_not_positive`, `reset_transformation_leaves_unchanged_instances_and_refuses_bad_ids`, `panels::symbols::tests::reset_transformation_is_enabled_for_a_transformed_instance_and_resets_it`. - **Expand of dimensions:** Object › Expand, Expand Appearance, Create Outlines and Outline Stroke turn a dimension whose art they change into a plain group of that art, which stays as it is through later moves, scales and edits of what it measured; undo gives the live dimension back (`crates/engine/tests/it/dimensions.rs`; VectorCraft's own rule; estimated, score unchanged). - - **Brush libraries** ([public source](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/apply-and-edit-strokes/create-new-brush-libraries.html), [import](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/apply-and-edit-strokes/import-brushes-from-brush-libraries-or-other-files.html), [Brushes panel](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/apply-and-edit-strokes/brushes-panel-overview.html)): Window › Brush Libraries, the Brushes panel's Brush Libraries Menu and its menu's Open Brush Library open each library in a library panel of its own, as swatch, graphic style and symbol libraries do. The built-in libraries (Art Ribbons, Calligraphic Nibs, Bristle Brushes, Pattern Borders, Scatter Shapes, Game Trails, Energy Beams, Dry Media) are original brushes generated in code (`crates/engine/src/cmd/brushlib.rs`); Striped Ribbon's art is filled with the library's own pattern and Sparkles scatters the library's own symbol; Game Trails adds motion art brushes (Speed Line, Dash Trail, Burst Ray, Zigzag); Energy Beams adds laser and plasma art brushes (Laser Bolt, Plasma Arc, Charge Wave, Spark Trail). A click on a brush adds it to the document's Brushes as one undo step with every resource its art uses (patterns, symbols, swatches, graphic styles, images), makes it the current brush and, with paths selected, applies it to them in the same step; a brush the document already has is not added again and a different brush of the same name comes in numbered ("Wave 2"). Shift/Cmd-clicks select brushes for Add to Brushes; a brush dragged onto a path is added and applied to it, and one dragged onto the Brushes panel is added. Save Brushes… / the Brushes panel's Save Brush Library… write the document's brushes (or the selected ones) as a `.vectorcraft` library into the `Brushes` folder next to the preferences, listed under User Defined, or to a file; Other Library… reads the brushes of any document VectorCraft opens. Thumbnails, in the library panels, the Brushes panel and Brush Options, render brush art with the patterns and symbols it uses. Copying brushed art now carries the patterns and symbols its brush's art uses too. Tests: `crates/engine/src/tests/brushlib.rs`, `crates/ui-egui/src/panels/tests_brush_libraries.rs`, `panels::brushes::tests::library_brush_previews_show_the_patterns_their_art_uses`, `canvas::tests::a_library_brush_dropped_on_a_path_is_added_and_applied_in_one_step`. + - **Brush libraries** ([public source](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/apply-and-edit-strokes/create-new-brush-libraries.html), [import](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/apply-and-edit-strokes/import-brushes-from-brush-libraries-or-other-files.html), [Brushes panel](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/apply-and-edit-strokes/brushes-panel-overview.html)): Window › Brush Libraries, the Brushes panel's Brush Libraries Menu and its menu's Open Brush Library open each library in a library panel of its own, as swatch, graphic style and symbol libraries do. The built-in libraries (Art Ribbons, Ink and Wash, Calligraphic Nibs, Bristle Brushes, Pattern Borders, Scatter Shapes, Game Trails, Energy Beams, Dry Media) are original brushes generated in code (`crates/engine/src/cmd/brushlib.rs`); Striped Ribbon's art is filled with the library's own pattern and Sparkles scatters the library's own symbol; Ink and Wash adds ink and wash brushes (the [help page](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/apply-and-edit-strokes/about-brushes.html) lists an Ink brush library of strokes with varied thickness and texture; the names, shapes and opacities are VectorCraft's own): Tapered Ink, an art brush broad near its start that thins to a fine tip; Brush Pen, a calligraphic brush whose size follows the pen pressure; and Soft Wash and Pooling Wash, art brushes of translucent gray layers that take the stroke color as tints; Game Trails adds motion art brushes (Speed Line, Dash Trail, Burst Ray, Zigzag); Energy Beams adds laser and plasma art brushes (Laser Bolt, Plasma Arc, Charge Wave, Spark Trail); Dry Media adds grainy art brushes (Chalk Grain, Graphite Pencil, Wax Crayon, Charcoal Dust). A click on a brush adds it to the document's Brushes as one undo step with every resource its art uses (patterns, symbols, swatches, graphic styles, images), makes it the current brush and, with paths selected, applies it to them in the same step; a brush the document already has is not added again and a different brush of the same name comes in numbered ("Wave 2"). Shift/Cmd-clicks select brushes for Add to Brushes; a brush dragged onto a path is added and applied to it, and one dragged onto the Brushes panel is added. Save Brushes… / the Brushes panel's Save Brush Library… write the document's brushes (or the selected ones) as a `.vectorcraft` library into the `Brushes` folder next to the preferences, listed under User Defined, or to a file; Other Library… reads the brushes of any document VectorCraft opens. Thumbnails, in the library panels, the Brushes panel and Brush Options, render brush art with the patterns and symbols it uses. Copying brushed art now carries the patterns and symbols its brush's art uses too. Tests: `crates/engine/src/tests/brushlib.rs`, `crates/ui-egui/src/panels/tests_brush_libraries.rs`, `panels::brushes::tests::library_brush_previews_show_the_patterns_their_art_uses`, `canvas::tests::a_library_brush_dropped_on_a_path_is_added_and_applied_in_one_step`. - **Bristle Brushes library** ([What's new](https://helpx.adobe.com/illustrator/desktop/new-features/whats-new.html), [About brushes](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/apply-and-edit-strokes/about-brushes.html)): Window › Brush Libraries › Bristle Brushes holds ten bristle brushes, one for each tip shape (Fine Liner, Pointed Flat, Stubby Round, Dry Scrub, Soft Dome, Filbert, Angled Round, Angled Flat, Fan Blender, Grass Fan). The brushes, their names and their values are VectorCraft's own, and every value lies inside the range the bristle brush keeps (`brush.libraries`, `brush.addFromLibrary`; `the_bristle_brushes_library_covers_every_tip_shape_within_the_bristle_ranges`). - **Dry Media library** ([help page](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/apply-and-edit-strokes/about-brushes.html)): Window › Brush Libraries › Dry Media holds four art brushes with textured strokes like dry drawing materials, each 100 pt long: Chalk Grain (about 8 pt high), Graphite Pencil (about 2.3 pt), Wax Crayon (about 5.5 pt) and Charcoal Dust (about 7 pt). The brushes, their names, sizes and art are VectorCraft's own: each is filled vector paths from a fixed seed, at most 80 paths and 500 anchors, stretched along the path with Tints colorization, and the chalk and crayon art has even-odd gaps where the paper shows (`brush.libraries`, `brush.addFromLibrary`; `the_dry_media_library_holds_grainy_art_brushes_with_bounded_art`). - A canvas context menu: right-click selects the object under the pointer and lists what applies to the selection (Undo/Redo, clipboard, group, isolation, join, Remove Anchor Points when anchors are selected, masks, compound paths, guides, Transform, Arrange, Select, Export Selection, and on type Bullets and Numbering and Spelling › Auto Spell Check, checked while it is on), or the view commands on empty canvas.