Gradient mesh: a fill colour recolours the point last clicked (#1050)

This commit is contained in:
MarekSpodBiedry 2026-10-10 23:44:02 +02:00 committed by bflatastic
parent a785b53e7b
commit 1217d58ca1
5 changed files with 82 additions and 6 deletions

View File

@ -16,7 +16,7 @@ pub fn specs() -> Vec<CommandSpec> {
"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<V
let item = item_target(s, p, cmd)?.of_kind(s, fill);
let selected = item.targets(s, p)?;
let ids = super::graph::paint_targets(&s.doc()?.doc, &selected);
// The gradient mesh point last clicked with the Mesh tool or Direct Selection: a solid fill
// recolours just that point of its mesh.
let mesh_point = if fill && matches!(paint, Paint::Solid { .. }) { focused_mesh_point(s) } else { None };
let label = if fill { "Fill Color" } else { "Stroke Color" };
edit_items_then(
s,
@ -416,6 +419,15 @@ fn apply_paint(s: &mut Session, p: &Value, paint: Paint, fill: bool) -> Result<V
label,
fill,
|n, index| {
if index.is_none()
&& let Paint::Solid { color, .. } = &paint
&& let Some((_, i)) = mesh_point.filter(|(id, _)| *id == n.id)
&& let NodeKind::Mesh(m) = &mut n.kind
&& let Some(q) = m.points.get_mut(i)
{
q.color = *color;
return Ok(());
}
if index.is_none()
&& let NodeKind::Text(t) = &mut n.kind
{
@ -457,6 +469,14 @@ fn apply_paint(s: &mut Session, p: &Value, paint: Paint, fill: bool) -> Result<V
ok()
}
/// The mesh point the active tool has focused (its `meshPoint` option: the Mesh tool's or Direct
/// Selection's point last clicked).
fn focused_mesh_point(s: &Session) -> 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() {

View File

@ -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();

View File

@ -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<Action> {
if let Some(out) = self.guide.pointer(cx, ev) {
return out;

View File

@ -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()
}

View File

@ -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<Overlay> {
let Some((id, index)) = self.focus else { return vec![] };