mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-15 15:54:53 +00:00
Merge pull request #6820 from dotCooCoo/feat/brushes-ink-wash-library
Brushes: built-in Ink and Wash library with tapered ink, brush pen and wash brushes
This commit is contained in:
@@ -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::<Vec<_>>(),
|
||||
["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::<Vec<_>>(),
|
||||
["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::<Vec<_>>(),
|
||||
[("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::BrushLibrary> {
|
||||
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<f32> = 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::<Vec<_>>())
|
||||
.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;
|
||||
|
||||
Reference in New Issue
Block a user