mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-15 08:45:25 +00:00
Live Paint: a triple-click matches what its double-click reached
When the third click of a triple-click landed on a face or edge that the double-click before it had painted (a neighbor the flood or the edge walk reached), the triple-click sent that part's new paint as match, since only the clicked part's paint was recorded. The bucket now records the fill or drawn stroke paint of every other part a double-click reaches, and a triple-click on one of them matches the paint it had before the double-click. docs/mcp.md says so.
This commit is contained in:
@@ -226,6 +226,9 @@ struct LpPress {
|
||||
/// Its fill (a face), or the stroke paint it draws with ([`lp_stroke_paint`], an edge), before
|
||||
/// the press.
|
||||
before: Paint,
|
||||
/// 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 fill of the face under `p` in the Live Paint group that `roots` make: the fill of the
|
||||
@@ -333,29 +336,50 @@ 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 }));
|
||||
self.note_press(Some(LpPress { part: Some(n.id), at, before, also: vec![] }));
|
||||
}
|
||||
|
||||
/// Note that the double-click after the last press also reaches `parts` of group `group`, with
|
||||
/// the paint each has now (a face's fill, or an edge's drawn stroke paint with `edge`).
|
||||
fn note_reached(&mut self, group: &Node, parts: Vec<NodeId>, edge: bool) {
|
||||
let Some(Some(last)) = self.presses.last_mut() else { return };
|
||||
let reached: std::collections::HashSet<NodeId> = parts.into_iter().filter(|id| last.part != Some(*id)).collect();
|
||||
last.also = group
|
||||
.children()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter(|c| reached.contains(&c.id))
|
||||
.map(|n| (n.id, if edge { lp_stroke_paint(n) } else { n.appearance.fill_paint() }))
|
||||
.collect();
|
||||
}
|
||||
|
||||
/// The paint `part` had before the first press of a click sequence of `clicks` presses that
|
||||
/// painted `part` (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.
|
||||
/// 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.
|
||||
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, q): (usize, &Option<LpPress>)| q.as_ref().is_some_and(|q| q.part == Some(part) || (i == 0 && q.part.is_none() && made_part(q)));
|
||||
if !run.last().is_some_and(|q| on_part((run.len() - 1, q))) {
|
||||
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)) {
|
||||
return now;
|
||||
}
|
||||
match run.iter().enumerate().find(|&iq| on_part(iq)) {
|
||||
Some((_, Some(first))) => first.before.clone(),
|
||||
_ => now,
|
||||
for (i, q) in run.iter().enumerate() {
|
||||
let Some(q) = q else { continue };
|
||||
if on_part(i, q) {
|
||||
return q.before.clone();
|
||||
}
|
||||
if let Some((_, paint)) = q.also.iter().find(|(id, _)| *id == part) {
|
||||
return paint.clone();
|
||||
}
|
||||
}
|
||||
now
|
||||
}
|
||||
|
||||
/// What a double-click (`scope` "connected", 2 `clicks`) or a triple-click ("samePaint", 3) at
|
||||
/// `p` runs: the command a click there runs, with `scope`, and the paint it compares as `match`.
|
||||
fn multi_click(&self, cx: &ToolContext, p: Point, mods: Mods, scope: &str, clicks: usize) -> Vec<Action> {
|
||||
/// A double-click notes the other faces or edges it reaches, for the triple-click's `match`.
|
||||
fn multi_click(&mut self, cx: &ToolContext, p: Point, mods: Mods, scope: &str, clicks: usize) -> Vec<Action> {
|
||||
// With Alt held, with Space held for the view tools, or with Cmd/Ctrl held (its presses went
|
||||
// to the selection tool Cmd lends), a double-click or triple-click paints nothing.
|
||||
if mods.alt || mods.space || mods.cmd {
|
||||
@@ -367,7 +391,13 @@ impl LivePaintBucketTool {
|
||||
&& let Some((g, Some(e))) = lp_part_at(cx.doc, p, cx.tol(4.0), true, &|id| lp_visible(cx, id))
|
||||
{
|
||||
let mut v = json!({ "group": g.0, "point": point, "tolerance": cx.tol(4.0), "scope": scope });
|
||||
if let Ok(like) = serde_json::to_value(self.before(cx, e.id, clicks, lp_stroke_paint(e))) {
|
||||
let like = self.before(cx, e.id, clicks, lp_stroke_paint(e));
|
||||
if clicks == 2
|
||||
&& let Some(group) = cx.doc.node(g)
|
||||
{
|
||||
self.note_reached(group, connected_edges(group, e.id, &like), true);
|
||||
}
|
||||
if let Ok(like) = serde_json::to_value(like) {
|
||||
v["match"] = like;
|
||||
}
|
||||
return vec![Action::Exec("livePaint.strokeEdge".into(), v)];
|
||||
@@ -377,6 +407,11 @@ impl LivePaintBucketTool {
|
||||
}
|
||||
let Some((g, f)) = live_paint_at(cx.doc, p).and_then(|g| Some((g, visible_face_at(cx.doc.node(g)?, p)?))) else { return vec![] };
|
||||
let mut v = json!({ "group": g.0, "point": point, "scope": scope });
|
||||
if clicks == 2
|
||||
&& let Some(group) = cx.doc.node(g)
|
||||
{
|
||||
self.note_reached(group, flood_faces(group, f.id), false);
|
||||
}
|
||||
if scope == "samePaint"
|
||||
&& let Ok(like) = serde_json::to_value(self.before(cx, f.id, clicks, f.appearance.fill_paint()))
|
||||
{
|
||||
@@ -522,7 +557,7 @@ impl Tool for LivePaintBucketTool {
|
||||
}
|
||||
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) }));
|
||||
self.note_press(Some(LpPress { part: None, at: p, before: made_face_fill(cx.doc, &roots, p), also: vec![] }));
|
||||
return self.begin_fill(json!({ "ids": json_ids(&roots), "point": point }), p);
|
||||
}
|
||||
self.note_press(None);
|
||||
|
||||
@@ -1206,6 +1206,40 @@ fn bucket_double_and_triple_click_widen_what_a_click_paints() {
|
||||
assert!(t.pointer(&cx, &ev(PointerKind::DoubleClick, 400.0, 400.0)).is_empty());
|
||||
}
|
||||
|
||||
/// A triple-click whose third click lands on a face the double-click's flood filled matches that
|
||||
/// face's fill from before the double-click.
|
||||
#[test]
|
||||
fn bucket_triple_click_on_a_face_the_double_click_flooded_matches_its_earlier_fill() {
|
||||
let (mut d, gid, f2, _) = live_group_doc();
|
||||
let red = vectorcraft_color::Paint::solid(vectorcraft_color::Color::rgb(1.0, 0.0, 0.0));
|
||||
let blue = vectorcraft_color::Paint::solid(vectorcraft_color::Color::rgb(0.0, 0.0, 1.0));
|
||||
let f1 = crate::builder::face_at(d.node(gid).unwrap(), Point::new(25.0, 25.0)).unwrap().id;
|
||||
for f in [f1, f2] {
|
||||
d.node_mut(f).unwrap().appearance.set_fill(red.clone());
|
||||
}
|
||||
let mut first = d.clone();
|
||||
first.node_mut(f1).unwrap().appearance.set_fill(blue.clone());
|
||||
let mut flooded = first.clone();
|
||||
flooded.node_mut(f2).unwrap().appearance.set_fill(blue.clone());
|
||||
let s = Selection::default();
|
||||
let p = paint();
|
||||
let (start, first, flooded) = (cx(&d, &s, &p), cx(&first, &s, &p), cx(&flooded, &s, &p));
|
||||
let mut t = LivePaintBucketTool::default();
|
||||
let click = |t: &mut LivePaintBucketTool, cx: &ToolContext, x: f64| {
|
||||
t.pointer(cx, &ev(PointerKind::Down, x, 25.0));
|
||||
t.pointer(cx, &ev(PointerKind::Up, x, 25.0));
|
||||
};
|
||||
click(&mut t, &start, 25.0);
|
||||
click(&mut t, &first, 25.0);
|
||||
t.pointer(&first, &ev(PointerKind::DoubleClick, 25.0, 25.0));
|
||||
click(&mut t, &flooded, 75.0);
|
||||
let like = serde_json::to_value(&red).unwrap();
|
||||
assert_eq!(
|
||||
t.pointer(&flooded, &ev(PointerKind::TripleClick, 75.0, 25.0)),
|
||||
vec![Action::Exec("livePaint.fill".into(), json!({ "group": gid.0, "point": [75.0, 25.0], "scope": "samePaint", "match": like }))]
|
||||
);
|
||||
}
|
||||
|
||||
/// On an edge that stores a stroke paint but draws none (its stroke hidden, or the edge fully
|
||||
/// transparent), a double-click or triple-click matches no paint, with or without the clicks
|
||||
/// before it.
|
||||
|
||||
+2
-1
@@ -3689,7 +3689,8 @@ already walks Image Trace. Short agent entry points:
|
||||
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, so the clicks before the last do not change what it matches;
|
||||
two clicks or the triple-click's three that painted 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. An edge takes the
|
||||
paint and weight in its topmost stroke that is shown, or in a new stroke above the others when it has none. These rules
|
||||
for what faces and edges join and what paints match are VectorCraft's own. The help page describes the gestures:
|
||||
|
||||
Reference in New Issue
Block a user