diff --git a/crates/engine/src/cmd/paint.rs b/crates/engine/src/cmd/paint.rs index 23611224..2bc62004 100644 --- a/crates/engine/src/cmd/paint.rs +++ b/crates/engine/src/cmd/paint.rs @@ -16,7 +16,7 @@ pub fn specs() -> Vec { "Fill", [], None, - "{color?: \"#rrggbb\"|[r,g,b]|{c,m,y,k}|{gray}|{l,a,b} (CIE Lab), none?: true, swatch?: name (a global or spot colour stays linked, so swatch edits recolour it; a tint swatch links to its base at its tint; a gradient swatch is recorded as the gradient's swatch and fits each object, keeping its aspect; the built-in \"[Registration]\" prints on every plate), tint?: 0..100 (% of a global or spot `swatch`; default 100, or a tint swatch's own), gradient?: {kind?: linear|radial|freeform, stops?: [{offset 0..1, color, opacity? 0..1 (or 0..100), midpoint? 0.13..0.87, swatch?: global or spot colour (or tint) swatch the stop links to (its colour comes from the swatch), tint?: 0..100}] (at least 2; default white→black), angle?: deg, start?: [x,y], end?: [x,y] (the vector in document coordinates, both or neither; type objects keep it in text space), aspect?: % (radial; without start/end the gradient is placed on each object's bounds), focal?: [x,y] (radial, with start/end: the focal point, where the first stop sits), swatch?: linked gradient swatch name}, item?: appearance item index|null (omitted: the Appearance panel's active item if it is a fill, else the top fill), ids?, focus?: true (false keeps the active proxy), keepModel?: false (in a CMYK document, RGB colours and gradient stops are stored as CMYK unless true; Gray stays Gray)} sets the selection's fill and the default (new art fits a gradient to itself); while the Type tool has characters selected (and no ids or item are given), the paint goes to those characters only", + "{color?: \"#rrggbb\"|[r,g,b]|{c,m,y,k}|{gray}|{l,a,b} (CIE Lab), none?: true, swatch?: name (a global or spot colour stays linked, so swatch edits recolour it; a tint swatch links to its base at its tint; a gradient swatch is recorded as the gradient's swatch and fits each object, keeping its aspect; the built-in \"[Registration]\" prints on every plate), tint?: 0..100 (% of a global or spot `swatch`; default 100, or a tint swatch's own), gradient?: {kind?: linear|radial|freeform, stops?: [{offset 0..1, color, opacity? 0..1 (or 0..100), midpoint? 0.13..0.87, swatch?: global or spot colour (or tint) swatch the stop links to (its colour comes from the swatch), tint?: 0..100}] (at least 2; default white→black), angle?: deg, start?: [x,y], end?: [x,y] (the vector in document coordinates, both or neither; type objects keep it in text space), aspect?: % (radial; without start/end the gradient is placed on each object's bounds), focal?: [x,y] (radial, with start/end: the focal point, where the first stop sits), swatch?: linked gradient swatch name}, item?: appearance item index|null (omitted: the Appearance panel's active item if it is a fill, else the top fill), ids?, focus?: true (false keeps the active proxy), keepModel?: false (in a CMYK document, RGB colours and gradient stops are stored as CMYK unless true; Gray stays Gray)} sets the selection's fill and the default (new art fits a gradient to itself); a solid colour recolours only the gradient mesh point last clicked with the Mesh tool or Direct Selection; while the Type tool has characters selected (and no ids or item are given), the paint goes to those characters only", has_doc, |s, p| set_paint(s, p, true) ), @@ -407,6 +407,9 @@ fn apply_paint(s: &mut Session, p: &Value, paint: Paint, fill: bool) -> Result Result Result Option<(NodeId, usize)> { + let o = s.tool_options(); + let pt = o.get("meshPoint")?; + Some((NodeId(pt.get("id")?.as_u64()?), usize::try_from(pt.get("index")?.as_u64()?).ok()?)) +} + /// A type run's stroke; a painted stroke on a run without one gets 1 pt. fn set_run_stroke(style: &mut CharStyle, paint: Paint) { if !paint.is_none() { diff --git a/crates/engine/src/tests_editcolors.rs b/crates/engine/src/tests_editcolors.rs index c3413d12..93ef8faa 100644 --- a/crates/engine/src/tests_editcolors.rs +++ b/crates/engine/src/tests_editcolors.rs @@ -3,8 +3,10 @@ use serde_json::{Value, json}; use vectorcraft_color::{Color, Paint}; use vectorcraft_doc::Node; +use vectorcraft_tools::{PointerEvent, PointerKind}; use super::*; +use crate::tooling::ViewInfo; fn session() -> Session { let mut s = Session::new(); @@ -99,6 +101,38 @@ fn edit_colors_reach_mesh_points() { assert_eq!(mesh_colors(&s, m), pts, "a mesh's points are fill colours"); } +/// Clicking a gradient mesh's point with the Mesh tool or Direct Selection picks it: a solid fill +/// colour then recolours that point alone (#1050). +#[test] +fn a_clicked_mesh_point_takes_the_fill_colour() { + let mut s = session(); + let m = mesh(&mut s); + let before = mesh_colors(&s, m); + let v = ViewInfo::default(); + let click = |s: &mut Session, i: usize| { + let NodeKind::Mesh(g) = &s.doc().unwrap().doc.node(m).unwrap().kind else { panic!("not a mesh") }; + let q = g.points[i].p; + for kind in [PointerKind::Down, PointerKind::Up] { + s.pointer(&PointerEvent::new(kind, q.x, q.y), v).unwrap(); + } + }; + // The Mesh tool selects the mesh whose point it clicks. + s.execute("select.none", &json!({})).unwrap(); + s.select_tool("mesh", v).unwrap(); + click(&mut s, 1); + assert_eq!(s.doc().unwrap().selection.objects, [m]); + s.execute("paint.setFill", &json!({"color": "#0000ff"})).unwrap(); + let after = mesh_colors(&s, m); + assert_eq!(after[1].to_hex(), "#0000ff", "the clicked point takes the colour"); + assert_eq!((after[0], after[2], after[3]), (before[0], before[2], before[3]), "the other points keep theirs"); + s.select_tool("directSelection", v).unwrap(); + click(&mut s, 2); + s.execute("paint.setFill", &json!({"color": "#ffff00"})).unwrap(); + let after = mesh_colors(&s, m); + assert_eq!((after[1].to_hex(), after[2].to_hex()), ("#0000ff".to_string(), "#ffff00".to_string())); + assert_eq!((after[0], after[3]), (before[0], before[3])); +} + #[test] fn blend_moves_a_mesh_to_the_blended_colour() { let mut s = session(); diff --git a/crates/tools/src/direct.rs b/crates/tools/src/direct.rs index a8c490b9..739423b2 100644 --- a/crates/tools/src/direct.rs +++ b/crates/tools/src/direct.rs @@ -15,7 +15,8 @@ //! Both pick the key objects of a blend, and click or drag ruler guides ([`crate::rulerguide`]). //! Direct Selection also edits a blend's spine: drag its points (a key object on a point moves //! with it) and, once a point is clicked, its handles; and the points of selected gradient meshes -//! and mesh envelopes and their handles ([`MeshEdit`]). Dragging a corner or an edge of area +//! and mesh envelopes and their handles ([`MeshEdit`]; a solid fill colour recolours a gradient +//! mesh's point last clicked). Dragging a corner or an edge of area //! type's frame reshapes the type area (`text.reshapeArea`): the text reflows at its size. //! Dragging the brackets of selected type on a path moves or flips it ([`crate::pathtype`]). //! @@ -291,6 +292,9 @@ impl Tool for DirectSelectionTool { fn busy(&self) -> bool { !matches!(self.state, State::Idle) || self.guide.busy() } + fn options(&self) -> Value { + self.mesh.options() + } fn pointer(&mut self, cx: &ToolContext, ev: &PointerEvent) -> Vec { if let Some(out) = self.guide.pointer(cx, ev) { return out; diff --git a/crates/tools/src/meshblend.rs b/crates/tools/src/meshblend.rs index 5958b550..e5b747da 100644 --- a/crates/tools/src/meshblend.rs +++ b/crates/tools/src/meshblend.rs @@ -8,7 +8,8 @@ //! the click); click inside a mesh or a mesh envelope → `object.mesh.addLine` (a gradient mesh's //! new point takes the current fill colour unless Shift); drag a mesh point, or a handle of the //! point last clicked → `object.mesh.movePoint` previews ([`MeshEdit`]); Alt-click a mesh point → -//! `object.mesh.deletePoint`. +//! `object.mesh.deletePoint`. Clicking a gradient mesh's point selects the mesh, and the next +//! solid fill colour recolours just that point. use serde_json::json; use vectorcraft_color::Paint; @@ -183,7 +184,14 @@ impl Tool for MeshTool { self.edit.unfocus(); return vec![Action::Exec("object.mesh.deletePoint".into(), json!({"id": g.id.0, "index": g.index}))]; } - return self.edit.press(g); + // A gradient mesh's point is picked for the next fill colour, so its mesh is + // what gets selected. + let mut out = vec![]; + if cx.selection.objects != [g.id] && cx.doc.node(g.id).is_some_and(|n| matches!(n.kind, NodeKind::Mesh(_))) { + out.push(Action::Exec("select.set".into(), json!({"ids": [g.id.0]}))); + } + out.extend(self.edit.press(g)); + return out; } self.edit.unfocus(); let Some((top, leaf)) = hit_top(cx, p) else { return vec![] }; @@ -236,6 +244,9 @@ impl Tool for MeshTool { None => Cursor::PenAdd, } } + fn options(&self) -> serde_json::Value { + self.edit.options() + } fn busy(&self) -> bool { self.edit.dragging() } diff --git a/crates/tools/src/meshedit.rs b/crates/tools/src/meshedit.rs index 3a6a53fe..1af0998c 100644 --- a/crates/tools/src/meshedit.rs +++ b/crates/tools/src/meshedit.rs @@ -1,10 +1,11 @@ //! Dragging mesh points and their handles on the canvas, shared by the Mesh tool and Direct //! Selection: gradient meshes and mesh envelopes alike (`object.mesh.movePoint`). Clicking a -//! point focuses it: its handles show and can be dragged too. +//! point focuses it: its handles show and can be dragged too, and on a gradient mesh a solid fill +//! colour recolours it (the tool's `meshPoint` option). use std::borrow::Cow; -use serde_json::json; +use serde_json::{Value, json}; use vectorcraft_doc::live::{GradientMesh, envelope_grid}; use vectorcraft_doc::{EnvelopeKind, Node, NodeId, NodeKind}; use vectorcraft_geom::Point; @@ -91,6 +92,12 @@ impl MeshEdit { self.focus = None; } + /// `{meshPoint: {id, index}|null}`: the focused point, which a solid fill colour recolours + /// (`paint.setFill` on a gradient mesh). + pub fn options(&self) -> Value { + json!({"meshPoint": self.focus.map(|(id, index)| json!({"id": id.0, "index": index}))}) + } + /// The focused point's handles (lines and ends). pub fn overlays(&self, cx: &ToolContext) -> Vec { let Some((id, index)) = self.focus else { return vec![] };