mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-15 21:57:04 +00:00
Merge pull request #7875 from dotCooCoo/fix/live-paint-bucket-click-history
Live Paint: Bucket double-click and triple-click match the paint from before a drag, a view-tool press or a joined edge
This commit is contained in:
@@ -126,7 +126,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Live Paint Bucket (Stroke)",
|
||||
[],
|
||||
None,
|
||||
"{group: id, point: [x,y], color?|swatch?|none? (default: current stroke), width?: pt (default: current weight), tolerance?: pt (4), scope?: \"one\" (default: the edge alone) | \"connected\" (a double-click: also each edge joined to it, end to end, through edges that draw with stroke paint match) | \"samePaint\" (a triple-click: also every edge of the group that draws with stroke paint match), match?: paint (as the document stores it; scope connected or samePaint only; default: the stroke paint the edge draws with; an edge draws with the paint of its topmost stroke that is shown, with paint, above 0 pt and not fully transparent, and with none when the edge is fully transparent or has no such stroke), tint?: 0..100, keepModel?: bool (both as paint.setFill)} paint the edge nearest point, skipping hidden edges: each edge reached has its strokes replaced by one stroke with the paint and width, shown, fully opaque and blending normally, in the place of its topmost stroke and with the dashes, caps and arrowheads of its topmost shown stroke (a new stroke above the fills when it has none; none leaves one stroke of none), and its fills stay as they are; when every edge it reaches already has only that one stroke (at any width for none), nothing changes (no undo step, and the recent colors stay as they are) → {group, edge, count (edges reached)}",
|
||||
"{group: id, point: [x,y], color?|swatch?|none? (default: current stroke), width?: pt (default: current weight), tolerance?: pt (4), scope?: \"one\" (default: the edge alone) | \"connected\" (a double-click: also each edge joined to it, end to end, through edges that draw with stroke paint match) | \"samePaint\" (a triple-click: also every edge of the group that draws with stroke paint match), match?: paint (as the document stores it; scope connected or samePaint only; default: the stroke paint the edge draws with; an edge draws with the paint of its topmost stroke that is shown, with paint, above 0 pt and not fully transparent, and with none when the edge is fully transparent or has no such stroke), through?: [id…] (scope connected only; at most 4096 edges counted as drawing with stroke paint match whatever they draw with, such as the edge a double-click's first click painted), tint?: 0..100, keepModel?: bool (both as paint.setFill)} paint the edge nearest point, skipping hidden edges: each edge reached has its strokes replaced by one stroke with the paint and width, shown, fully opaque and blending normally, in the place of its topmost stroke and with the dashes, caps and arrowheads of its topmost shown stroke (a new stroke above the fills when it has none; none leaves one stroke of none), and its fills stay as they are; when every edge it reaches already has only that one stroke (at any width for none), nothing changes (no undo step, and the recent colors stay as they are) → {group, edge, count (edges reached)}",
|
||||
has_doc,
|
||||
lp_stroke_edge
|
||||
),
|
||||
|
||||
@@ -567,6 +567,16 @@ pub(super) fn lp_stroke_edge(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let tol = number_or(p, "tolerance", 4.0, C)?.clamp(0.0, 1000.0);
|
||||
let scope = scope_param(p, C)?;
|
||||
let like = match_param(p, C, scope != LpScope::One, "\"connected\" or \"samePaint\"")?;
|
||||
let through: Vec<NodeId> = match p.get("through") {
|
||||
None => vec![],
|
||||
Some(_) if scope != LpScope::Connected => return Err(bad(C, "through applies to scope \"connected\"")),
|
||||
Some(v) => {
|
||||
let ids = v.as_array().filter(|ids| ids.len() <= 4096).ok_or_else(|| bad(C, "through must be an array of at most 4096 edge ids"))?;
|
||||
ids.iter()
|
||||
.map(|id| id.as_u64().map(NodeId).ok_or_else(|| bad(C, "through must be an array of at most 4096 edge ids")))
|
||||
.collect::<Result<_>>()?
|
||||
}
|
||||
};
|
||||
let (eid, count, changed) = {
|
||||
let d = &s.doc()?.doc;
|
||||
let n = d.node(g).filter(|n| is_live_paint(n)).ok_or_else(|| bad(C, format!("{g} is not a Live Paint group")))?;
|
||||
@@ -575,7 +585,7 @@ pub(super) fn lp_stroke_edge(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let like = like.unwrap_or_else(|| b::lp_stroke_paint(start));
|
||||
let targets = match scope {
|
||||
LpScope::One => vec![eid],
|
||||
LpScope::Connected => b::connected_edges(n, eid, &like),
|
||||
LpScope::Connected => b::connected_edges_through(n, eid, &like, &through),
|
||||
LpScope::SamePaint => std::iter::once(eid)
|
||||
.chain(b::edges(n).into_iter().filter(|e| e.visible && e.id != eid && b::lp_stroke_paint(e) == like).map(|e| e.id))
|
||||
.collect(),
|
||||
|
||||
@@ -3908,6 +3908,266 @@ fn live_paint_bucket_triple_click_fills_every_face_of_the_same_fill() {
|
||||
assert_eq!([a, b, c, d].map(|f| fill_of(&s, f)), [green.clone(), green, red.clone(), red]);
|
||||
}
|
||||
|
||||
/// A triple-click whose first click is released across an edge, in the face it then clicks twice,
|
||||
/// matches that face's fill from before the first click: the release painted it too.
|
||||
#[test]
|
||||
fn live_paint_bucket_triple_click_after_a_release_across_an_edge_matches_the_fill_before() {
|
||||
let mut s = session();
|
||||
let g = live_squares(&mut s, &[0.0, 50.0, 100.0, 150.0]);
|
||||
let [a, b, c, d] = faces_at(&s, g, &[(25.0, 25.0), (75.0, 25.0), (125.0, 25.0), (175.0, 25.0)])[..] else { panic!("four faces") };
|
||||
let (red, blue) = (Paint::solid(Color::rgb(1.0, 0.0, 0.0)), Paint::solid(Color::rgb(0.0, 0.0, 1.0)));
|
||||
let green = Paint::solid(Color::from_hex("#00ff00").unwrap());
|
||||
let all = edges_in(&s, g, Rect::new(-1.0, -1.0, 201.0, 51.0));
|
||||
paint_parts(&mut s, &all, &Paint::solid(Color::rgb(0.0, 0.0, 0.0)), &[(a, red.clone()), (b, red.clone()), (c, blue.clone()), (d, red.clone())]);
|
||||
let v = ViewInfo::default();
|
||||
s.select_tool("livePaintBucket", v).unwrap();
|
||||
s.paint.fill = green.clone();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Down, 45.0, 25.0), v).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Up, 55.0, 25.0), v).unwrap();
|
||||
assert_eq!([a, b].map(|f| fill_of(&s, f)), [green.clone(), green.clone()], "the release across x = 50 fills both faces");
|
||||
for kind in [PointerKind::Down, PointerKind::Up, PointerKind::DoubleClick, PointerKind::Down, PointerKind::Up, PointerKind::TripleClick] {
|
||||
s.pointer(&PointerEvent::new(kind, 55.0, 25.0), v).unwrap();
|
||||
}
|
||||
assert_eq!([a, b, c, d].map(|f| fill_of(&s, f)), [green.clone(), green.clone(), blue, green]);
|
||||
}
|
||||
|
||||
/// A triple-click whose third press lands in another face and is released in the face its first
|
||||
/// two clicks painted still matches that face's fill from before the first click.
|
||||
#[test]
|
||||
fn live_paint_bucket_triple_click_released_from_another_face_matches_the_fill_before() {
|
||||
let mut s = session();
|
||||
let g = live_squares(&mut s, &[0.0, 50.0, 100.0, 150.0]);
|
||||
let [a, b, c, d] = faces_at(&s, g, &[(25.0, 25.0), (75.0, 25.0), (125.0, 25.0), (175.0, 25.0)])[..] else { panic!("four faces") };
|
||||
let (red, blue) = (Paint::solid(Color::rgb(1.0, 0.0, 0.0)), Paint::solid(Color::rgb(0.0, 0.0, 1.0)));
|
||||
let green = Paint::solid(Color::from_hex("#00ff00").unwrap());
|
||||
let all = edges_in(&s, g, Rect::new(-1.0, -1.0, 201.0, 51.0));
|
||||
paint_parts(&mut s, &all, &Paint::solid(Color::rgb(0.0, 0.0, 0.0)), &[(a, red.clone()), (b, red.clone()), (c, blue.clone()), (d, red.clone())]);
|
||||
let v = ViewInfo::default();
|
||||
s.select_tool("livePaintBucket", v).unwrap();
|
||||
s.paint.fill = green.clone();
|
||||
for kind in [PointerKind::Down, PointerKind::Up, PointerKind::Down, PointerKind::Up, PointerKind::DoubleClick] {
|
||||
s.pointer(&PointerEvent::new(kind, 55.0, 25.0), v).unwrap();
|
||||
}
|
||||
s.pointer(&PointerEvent::new(PointerKind::Down, 45.0, 25.0), v).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Up, 55.0, 25.0), v).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::TripleClick, 55.0, 25.0), v).unwrap();
|
||||
assert_eq!([a, b, c, d].map(|f| fill_of(&s, f)), [green.clone(), green.clone(), blue, green]);
|
||||
}
|
||||
|
||||
/// A triple-click whose first click makes the Live Paint group and is released across an edge, in
|
||||
/// the face it then clicks twice, matches the fill that face was made with.
|
||||
#[test]
|
||||
fn live_paint_bucket_triple_click_after_making_the_group_across_an_edge_matches_the_made_fill() {
|
||||
let mut s = session();
|
||||
let ids: Vec<NodeId> = [0.0, 50.0, 200.0, 300.0].iter().map(|&x| rect(&mut s, x, 0.0, 50.0, 50.0)).collect();
|
||||
let (red, blue) = (Color::rgb(1.0, 0.0, 0.0), Color::rgb(0.0, 0.0, 1.0));
|
||||
for (id, c) in ids.iter().zip([red, red, blue, red]) {
|
||||
set_fill(&mut s, *id, c);
|
||||
}
|
||||
s.execute("select.set", &json!({"ids": ids.iter().map(|i| i.0).collect::<Vec<_>>()})).unwrap();
|
||||
let green = Paint::solid(Color::from_hex("#00ff00").unwrap());
|
||||
let v = ViewInfo::default();
|
||||
s.select_tool("livePaintBucket", v).unwrap();
|
||||
s.paint.fill = green.clone();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Down, 45.0, 25.0), v).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Up, 55.0, 25.0), v).unwrap();
|
||||
let g = vectorcraft_tools::builder::live_paint_at(&s.doc().unwrap().doc, Point::new(25.0, 25.0)).expect("the click made a group");
|
||||
let faces = faces_at(&s, g, &[(25.0, 25.0), (75.0, 25.0), (225.0, 25.0), (325.0, 25.0)]);
|
||||
assert_eq!(faces[..2].iter().map(|f| fill_of(&s, *f)).collect::<Vec<_>>(), [green.clone(), green.clone()], "the release fills both faces");
|
||||
for kind in [PointerKind::Down, PointerKind::Up, PointerKind::DoubleClick, PointerKind::Down, PointerKind::Up, PointerKind::TripleClick] {
|
||||
s.pointer(&PointerEvent::new(kind, 55.0, 25.0), v).unwrap();
|
||||
}
|
||||
assert_eq!(faces.iter().map(|f| fill_of(&s, *f)).collect::<Vec<_>>(), [green.clone(), green.clone(), Paint::solid(blue), green]);
|
||||
}
|
||||
|
||||
/// When the first click makes the Live Paint group and is released in a face covered by a source
|
||||
/// path with no fill in front of a filled one, the face was made with the filled source's fill, as
|
||||
/// Make gives it, so the triple-click there matches that fill and not none.
|
||||
#[test]
|
||||
fn live_paint_bucket_triple_click_after_making_the_group_under_an_unfilled_path_matches_the_made_fill() {
|
||||
let mut s = session();
|
||||
let red = Color::rgb(1.0, 0.0, 0.0);
|
||||
let (a, b, d) = (rect(&mut s, 0.0, 0.0, 50.0, 50.0), rect(&mut s, 50.0, 0.0, 50.0, 50.0), rect(&mut s, 200.0, 0.0, 50.0, 50.0));
|
||||
for id in [a, b, d] {
|
||||
set_fill(&mut s, id, red);
|
||||
}
|
||||
let (c, e) = (rect(&mut s, 60.0, 10.0, 30.0, 30.0), rect(&mut s, 300.0, 0.0, 50.0, 50.0));
|
||||
s.edit("t", |doc, _| {
|
||||
for id in [c, e] {
|
||||
doc.node_mut(id).unwrap().appearance.set_fill(Paint::None);
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
s.execute("select.set", &json!({"ids": [a.0, b.0, d.0, c.0, e.0]})).unwrap();
|
||||
let green = Paint::solid(Color::from_hex("#00ff00").unwrap());
|
||||
let v = ViewInfo::default();
|
||||
s.select_tool("livePaintBucket", v).unwrap();
|
||||
s.paint.fill = green.clone();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Down, 45.0, 25.0), v).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Up, 75.0, 25.0), v).unwrap();
|
||||
let g = vectorcraft_tools::builder::live_paint_at(&s.doc().unwrap().doc, Point::new(25.0, 25.0)).expect("the click made a group");
|
||||
let faces = faces_at(&s, g, &[(75.0, 25.0), (225.0, 25.0), (325.0, 25.0)]);
|
||||
assert_eq!(fill_of(&s, faces[0]), green, "the release fills the face under the unfilled path");
|
||||
assert_eq!([faces[1], faces[2]].map(|f| fill_of(&s, f)), [Paint::solid(red), Paint::None], "Make gave the far faces red and none");
|
||||
for kind in [PointerKind::Down, PointerKind::Up, PointerKind::DoubleClick, PointerKind::Down, PointerKind::Up, PointerKind::TripleClick] {
|
||||
s.pointer(&PointerEvent::new(kind, 75.0, 25.0), v).unwrap();
|
||||
}
|
||||
assert_eq!([faces[1], faces[2]].map(|f| fill_of(&s, f)), [green, Paint::None], "the triple-click matches the red the face was made with");
|
||||
}
|
||||
|
||||
/// A triple-click whose first click adds paths to a selected Live Paint group and is released in a
|
||||
/// face of that group matches the face's fill, not the edge around it.
|
||||
#[test]
|
||||
fn live_paint_bucket_triple_click_after_merging_across_an_edge_matches_the_face_fill() {
|
||||
let mut s = session();
|
||||
let g = live_squares(&mut s, &[0.0]);
|
||||
let [a] = faces_at(&s, g, &[(25.0, 25.0)])[..] else { panic!("one face") };
|
||||
let red = Paint::solid(Color::rgb(1.0, 0.0, 0.0));
|
||||
let all = edges_in(&s, g, Rect::new(-1.0, -1.0, 51.0, 51.0));
|
||||
paint_parts(&mut s, &all, &Paint::solid(Color::rgb(0.0, 0.0, 0.0)), &[(a, red.clone())]);
|
||||
let (next, far) = (rect(&mut s, 50.0, 0.0, 50.0, 50.0), rect(&mut s, 200.0, 0.0, 50.0, 50.0));
|
||||
set_fill(&mut s, next, Color::rgb(0.0, 0.0, 1.0));
|
||||
set_fill(&mut s, far, Color::rgb(1.0, 0.0, 0.0));
|
||||
s.execute("select.set", &json!({"ids": [g.0, next.0, far.0]})).unwrap();
|
||||
let green = Paint::solid(Color::from_hex("#00ff00").unwrap());
|
||||
let v = ViewInfo::default();
|
||||
s.select_tool("livePaintBucket", v).unwrap();
|
||||
s.paint.fill = green.clone();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Down, 55.0, 25.0), v).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Up, 45.0, 25.0), v).unwrap();
|
||||
let g = vectorcraft_tools::builder::live_paint_at(&s.doc().unwrap().doc, Point::new(25.0, 25.0)).expect("the click merged the paths");
|
||||
let faces = faces_at(&s, g, &[(25.0, 25.0), (225.0, 25.0)]);
|
||||
assert_eq!(fill_of(&s, faces[0]), green, "the release fills the group's face");
|
||||
for kind in [PointerKind::Down, PointerKind::Up, PointerKind::DoubleClick, PointerKind::Down, PointerKind::Up, PointerKind::TripleClick] {
|
||||
s.pointer(&PointerEvent::new(kind, 45.0, 25.0), v).unwrap();
|
||||
}
|
||||
assert_eq!(fill_of(&s, faces[1]), green, "the far face had the red the face had");
|
||||
}
|
||||
|
||||
/// When the first click merges two selected Live Paint groups, a face of the group in front takes
|
||||
/// its source path's fill, so a triple-click there after a release across an edge matches that fill
|
||||
/// and not the paint the face had before the merge.
|
||||
#[test]
|
||||
fn live_paint_bucket_triple_click_after_merging_two_groups_matches_the_merged_fill() {
|
||||
let mut s = session();
|
||||
let back = live_squares(&mut s, &[0.0]);
|
||||
let source = rect(&mut s, 100.0, 0.0, 50.0, 50.0);
|
||||
set_fill(&mut s, source, Color::rgb(1.0, 0.0, 0.0));
|
||||
s.execute("select.set", &json!({"ids": [source.0]})).unwrap();
|
||||
let front = NodeId(s.execute("livePaint.make", &json!({})).unwrap()["id"].as_u64().unwrap());
|
||||
let [f] = faces_at(&s, front, &[(125.0, 25.0)])[..] else { panic!("one face") };
|
||||
let blue = Paint::solid(Color::rgb(0.0, 0.0, 1.0));
|
||||
paint_parts(&mut s, &[], &Paint::None, &[(f, blue.clone())]);
|
||||
let (next, far) = (rect(&mut s, 150.0, 0.0, 50.0, 50.0), rect(&mut s, 300.0, 0.0, 50.0, 50.0));
|
||||
set_fill(&mut s, next, Color::rgb(1.0, 1.0, 0.0));
|
||||
set_fill(&mut s, far, Color::rgb(1.0, 0.0, 0.0));
|
||||
s.execute("select.set", &json!({"ids": [back.0, front.0, next.0, far.0]})).unwrap();
|
||||
let merged = NodeId(s.execute("livePaint.merge", &json!({})).unwrap()["id"].as_u64().unwrap());
|
||||
let [m] = faces_at(&s, merged, &[(125.0, 25.0)])[..] else { panic!("one face") };
|
||||
assert_eq!(fill_of(&s, m), Paint::solid(Color::rgb(1.0, 0.0, 0.0)), "Merge gives the front group's face its source's fill");
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
s.execute("select.set", &json!({"ids": [back.0, front.0, next.0, far.0]})).unwrap();
|
||||
let green = Paint::solid(Color::from_hex("#00ff00").unwrap());
|
||||
let v = ViewInfo::default();
|
||||
s.select_tool("livePaintBucket", v).unwrap();
|
||||
s.paint.fill = green.clone();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Down, 155.0, 25.0), v).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Up, 145.0, 25.0), v).unwrap();
|
||||
let g = vectorcraft_tools::builder::live_paint_at(&s.doc().unwrap().doc, Point::new(125.0, 25.0)).expect("the click merged the groups");
|
||||
let faces = faces_at(&s, g, &[(125.0, 25.0), (325.0, 25.0)]);
|
||||
assert_eq!(fill_of(&s, faces[0]), green, "the release fills the front group's face");
|
||||
for kind in [PointerKind::Down, PointerKind::Up, PointerKind::DoubleClick, PointerKind::Down, PointerKind::Up, PointerKind::TripleClick] {
|
||||
s.pointer(&PointerEvent::new(kind, 145.0, 25.0), v).unwrap();
|
||||
}
|
||||
assert_eq!(fill_of(&s, faces[1]), green, "the far face had the red the merge gave the face");
|
||||
}
|
||||
|
||||
/// When the first click merges paths into a selected Live Paint group whose face has an unplaced
|
||||
/// gradient, Merge fixes the gradient to the face's fit, so a triple-click there after a release
|
||||
/// across an edge matches the fixed gradient: a far face whose source has that same fixed gradient
|
||||
/// takes the bucket's paint.
|
||||
#[test]
|
||||
fn live_paint_bucket_triple_click_after_merging_matches_the_fixed_gradient() {
|
||||
use vectorcraft_color::{Gradient, GradientKind, GradientPaint, GradientStop};
|
||||
let stops = vec![GradientStop::new(0.0, Color::BLACK), GradientStop::new(1.0, Color::WHITE)];
|
||||
let gradient = Paint::Gradient(Box::new(GradientPaint::new(Gradient::new(GradientKind::Linear, stops))));
|
||||
let mut s = session();
|
||||
let g = live_squares(&mut s, &[0.0]);
|
||||
let [f] = faces_at(&s, g, &[(25.0, 25.0)])[..] else { panic!("one face") };
|
||||
paint_parts(&mut s, &[], &Paint::None, &[(f, gradient.clone())]);
|
||||
let (next, far) = (rect(&mut s, 50.0, 0.0, 50.0, 50.0), rect(&mut s, 200.0, 0.0, 50.0, 50.0));
|
||||
set_fill(&mut s, next, Color::rgb(1.0, 1.0, 0.0));
|
||||
s.execute("select.set", &json!({"ids": [g.0, next.0]})).unwrap();
|
||||
let merged = NodeId(s.execute("livePaint.merge", &json!({})).unwrap()["id"].as_u64().unwrap());
|
||||
let [m] = faces_at(&s, merged, &[(25.0, 25.0)])[..] else { panic!("one face") };
|
||||
let fixed = fill_of(&s, m);
|
||||
assert_ne!(fixed, gradient, "Merge fixes the face's gradient to its fit");
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
s.edit("t", |d, _| {
|
||||
d.node_mut(far).unwrap().appearance.set_fill(fixed.clone());
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
s.execute("select.set", &json!({"ids": [g.0, next.0, far.0]})).unwrap();
|
||||
let v = ViewInfo::default();
|
||||
s.select_tool("livePaintBucket", v).unwrap();
|
||||
let green = Paint::solid(Color::from_hex("#00ff00").unwrap());
|
||||
s.paint.fill = green.clone();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Down, 55.0, 25.0), v).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Up, 45.0, 25.0), v).unwrap();
|
||||
let g = vectorcraft_tools::builder::live_paint_at(&s.doc().unwrap().doc, Point::new(45.0, 25.0)).expect("the click merged the paths");
|
||||
let [far_face] = faces_at(&s, g, &[(225.0, 25.0)])[..] else { panic!("the far face") };
|
||||
assert_eq!(fill_of(&s, far_face), fixed, "the far face keeps its source's fixed gradient");
|
||||
for kind in [PointerKind::Down, PointerKind::Up, PointerKind::DoubleClick, PointerKind::Down, PointerKind::Up, PointerKind::TripleClick] {
|
||||
s.pointer(&PointerEvent::new(kind, 45.0, 25.0), v).unwrap();
|
||||
}
|
||||
assert_eq!(fill_of(&s, far_face), green, "the far face had the fixed gradient the merge gave the face");
|
||||
}
|
||||
|
||||
/// When the first click merges a line that splits a painted face of the selected Live Paint group,
|
||||
/// Merge keeps the face's paint in one part only and the other part takes its source path's fill;
|
||||
/// a triple-click in that other part after a release across an edge matches the source's fill.
|
||||
#[test]
|
||||
fn live_paint_bucket_triple_click_after_a_merge_splits_a_face_matches_the_part_fill() {
|
||||
let mut s = session();
|
||||
let source = rect(&mut s, 0.0, 0.0, 50.0, 50.0);
|
||||
set_fill(&mut s, source, Color::rgb(1.0, 0.0, 0.0));
|
||||
s.execute("select.set", &json!({"ids": [source.0]})).unwrap();
|
||||
let g = NodeId(s.execute("livePaint.make", &json!({})).unwrap()["id"].as_u64().unwrap());
|
||||
let [f] = faces_at(&s, g, &[(25.0, 25.0)])[..] else { panic!("one face") };
|
||||
paint_parts(&mut s, &[], &Paint::None, &[(f, Paint::solid(Color::rgb(0.0, 0.0, 1.0)))]);
|
||||
let line = NodeId(s.execute("shape.line", &json!({"x1": 40, "y1": -10, "x2": 40, "y2": 60})).unwrap()["id"].as_u64().unwrap());
|
||||
let next = rect(&mut s, 50.0, 0.0, 50.0, 50.0);
|
||||
set_fill(&mut s, next, Color::rgb(1.0, 1.0, 0.0));
|
||||
let (red_far, blue_far) = (rect(&mut s, 200.0, 0.0, 50.0, 50.0), rect(&mut s, 300.0, 0.0, 50.0, 50.0));
|
||||
set_fill(&mut s, red_far, Color::rgb(1.0, 0.0, 0.0));
|
||||
set_fill(&mut s, blue_far, Color::rgb(0.0, 0.0, 1.0));
|
||||
let ids = json!({"ids": [g.0, line.0, next.0, red_far.0, blue_far.0]});
|
||||
s.execute("select.set", &ids).unwrap();
|
||||
let merged = NodeId(s.execute("livePaint.merge", &json!({})).unwrap()["id"].as_u64().unwrap());
|
||||
let parts = faces_at(&s, merged, &[(20.0, 25.0), (45.0, 25.0)]);
|
||||
assert_eq!(
|
||||
parts.iter().map(|p| fill_of(&s, *p)).collect::<Vec<_>>(),
|
||||
[Paint::solid(Color::rgb(0.0, 0.0, 1.0)), Paint::solid(Color::rgb(1.0, 0.0, 0.0))],
|
||||
"Merge keeps the paint in the part that holds the face's inside point"
|
||||
);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
s.execute("select.set", &ids).unwrap();
|
||||
let green = Paint::solid(Color::from_hex("#00ff00").unwrap());
|
||||
let v = ViewInfo::default();
|
||||
s.select_tool("livePaintBucket", v).unwrap();
|
||||
s.paint.fill = green.clone();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Down, 55.0, 25.0), v).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Up, 45.0, 25.0), v).unwrap();
|
||||
for kind in [PointerKind::Down, PointerKind::Up, PointerKind::DoubleClick, PointerKind::Down, PointerKind::Up, PointerKind::TripleClick] {
|
||||
s.pointer(&PointerEvent::new(kind, 45.0, 25.0), v).unwrap();
|
||||
}
|
||||
let g = vectorcraft_tools::builder::live_paint_at(&s.doc().unwrap().doc, Point::new(45.0, 25.0)).expect("the click merged the paths");
|
||||
let far = faces_at(&s, g, &[(225.0, 25.0), (325.0, 25.0)]);
|
||||
assert_eq!(far.iter().map(|p| fill_of(&s, *p)).collect::<Vec<_>>(), [green, Paint::solid(Color::rgb(0.0, 0.0, 1.0))]);
|
||||
}
|
||||
|
||||
/// A triple-click whose first click makes the Live Paint group from the selected paths matches the
|
||||
/// fill the face was made with.
|
||||
#[test]
|
||||
@@ -3978,6 +4238,116 @@ fn live_paint_bucket_double_and_triple_click_paint_joined_and_same_stroke_edges(
|
||||
assert_eq!(node(&s, apart[0]).appearance.stroke_paint(), black);
|
||||
}
|
||||
|
||||
/// A double-click whose two clicks land on two joined edges paints every edge joined to them that
|
||||
/// had their stroke: the edge the first click painted still joins the others.
|
||||
#[test]
|
||||
fn live_paint_bucket_double_click_across_a_junction_paints_the_joined_edges() {
|
||||
// A square with a line across its middle, cut at x = 30 and x = 70 by two lines from the top: the
|
||||
// middle line's three edges join end to end.
|
||||
let mut s = session();
|
||||
let mut ids = vec![rect(&mut s, 0.0, 0.0, 100.0, 100.0).0];
|
||||
for anchors in [
|
||||
json!([{"x": 0, "y": 50}, {"x": 100, "y": 50}]),
|
||||
json!([{"x": 30, "y": 0}, {"x": 30, "y": 70}]),
|
||||
json!([{"x": 70, "y": 0}, {"x": 70, "y": 70}]),
|
||||
] {
|
||||
ids.push(s.execute("path.create", &json!({ "anchors": anchors })).unwrap()["id"].as_u64().unwrap());
|
||||
}
|
||||
s.execute("select.set", &json!({"ids": ids})).unwrap();
|
||||
let g = NodeId(s.execute("livePaint.make", &json!({})).unwrap()["id"].as_u64().unwrap());
|
||||
let (black, red) = (Paint::solid(Color::rgb(0.0, 0.0, 0.0)), Paint::solid(Color::rgb(1.0, 0.0, 0.0)));
|
||||
let green = Paint::solid(Color::from_hex("#00ff00").unwrap());
|
||||
let all = edges_in(&s, g, Rect::new(-1.0, -1.0, 101.0, 101.0));
|
||||
paint_parts(&mut s, &all, &red, &[]);
|
||||
let middle = edges_in(&s, g, Rect::new(-1.0, 49.0, 101.0, 51.0));
|
||||
assert_eq!(middle.len(), 3, "the middle line's three edges");
|
||||
paint_parts(&mut s, &middle, &black, &[]);
|
||||
let v = ViewInfo::default();
|
||||
s.select_tool("livePaintBucket", v).unwrap();
|
||||
s.paint.stroke = green.clone();
|
||||
let shift = Mods { shift: true, ..Default::default() };
|
||||
for (kind, x) in
|
||||
[(PointerKind::Down, 50.0), (PointerKind::Up, 50.0), (PointerKind::Down, 85.0), (PointerKind::Up, 85.0), (PointerKind::DoubleClick, 85.0)]
|
||||
{
|
||||
s.pointer(&PointerEvent::new(kind, x, 50.0).with_mods(shift), v).unwrap();
|
||||
}
|
||||
assert_eq!(middle.iter().map(|e| node(&s, *e).appearance.stroke_paint()).collect::<Vec<_>>(), [green.clone(), green.clone(), green]);
|
||||
// `through` takes edge ids, with scope "connected" only.
|
||||
for p in [
|
||||
json!({"group": g.0, "point": [15, 50], "scope": "connected", "through": "x"}),
|
||||
json!({"group": g.0, "point": [15, 50], "scope": "connected", "through": [-1]}),
|
||||
json!({"group": g.0, "point": [15, 50], "scope": "samePaint", "through": []}),
|
||||
json!({"group": g.0, "point": [15, 50], "through": []}),
|
||||
] {
|
||||
assert!(s.execute("livePaint.strokeEdge", &p).is_err(), "{p}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A double-click whose second press lands on the next joined edge and is released back on the
|
||||
/// edge the first click painted (at the end of the line) matches that edge's stroke paint from
|
||||
/// before the first click, and the edge the second press painted still joins the edge past it.
|
||||
#[test]
|
||||
fn live_paint_bucket_double_click_released_back_on_the_first_edge_paints_the_joined_edges() {
|
||||
let mut s = session();
|
||||
let mut ids = vec![rect(&mut s, 0.0, 0.0, 100.0, 100.0).0];
|
||||
for anchors in [
|
||||
json!([{"x": 0, "y": 50}, {"x": 100, "y": 50}]),
|
||||
json!([{"x": 30, "y": 0}, {"x": 30, "y": 70}]),
|
||||
json!([{"x": 70, "y": 0}, {"x": 70, "y": 70}]),
|
||||
] {
|
||||
ids.push(s.execute("path.create", &json!({ "anchors": anchors })).unwrap()["id"].as_u64().unwrap());
|
||||
}
|
||||
s.execute("select.set", &json!({"ids": ids})).unwrap();
|
||||
let g = NodeId(s.execute("livePaint.make", &json!({})).unwrap()["id"].as_u64().unwrap());
|
||||
let (black, red) = (Paint::solid(Color::rgb(0.0, 0.0, 0.0)), Paint::solid(Color::rgb(1.0, 0.0, 0.0)));
|
||||
let green = Paint::solid(Color::from_hex("#00ff00").unwrap());
|
||||
let all = edges_in(&s, g, Rect::new(-1.0, -1.0, 101.0, 101.0));
|
||||
paint_parts(&mut s, &all, &red, &[]);
|
||||
let middle = edges_in(&s, g, Rect::new(-1.0, 49.0, 101.0, 51.0));
|
||||
assert_eq!(middle.len(), 3, "the middle line's three edges");
|
||||
paint_parts(&mut s, &middle, &black, &[]);
|
||||
let v = ViewInfo::default();
|
||||
s.select_tool("livePaintBucket", v).unwrap();
|
||||
s.paint.stroke = green.clone();
|
||||
let shift = Mods { shift: true, ..Default::default() };
|
||||
for (kind, x) in
|
||||
[(PointerKind::Down, 15.0), (PointerKind::Up, 15.0), (PointerKind::Down, 50.0), (PointerKind::Up, 15.0), (PointerKind::DoubleClick, 15.0)]
|
||||
{
|
||||
s.pointer(&PointerEvent::new(kind, x, 50.0).with_mods(shift), v).unwrap();
|
||||
}
|
||||
assert_eq!(middle.iter().map(|e| node(&s, *e).appearance.stroke_paint()).collect::<Vec<_>>(), [green.clone(), green.clone(), green]);
|
||||
}
|
||||
|
||||
/// A triple-click whose second press holds Cmd (it goes to the selection tool Cmd lends) matches
|
||||
/// the fill the face had before the first click: the lent press counts as one that paints nothing.
|
||||
#[test]
|
||||
fn live_paint_bucket_triple_click_with_a_cmd_press_between_matches_the_fill_before() {
|
||||
let mut s = session();
|
||||
let g = live_squares(&mut s, &[0.0, 50.0, 100.0, 150.0]);
|
||||
let [a, b, c, d] = faces_at(&s, g, &[(25.0, 25.0), (75.0, 25.0), (125.0, 25.0), (175.0, 25.0)])[..] else { panic!("four faces") };
|
||||
let (red, blue) = (Paint::solid(Color::rgb(1.0, 0.0, 0.0)), Paint::solid(Color::rgb(0.0, 0.0, 1.0)));
|
||||
let green = Paint::solid(Color::from_hex("#00ff00").unwrap());
|
||||
let all = edges_in(&s, g, Rect::new(-1.0, -1.0, 201.0, 51.0));
|
||||
paint_parts(&mut s, &all, &Paint::solid(Color::rgb(0.0, 0.0, 0.0)), &[(a, red.clone()), (b, red.clone()), (c, blue.clone()), (d, red.clone())]);
|
||||
let v = ViewInfo::default();
|
||||
s.select_tool("livePaintBucket", v).unwrap();
|
||||
s.paint.fill = green.clone();
|
||||
let cmd = Mods { cmd: true, ..Default::default() };
|
||||
for (kind, m) in [
|
||||
(PointerKind::Down, Mods::default()),
|
||||
(PointerKind::Up, Mods::default()),
|
||||
(PointerKind::Down, cmd),
|
||||
(PointerKind::Up, cmd),
|
||||
(PointerKind::Down, Mods::default()),
|
||||
(PointerKind::Up, Mods::default()),
|
||||
(PointerKind::TripleClick, Mods::default()),
|
||||
] {
|
||||
s.pointer(&PointerEvent::new(kind, 25.0, 25.0).with_mods(m), v).unwrap();
|
||||
}
|
||||
assert_eq!(s.tool_id(), "livePaintBucket");
|
||||
assert_eq!([a, b, c, d].map(|f| fill_of(&s, f)), [green.clone(), green.clone(), blue, green]);
|
||||
}
|
||||
|
||||
/// A `scope` that is not one of the three, a `match` that is not a paint or is given with a scope
|
||||
/// that compares none, and a scope with a drag's points are refused; nothing changes.
|
||||
#[test]
|
||||
|
||||
@@ -255,6 +255,9 @@ impl Session {
|
||||
}
|
||||
// The Selection and Group Selection tools act on the instance whole.
|
||||
self.select_whole_instance(id);
|
||||
// The tool lent to does not see the press: it counts as a press elsewhere for the clicks a
|
||||
// double-click or triple-click compares.
|
||||
self.tool.press_elsewhere();
|
||||
let lent = self.make_tool(id);
|
||||
self.selection_at_lend = self.active().map(|d| (d.uid, d.selection.clone()));
|
||||
self.lender = Some(std::mem::replace(&mut self.tool, lent));
|
||||
@@ -1086,6 +1089,12 @@ impl Session {
|
||||
self.with_tool_cx(view, |t, cx| t.hover_ended(cx));
|
||||
}
|
||||
|
||||
/// A press on the canvas went to the view tools and not to the active tool
|
||||
/// ([`vectorcraft_tools::Tool::press_elsewhere`]).
|
||||
pub fn tool_press_elsewhere(&mut self) {
|
||||
self.tool.press_elsewhere();
|
||||
}
|
||||
|
||||
/// Whether the active tool's hover depends on what lies under a still pointer
|
||||
/// ([`vectorcraft_tools::Tool::wants_rehover`]).
|
||||
pub fn tool_wants_rehover(&self) -> bool {
|
||||
|
||||
@@ -41,9 +41,10 @@ pub use live_paint::{
|
||||
};
|
||||
use live_paint::{check_live_paint_options, lp_color, lp_rgb_color};
|
||||
pub use lookup::{
|
||||
EDGE_NAME, FACE_NAME, Face, LIVE_PAINT_NAME, LIVE_PAINT_PREFIX, LP_JOIN_TOL, SOURCES_NAME, builder_members, connected_edges, drawn_outlines,
|
||||
edge_near, edges, face_at, faces, flood_faces, is_live_paint, leaf_shapes, live_paint_at, live_paint_takes, lp_stroke_paint, node_outline,
|
||||
outline_snap, path_contains, shape_builder_shapes, shapes_for, sources, visible_edge_near, visible_face_at,
|
||||
EDGE_NAME, FACE_NAME, Face, LIVE_PAINT_NAME, LIVE_PAINT_PREFIX, LP_JOIN_TOL, SOURCES_NAME, builder_members, connected_edges,
|
||||
connected_edges_through, drawn_outlines, edge_near, edges, face_at, faces, flood_faces, is_live_paint, leaf_shapes, live_paint_at,
|
||||
live_paint_takes, lp_stroke_paint, node_outline, outline_snap, path_contains, shape_builder_shapes, shapes_for, sources, visible_edge_near,
|
||||
visible_face_at,
|
||||
};
|
||||
pub use map::{
|
||||
BuilderMap, HATCH, HIGHLIGHT_RED, Hit, MergeGeometry, MergeInput, PreparedMerge, Rest, Touched, hatch, maps_built, merge_geometry, merge_inputs,
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
use super::cache::RegionCache;
|
||||
use super::lookup::{
|
||||
connected_edges, edges, faces, flood_faces, is_live_paint, leaf_shapes, live_paint_at, live_paint_at_where, lp_stroke_paint, node_outline,
|
||||
visible_edge_near, visible_face_at,
|
||||
connected_edges, connected_edges_through, edges, face_at, faces, flood_faces, is_live_paint, leaf_shapes, live_paint_at, live_paint_at_where,
|
||||
lp_stroke_paint, node_outline, visible_edge_near, visible_face_at,
|
||||
};
|
||||
use super::map::{HIGHLIGHT_RED, sorted_roots};
|
||||
use super::map::{BuilderMap, HIGHLIGHT_RED, sorted_roots};
|
||||
use super::{
|
||||
Action, Arc, BezPath, CLICK_TOL_PX, Cursor, Document, EDGE_NAME, FACE_NAME, FillRule, Mods, Node, NodeId, Overlay, Point, PointerEvent,
|
||||
PointerKind, Rect, Tool, ToolContext, ToolKey, Value, color_swatches, json, json_ids, path_contains, pick_swatch, plain, swatch_chips,
|
||||
};
|
||||
use vectorcraft_color::Paint;
|
||||
use vectorcraft_pathops as po;
|
||||
|
||||
// ---------- Live Paint Bucket ----------
|
||||
|
||||
@@ -178,10 +179,12 @@ enum LpPaint {
|
||||
///
|
||||
/// A double-click on a face fills it and the faces reached from it across edges that draw no
|
||||
/// stroke (`scope: "connected"`); on an edge it paints the edges joined to it that have its stroke.
|
||||
/// A triple-click on a face fills every face of the group with the face's fill, and on an edge it
|
||||
/// paints every edge of the group with the edge's stroke (`scope: "samePaint"`). Each click of a
|
||||
/// double-click or triple-click paints as it arrives, as its own undo step, and the paint compared
|
||||
/// is the one the face or edge had before the first of its clicks that painted it. A first click
|
||||
/// A triple-click on a face fills every face of the group that has the face's fill, and on an edge
|
||||
/// it paints every edge of the group that draws with the edge's stroke paint (`scope:
|
||||
/// "samePaint"`); each of them takes the bucket's paint. Each click of a double-click or
|
||||
/// triple-click paints as it arrives, as its own undo step when it changes a paint, and the paint
|
||||
/// compared is the one the face or edge had before the first of its clicks that painted it,
|
||||
/// including a click whose drag or release crossed it. A first click
|
||||
/// that made the Live Paint group counts when the face it filled is the clicked face. Shift and the
|
||||
/// options choose a face or an edge as for a click; with Alt or Cmd/Ctrl held a double-click or
|
||||
/// triple-click paints nothing. With Space held for the view tools, a click, double-click or triple-click paints
|
||||
@@ -201,6 +204,8 @@ pub struct LivePaintBucketTool {
|
||||
at: Option<(Point, Mods)>,
|
||||
/// The fill command and polyline previewed from the press's snapshot.
|
||||
fill_drag: Option<(Value, Vec<Vec<Point>>)>,
|
||||
/// What a fill drag paints, as it was at its press.
|
||||
drag_from: LpDragFrom,
|
||||
/// The last [`LP_PRESSES`] presses, oldest first: the face or edge each one painted, with its
|
||||
/// paint before that press, or None for a press that painted no face or edge.
|
||||
presses: Vec<Option<LpPress>>,
|
||||
@@ -229,22 +234,128 @@ struct LpPress {
|
||||
/// The other faces or edges the double-click that followed the press reached, each with its
|
||||
/// fill or drawn stroke paint before the double-click.
|
||||
also: Vec<(NodeId, Paint)>,
|
||||
/// The other faces the press's drag crossed, its release included, each with its fill before
|
||||
/// the press.
|
||||
dragged: Vec<(NodeId, Paint)>,
|
||||
/// For a press that makes the Live Paint group: what the group is made from, with the path
|
||||
/// chunks of the press's drag, its release included.
|
||||
dragged_made: Option<Arc<(LpMade, Vec<Vec<Point>>)>>,
|
||||
}
|
||||
|
||||
/// The fill of the face under `p` in the Live Paint group that `roots` make: the fill of the
|
||||
/// front-most of their paths whose closed subpaths hold `p`, else none. Open subpaths bound no face.
|
||||
fn made_face_fill(doc: &Document, roots: &[NodeId], p: Point) -> Paint {
|
||||
let mut leaves = vec![];
|
||||
for n in roots.iter().filter_map(|id| doc.node(*id)) {
|
||||
leaf_shapes(n, &mut leaves);
|
||||
/// What a Live Paint Bucket fill drag paints, as it was at the press.
|
||||
#[derive(Default)]
|
||||
enum LpDragFrom {
|
||||
#[default]
|
||||
None,
|
||||
/// The faces of an existing Live Paint group.
|
||||
Faces(Vec<Arc<Node>>),
|
||||
/// What the group is made from, for a drag that makes the group.
|
||||
Made(LpMade),
|
||||
}
|
||||
|
||||
/// The faces among `faces` (a Live Paint group's faces, back to front) that a drag along the path
|
||||
/// chunks `chunks` crosses, as `livePaint.fill` finds them, each with its fill.
|
||||
fn crossed_faces(faces: &[Arc<Node>], chunks: &[Vec<Point>]) -> Vec<(NodeId, Paint)> {
|
||||
let mut hit = vec![];
|
||||
let mut regions = vec![];
|
||||
// Front first, as visible_face_at looks.
|
||||
for f in faces.iter().rev().filter(|f| f.visible) {
|
||||
if let Some(path) = f.path_data()
|
||||
&& let Some(bounds) = path.bounds()
|
||||
{
|
||||
hit.push(f);
|
||||
regions.push((po::Region { path: path.clone(), sources: vec![] }, path.to_bezpath(), bounds));
|
||||
}
|
||||
}
|
||||
let map = BuilderMap { regions, ..Default::default() };
|
||||
let mut crossed: Vec<(NodeId, Paint)> = vec![];
|
||||
for chunk in chunks {
|
||||
for f in map.hits_along(chunk, None).regions.iter().filter_map(|i| hit.get(*i)) {
|
||||
if !crossed.iter().any(|(id, _)| *id == f.id) {
|
||||
crossed.push((f.id, f.appearance.fill_paint()));
|
||||
}
|
||||
}
|
||||
}
|
||||
crossed
|
||||
}
|
||||
|
||||
/// What the Live Paint group a fill press makes from the selection is made from, as it was at the
|
||||
/// press, following Live Paint Make and Merge.
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct LpMade {
|
||||
/// The paint Merge keeps from the faces of the first selected Live Paint group that draw: a
|
||||
/// point inside each face with its fill, given to the made face that holds the point.
|
||||
kept: Vec<(Point, Paint)>,
|
||||
/// The paths the faces are made from, back to front: Merge's group's source paths first, then
|
||||
/// the other objects', a Live Paint group giving its source paths.
|
||||
sources: Vec<Node>,
|
||||
}
|
||||
|
||||
/// What the Live Paint group the selected `roots` make is made from ([`LpMade`]).
|
||||
fn made_leaves(doc: &Document, roots: &[NodeId]) -> LpMade {
|
||||
let nodes: Vec<&Node> = roots.iter().filter_map(|id| doc.node(*id)).collect();
|
||||
let target = nodes.iter().position(|n| is_live_paint(n));
|
||||
let mut kept = vec![];
|
||||
let mut sources = vec![];
|
||||
for i in target.into_iter().chain((0..nodes.len()).filter(|i| Some(*i) != target)) {
|
||||
let Some(n) = nodes.get(i) else { continue };
|
||||
if !is_live_paint(n) {
|
||||
leaf_shapes(n, &mut sources);
|
||||
continue;
|
||||
}
|
||||
for s in super::lookup::sources(n).and_then(|g| g.children()).into_iter().flatten() {
|
||||
leaf_shapes(s, &mut sources);
|
||||
}
|
||||
if Some(i) == target {
|
||||
let draws = |f: &&Arc<Node>| f.visible && f.opacity > 0.0 && (f.appearance.has_drawn_fill() || f.appearance.has_drawn_stroke());
|
||||
// Merge fixes a kept unplaced gradient to its fit on the face it comes from.
|
||||
let kept_fill = |f: &Arc<Node>| {
|
||||
let mut n = (**f).clone();
|
||||
n.pin_gradients();
|
||||
n.appearance.fill_paint()
|
||||
};
|
||||
kept.extend(faces(n).into_iter().filter(draws).filter_map(|f| Some((po::interior_point(f.path_data()?)?, kept_fill(f)))));
|
||||
}
|
||||
}
|
||||
LpMade { kept, sources: sources.into_iter().cloned().collect() }
|
||||
}
|
||||
|
||||
/// The fill a face made at `p` from the source paths of `made` gets: the topmost fill that draws
|
||||
/// of the front-most source path whose closed subpaths hold `p` and that has such a fill, else
|
||||
/// none. Open subpaths bound no face.
|
||||
fn made_face_fill(made: &LpMade, p: Point) -> Paint {
|
||||
let holds = |l: &Node| {
|
||||
node_outline(l).is_some_and(|(path, rule)| {
|
||||
let closed = vectorcraft_geom::PathData::new(path.subpaths.into_iter().filter(|sp| sp.closed && sp.anchors.len() >= 2).collect());
|
||||
!closed.is_empty() && path_contains(&closed, rule, p)
|
||||
})
|
||||
};
|
||||
leaves.iter().rev().find(|l| holds(l)).map_or(Paint::None, |l| l.appearance.fill_paint())
|
||||
// As Make: the front-most covering source with a fill that draws (shown, with paint and an
|
||||
// opacity above 0, on a source with an opacity above 0) gives the face its topmost such fill,
|
||||
// an unplaced gradient fixed to its fit on the source.
|
||||
made.sources
|
||||
.iter()
|
||||
.rev()
|
||||
.filter(|l| l.opacity > 0.0 && l.appearance.has_drawn_fill() && holds(l))
|
||||
.find_map(|l| {
|
||||
let mut n = l.clone();
|
||||
n.pin_gradients();
|
||||
n.appearance.items.iter().rev().find_map(|i| match i {
|
||||
vectorcraft_doc::AppearanceItem::Fill(f) if f.visible && !f.paint.is_none() && f.opacity > 0.0 => Some(f.paint.clone()),
|
||||
_ => None,
|
||||
})
|
||||
})
|
||||
.unwrap_or(Paint::None)
|
||||
}
|
||||
|
||||
/// The fill face `face` of Live Paint group `group` was made with from `made`: the kept paint
|
||||
/// whose point the face holds (the last one, as Merge applies them in order), else the fill of the
|
||||
/// source path at a point inside the face.
|
||||
fn made_part_fill(made: &LpMade, group: &Node, face: &Node) -> Paint {
|
||||
if let Some((_, paint)) = made.kept.iter().rev().find(|(p, _)| face_at(group, *p).is_some_and(|f| f.id == face.id)) {
|
||||
return paint.clone();
|
||||
}
|
||||
face.path_data().and_then(po::interior_point).map_or(Paint::None, |p| made_face_fill(made, p))
|
||||
}
|
||||
|
||||
/// How many presses the Live Paint Bucket keeps: the three of a triple-click.
|
||||
@@ -260,6 +371,7 @@ impl Default for LivePaintBucketTool {
|
||||
hover: LpHover::None,
|
||||
at: None,
|
||||
fill_drag: None,
|
||||
drag_from: LpDragFrom::None,
|
||||
presses: Vec::new(),
|
||||
fills: true,
|
||||
strokes: false,
|
||||
@@ -336,7 +448,26 @@ impl LivePaintBucketTool {
|
||||
/// Note a press at `at` that paints face or edge (`edge`) `n`.
|
||||
fn note_part(&mut self, n: &Node, edge: bool, at: Point) {
|
||||
let before = if edge { lp_stroke_paint(n) } else { n.appearance.fill_paint() };
|
||||
self.note_press(Some(LpPress { part: Some(n.id), at, before, also: vec![] }));
|
||||
self.note_press(Some(LpPress { part: Some(n.id), at, before, also: vec![], dragged: vec![], dragged_made: None }));
|
||||
}
|
||||
|
||||
/// End the fill drag, noting on the last press the other faces its drag crossed, with their
|
||||
/// fills before the press (for a press that makes the group, what the group is made from and
|
||||
/// the drag, for [`Self::before`] to look the faces up in the group made). → whether a fill
|
||||
/// drag was on.
|
||||
fn end_fill(&mut self) -> bool {
|
||||
let from = std::mem::take(&mut self.drag_from);
|
||||
let Some((_, chunks)) = self.fill_drag.take() else { return false };
|
||||
let Some(Some(last)) = self.presses.last_mut() else { return true };
|
||||
match from {
|
||||
LpDragFrom::None => {}
|
||||
LpDragFrom::Faces(faces) => {
|
||||
let part = last.part;
|
||||
last.dragged = crossed_faces(&faces, &chunks).into_iter().filter(|(id, _)| part != Some(*id)).collect();
|
||||
}
|
||||
LpDragFrom::Made(made) => last.dragged_made = Some(Arc::new((made, chunks))),
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Note that the double-click after the last press also reaches `parts` of group `group`, with
|
||||
@@ -356,12 +487,17 @@ impl LivePaintBucketTool {
|
||||
/// The paint `part` had before the first press of a click sequence of `clicks` presses that
|
||||
/// painted `part`, or before the double-click after a press when that double-click reached it
|
||||
/// (the sequence's first press counts when it made the group `part` is in and the face it
|
||||
/// filled there is `part`); `now` when the last press did not paint it.
|
||||
/// filled there is `part`), a press's drag or release crossing `part` included; `now` when the
|
||||
/// last press did not paint it.
|
||||
fn before(&self, cx: &ToolContext, part: NodeId, clicks: usize, now: Paint) -> Paint {
|
||||
let Some(run) = self.presses.len().checked_sub(clicks).and_then(|from| self.presses.get(from..)) else { return now };
|
||||
let made_part = |q: &LpPress| live_paint_at(cx.doc, q.at).and_then(|g| visible_face_at(cx.doc.node(g)?, q.at)).is_some_and(|f| f.id == part);
|
||||
let on_part = |i: usize, q: &LpPress| q.part == Some(part) || (i == 0 && q.part.is_none() && made_part(q));
|
||||
if !run.last().and_then(Option::as_ref).is_some_and(|q| on_part(run.len() - 1, q)) {
|
||||
let made_part = |at: Point| live_paint_at(cx.doc, at).and_then(|g| visible_face_at(cx.doc.node(g)?, at)).is_some_and(|f| f.id == part);
|
||||
let on_part = |i: usize, q: &LpPress| q.part == Some(part) || (i == 0 && q.part.is_none() && made_part(q.at));
|
||||
let dragged = |q: &LpPress| q.dragged.iter().any(|(id, _)| *id == part);
|
||||
// An earlier press of the sequence that painted `part` counts too: a press on the next edge
|
||||
// released back on `part` paints that edge, and the double-click targets `part`.
|
||||
let painted = run.iter().enumerate().any(|(i, q)| q.as_ref().is_some_and(|q| on_part(i, q)));
|
||||
if !painted && !run.last().and_then(Option::as_ref).is_some_and(dragged) {
|
||||
return now;
|
||||
}
|
||||
for (i, q) in run.iter().enumerate() {
|
||||
@@ -369,7 +505,17 @@ impl LivePaintBucketTool {
|
||||
if on_part(i, q) {
|
||||
return q.before.clone();
|
||||
}
|
||||
if let Some((_, paint)) = q.also.iter().find(|(id, _)| *id == part) {
|
||||
// A face of the group the press made (the one under the press) that its drag crossed has
|
||||
// the fill it was made with.
|
||||
if i == 0
|
||||
&& let Some(drag) = &q.dragged_made
|
||||
&& let Some(group) = live_paint_at(cx.doc, q.at).and_then(|g| cx.doc.node(g))
|
||||
&& let Some(face) = faces(group).into_iter().find(|f| f.id == part)
|
||||
&& !crossed_faces(std::slice::from_ref(face), &drag.1).is_empty()
|
||||
{
|
||||
return made_part_fill(&drag.0, group, face);
|
||||
}
|
||||
if let Some((_, paint)) = q.dragged.iter().chain(&q.also).find(|(id, _)| *id == part) {
|
||||
return paint.clone();
|
||||
}
|
||||
}
|
||||
@@ -395,7 +541,23 @@ impl LivePaintBucketTool {
|
||||
if clicks == 2
|
||||
&& let Some(group) = cx.doc.node(g)
|
||||
{
|
||||
self.note_reached(group, connected_edges(group, e.id, &like), true);
|
||||
// The other edge either click painted joins as the stroke paint it had before.
|
||||
let through: Vec<NodeId> = self
|
||||
.presses
|
||||
.len()
|
||||
.checked_sub(2)
|
||||
.and_then(|i| self.presses.get(i..))
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.flatten()
|
||||
.filter(|q| q.before == like)
|
||||
.filter_map(|q| q.part)
|
||||
.filter(|id| *id != e.id)
|
||||
.collect();
|
||||
self.note_reached(group, connected_edges_through(group, e.id, &like, &through), true);
|
||||
if !through.is_empty() {
|
||||
v["through"] = json_ids(&through);
|
||||
}
|
||||
}
|
||||
if let Ok(like) = serde_json::to_value(like) {
|
||||
v["match"] = like;
|
||||
@@ -495,7 +657,7 @@ impl Tool for LivePaintBucketTool {
|
||||
fn pointer(&mut self, cx: &ToolContext, ev: &PointerEvent) -> Vec<Action> {
|
||||
let p = ev.pos;
|
||||
// A second press finishes the preceding click, including callers without a release.
|
||||
if ev.kind == PointerKind::Down && self.fill_drag.take().is_some() {
|
||||
if ev.kind == PointerKind::Down && self.end_fill() {
|
||||
let mut acts = vec![Action::Commit];
|
||||
acts.extend(self.pointer(cx, ev));
|
||||
return acts;
|
||||
@@ -553,11 +715,15 @@ impl Tool for LivePaintBucketTool {
|
||||
Some(f) => self.note_part(f, false, p),
|
||||
None => self.note_press(None),
|
||||
}
|
||||
self.drag_from = LpDragFrom::Faces(cx.doc.node(g).map(|n| faces(n).into_iter().cloned().collect()).unwrap_or_default());
|
||||
return self.begin_fill(json!({ "group": g.0, "point": point }), p);
|
||||
}
|
||||
if self.cache.at(cx, p).is_some() {
|
||||
let roots = sorted_roots(cx.doc, &cx.selection.objects);
|
||||
self.note_press(Some(LpPress { part: None, at: p, before: made_face_fill(cx.doc, &roots, p), also: vec![] }));
|
||||
let made = made_leaves(cx.doc, &roots);
|
||||
let before = made_face_fill(&made, p);
|
||||
self.note_press(Some(LpPress { part: None, at: p, before, also: vec![], dragged: vec![], dragged_made: None }));
|
||||
self.drag_from = LpDragFrom::Made(made);
|
||||
return self.begin_fill(json!({ "ids": json_ids(&roots), "point": point }), p);
|
||||
}
|
||||
self.note_press(None);
|
||||
@@ -568,7 +734,7 @@ impl Tool for LivePaintBucketTool {
|
||||
// The swatch preview shows again at the release point.
|
||||
self.at = Some((p, ev.mods));
|
||||
let mut acts = self.drag_fill(p);
|
||||
if self.fill_drag.take().is_some() {
|
||||
if self.end_fill() {
|
||||
acts.push(Action::Commit);
|
||||
}
|
||||
acts
|
||||
@@ -586,6 +752,7 @@ impl Tool for LivePaintBucketTool {
|
||||
/// the active paint, while no drag is on.
|
||||
fn key(&mut self, cx: &ToolContext, key: ToolKey, mods: Mods) -> Vec<Action> {
|
||||
if key == ToolKey::Escape {
|
||||
self.drag_from = LpDragFrom::None;
|
||||
return if self.fill_drag.take().is_some() { vec![Action::Cancel] } else { vec![] };
|
||||
}
|
||||
if !self.claims_key_with(cx, key, mods) {
|
||||
@@ -621,7 +788,7 @@ impl Tool for LivePaintBucketTool {
|
||||
|
||||
fn deactivate(&mut self, _cx: &ToolContext) -> Vec<Action> {
|
||||
self.presses.clear();
|
||||
if self.fill_drag.take().is_some() { vec![Action::Commit] } else { vec![] }
|
||||
if self.end_fill() { vec![Action::Commit] } else { vec![] }
|
||||
}
|
||||
|
||||
/// The pointer hovers nothing for the tool: its highlight and its swatch preview go.
|
||||
@@ -630,6 +797,12 @@ impl Tool for LivePaintBucketTool {
|
||||
self.at = None;
|
||||
}
|
||||
|
||||
/// A press the view tools took counts as a press that paints no face or edge, for the clicks
|
||||
/// a double-click or triple-click compares.
|
||||
fn press_elsewhere(&mut self) {
|
||||
self.note_press(None);
|
||||
}
|
||||
|
||||
fn overlays(&self, cx: &ToolContext) -> Vec<Overlay> {
|
||||
let mut out = lp_overlays(&self.hover, &self.cache, self.color, self.width as f32, (self.width * LP_BUCKET_EDGE_FRACTION) as f32);
|
||||
// No chips during a fill drag, or while Alt makes the tool sample with the eyedropper.
|
||||
|
||||
@@ -405,8 +405,15 @@ impl LpEdgeGrid {
|
||||
/// end of an edge already reached.
|
||||
/// Empty when `start` is not an edge of `group` that is not hidden.
|
||||
pub fn connected_edges(group: &Node, start: NodeId, paint: &Paint) -> Vec<NodeId> {
|
||||
connected_edges_through(group, start, paint, &[])
|
||||
}
|
||||
|
||||
/// [`connected_edges`], with the edges in `through` counted as drawing with stroke paint `paint`
|
||||
/// whatever paint they draw with now.
|
||||
pub fn connected_edges_through(group: &Node, start: NodeId, paint: &Paint, through: &[NodeId]) -> Vec<NodeId> {
|
||||
use std::collections::{HashMap, HashSet};
|
||||
let es: Vec<&Arc<Node>> = edges(group).into_iter().filter(|e| e.visible && (e.id == start || lp_stroke_paint(e) == *paint)).collect();
|
||||
let es: Vec<&Arc<Node>> =
|
||||
edges(group).into_iter().filter(|e| e.visible && (e.id == start || through.contains(&e.id) || lp_stroke_paint(e) == *paint)).collect();
|
||||
// The first and last points of each subpath of each edge.
|
||||
let ends: Vec<Vec<Point>> = es
|
||||
.iter()
|
||||
|
||||
@@ -1386,6 +1386,39 @@ fn bucket_triple_click_with_a_click_on_another_face_between_matches_the_first_fi
|
||||
);
|
||||
}
|
||||
|
||||
/// A triple-click whose second press the view tools take (held Space) matches the fill the face had
|
||||
/// before the first click: that press counts as one that paints nothing. The pointer leaving the
|
||||
/// canvas between three clicks adds no press.
|
||||
#[test]
|
||||
fn bucket_triple_click_with_a_view_tool_press_between_matches_the_first_fill() {
|
||||
use vectorcraft_color::{Color, Paint};
|
||||
let red = Paint::solid(Color::rgb(1.0, 0.0, 0.0));
|
||||
let (d, gid, f2, _) = live_group_doc();
|
||||
let mut painted = d.clone();
|
||||
painted.node_mut(f2).unwrap().appearance.set_fill(red.clone());
|
||||
let s = Selection::default();
|
||||
let p = paint();
|
||||
let (cx, later) = (cx(&d, &s, &p), cx(&painted, &s, &p));
|
||||
let own = serde_json::to_value(d.node(f2).unwrap().appearance.fill_paint()).unwrap();
|
||||
assert_ne!(own, serde_json::to_value(&red).unwrap());
|
||||
let want = vec![Action::Exec("livePaint.fill".into(), json!({ "group": gid.0, "point": [75.0, 25.0], "scope": "samePaint", "match": own }))];
|
||||
fn click(t: &mut LivePaintBucketTool, ctx: &ToolContext) {
|
||||
t.pointer(ctx, &ev(PointerKind::Down, 75.0, 25.0));
|
||||
t.pointer(ctx, &ev(PointerKind::Up, 75.0, 25.0));
|
||||
}
|
||||
let mut t = LivePaintBucketTool::default();
|
||||
click(&mut t, &cx);
|
||||
t.press_elsewhere();
|
||||
click(&mut t, &later);
|
||||
assert_eq!(t.pointer(&later, &ev(PointerKind::TripleClick, 75.0, 25.0)), want, "a view tool's press between");
|
||||
let mut t = LivePaintBucketTool::default();
|
||||
click(&mut t, &cx);
|
||||
click(&mut t, &later);
|
||||
t.hover_ended(&later);
|
||||
click(&mut t, &later);
|
||||
assert_eq!(t.pointer(&later, &ev(PointerKind::TripleClick, 75.0, 25.0)), want, "the pointer leaving between");
|
||||
}
|
||||
|
||||
/// With Space held for the view tools, the pointer outlines no face or edge, as a press there
|
||||
/// paints nothing.
|
||||
#[test]
|
||||
|
||||
@@ -291,6 +291,9 @@ pub trait Tool: Send {
|
||||
/// another document became active. The Blend tool drops its first pick and stops adding to the
|
||||
/// blend it just made.
|
||||
fn history_moved(&mut self) {}
|
||||
/// A press on the canvas went to the view tools (held Space, the middle button) and not to the
|
||||
/// tool. The Live Paint Bucket counts it among the clicks a triple-click compares.
|
||||
fn press_elsewhere(&mut self) {}
|
||||
/// Time passes while the pointer button is held (`dt` seconds since the last tick, whether or
|
||||
/// not the pointer moved): tools that keep working while the brush holds still (Twirl, Pucker,
|
||||
/// Bloat) act on it. The host supplies the time, so tests and agents drive it exactly.
|
||||
|
||||
@@ -121,6 +121,7 @@ pub(super) fn handle_input(app: &mut VectorcraftApp, ui: &Ui, resp: &egui::Respo
|
||||
{
|
||||
// Middle-button drag pans the view whatever the tool.
|
||||
ui.data_mut(|dd| dd.insert_temp(drag_id(), Drag::Pan { start, center: v.center, middle: true }));
|
||||
app.session.tool_press_elsewhere();
|
||||
app.session.tool_hover_ended(view);
|
||||
} else if let Some(d) = drag {
|
||||
// The pointer gone (a pen lifted with its press), the drag ends where it was last seen.
|
||||
@@ -342,6 +343,9 @@ fn begin_drag(app: &mut VectorcraftApp, ui: &Ui, f: &Pointing, p: Pos2) {
|
||||
if !space && matches!(d, Drag::ZoomBox { .. } | Drag::ZoomScrub { .. } | Drag::Pan { .. } | Drag::RotateView { .. }) {
|
||||
crate::shortcuts::note_tool_use(app);
|
||||
}
|
||||
if !matches!(d, Drag::Tool) {
|
||||
app.session.tool_press_elsewhere();
|
||||
}
|
||||
// Panning, zooming or turning the view takes the pointer from the tool's hover.
|
||||
if !tool_holds(d) {
|
||||
app.session.tool_hover_ended(view);
|
||||
|
||||
+7
-4
@@ -3957,8 +3957,9 @@ already walks Image Trace. Short agent entry points:
|
||||
group that draws with that stroke paint; the stroke's weight is not compared. An edge draws with the paint of its
|
||||
topmost stroke that is shown, with paint, above 0 pt and not fully transparent, and with none when the edge is fully
|
||||
transparent or has no such stroke. Each click of a double-click or triple-click runs as it
|
||||
arrives and is its own undo step. `match` is the paint the face or edge had before the first of the double-click's
|
||||
two clicks or the triple-click's three that painted it, or before the double-click when the triple-click's double-click
|
||||
arrives and is its own undo step when it changes a paint. `match` is the paint the face or edge had before the first of
|
||||
the double-click's two clicks or the triple-click's three that painted it (including a click whose drag or release crossed it), or
|
||||
before the double-click when the triple-click's double-click
|
||||
reached it, so the clicks before the last do not change what it matches;
|
||||
a first click that made the Live Paint group counts when the face it filled is the clicked face. Hidden faces and edges are not painted. A face that
|
||||
the Bucket fills has all its fills replaced by one fill with the paint, shown, fully opaque and blending normally, in the
|
||||
@@ -3972,8 +3973,10 @@ already walks Image Trace. Short agent entry points:
|
||||
`livePaint.fill {scope}` and `livePaint.strokeEdge {scope}` take `scope` as `"one"` (the default), `"connected"` or
|
||||
`"samePaint"`; any other value is an error and changes nothing. `match` is a paint in the model JSON that
|
||||
`document.node {id}` writes for an appearance's paints (`{"type": "none"}` for none), taken only with a scope that compares paints, and
|
||||
defaults to the face's fill or the stroke paint the edge draws with. `livePaint.fill` refuses a `scope` other than `"one"` with
|
||||
`points`. Both replies add `count`, the number of faces or edges the click reaches, including those that already had the
|
||||
defaults to the face's fill or the stroke paint the edge draws with. `livePaint.strokeEdge {scope: "connected", through}`
|
||||
takes `through`, at most 4096 edge ids that the join counts as drawing with `match` whatever they draw with now; the tool
|
||||
passes the edge the double-click's first click painted when that edge drew with `match` before the click. `through` with another scope, or one that is not an array of ids, is an error. `livePaint.fill` refuses
|
||||
a `scope` other than `"one"` with `points`. Both replies add `count`, the number of faces or edges the click reaches, including those that already had the
|
||||
paint.
|
||||
With the Live Paint Bucket selected and no drag in progress, Left and Right without Shift, Alt or Cmd/Ctrl (which
|
||||
keep their shortcuts) run `paint.setFill {swatch, ids: []}` (or
|
||||
|
||||
@@ -1574,7 +1574,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
Object › Live Paint › Merge adds the other selected paths and Live Paint groups to the bottom selected Live Paint group ([Live Paint groups](https://web.archive.org/web/20241230205225/https://helpx.adobe.com/illustrator/using/live-paint-groups.html)), which keeps its id, the paint of its faces and edges, and its name, lock, visibility, transparency, appearance, opacity mask and graphic style link. Object › Live Paint › Make with a Live Paint group selected, and a Live Paint Bucket click on a selected path beside a selected Live Paint group, add the paths to that group the same way. Make, Merge and the Live Paint Bucket take paths, compound paths and groups of them; with type, an image or another object among the selection they report that it has to be converted to paths first and change nothing, as type and raster images can't go into a Live Paint group directly ([Create Live Paint groups](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/learn-painting-basics/create-live-paint-groups.html)); before, such an object went among the group's hidden sources and vanished from the canvas. Evidence: `tests_build::live_paint_refuses_type_and_images_and_changes_nothing`.
|
||||
With the Live Paint Bucket selected, Alt-click on vector art samples its fill, stroke and stroke weight into the paint defaults ([Paint with the Live Paint Bucket tool](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/learn-painting-basics/paint-with-the-live-paint-bucket-tool.html)). On a placed image it samples the image pixel as a solid fill with no stroke. Alt+Shift-click samples only the color under the pointer, including an intermediate gradient color or image pixels, into the active fill or stroke ([Default keyboard shortcuts](https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html)). Neither click paints or changes the selection or selected art, and the next Bucket click paints with the sampled paint. Evidence: `tests_build::live_paint_bucket_alt_sample_paints_next_face_without_changing_selection`.
|
||||
Double-clicking the Live Paint Bucket opens Live Paint Bucket Options ([Paint with the Live Paint Bucket tool](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/learn-painting-basics/paint-with-the-live-paint-bucket-tool.html)): Paint Fills (on) and Paint Strokes (off) set whether a click paints faces, edges or both, Highlight turns the outline of the face or edge under the pointer on or off, and Color and Width set it (red and 4 px by default). Faces are outlined at the Width and edges at half of it, as the page says faces are highlighted with a thick line and edges with a thin line. With both on, a click paints the edge within 4 px of the pointer, else the face, and Shift switches to Paint Fills only; with one on, Shift switches to the other. The options last across tool switches and are saved with the preferences (`tool.setOption {tool: "livePaintBucket"}`). Evidence: `builder::tests::{bucket_options_choose_what_a_click_paints, bucket_options_set_the_highlight, bucket_highlights_an_edge_thinner_than_a_face}`, `dialogs::live_paint_bucket::tests::double_clicking_the_tool_opens_its_options_and_ok_sets_them`.
|
||||
Live Paint Bucket double-click and triple-click, as [the help page](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/learn-painting-basics/paint-with-the-live-paint-bucket-tool.html) describes them: a double-click on a face fills it and the faces reached across edges that draw no stroke, and on an edge it paints the edges joined to it with the same stroke; a triple-click fills every face of the group with the face's fill, or paints every edge with the edge's stroke (`livePaint.fill` and `livePaint.strokeEdge` `{scope: "connected" | "samePaint", match?}`; `pointer_gesture` and `ui.pointer` take `tripleclick`). Each click paints as it arrives, as its own undo step, and the paint compared is the one the face or edge had before the first of its clicks that painted it; a first click that made the Live Paint group counts when the face it filled is the clicked face. With Space held for the view tools, a click, double-click or triple-click paints nothing and no face or edge is outlined, and with Cmd/Ctrl held a double-click or triple-click paints nothing. Which faces and edges join and which paints match are VectorCraft's own rules. Evidence: `tests_build::{live_paint_bucket_double_click_floods_across_unstroked_edges, live_paint_bucket_double_click_floods_curved_and_narrow_faces, live_paint_bucket_double_click_floods_across_hidden_edges_and_past_gaps, live_paint_bucket_triple_click_fills_every_face_of_the_same_fill, live_paint_bucket_triple_click_after_making_the_group_matches_the_made_fill, live_paint_bucket_double_and_triple_click_paint_joined_and_same_stroke_edges, live_paint_scope_and_match_are_checked}`, `builder::tests::{bucket_double_and_triple_click_widen_what_a_click_paints, bucket_triple_click_after_making_the_group_on_another_face_matches_its_own_face, bucket_triple_click_with_a_click_on_another_face_between_matches_the_first_fill, bucket_highlights_nothing_with_space_held}`, `tests::build::{live_paint_scoped_fill_and_stroke_skip_parts_with_the_paint, live_paint_bucket_cmd_double_and_triple_click_paint_nothing}`, `tests::livepaint::{a_triple_click_fills_every_face_of_the_same_fill, clicks_with_space_held_paint_nothing}` (MCP).
|
||||
Live Paint Bucket double-click and triple-click, as [the help page](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/learn-painting-basics/paint-with-the-live-paint-bucket-tool.html) describes them: a double-click on a face fills it and the faces reached across edges that draw no stroke, and on an edge it paints the edges joined to it with the same stroke; a triple-click fills every face of the group that has the face's fill, or paints every edge that draws with the edge's stroke paint, each with the bucket's paint (`livePaint.fill` and `livePaint.strokeEdge` `{scope: "connected" | "samePaint", match?}`; `pointer_gesture` and `ui.pointer` take `tripleclick`). Each click paints as it arrives, as its own undo step when it changes a paint, and the paint compared is the one the face or edge had before the first of its clicks that painted it, including a click whose drag or release crossed it; a first click that made the Live Paint group counts when the face it filled is the clicked face. With Space held for the view tools, a click, double-click or triple-click paints nothing and no face or edge is outlined, and with Cmd/Ctrl held a double-click or triple-click paints nothing. Which faces and edges join and which paints match are VectorCraft's own rules. Evidence: `tests_build::{live_paint_bucket_double_click_floods_across_unstroked_edges, live_paint_bucket_double_click_floods_curved_and_narrow_faces, live_paint_bucket_double_click_floods_across_hidden_edges_and_past_gaps, live_paint_bucket_triple_click_fills_every_face_of_the_same_fill, live_paint_bucket_triple_click_after_a_release_across_an_edge_matches_the_fill_before, live_paint_bucket_triple_click_released_from_another_face_matches_the_fill_before, live_paint_bucket_triple_click_after_making_the_group_across_an_edge_matches_the_made_fill, live_paint_bucket_triple_click_after_making_the_group_under_an_unfilled_path_matches_the_made_fill, live_paint_bucket_triple_click_after_merging_across_an_edge_matches_the_face_fill, live_paint_bucket_triple_click_after_merging_two_groups_matches_the_merged_fill, live_paint_bucket_triple_click_after_a_merge_splits_a_face_matches_the_part_fill, live_paint_bucket_triple_click_after_merging_matches_the_fixed_gradient, live_paint_bucket_triple_click_after_making_the_group_matches_the_made_fill, live_paint_bucket_double_and_triple_click_paint_joined_and_same_stroke_edges, live_paint_bucket_double_click_across_a_junction_paints_the_joined_edges, live_paint_scope_and_match_are_checked}`, `builder::tests::{bucket_double_and_triple_click_widen_what_a_click_paints, bucket_triple_click_after_making_the_group_on_another_face_matches_its_own_face, bucket_triple_click_with_a_click_on_another_face_between_matches_the_first_fill, bucket_triple_click_with_a_view_tool_press_between_matches_the_first_fill, bucket_highlights_nothing_with_space_held}`, `tests::build::{live_paint_scoped_fill_and_stroke_skip_parts_with_the_paint, live_paint_bucket_cmd_double_and_triple_click_paint_nothing}`, `tests::livepaint::{a_triple_click_fills_every_face_of_the_same_fill, clicks_with_space_held_paint_nothing}` (MCP).
|
||||
Live Paint Bucket Options has Cursor Swatch Preview, as the [help page](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/learn-painting-basics/paint-with-the-live-paint-bucket-tool.html) lists: with the Bucket selected, the active fill or stroke shows above the pointer between the color swatches before and after it in the Swatches panel, and the Left and Right arrow keys pick the swatch before or after it, as the page describes. The selected art keeps its paint. As VectorCraft's own rules, the option is on at first, the arrows step the stroke when the Stroke proxy is in front, they wrap around at the ends and still step with the preview off, a color that is no swatch shows alone until Right picks the first swatch or Left the last, and the arrows do nothing during a fill drag or with no color swatch. Evidence: `tests::build::{live_paint_bucket_arrow_keys_step_the_active_paint_through_the_swatches, live_paint_bucket_arrow_keys_handle_unmatched_paint_few_swatches_and_drags, live_paint_bucket_arrow_keys_pass_swatches_of_the_same_color_for_fill_and_stroke, swatch_arrow_keys_leave_modified_arrows_to_the_shortcuts}`.
|
||||
Object › Live Paint › Make turns the selected paths into a Live Paint group of faces and edges ([the help page](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/learn-painting-basics/create-live-paint-groups.html)). As VectorCraft's own rule, each face takes every fill row that draws (shown, with paint and an opacity above 0) of its front-most covering source that has one and an opacity above 0, and each edge every stroke row that draws (also with a weight above 0) of its source, in order with their opacity, blending mode, effects and stroke settings, with gradients kept at their fit on the source and with the source's opacity and blending mode. Where a path is cut into several edges, each edge's dashes go on from where it starts along the path with the lengths set (dashes aligned to corners and path ends are not fitted to the cut edges), the arrowheads stay at the path's own ends, and a width profile or brush stays only on an edge that runs the whole path; each edge runs the way its path does, a gradient applied along a stroke starts again on each cut edge, and an inside or outside stroke is drawn centered on an open piece of a closed path. A painted lower row under an unpainted or hidden top row is kept, a hidden top row does not become visible, and a source in front whose fill does not draw does not hide the fill behind it. Rows that do not draw stay with the stored sources, which Release returns unchanged; the source's object effects are not carried. Evidence: `tests::build::{live_paint_make_keeps_lower_drawn_stroke_rows, live_paint_make_fits_dashes_and_arrowheads_to_each_edge, live_paint_make_starts_a_closed_edge_where_its_path_starts, live_paint_make_keeps_the_direction_of_gradient_strokes, live_paint_make_keeps_all_drawn_fill_rows_and_attributes, live_paint_make_uses_frontmost_source_with_drawn_fill}`.
|
||||
Live Paint Make preserves the front-most covering source's fill when compound paths share a curved boundary, and Expand keeps the face fills. Regression evidence: `planar::tests::coincident_curves_keep_their_source_faces` checks source order, winding, fill rules and nearby boundaries; `tests::build::live_paint_make_and_expand_keep_abutting_compound_colors` checks native compound paths, Make, Expand and Undo/Redo. This preserves VectorCraft's source-fill rule without changing arrangement precision.
|
||||
|
||||
Reference in New Issue
Block a user