mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-15 13:36:53 +00:00
MCP: livePaint.info lists every appearance row of a face or edge
livePaint.info reported only the top fill of each face and the top
stroke and weight of each edge, so a lower row that draws under an
unpainted top row was not visible to a client. Each face and edge now
also carries `appearance`: every fill and stroke row, bottom first, as
{index, kind, paint, opacity, visible, width (strokes only), drawn}.
`drawn` follows Expand's rule row by row: the part is shown with an
opacity above 0, and the row is shown, has paint and an opacity above
0, and a stroke has a weight above 0. The existing `fill`, `stroke`,
`width`, `bounds`, `sources`, `gapOptions` and `gaps` fields are
unchanged.
The params text no longer says the group defaults to the selection:
the command takes `group`, or `id` when `group` is left out, and
returns an error without either.
This commit is contained in:
@@ -135,7 +135,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Live Paint Info",
|
||||
[],
|
||||
None,
|
||||
"{group, id?: object id (default: the selection)} → {faces: [{id, fill, bounds}], edges: [{id, stroke, width}], sources, gapOptions: {detect, size: \"small\"|\"medium\"|\"large\"|pt, points, color: #rrggbb} (as livePaint.gapOptions sets them), gaps: [[x0,y0,x1,y1]…] (as livePaint.gapOptions returns them)}",
|
||||
"{group?|id?: Live Paint group id (one required; id is read when group is left out)} → {faces: [{id, fill (top fill), bounds, appearance: [row…]}], edges: [{id, stroke (top stroke), width (top stroke's), appearance: [row…]}], where a row is {index (in the part's fill and stroke stack, bottom first), kind: \"fill\"|\"stroke\", paint, opacity: 0..1, visible, width?: pt (strokes only), drawn (the part is shown with opacity above 0, and the row is shown, has paint and opacity above 0, a stroke also width above 0)}, sources, gapOptions: {detect, size: \"small\"|\"medium\"|\"large\"|pt, points, color: #rrggbb} (as livePaint.gapOptions sets them), gaps: [[x0,y0,x1,y1]…] (as livePaint.gapOptions returns them)}",
|
||||
has_doc,
|
||||
lp_info
|
||||
),
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use super::{
|
||||
Appearance, Arc, Beside, Document, EDGE_NAME, EngineError, FACE_NAME, GapOptions, GapSize, LIVE_PAINT_NAME, Node, NodeId, PathData, Point, Rect,
|
||||
Result, SOURCES_NAME, Selection, Session, SubPath, Value, b, bad, build_children, edge_near, face_at, finite, ids_json, is_live_paint, json,
|
||||
number_or, object_param, paint_from, po, point_param, repaint_from, replace_roots, root_ids, source_nodes, source_shapes, switch_or,
|
||||
Appearance, AppearanceItem, Arc, Beside, Document, EDGE_NAME, EngineError, FACE_NAME, GapOptions, GapSize, LIVE_PAINT_NAME, Node, NodeId,
|
||||
PathData, Point, Rect, Result, SOURCES_NAME, Selection, Session, SubPath, Value, b, bad, build_children, edge_near, face_at, finite, ids_json,
|
||||
is_live_paint, json, number_or, object_param, paint_from, po, point_param, repaint_from, replace_roots, root_ids, source_nodes, source_shapes,
|
||||
switch_or,
|
||||
};
|
||||
|
||||
/// `options` as stored on a Live Paint group: none for the defaults.
|
||||
@@ -524,6 +525,37 @@ pub(super) fn lp_stroke_edge(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Ok(json!({ "group": g.0, "edge": edge.0 }))
|
||||
}
|
||||
|
||||
/// Every fill and stroke row of face or edge `n`, bottom first, as `livePaint.info` lists them. A
|
||||
/// row is `drawn` when the part is shown with an opacity above 0 and the row is shown, has paint
|
||||
/// and has an opacity above 0 (a stroke also a weight above 0): the rule of [`draws`], row by row.
|
||||
fn appearance_rows(n: &Node) -> Vec<Value> {
|
||||
let part_shown = n.visible && n.opacity > 0.0;
|
||||
n.appearance
|
||||
.items
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, item)| {
|
||||
let shown = part_shown && item.visible() && !item.paint().is_none() && item.opacity() > 0.0;
|
||||
let mut row = json!({
|
||||
"index": i,
|
||||
"kind": item.kind_name(),
|
||||
"paint": item.paint().label(),
|
||||
"opacity": item.opacity(),
|
||||
"visible": item.visible(),
|
||||
});
|
||||
let drawn = match item {
|
||||
AppearanceItem::Fill(_) => shown,
|
||||
AppearanceItem::Stroke(st) => {
|
||||
row["width"] = json!(st.width);
|
||||
shown && st.width > 0.0
|
||||
}
|
||||
};
|
||||
row["drawn"] = json!(drawn);
|
||||
row
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(super) fn lp_info(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let g = match object_param(p, "group", "livePaint.info")? {
|
||||
Some(g) => g,
|
||||
@@ -535,12 +567,24 @@ pub(super) fn lp_info(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
.iter()
|
||||
.map(|f| {
|
||||
let bb = f.geometric_bounds().unwrap_or(Rect::ZERO);
|
||||
json!({ "id": f.id.0, "fill": f.appearance.fill_paint().label(), "bounds": [bb.x0, bb.y0, bb.x1, bb.y1] })
|
||||
json!({
|
||||
"id": f.id.0,
|
||||
"fill": f.appearance.fill_paint().label(),
|
||||
"bounds": [bb.x0, bb.y0, bb.x1, bb.y1],
|
||||
"appearance": appearance_rows(f),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let edges: Vec<Value> = b::edges(n)
|
||||
.iter()
|
||||
.map(|e| json!({ "id": e.id.0, "stroke": e.appearance.stroke_paint().label(), "width": e.appearance.stroke_width() }))
|
||||
.map(|e| {
|
||||
json!({
|
||||
"id": e.id.0,
|
||||
"stroke": e.appearance.stroke_paint().label(),
|
||||
"width": e.appearance.stroke_width(),
|
||||
"appearance": appearance_rows(e),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let sources = b::sources(n).and_then(|g| g.children()).map(|c| c.len()).unwrap_or(0);
|
||||
let options = n.gap_options();
|
||||
|
||||
@@ -3018,6 +3018,76 @@ fn live_paint_expand_keeps_the_faces_and_edges_that_draw() {
|
||||
assert_eq!(rows(&n, lower_stroke), rows(&before, lower_stroke));
|
||||
}
|
||||
|
||||
/// `livePaint.info` lists every fill and stroke row of each face and edge, bottom first, and says
|
||||
/// which rows draw by Expand's rule, while `fill`, `stroke` and `width` stay the top rows.
|
||||
#[test]
|
||||
fn live_paint_info_lists_every_appearance_row() {
|
||||
use vectorcraft_doc::{FillLayer, StrokeLayer};
|
||||
let fill = |p: Paint| AppearanceItem::Fill(FillLayer::new(p));
|
||||
let stroke = |p: Paint, w: f64| AppearanceItem::Stroke(StrokeLayer::new(p, w));
|
||||
let red = || Paint::solid(Color::rgb(1.0, 0.0, 0.0));
|
||||
let blue = || Paint::solid(Color::rgb(0.0, 0.0, 1.0));
|
||||
let green = || Paint::solid(Color::rgb(0.0, 1.0, 0.0));
|
||||
let hidden = |mut i: AppearanceItem| {
|
||||
match &mut i {
|
||||
AppearanceItem::Fill(f) => f.visible = false,
|
||||
AppearanceItem::Stroke(st) => st.visible = false,
|
||||
}
|
||||
i
|
||||
};
|
||||
let faded = |mut i: AppearanceItem, o: f32| {
|
||||
match &mut i {
|
||||
AppearanceItem::Fill(f) => f.opacity = o,
|
||||
AppearanceItem::Stroke(st) => st.opacity = o,
|
||||
}
|
||||
i
|
||||
};
|
||||
let mut s = session();
|
||||
let g = live(&mut s);
|
||||
// A drawn lower fill and a half-opaque stroke under an unpainted top fill.
|
||||
let lower = set_live_part(&mut s, g, Some((25.0, 50.0)), None, vec![fill(red()), faded(stroke(blue(), 2.0), 0.5), fill(Paint::None)], 1.0);
|
||||
let quiet = set_live_part(&mut s, g, Some((125.0, 50.0)), None, vec![hidden(fill(red())), faded(fill(red()), 0.0)], 1.0);
|
||||
let bare = set_live_part(&mut s, g, Some((75.0, 50.0)), None, vec![], 1.0);
|
||||
// Two strokes with a fill between them.
|
||||
let two = set_live_part(&mut s, g, None, Some((0.0, 50.0)), vec![stroke(red(), 4.0), fill(green()), stroke(blue(), 2.0)], 1.0);
|
||||
let thin = set_live_part(&mut s, g, None, Some((100.0, 50.0)), vec![stroke(red(), 0.0)], 1.0);
|
||||
let clear_edge = set_live_part(&mut s, g, None, Some((75.0, 0.0)), vec![stroke(red(), 2.0)], 0.0);
|
||||
let hidden_edge = set_live_part(&mut s, g, None, Some((150.0, 50.0)), vec![stroke(red(), 2.0)], 1.0);
|
||||
s.edit("t", |d, _| {
|
||||
d.node_mut(hidden_edge).unwrap().visible = false;
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
let parts = [lower, quiet, bare, two, thin, clear_edge, hidden_edge];
|
||||
assert_eq!(parts.iter().collect::<std::collections::HashSet<_>>().len(), parts.len());
|
||||
|
||||
let info = s.execute("livePaint.info", &json!({"group": g.0})).unwrap();
|
||||
let part = |key: &str, id: NodeId| info[key].as_array().unwrap().iter().find(|p| p["id"] == id.0).unwrap().clone();
|
||||
let fill_row = |i: usize, p: Paint, o: f64, v: bool, drawn: bool| json!({"index": i, "kind": "fill", "paint": p.label(), "opacity": o, "visible": v, "drawn": drawn});
|
||||
let stroke_row = |i: usize, p: Paint, o: f64, v: bool, w: f64, drawn: bool| json!({"index": i, "kind": "stroke", "paint": p.label(), "opacity": o, "visible": v, "width": w, "drawn": drawn});
|
||||
|
||||
let f = part("faces", lower);
|
||||
assert_eq!(f["fill"], json!(Paint::None.label()), "fill stays the top fill");
|
||||
assert!(f["bounds"].is_array());
|
||||
assert_eq!(
|
||||
f["appearance"],
|
||||
json!([fill_row(0, red(), 1.0, true, true), stroke_row(1, blue(), 0.5, true, 2.0, true), fill_row(2, Paint::None, 1.0, true, false)])
|
||||
);
|
||||
assert_eq!(part("faces", quiet)["appearance"], json!([fill_row(0, red(), 1.0, false, false), fill_row(1, red(), 0.0, true, false)]));
|
||||
assert_eq!(part("faces", bare)["appearance"], json!([]));
|
||||
|
||||
let e = part("edges", two);
|
||||
assert_eq!((e["stroke"].clone(), e["width"].clone()), (json!(blue().label()), json!(2.0)), "stroke and width stay the top stroke's");
|
||||
assert_eq!(
|
||||
e["appearance"],
|
||||
json!([stroke_row(0, red(), 1.0, true, 4.0, true), fill_row(1, green(), 1.0, true, true), stroke_row(2, blue(), 1.0, true, 2.0, true)])
|
||||
);
|
||||
assert_eq!(part("edges", thin)["appearance"], json!([stroke_row(0, red(), 1.0, true, 0.0, false)]));
|
||||
for id in [clear_edge, hidden_edge] {
|
||||
assert_eq!(part("edges", id)["appearance"], json!([stroke_row(0, red(), 1.0, true, 2.0, false)]), "a part that is not shown draws no row");
|
||||
}
|
||||
}
|
||||
|
||||
/// Merge carries a face's whole appearance when one of its rows draws, and carries nothing from a
|
||||
/// face that draws nothing.
|
||||
#[test]
|
||||
|
||||
+8
-1
@@ -3603,7 +3603,7 @@ already walks Image Trace. Short agent entry points:
|
||||
is changed, renamed or deleted. In the app,
|
||||
`ui.imageTracePresetDialog {params?, rename?}` opens the Image Trace panel's Save as New Preset… or Rename… dialog.
|
||||
- Live Paint: `livePaint.make`, `livePaint.merge`, `livePaint.fill {group?, point}`, `livePaint.strokeEdge`,
|
||||
`livePaint.info {group}`, `livePaint.release`, `livePaint.expand`, and `livePaint.gapOptions {ids?, detect?, size?,
|
||||
`livePaint.info {group?|id?}`, `livePaint.release`, `livePaint.expand`, and `livePaint.gapOptions {ids?, detect?, size?,
|
||||
color?, closeGaps?}` (Object › Live Paint › Gap Options). Make gives each covered face the fill of its front-most
|
||||
covering source, including compound paths with shared curved boundaries. Expand keeps those face fills.
|
||||
`livePaint.expand` keeps each face and edge that draws, with all its fill and stroke rows: it is shown, its opacity
|
||||
@@ -3612,6 +3612,13 @@ already walks Image Trace. Short agent entry points:
|
||||
row is hidden is removed. `livePaint.merge` copies the fill and stroke rows, visibility, opacity and blending mode of
|
||||
each face that draws by this rule, and of each edge, to the rebuilt parts, with their gradients kept at their fit on
|
||||
the old outline and without the strokes' pen pressure, tilt, bearing and rotation.
|
||||
`livePaint.info` lists each face's top fill as `fill` and each edge's top stroke and its weight as `stroke` and
|
||||
`width`, and lists every fill and stroke row of each face and edge in `appearance`, bottom first, as
|
||||
`{index, kind, paint, opacity, visible, width?, drawn}`: `index` counts fills and strokes together, `opacity` runs
|
||||
from 0 to 1, and `width` (strokes only) is the weight in points. `drawn` is true when the row draws by the rule
|
||||
above: the part is shown with an opacity above 0, and the row is shown, has paint and has an opacity above 0 (a
|
||||
stroke also a weight above 0). An unpainted top row can sit over a lower row that draws, so `fill`, `stroke` and
|
||||
`width` alone may not describe what the part draws.
|
||||
With `detect: true`, paint stops at the gaps that run
|
||||
from a loose end of a path to the nearest path, up to `size` wide (`small` 3 pt, `medium` 6 pt, `large` 12 pt, or a
|
||||
number of points up to 72); `closeGaps: true` adds an unpainted path across each of them. `color` (the Gap Preview
|
||||
|
||||
@@ -1496,6 +1496,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
Live Paint Bucket Options has Cursor Swatch Preview, as the [help page](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/learn-painting-basics/paint-with-the-live-paint-bucket-tool.html) lists: with the Bucket selected, the active fill or stroke shows above the pointer between the color swatches before and after it in the Swatches panel, and the Left and Right arrow keys pick the swatch before or after it, as the page describes. The selected art keeps its paint. As VectorCraft's own rules, the option is on at first, the arrows step the stroke when the Stroke proxy is in front, they wrap around at the ends and still step with the preview off, a color that is no swatch shows alone until Right picks the first swatch or Left the last, and the arrows do nothing during a fill drag or with no color swatch. Evidence: `tests::build::{live_paint_bucket_arrow_keys_step_the_active_paint_through_the_swatches, live_paint_bucket_arrow_keys_handle_unmatched_paint_few_swatches_and_drags, live_paint_bucket_arrow_keys_pass_swatches_of_the_same_color_for_fill_and_stroke, swatch_arrow_keys_leave_modified_arrows_to_the_shortcuts}`.
|
||||
Live Paint Make preserves the front-most covering source's fill when compound paths share a curved boundary, and Expand keeps the face fills. Regression evidence: `planar::tests::coincident_curves_keep_their_source_faces` checks source order, winding, fill rules and nearby boundaries; `tests::build::live_paint_make_and_expand_keep_abutting_compound_colors` checks native compound paths, Make, Expand and Undo/Redo. This preserves VectorCraft's source-fill rule without changing arrangement precision.
|
||||
Object › Live Paint › Expand keeps the faces and edges that draw on the canvas, with all their fill and stroke rows, so the expanded paths look as the painted group did ([Create Live Paint groups](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/learn-painting-basics/create-live-paint-groups.html)). As VectorCraft's own rule, a face or edge draws when it is shown, its opacity is above 0, and any of its fill or stroke rows is shown, has paint and an opacity above 0 (a stroke also needs a weight above 0), whichever row is on top: a painted lower row under an unpainted top row is kept, and a part whose only painted row is hidden is removed. Merge copies the fill and stroke rows, visibility, opacity and blending mode of each face that draws by this rule, and of each edge, to the rebuilt parts, with their gradients kept at their fit on the old outline and without the strokes' pen pressure, tilt, bearing and rotation. Evidence: `tests::build::{live_paint_expand_keeps_the_faces_and_edges_that_draw, live_paint_merge_carries_the_faces_that_draw, live_paint_merge_leaves_pen_pressure_behind, live_paint_merge_keeps_the_fit_of_carried_gradients, live_paint_merge_keeps_the_visibility_opacity_and_blending_of_carried_parts}`.
|
||||
`livePaint.info` lists every fill and stroke row of each face and edge, bottom first, with its paint, opacity, visibility, weight for a stroke, and whether it draws by the rule above, beside the top fill, stroke and weight it already reported. Evidence: `tests::build::live_paint_info_lists_every_appearance_row`.
|
||||
Live Paint Gap Options (Object › Live Paint › Gap Options…, `livePaint.gapOptions`): with Gap Detection on, paint stops at the gaps that run from a loose end of a path to the nearest path, up to Small (3 pt), Medium (6 pt), Large (12 pt) or a custom size; Close Gaps With Paths adds an unpainted path across each gap; Preview applies the options live and draws the gaps in the Gap Preview Color. The options are saved, copied and undone with the group, and Merge keeps the bottom group's options when its Gap Detection is on, or else turns Gap Detection on with Small when another merged group has it on. When Gap Detection is off in every merged group, it stays off.
|
||||
View › Show Live Paint Gaps (`view.livePaintGaps`) draws the gaps of the selected Live Paint groups up to their Paint Stops At size, each in its group's Gap Preview Color, until Hide Live Paint Gaps or Hide Edges hides them ([Live Paint gaps](https://web.archive.org/web/20260105111201/https://helpx.adobe.com/illustrator/desktop/paint-and-fill/learn-painting-basics/find-and-close-gaps-in-live-paint-groups.html)).
|
||||
Double-clicking the Live Paint Selection tool opens its Tool Options ([Live Paint Selection tool options](https://web.archive.org/web/20250904012025/https://helpx.adobe.com/illustrator/using/selecting-objects.html)): with Select Fills off, a click selects an edge instead of a face; with Select Strokes off, Alt-click selects a face instead of an edge; Color and Width set the color and width of the highlight on the face or edge under the pointer (red and 4 px by default). The options last across tool switches and are saved with the preferences (`tool.setOption {tool: "livePaintSelection"}`).
|
||||
|
||||
Reference in New Issue
Block a user