From 2264edd0c9853b6e308ce8432e3a16e800a2c474 Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Tue, 3 Nov 2026 15:35:42 +0100 Subject: [PATCH] Lasso picks gradient mesh points instead of dropping the mesh Fixes #5351 Co-Authored-By: Claude Opus 5.5 --- crates/engine/src/cmd/paint.rs | 4 +- crates/engine/src/tests/xform.rs | 21 ++++++++++ crates/tools/src/meshedit.rs | 21 ++++++++++ crates/tools/src/xform/wand.rs | 71 +++++++++++++++++++++++++++++--- docs/mcp.md | 3 +- 5 files changed, 112 insertions(+), 8 deletions(-) diff --git a/crates/engine/src/cmd/paint.rs b/crates/engine/src/cmd/paint.rs index ec59e4e9e..e53b9b8f3 100644 --- a/crates/engine/src/cmd/paint.rs +++ b/crates/engine/src/cmd/paint.rs @@ -697,8 +697,8 @@ fn black_or_white(c: &Color) -> bool { } } -/// The points the active tool has picked (its `meshPoints` option: the Mesh tool's or Direct -/// Selection's) on the gradient meshes among `ids`, in range, each mesh with its points. +/// The points the active tool has picked (its `meshPoints` option: the Mesh tool's, Direct +/// Selection's or the Lasso's) on the gradient meshes among `ids`, in range, each mesh with its points. fn picked_mesh_points(s: &Session, ids: &[NodeId]) -> Vec<(NodeId, Vec)> { let o = s.tool_options(); let (Some(picked), Ok(st)) = (o.get("meshPoints").and_then(Value::as_array), s.doc()) else { return vec![] }; diff --git a/crates/engine/src/tests/xform.rs b/crates/engine/src/tests/xform.rs index eb503cce9..5dfd7ca09 100644 --- a/crates/engine/src/tests/xform.rs +++ b/crates/engine/src/tests/xform.rs @@ -976,6 +976,27 @@ fn lasso_selects_anchor_subset() { assert_eq!(st.selection.partial(a).map(|p| p.len()), Some(2)); } +/// #5351: a Lasso loop round a gradient mesh's points keeps the mesh selected and picks the +/// points inside it, so a solid fill colour recolours just those points. +#[test] +fn lasso_picks_gradient_mesh_points() { + let mut s = session(); + let a = rect(&mut s, 100.0, 100.0, 100.0, 100.0); + s.execute("select.set", &json!({"ids": [a.0]})).unwrap(); + s.execute("object.mesh.create", &json!({"rows": 2, "cols": 2})).unwrap(); + // Round the top row of points (100,100), (150,100) and (200,100). + gesture(&mut s, "lasso", &[(90.0, 90.0), (210.0, 90.0), (210.0, 110.0), (90.0, 110.0), (90.0, 110.0)], Mods::default()); + let st = s.doc().unwrap(); + let Some(NodeKind::Mesh(m)) = st.doc.node(a).map(|n| &n.kind) else { panic!("still a gradient mesh") }; + assert_eq!(m.points.len(), 9); + assert_eq!(st.selection.objects, vec![a], "the mesh stays selected"); + s.execute("paint.setFill", &json!({"color": "#ff0000"})).unwrap(); + let Some(NodeKind::Mesh(m)) = s.doc().unwrap().doc.node(a).map(|n| &n.kind) else { panic!("still a gradient mesh") }; + let red: Vec = (0..m.points.len()).filter(|i| m.points[*i].color.to_hex() == "#ff0000").collect(); + let top: Vec = (0..3).map(|c| m.idx(0, c)).collect(); + assert_eq!(red, top, "only the points inside the loop are recoloured"); +} + /// Shift-drag a marquee with the Selection tool (#483): the selected objects it reaches are /// deselected and the others selected, the rest of the selection staying as it was. #[test] diff --git a/crates/tools/src/meshedit.rs b/crates/tools/src/meshedit.rs index e0ef779fb..c0c1d9623 100644 --- a/crates/tools/src/meshedit.rs +++ b/crates/tools/src/meshedit.rs @@ -257,6 +257,27 @@ impl MeshEdit { meshes.into_iter().map(|id| (id, self.picked.range((id, 0)..=(id, usize::MAX)).next().is_some())).collect() } + /// A Lasso's picks: the points `inside` it are picked alone, or added to the picked points + /// (`add`), or taken away from them (`sub`). Returns each mesh with points inside and whether + /// any of its points are picked now. + pub fn lasso(&mut self, inside: &[(NodeId, usize)], add: bool, sub: bool) -> Vec<(NodeId, bool)> { + if !add && !sub { + return self.marquee(inside, false); + } + for k in inside { + if sub { + self.picked.remove(k); + } else { + self.picked.insert(*k); + } + } + self.click = None; + self.focus = self.focus.filter(|f| self.picked.contains(f)); + let mut meshes: Vec = inside.iter().map(|(id, _)| *id).collect(); + meshes.dedup(); + meshes.into_iter().map(|id| (id, self.picked.range((id, 0)..=(id, usize::MAX)).next().is_some())).collect() + } + /// An arrow key's nudge of the picked points of the meshes `shown` accepts /// (`object.mesh.movePoints`); `None` during a drag, for other keys or without picked points. pub fn nudge(&self, cx: &ToolContext, key: ToolKey, mods: Mods, shown: &dyn Fn(NodeId) -> bool) -> Option> { diff --git a/crates/tools/src/xform/wand.rs b/crates/tools/src/xform/wand.rs index 530c9302d..41f42718a 100644 --- a/crates/tools/src/xform/wand.rs +++ b/crates/tools/src/xform/wand.rs @@ -3,7 +3,8 @@ //! Magic Wand: click an object to select every object with similar attributes. The matching and //! its settings (Magic Wand panel) live in the engine's `select.magicWand` command; Shift adds, //! Alt subtracts. Lasso: freehand loop selecting anchor points inside it (direct-selection -//! style); Shift adds, Alt subtracts. +//! style), and picking the gradient mesh points inside it with their meshes selected (the +//! tool's `meshPoints` option, which a solid fill colour recolours); Shift adds, Alt subtracts. use std::collections::BTreeSet; @@ -13,6 +14,7 @@ use vectorcraft_doc::{AnchorRef, NodeId, NodeKind}; use vectorcraft_geom::Point; use super::paint_owner; +use crate::meshedit::{MeshEdit, grid_of}; use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext, ToolKey}; #[derive(Default)] @@ -67,6 +69,8 @@ pub fn point_in_polygon(poly: &[Point], p: Point) -> bool { pub struct LassoTool { points: Vec, active: bool, + /// The picked gradient mesh points. + mesh: MeshEdit, } fn anchors_json(v: &BTreeSet) -> Value { @@ -105,7 +109,11 @@ impl Tool for LassoTool { } // Anchors inside the loop for every editable path the Lasso may pick. let mut hits: Vec<(NodeId, BTreeSet)> = vec![]; + let mut inside: Vec<(NodeId, usize)> = vec![]; crate::draw2::walk_pickable(cx, |n| { + if let Some(grid) = grid_of(n) { + inside.extend(grid.points.iter().enumerate().filter(|(_, q)| point_in_polygon(&poly, q.p)).map(|(i, _)| (n.id, i))); + } if let NodeKind::Path { path, .. } = &n.kind { let v: BTreeSet = path.anchors().filter(|(_, _, a)| point_in_polygon(&poly, a.p)).map(|(s, i, _)| (s, i)).collect(); @@ -116,6 +124,14 @@ impl Tool for LassoTool { }); let editable = cx.doc.editable(); hits.retain(|(id, _)| editable(*id) && cx.doc.is_visible(*id)); + // The mesh points inside the loop are picked, and their meshes selected whole; + // Alt deselects a mesh only when it loses its last picked point. + inside.retain(|(id, _)| editable(*id) && cx.doc.is_visible(*id)); + for (id, any) in self.mesh.lasso(&inside, add, sub) { + if any != sub { + hits.push((id, BTreeSet::new())); + } + } let items: Vec = hits.iter().map(|(id, v)| json!({ "id": id.0, "anchors": anchors_json(v) })).collect(); let mode = if sub { "subtract" @@ -138,11 +154,16 @@ impl Tool for LassoTool { vec![] } - fn overlays(&self, _cx: &ToolContext) -> Vec { - if !self.active || self.points.len() < 2 { - return vec![]; + fn options(&self) -> Value { + self.mesh.options() + } + + fn overlays(&self, cx: &ToolContext) -> Vec { + let mut out = self.mesh.overlays(cx, &|id| cx.selection.contains(id)); + if self.active && self.points.len() >= 2 { + out.push(Overlay::Path { path: super::polygon(&self.points, false), color: [0x40, 0x40, 0x40], width: 1.0, dashed: true }); } - vec![Overlay::Path { path: super::polygon(&self.points, false), color: [0x40, 0x40, 0x40], width: 1.0, dashed: true }] + out } fn cursor(&self, _cx: &ToolContext, _p: Point, _m: Mods) -> Cursor { @@ -222,4 +243,44 @@ mod tests { assert_eq!(v["mode"], "set"); assert!(point_in_polygon(&[Point::new(0.0, 0.0), Point::new(10.0, 0.0), Point::new(0.0, 10.0)], Point::new(2.0, 2.0))); } + + /// A Lasso loop round a corner of a path warped by a mesh envelope still selects the path's + /// anchor there, besides picking the envelope's grid point (#5351). + #[test] + fn lasso_keeps_picking_the_anchors_of_an_envelopes_path() { + use std::sync::Arc; + use vectorcraft_doc::Node; + use vectorcraft_doc::live::{EnvelopeKind, EnvelopeOptions, grid_points}; + use vectorcraft_geom::{Affine, Rect}; + let (mut d, a) = doc_with_rect(); + let rect = d.node(a).cloned().unwrap(); + let kind = EnvelopeKind::Mesh { rows: 1, cols: 1, points: grid_points(Rect::new(100.0, 100.0, 200.0, 200.0), 1, 1), handles: vec![] }; + let env = NodeKind::Envelope { + content: vec![Arc::new(rect)], + kind: Box::new(kind), + fidelity: 50.0, + editing: false, + options: EnvelopeOptions::NEW, + frame: Box::new(Affine::IDENTITY), + }; + d.remove(a).unwrap(); + let e = d.alloc_id(); + let l = d.layers[0].id; + d.insert(Some(l), 0, Node::new(e, env)).unwrap(); + let (s, p) = (Selection::default(), paint()); + let cx = cx(&d, &s, &p); + let mut t = LassoTool::default(); + // A triangle around the top-left corner (100,100) only. + t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 80.0, 80.0)); + t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 140.0, 80.0)); + t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 80.0, 140.0)); + let r = t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 80.0, 140.0)); + let Some(Action::Exec(c, v)) = r.first() else { panic!("a selection command: {r:?}") }; + assert_eq!(c, "select.anchorsMany"); + let items = v["items"].as_array().unwrap(); + let path = items.iter().find(|it| it["id"] == a.0).expect("the warped path's anchor is selected"); + assert_eq!(path["anchors"].as_array().unwrap().len(), 1); + assert!(items.iter().any(|it| it["id"] == e.0), "the envelope is selected with its grid point"); + assert_eq!(t.options()["meshPoints"].as_array().unwrap().len(), 1); + } } diff --git a/docs/mcp.md b/docs/mcp.md index 11a4a0941..0ca4e3235 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -6177,7 +6177,8 @@ Envelope Options. The Mesh tool and Direct Selection pick mesh points on gradient meshes and mesh envelopes. A click picks one point (its handles show); Shift-click adds a point or takes a picked one away. Direct Selection's marquee picks the points -inside it (Shift toggles them) and selects their meshes; its click on a gradient mesh's line picks the line's two +inside it (Shift toggles them) and selects their meshes, as does a Lasso loop round them (Shift adds, Alt takes +away); its click on a gradient mesh's line picks the line's two ends, and a click inside a patch picks the four points around it. The picked points show filled. Dragging one of them moves them all as one undo step (`object.mesh.movePoints`), a click on one of several picked points picks it alone, and the arrow keys nudge them by the keyboard increment. The active tool lists them in its options