mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-25 18:27:02 -06:00
Merge pull request #4069 from dotCooCoo/fix/set-paint-object-count
MCP: set_paint reports how many objects it painted
This commit is contained in:
@@ -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, method?: classic|perceptual (as paint.editGradient; default classic), 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), dither?: bool (linear and radial fills: drawn with fine noise that hides banding, and as pixels in SVG and PDF exports; strokes ignore it), 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?|id?, 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), target?: \"object\"|\"contents\", span?: \"object\" (default: each target fits a gradient to its own bounds)|\"selection\" (a linear or radial `gradient` without start/end is placed once on the union of the targets' bounds, so it runs across them)} 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",
|
||||
"{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, method?: classic|perceptual (as paint.editGradient; default classic), 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), dither?: bool (linear and radial fills: drawn with fine noise that hides banding, and as pixels in SVG and PDF exports; strokes ignore it), 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?|id?, 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), target?: \"object\"|\"contents\", span?: \"object\" (default: each target fits a gradient to its own bounds)|\"selection\" (a linear or radial `gradient` without start/end is placed once on the union of the targets' bounds, so it runs across them)} 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 → {objects: how many objects took the paint (the objects inside a group, unless an appearance item of the group's own is targeted; 0 when only the default changed)}",
|
||||
has_doc,
|
||||
|s, p| set_paint(s, p, true)
|
||||
),
|
||||
@@ -59,7 +59,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
Ok(json!({ "fillActive": s.fill_active }))
|
||||
}
|
||||
),
|
||||
cmd!("paint.none", "None", [], Some("/"), "{} set the active proxy (fill or stroke) to None", has_doc, |s, _| {
|
||||
cmd!("paint.none", "None", [], Some("/"), "{} set the active proxy (fill or stroke) to None → {objects}", has_doc, |s, _| {
|
||||
let f = s.fill_active;
|
||||
set_paint(s, &json!({"none": true}), f)
|
||||
}),
|
||||
@@ -101,7 +101,7 @@ pub fn proxy_specs() -> Vec<CommandSpec> {
|
||||
"Apply Last Color",
|
||||
[],
|
||||
Some(","),
|
||||
"{stroke?: bool (default: the active proxy), ids?|id?, item?: appearance item index|null, target?: \"object\"|\"contents\"} apply the last solid colour used (paint.recent lastColor) to the active proxy of the selection and the default",
|
||||
"{stroke?: bool (default: the active proxy), ids?|id?, item?: appearance item index|null, target?: \"object\"|\"contents\"} apply the last solid colour used (paint.recent lastColor) to the active proxy of the selection and the default → {objects} (as paint.setFill)",
|
||||
has_doc,
|
||||
|s, p| {
|
||||
let paint = Paint::solid(s.last_solid);
|
||||
@@ -113,7 +113,7 @@ pub fn proxy_specs() -> Vec<CommandSpec> {
|
||||
"Apply Last Gradient",
|
||||
[],
|
||||
Some("."),
|
||||
"{stroke?: bool (default: the active proxy), ids?|id?, item?: appearance item index|null, target?: \"object\"|\"contents\"} apply the last gradient used (paint.recent lastGradient), fitted to each object, to the active proxy of the selection and the default",
|
||||
"{stroke?: bool (default: the active proxy), ids?|id?, item?: appearance item index|null, target?: \"object\"|\"contents\"} apply the last gradient used (paint.recent lastGradient), fitted to each object, to the active proxy of the selection and the default → {objects} (as paint.setFill)",
|
||||
has_doc,
|
||||
|s, p| {
|
||||
let paint = super::gradient::unplaced(&Paint::Gradient(Box::new(s.last_gradient.clone())));
|
||||
@@ -419,7 +419,7 @@ fn apply_to_proxy(s: &mut Session, p: &Value, paint: Paint, cmd: &str) -> Result
|
||||
|
||||
/// Set the fill (or stroke) of the targets of `p` (the `item` and `ids` params, see
|
||||
/// `paint.setFill`) and of the defaults for new art, focus its proxy unless `focus` is false, and
|
||||
/// remember the paint.
|
||||
/// remember the paint. Replies `{objects}`: how many objects took the paint.
|
||||
fn apply_paint(s: &mut Session, p: &Value, paint: Paint, fill: bool) -> Result<Value> {
|
||||
let cmd = if fill { "paint.setFill" } else { "paint.setStroke" };
|
||||
let focus = switch_or(p, "focus", true, cmd)?;
|
||||
@@ -452,7 +452,7 @@ fn apply_paint(s: &mut Session, p: &Value, paint: Paint, fill: bool) -> Result<V
|
||||
s.fill_active = fill;
|
||||
}
|
||||
s.remember_paint(&paint);
|
||||
return ok();
|
||||
return Ok(json!({"objects": 1}));
|
||||
}
|
||||
// Resolve the requested targets before mutating defaults or the active proxy.
|
||||
let item = item_target(s, p, cmd)?.of_kind(s, fill);
|
||||
@@ -535,7 +535,7 @@ fn apply_paint(s: &mut Session, p: &Value, paint: Paint, fill: bool) -> Result<V
|
||||
s.fill_active = fill;
|
||||
}
|
||||
s.remember_paint(&paint);
|
||||
ok()
|
||||
Ok(json!({"objects": ids.iter().collect::<std::collections::BTreeSet<_>>().len()}))
|
||||
}
|
||||
|
||||
/// Does `p` ask for `span: "selection"` (#2127)? Only a linear or radial gradient the targets
|
||||
|
||||
@@ -20,7 +20,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Stroke Options",
|
||||
["Window", "Stroke"],
|
||||
None,
|
||||
"{weight?: pt, cap?: butt|round|square, join?: miter|round|bevel, miterLimit?, align?: center|inside|outside, dash?: [d,g,…]|null (a 0 dash with a round or projecting cap draws dots or squares; a new pattern keeps the current offset and alignment), dashOffset? (exact dashes only), alignDashes?: bool (true: dashes fitted to corners and path ends, every run between them holding whole periods with a dash centred on each corner and end; false, the default for a new pattern: exact lengths), startArrow?, endArrow?: Arrow|ArrowOpen|Barbed|Concave|DoubleArrow|HalfArrowLeft|HalfArrowRight|Chevron|Feather|Swallowtail|Triangle|TriangleOpen|TriangleReverse|TriangleBar|Kite|Leaf|Drop|Circle|CircleOpen|HalfCircle|Oval|Target|Square|SquareOpen|Tag|TagOpen|Diamond|DiamondOpen|Hexagon|HexagonOpen|Star|Cross|Plus|Bar|DoubleBar|DotOnBar|Slash|DoubleSlash|Bracket|Fork|null (HalfArrowLeft/Right: one barb, left or right of the direction the head points), arrowAlign?: \"extend\" (tip past the end point, default)|\"tip\" (tip on the end point; the stroke is shortened), profile?: \"uniform\"|\"lens\"|\"taperStart\"|\"taperEnd\"|\"pinch\"|\"teardrop\"|\"wave\"|a saved profile's name (stroke.widthProfile.list), item?: stroke item index|null (omitted: the Appearance panel's active item if it is a stroke, else the top stroke, created when missing), ids?|id?, target?: \"object\"|\"contents\"} Without a stroke item, type takes weight, cap, join, miterLimit, align (around its glyph outlines; the type stays live) and the dash options as its characters' stroke (every run; text.setRangeStyle `strokeOptions` styles a range), and images and symbol instances (also in groups) are left alone. With nothing selected (and no ids) they set up the next object drawn (appearance.newArt; weight always does)",
|
||||
"{weight?: pt, cap?: butt|round|square, join?: miter|round|bevel, miterLimit?, align?: center|inside|outside, dash?: [d,g,…]|null (a 0 dash with a round or projecting cap draws dots or squares; a new pattern keeps the current offset and alignment), dashOffset? (exact dashes only), alignDashes?: bool (true: dashes fitted to corners and path ends, every run between them holding whole periods with a dash centred on each corner and end; false, the default for a new pattern: exact lengths), startArrow?, endArrow?: Arrow|ArrowOpen|Barbed|Concave|DoubleArrow|HalfArrowLeft|HalfArrowRight|Chevron|Feather|Swallowtail|Triangle|TriangleOpen|TriangleReverse|TriangleBar|Kite|Leaf|Drop|Circle|CircleOpen|HalfCircle|Oval|Target|Square|SquareOpen|Tag|TagOpen|Diamond|DiamondOpen|Hexagon|HexagonOpen|Star|Cross|Plus|Bar|DoubleBar|DotOnBar|Slash|DoubleSlash|Bracket|Fork|null (HalfArrowLeft/Right: one barb, left or right of the direction the head points), arrowAlign?: \"extend\" (tip past the end point, default)|\"tip\" (tip on the end point; the stroke is shortened), profile?: \"uniform\"|\"lens\"|\"taperStart\"|\"taperEnd\"|\"pinch\"|\"teardrop\"|\"wave\"|a saved profile's name (stroke.widthProfile.list), item?: stroke item index|null (omitted: the Appearance panel's active item if it is a stroke, else the top stroke, created when missing), ids?|id?, target?: \"object\"|\"contents\"} Without a stroke item, type takes weight, cap, join, miterLimit, align (around its glyph outlines; the type stays live) and the dash options as its characters' stroke (every run; text.setRangeStyle `strokeOptions` styles a range), and images and symbol instances (also in groups) are left alone. With nothing selected (and no ids) they set up the next object drawn (appearance.newArt; weight always does) → {objects: how many objects took the stroke options}",
|
||||
has_doc,
|
||||
stroke_set
|
||||
),
|
||||
@@ -258,7 +258,7 @@ fn stroke_set(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if new_art && let Some(st) = s.new_art_stroke_mut() {
|
||||
set(st);
|
||||
}
|
||||
ok()
|
||||
Ok(json!({"objects": ids.iter().collect::<std::collections::BTreeSet<_>>().len()}))
|
||||
}
|
||||
|
||||
impl Session {
|
||||
|
||||
@@ -449,6 +449,63 @@ fn inspect_document_reports_the_paint_of_type() {
|
||||
assert_eq!((t["fill"].as_str(), t["stroke"].as_str(), t["strokeWidth"].as_f64()), (Some("#0000ff"), Some("#00ff00"), Some(3.0)), "{t}");
|
||||
}
|
||||
|
||||
/// set_paint says how many objects it painted, and says so when it only set the paint for new art.
|
||||
#[test]
|
||||
fn set_paint_reports_how_many_objects_it_painted() {
|
||||
let mut s = server();
|
||||
let r = call(&mut s, 2, "draw_shape", json!({"shape": "rectangle", "x": 0, "y": 0, "width": 10, "height": 10, "fill": "#ffffff"}));
|
||||
let id = serde_json::from_str::<Value>(&text_of(&r)).unwrap()["id"].clone();
|
||||
let fill = |s: &mut Server, n: u64| {
|
||||
let v: Value = serde_json::from_str(&text_of(&call(s, n, "inspect_document", json!({})))).unwrap();
|
||||
v["layers"][0]["children"].as_array().unwrap().iter().find(|c| c["id"] == id).unwrap()["fill"].clone()
|
||||
};
|
||||
let paint = |s: &mut Server, n: u64, args: Value| {
|
||||
let r = call(s, n, "set_paint", args);
|
||||
assert_eq!(r["isError"], false, "{r}");
|
||||
serde_json::from_str::<Value>(&text_of(&r)).unwrap()
|
||||
};
|
||||
|
||||
// Nothing selected and no ids: only the paint for new art changes.
|
||||
let r = call(&mut s, 3, "run_command", json!({"command": "select.set", "params": {"ids": []}}));
|
||||
assert_eq!(r["isError"], false, "{r}");
|
||||
let v = paint(&mut s, 4, json!({"fill": "#ff0000"}));
|
||||
assert_eq!(v["applied"], json!({"fill": "#ff0000"}), "{v}");
|
||||
assert_eq!(v["objects"], 0, "{v}");
|
||||
assert!(v["note"].as_str().is_some_and(|n| n.contains("new art")), "{v}");
|
||||
assert_eq!(fill(&mut s, 5), "#ffffff");
|
||||
|
||||
// The ids given, counted once each.
|
||||
let v = paint(&mut s, 6, json!({"fill": "#00ff00", "ids": [id, id]}));
|
||||
assert_eq!(v["objects"], 1, "{v}");
|
||||
assert!(v.get("note").is_none(), "{v}");
|
||||
assert_eq!(fill(&mut s, 7), "#00ff00");
|
||||
|
||||
// The selection.
|
||||
let r = call(&mut s, 8, "run_command", json!({"command": "select.set", "params": {"ids": [id]}}));
|
||||
assert_eq!(r["isError"], false, "{r}");
|
||||
let v = paint(&mut s, 9, json!({"fill": "#0000ff"}));
|
||||
assert_eq!(v["objects"], 1, "{v}");
|
||||
assert!(v.get("note").is_none(), "{v}");
|
||||
assert_eq!(fill(&mut s, 10), "#0000ff");
|
||||
|
||||
// Empty `ids` paint no object: only the paint for new art changes.
|
||||
let v = paint(&mut s, 11, json!({"strokeWidth": 3, "ids": []}));
|
||||
assert_eq!(v["objects"], 0, "{v}");
|
||||
assert!(v["note"].as_str().is_some_and(|n| n.contains("new art")), "{v}");
|
||||
|
||||
// A selected group counts the objects inside it.
|
||||
let r = call(&mut s, 12, "draw_shape", json!({"shape": "ellipse", "x": 20, "y": 0, "width": 10, "height": 10}));
|
||||
assert_eq!(r["isError"], false, "{r}");
|
||||
for (n, command) in [(13, "select.all"), (14, "object.group")] {
|
||||
let r = call(&mut s, n, "run_command", json!({"command": command}));
|
||||
assert_eq!(r["isError"], false, "{r}");
|
||||
}
|
||||
let v = paint(&mut s, 15, json!({"stroke": "#000000"}));
|
||||
assert_eq!(v["objects"], 2, "{v}");
|
||||
let v = paint(&mut s, 16, json!({"strokeWidth": 2}));
|
||||
assert_eq!(v["objects"], 2, "{v}");
|
||||
}
|
||||
|
||||
/// #1003: type made on or in a path takes add_text's `color` too, over the current fill.
|
||||
#[test]
|
||||
fn add_text_on_or_in_a_path_takes_its_color() {
|
||||
|
||||
+17
-6
@@ -290,7 +290,7 @@ pub fn tool_definitions() -> Vec<Value> {
|
||||
tool(
|
||||
"set_paint",
|
||||
"Set paint",
|
||||
"Set fill, stroke and/or stroke weight on the selection (or `ids`) and as the default for new art.",
|
||||
"Set fill, stroke and/or stroke weight on the selection (or `ids`) and as the default for new art. Returns {applied, objects}: `objects` counts the objects painted, as paint.setFill, paint.setStroke and stroke.set report them (the largest of their counts); when it is 0, a `note` says that only the default for new art changed.",
|
||||
obj(
|
||||
json!({
|
||||
"fill": paint_schema("Fill"),
|
||||
@@ -590,15 +590,20 @@ fn paint_steps(a: &Args) -> Result<(Vec<Step<'static>>, Map<String, Value>), Str
|
||||
Ok((steps, applied))
|
||||
}
|
||||
|
||||
fn apply_paint(b: &mut dyn Backend, a: &Args, ids: Option<Value>) -> Result<Value, String> {
|
||||
/// Sets the paint `a` gives on `ids` (default: the selection) and as the default for new art.
|
||||
/// Returns the paint applied and the most objects one of its edits painted (the `objects` of the
|
||||
/// `paint.setFill`, `paint.setStroke` and `stroke.set` replies).
|
||||
fn apply_paint(b: &mut dyn Backend, a: &Args, ids: Option<Value>) -> Result<(Value, u64), String> {
|
||||
let (steps, applied) = paint_steps(a)?;
|
||||
let mut objects = 0;
|
||||
for (cmd, mut p) in steps {
|
||||
if let Some(ids) = &ids {
|
||||
p["ids"] = ids.clone();
|
||||
}
|
||||
exec(b, cmd, p)?;
|
||||
let r = exec(b, cmd, p)?;
|
||||
objects = objects.max(r.get("objects").and_then(Value::as_u64).unwrap_or(0));
|
||||
}
|
||||
Ok(Value::Object(applied))
|
||||
Ok((Value::Object(applied), objects))
|
||||
}
|
||||
|
||||
/// Checks `draw_path` / `draw_shape`'s `dash`, `cap`, `join`, `opacity`, `name` and `effects`
|
||||
@@ -1063,8 +1068,14 @@ fn dispatch(b: &mut dyn Backend, name: &str, a: &Args) -> Result<ToolResult, Str
|
||||
if !["fill", "stroke", "strokeWidth"].iter().any(|k| a.contains_key(*k)) {
|
||||
return Err("give at least one of `fill`, `stroke`, `strokeWidth`".into());
|
||||
}
|
||||
let ids = a.get("ids").cloned();
|
||||
j(json!({"applied": apply_paint(b, a, ids)?}))
|
||||
let (applied, objects) = apply_paint(b, a, a.get("ids").cloned())?;
|
||||
let mut reply = json!({"applied": applied, "objects": objects});
|
||||
if objects == 0 {
|
||||
reply["note"] = json!(
|
||||
"no object was painted (nothing selected, or `ids` is empty or holds no object this paint applies to): only the paint for new art changed"
|
||||
);
|
||||
}
|
||||
j(reply)
|
||||
}
|
||||
"press_key" => {
|
||||
let mut p = json!({"key": req_str(a, "key")?});
|
||||
|
||||
+1
-1
@@ -413,7 +413,7 @@ objects' fills or strokes differ (`fillMixed` / `strokeMixed`, drawn as a "?" pr
|
||||
| `pointer_gesture` | `{events:[{kind,x,y,mods?}], tool?, mods?}` | `kind` is one of `down`, `drag`, `up`, `move`, `doubleclick`. Events go through the same path as the mouse. Headless, a `move` replies after the tool's highlight under the pointer is ready, including the Shape Builder's and the Live Paint Bucket's regions of a large selection, which the desktop app finds off the UI thread and highlights after the reply. |
|
||||
| `draw_path` | `{points \| d, closed?, fill?, stroke?, strokeWidth?, dash?, cap?, join?, opacity?, name?, effects?}` | `points` is `[[x,y],…]` or `[{x,y,in?,out?,smooth?},…]`. `d` is SVG path data. The options are below. |
|
||||
| `draw_shape` | `{shape, …geometry, fill?, stroke?, strokeWidth?, dash?, cap?, join?, opacity?, name?, effects?}` | `rectangle`/`ellipse`: `x,y,width,height` (plus `radius` for corners). `polygon`: `cx,cy,radius,sides`. `star`: `cx,cy,radius1,radius2,points`. `line`: `x1,y1,x2,y2`. |
|
||||
| `set_paint` | `{fill?, stroke?, strokeWidth?, ids?}` | Applies to the selection (or `ids`) and becomes the default for new art. |
|
||||
| `set_paint` | `{fill?, stroke?, strokeWidth?, ids?}` | Applies to the selection (or `ids`) and becomes the default for new art. Returns `{applied, objects}`: `objects` is the number of objects painted, as the `objects` of the `paint.setFill`, `paint.setStroke` and `stroke.set` replies (the largest of them): a selected group counts the objects inside it. When it is 0, a `note` says that only the paint for new art changed. |
|
||||
| `press_key` | `{key, mods?}` | Remote: a real key event. Headless: runs the command or tool bound to that shortcut, or sends the key to the busy tool (digits too: `5` while dragging with the Perspective Selection tool; `e` while dragging a Selection or Group Selection marquee). |
|
||||
| `type_text` | `{text}` | Remote only. |
|
||||
| `invoke_menu` | `{command, params?}` | Invokes a menu item by command id. Includes UI commands such as `view.*` and `window.*` in remote mode. |
|
||||
|
||||
Reference in New Issue
Block a user