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

This commit is contained in:
MarekSpodBiedry
2026-10-11 07:13:33 +07:00
committed by bflatastic
parent a785b53e7b
commit 1217d58ca1
5 changed files with 82 additions and 6 deletions
+21 -1
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() {
+34
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();