mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-11 02:19:56 -06:00
Graphs: graphic styles and Default and Swap Fill and Stroke update a series' stored paint
Squashed from #6094 onto main.
This commit is contained in:
@@ -661,6 +661,35 @@ fn painted_rows(d: &vectorcraft_doc::Document, rows: impl IntoIterator<Item = (N
|
||||
rows.into_iter().filter(|(id, fill)| d.node(*id).is_some_and(|n| !super::graph::shown_paint(n, *fill).0.is_none())).collect()
|
||||
}
|
||||
|
||||
/// Store on the graph what an edit of `ids` (from `before` to `d`) leaves on the topmost visible fill and stroke of the
|
||||
/// series marks among them: a paint, weight or opacity that changed, and the paint (`paints`) and opacity (`opacities`) of
|
||||
/// the kinds of row the edit wrote, as (fill, stroke). With `top_only` the edit wrote the topmost row of each kind, which
|
||||
/// counts only when it is visible. Returns whether the stored paint changed.
|
||||
pub(super) fn capture_series_writes(
|
||||
before: &vectorcraft_doc::Document,
|
||||
d: &mut vectorcraft_doc::Document,
|
||||
ids: &[NodeId],
|
||||
paints: (bool, bool),
|
||||
opacities: (bool, bool),
|
||||
top_only: bool,
|
||||
) -> bool {
|
||||
let top_shows = |d: &vectorcraft_doc::Document, id: NodeId, kind: bool| {
|
||||
let items = d.node(id).map_or(&[][..], |n| &n.appearance.items[..]);
|
||||
items.iter().rev().find(|it| it.is_fill() == kind).is_some_and(AppearanceItem::visible)
|
||||
};
|
||||
let written = |d: &vectorcraft_doc::Document, (fill, stroke): (bool, bool)| -> Vec<(NodeId, bool)> {
|
||||
let rows = ids.iter().flat_map(|id| [fill.then_some((*id, true)), stroke.then_some((*id, false))]).flatten();
|
||||
painted_rows(d, rows.filter(|(id, kind)| !top_only || top_shows(d, *id, *kind)))
|
||||
};
|
||||
let mut opacity_rows = top_opacity_changed(before, d, ids);
|
||||
opacity_rows.extend(written(d, opacities));
|
||||
let mut rows = top_paint_changed(before, d, ids);
|
||||
rows.extend(written(d, paints));
|
||||
let kept = super::graph::capture_series_opacity(d, &opacity_rows);
|
||||
let painted = super::graph::capture_series_top_paints(d, &rows);
|
||||
kept || painted
|
||||
}
|
||||
|
||||
/// The kind of the row at `at` (fill `true`, stroke `false`) when it is the topmost visible row of that kind in `ap`.
|
||||
fn top_row_kind(ap: &Appearance, at: usize) -> Option<bool> {
|
||||
let fill = ap.items.get(at)?.is_fill();
|
||||
|
||||
@@ -1316,6 +1316,195 @@ fn a_paint_on_a_design_copy_or_the_other_kind_of_row_is_not_kept() {
|
||||
assert_eq!(spec(&s, id), kept);
|
||||
}
|
||||
|
||||
/// A graphic style `name` made from a new rectangle with a blue fill at 50% opacity, and without a stroke row unless
|
||||
/// `stroke`: the rectangle and the blue paint.
|
||||
fn blue_style(s: &mut Session, name: &str, stroke: bool) -> (NodeId, Paint) {
|
||||
let r = NodeId(s.execute("shape.rectangle", &json!({"x": 600, "y": 600, "width": 10, "height": 10})).unwrap()["id"].as_u64().unwrap());
|
||||
s.execute("appearance.setItem", &json!({"ids": [r.0], "index": top_row(s, r, true), "color": "#0000ff", "opacity": 50})).unwrap();
|
||||
if !stroke {
|
||||
s.execute("appearance.removeItem", &json!({"ids": [r.0], "index": top_row(s, r, false)})).unwrap();
|
||||
}
|
||||
s.execute("graphicStyle.new", &json!({"id": r.0, "name": name})).unwrap();
|
||||
let blue = s.doc().unwrap().doc.node(r).unwrap().appearance.fill_paint();
|
||||
(r, blue)
|
||||
}
|
||||
|
||||
/// Series 0's marks with a red 2 pt stroke stored from `marks[1]` and a 40% fill opacity stored from `marks[2]`.
|
||||
fn stored_red_stroke() -> (Session, NodeId, Vec<NodeId>) {
|
||||
let (mut s, id, _, marks) = column_marks();
|
||||
let stroke = top_row(&s, marks[1], false);
|
||||
s.execute("appearance.setItem", &json!({"ids": [marks[1].0], "index": stroke, "color": "#ff0000", "weight": 2})).unwrap();
|
||||
s.execute("appearance.setItem", &json!({"ids": [marks[2].0], "index": top_row(&s, marks[2], true), "opacity": 40})).unwrap();
|
||||
let kept = spec(&s, id).series_paints[0].clone();
|
||||
assert_eq!((kept.stroke_width, kept.fill_opacity), (Some(2.0), Some(0.4)));
|
||||
(s, id, marks)
|
||||
}
|
||||
|
||||
/// After the graph is drawn again, every mark of series 0 has `fill` at `opacity` and `stroke` at `weight`; two undos (the
|
||||
/// data edit, then the command) bring back the paints the graph stored in `was`.
|
||||
fn series_0_is(s: &mut Session, id: NodeId, (fill, opacity, stroke, weight): (&Paint, f32, &Paint, f64), was: &[vectorcraft_doc::SeriesPaint]) {
|
||||
s.execute("graph.setData", &json!({"id": id.0, "rows": [[1, 2], [3, 4]]})).unwrap();
|
||||
let n = s.doc().unwrap().doc.node(id).unwrap().clone();
|
||||
for m in series(&n, 0).children().unwrap() {
|
||||
let f = m.appearance.fill().unwrap();
|
||||
assert_eq!((&f.paint, f.opacity), (fill, opacity));
|
||||
assert_eq!((&m.appearance.stroke_paint(), m.appearance.stroke_width()), (stroke, weight));
|
||||
}
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(spec(s, id).series_paints, was);
|
||||
}
|
||||
|
||||
/// A graphic style applied to a series mark is stored on the graph, its fill opacity included. A style without a stroke,
|
||||
/// applied to a mark that shows one, leaves the series without a stroke.
|
||||
#[test]
|
||||
fn a_graphic_style_applied_to_a_series_mark_is_kept() {
|
||||
let (mut s, id, marks) = stored_red_stroke();
|
||||
let (_, blue) = blue_style(&mut s, "Blue", false);
|
||||
let was = spec(&s, id).series_paints;
|
||||
s.execute("graphicStyle.apply", &json!({"name": "Blue", "ids": [marks[1].0]})).unwrap();
|
||||
let kept = spec(&s, id).series_paints[0].clone();
|
||||
assert_eq!((kept.fill, kept.fill_opacity, kept.stroke), (Some(blue.clone()), Some(0.5), Some(Paint::None)));
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(spec(&s, id).series_paints, was, "one undo takes back the style and what the graph stored");
|
||||
s.execute("edit.redo", &json!({})).unwrap();
|
||||
series_0_is(&mut s, id, (&blue, 0.5, &Paint::None, 0.0), &was);
|
||||
}
|
||||
|
||||
/// A graphic style added on top of a series mark's appearance (Alt-click) stores the kinds of row the style shows: a style
|
||||
/// with only a fill leaves the stroke the series stores.
|
||||
#[test]
|
||||
fn a_graphic_style_added_to_a_series_mark_keeps_only_its_own_kinds() {
|
||||
let (mut s, id, marks) = stored_red_stroke();
|
||||
let stroke = top_row(&s, marks[0], false);
|
||||
let red = spec(&s, id).series_paints[0].stroke.clone().unwrap();
|
||||
s.execute("appearance.setItem", &json!({"ids": [marks[0].0], "index": stroke, "color": "#00ff00", "weight": 3})).unwrap();
|
||||
s.execute("appearance.setItem", &json!({"ids": [marks[1].0], "index": stroke, "color": "#ff0000", "weight": 2})).unwrap();
|
||||
let (_, blue) = blue_style(&mut s, "Blue", false);
|
||||
let was = spec(&s, id).series_paints;
|
||||
s.execute("graphicStyle.apply", &json!({"name": "Blue", "ids": [marks[0].0], "add": true})).unwrap();
|
||||
let kept = spec(&s, id).series_paints[0].clone();
|
||||
assert_eq!((kept.fill, kept.fill_opacity), (Some(blue.clone()), Some(0.5)));
|
||||
assert_eq!((kept.stroke.clone(), kept.stroke_width), (Some(red.clone()), Some(2.0)));
|
||||
series_0_is(&mut s, id, (&blue, 0.5, &red, 2.0), &was);
|
||||
}
|
||||
|
||||
/// A graphic style whose stroke row is hidden, added on top of a series mark (Alt-click), stores its fill and leaves the
|
||||
/// stroke the series stores, although the mark shows a stroke of its own.
|
||||
#[test]
|
||||
fn a_graphic_style_with_a_hidden_stroke_added_to_a_series_mark_keeps_the_stored_stroke() {
|
||||
let (mut s, id, marks) = stored_red_stroke();
|
||||
let stroke = top_row(&s, marks[0], false);
|
||||
let red = spec(&s, id).series_paints[0].stroke.clone().unwrap();
|
||||
s.execute("appearance.setItem", &json!({"ids": [marks[0].0], "index": stroke, "color": "#00ff00", "weight": 3})).unwrap();
|
||||
s.execute("appearance.setItem", &json!({"ids": [marks[1].0], "index": stroke, "color": "#ff0000", "weight": 2})).unwrap();
|
||||
let (rect, blue) = blue_style(&mut s, "Blue", true);
|
||||
s.execute("appearance.setItem", &json!({"ids": [rect.0], "index": top_row(&s, rect, false), "visible": false})).unwrap();
|
||||
s.execute("graphicStyle.new", &json!({"id": rect.0, "name": "Hidden stroke"})).unwrap();
|
||||
let was = spec(&s, id).series_paints;
|
||||
s.execute("graphicStyle.apply", &json!({"name": "Hidden stroke", "ids": [marks[0].0], "add": true})).unwrap();
|
||||
let kept = spec(&s, id).series_paints[0].clone();
|
||||
assert_eq!((kept.fill, kept.fill_opacity), (Some(blue.clone()), Some(0.5)));
|
||||
assert_eq!((kept.stroke.clone(), kept.stroke_width), (Some(red.clone()), Some(2.0)));
|
||||
series_0_is(&mut s, id, (&blue, 0.5, &red, 2.0), &was);
|
||||
}
|
||||
|
||||
/// Redefine Graphic Style restyles the series marks still linked to the style, and the graph stores the new look.
|
||||
#[test]
|
||||
fn redefining_a_graphic_style_updates_a_linked_series_mark() {
|
||||
let (mut s, id, _, marks) = column_marks();
|
||||
let (rect, _) = blue_style(&mut s, "Blue", true);
|
||||
s.execute("graphicStyle.apply", &json!({"name": "Blue", "ids": [marks[0].0]})).unwrap();
|
||||
s.execute("appearance.setItem", &json!({"ids": [rect.0], "index": top_row(&s, rect, true), "color": "#00ff00", "opacity": 100})).unwrap();
|
||||
let green = s.doc().unwrap().doc.node(rect).unwrap().appearance.fill_paint();
|
||||
let was = spec(&s, id).series_paints;
|
||||
s.execute("graphicStyle.redefine", &json!({"name": "Blue", "id": rect.0})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc.node(marks[0]).unwrap().appearance.fill_paint(), green, "the mark follows the style");
|
||||
let kept = spec(&s, id).series_paints[0].clone();
|
||||
assert_eq!((kept.fill, kept.fill_opacity), (Some(green.clone()), None));
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(spec(&s, id).series_paints, was, "one undo takes back the redefinition and what the graph stored");
|
||||
s.execute("edit.redo", &json!({})).unwrap();
|
||||
let (stroke, weight) = {
|
||||
let r = s.doc().unwrap().doc.node(rect).unwrap().clone();
|
||||
(r.appearance.stroke_paint(), r.appearance.stroke_width())
|
||||
};
|
||||
series_0_is(&mut s, id, (&green, 1.0, &stroke, weight), &was);
|
||||
}
|
||||
|
||||
/// Redefine Graphic Style made from a series mark does not edit that mark, so the graph keeps the paint another mark of the
|
||||
/// series stored after it: the graph drawn again shows the same marks with or without the redefinition, and undo brings
|
||||
/// back the stored paint.
|
||||
#[test]
|
||||
fn redefining_a_graphic_style_from_a_series_mark_keeps_the_paint_another_mark_stored() {
|
||||
let (mut s, id, _, marks) = column_marks();
|
||||
blue_style(&mut s, "Blue", true);
|
||||
s.execute("graphicStyle.apply", &json!({"name": "Blue", "ids": [marks[1].0]})).unwrap();
|
||||
s.execute("appearance.setItem", &json!({"ids": [marks[1].0], "index": top_row(&s, marks[1], true), "color": "#00ff00"})).unwrap();
|
||||
s.execute("appearance.setItem", &json!({"ids": [marks[0].0], "index": top_row(&s, marks[0], true), "color": "#ff0000"})).unwrap();
|
||||
let red = s.doc().unwrap().doc.node(marks[0]).unwrap().appearance.fill_paint();
|
||||
let was = spec(&s, id).series_paints;
|
||||
assert_eq!(was[0].fill, Some(red.clone()));
|
||||
s.execute("graph.setData", &json!({"id": id.0, "rows": [[1, 2], [3, 4]]})).unwrap();
|
||||
let without = marks_of(&s, id, 0);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
s.execute("graphicStyle.redefine", &json!({"name": "Blue", "id": marks[1].0})).unwrap();
|
||||
assert_eq!(spec(&s, id).series_paints, was, "the source mark was not edited");
|
||||
s.execute("graph.setData", &json!({"id": id.0, "rows": [[1, 2], [3, 4]]})).unwrap();
|
||||
assert_eq!(marks_of(&s, id, 0), without);
|
||||
assert!(without.iter().all(|m| m.0 == red));
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(spec(&s, id).series_paints, was);
|
||||
}
|
||||
|
||||
/// Default Fill and Stroke on a series mark stores a white fill and a 1 pt black stroke, both opaque, on the graph.
|
||||
#[test]
|
||||
fn default_fill_and_stroke_on_a_series_mark_is_kept() {
|
||||
let (mut s, id, marks) = stored_red_stroke();
|
||||
let was = spec(&s, id).series_paints;
|
||||
s.execute("paint.default", &json!({"ids": [marks[0].0]})).unwrap();
|
||||
let (white, black) = (Paint::solid(vectorcraft_color::Color::WHITE), Paint::solid(vectorcraft_color::Color::BLACK));
|
||||
let kept = spec(&s, id).series_paints[0].clone();
|
||||
assert_eq!((kept.fill.clone(), kept.fill_opacity), (Some(white.clone()), None));
|
||||
assert_eq!((kept.stroke.clone(), kept.stroke_width), (Some(black.clone()), Some(1.0)));
|
||||
series_0_is(&mut s, id, (&white, 1.0, &black, 1.0), &was);
|
||||
}
|
||||
|
||||
/// Swap Fill and Stroke on a series mark stores the swapped paints and the stroke weight, and leaves the opacity the series
|
||||
/// stores.
|
||||
#[test]
|
||||
fn swapping_a_series_marks_fill_and_stroke_is_kept() {
|
||||
let (mut s, id, marks) = stored_red_stroke();
|
||||
let stroke = top_row(&s, marks[0], false);
|
||||
s.execute("appearance.setItem", &json!({"ids": [marks[0].0], "index": stroke, "color": "#00ff00", "weight": 3})).unwrap();
|
||||
let mark = s.doc().unwrap().doc.node(marks[0]).unwrap().clone();
|
||||
let (gray, green) = (mark.appearance.fill_paint(), mark.appearance.stroke_paint());
|
||||
let was = spec(&s, id).series_paints;
|
||||
s.execute("paint.swap", &json!({"ids": [marks[0].0]})).unwrap();
|
||||
let kept = spec(&s, id).series_paints[0].clone();
|
||||
assert_eq!((kept.fill.clone(), kept.fill_opacity), (Some(green.clone()), Some(0.4)));
|
||||
assert_eq!((kept.stroke.clone(), kept.stroke_width), (Some(gray.clone()), Some(3.0)));
|
||||
series_0_is(&mut s, id, (&green, 0.4, &gray, 3.0), &was);
|
||||
}
|
||||
|
||||
/// Swap Fill and Stroke writes the topmost fill even when it is hidden: the visible fill under it does not replace the
|
||||
/// fill the series stores.
|
||||
#[test]
|
||||
fn swapping_onto_a_hidden_top_fill_leaves_the_stored_fill() {
|
||||
let (mut s, id, _, marks) = column_marks();
|
||||
s.execute("appearance.addFill", &json!({"ids": [marks[0].0]})).unwrap();
|
||||
let (_, upper) = fill_rows(&s, marks[0]);
|
||||
s.execute("appearance.setItem", &json!({"ids": [marks[0].0], "index": upper, "visible": false})).unwrap();
|
||||
s.execute("appearance.setItem", &json!({"ids": [marks[1].0], "index": top_row(&s, marks[1], true), "color": "#ff0000"})).unwrap();
|
||||
let red = spec(&s, id).series_paints[0].fill.clone();
|
||||
assert_eq!(red, Some(s.doc().unwrap().doc.node(marks[1]).unwrap().appearance.fill_paint()));
|
||||
let stroke = top_row(&s, marks[0], false);
|
||||
s.execute("appearance.setItem", &json!({"ids": [marks[0].0], "index": stroke, "color": "#00ff00", "weight": 3})).unwrap();
|
||||
s.execute("paint.swap", &json!({"ids": [marks[0].0]})).unwrap();
|
||||
assert_eq!(spec(&s, id).series_paints[0].fill, red);
|
||||
}
|
||||
|
||||
/// A kept opacity read from a file outside 0..1 is held to that range, and one that is not a number is left out.
|
||||
#[test]
|
||||
fn a_kept_series_opacity_from_a_file_is_held_to_its_range() {
|
||||
|
||||
@@ -39,7 +39,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Swap Fill and Stroke",
|
||||
[],
|
||||
Some("Shift+X"),
|
||||
"{ids?|id?} swap the fill and stroke of the selection (type too) and of the defaults",
|
||||
"{ids?|id?} swap the fill and stroke of the selection (type too) and of the defaults. On a path directly in a graph series, the graph also stores the swapped paints and the stroke weight (exceptions: docs/mcp.md)",
|
||||
has_doc,
|
||||
swap
|
||||
),
|
||||
@@ -48,7 +48,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Default Fill and Stroke",
|
||||
[],
|
||||
Some("D"),
|
||||
"{ids?|id?} white fill and 1 pt black stroke for the selection and the defaults; type gets black fill and no stroke",
|
||||
"{ids?|id?} white fill and 1 pt black stroke for the selection and the defaults; type gets black fill and no stroke. On a path directly in a graph series, the graph also stores the paint, weight and opacity of the topmost visible fill and stroke (exceptions: docs/mcp.md)",
|
||||
has_doc,
|
||||
default_paint
|
||||
),
|
||||
@@ -851,7 +851,9 @@ fn swap(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
restore_unpainted_stroke(&mut n.appearance, None, &f);
|
||||
n.appearance.set_stroke(f);
|
||||
}
|
||||
Ok(super::appearance::count_changed(d, &before, &ids))
|
||||
// The swap writes the paint of the topmost fill and stroke, not their opacity.
|
||||
let kept = super::appearance::capture_series_writes(&before, d, &ids, (true, true), (false, false), true);
|
||||
Ok(super::appearance::count_changed(d, &before, &ids) + usize::from(kept))
|
||||
})?;
|
||||
}
|
||||
if s.paint.stroke.is_none() && !s.paint.fill.is_none() && s.paint.stroke_width == 0.0 {
|
||||
@@ -879,7 +881,8 @@ fn default_paint(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
_ => n.appearance = Appearance::default_art().into(),
|
||||
}
|
||||
}
|
||||
Ok(super::appearance::count_changed(d, &before, &ids))
|
||||
let kept = super::appearance::capture_series_writes(&before, d, &ids, (true, true), (true, true), false);
|
||||
Ok(super::appearance::count_changed(d, &before, &ids) + usize::from(kept))
|
||||
})?;
|
||||
}
|
||||
super::newart::reset(s);
|
||||
|
||||
@@ -30,7 +30,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Apply Graphic Style",
|
||||
["Window", "Graphic Styles"],
|
||||
None,
|
||||
"{name, add?: bool, ids?|id? (layers too), target?: \"object\"|\"contents\"} give the objects (default: the selection; a group or layer itself, its fills and strokes painting its members and its effects applying to them as one piece; target contents: the objects inside instead) the style's appearance, opacity, blend mode, isolate and knockout, and link them to it (placed gradients land at the same place relative to each object's bounds); add: true (Alt-click) adds its fills, strokes and effects on top of the existing appearance instead and unlinks. With nothing selected (and no ids) the next object drawn takes the style instead (see appearance.newArt) → {newArt: true}",
|
||||
"{name, add?: bool, ids?|id? (layers too), target?: \"object\"|\"contents\"} give the objects (default: the selection; a group or layer itself, its fills and strokes painting its members and its effects applying to them as one piece; target contents: the objects inside instead) the style's appearance, opacity, blend mode, isolate and knockout, and link them to it (placed gradients land at the same place relative to each object's bounds); add: true (Alt-click) adds its fills, strokes and effects on top of the existing appearance instead and unlinks. On a path directly in a graph series, the graph also stores the paint, weight and opacity of the topmost visible fill and stroke (with `add`: of the kinds of row the style shows; exceptions: docs/mcp.md). With nothing selected (and no ids) the next object drawn takes the style instead (see appearance.newArt) → {newArt: true}",
|
||||
has_doc,
|
||||
style_apply
|
||||
),
|
||||
@@ -67,7 +67,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Redefine Graphic Style",
|
||||
["Window", "Appearance"],
|
||||
None,
|
||||
"{name?, id?} replace the style (default: the one the object was last linked to) with the look of object `id` or the first selected object; the objects still linked to it update, and that object becomes linked → {name}",
|
||||
"{name?, id?} replace the style (default: the one the object was last linked to) with the look of object `id` or the first selected object; the objects still linked to it update, and that object becomes linked. On a linked path directly in a graph series, the graph also stores the paint, weight and opacity of the topmost visible fill and stroke (exceptions: docs/mcp.md) → {name}",
|
||||
has_doc,
|
||||
style_redefine
|
||||
),
|
||||
@@ -333,6 +333,7 @@ fn style_apply(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
s.new_art_style(&g, add);
|
||||
return Ok(json!({ "newArt": true }));
|
||||
}
|
||||
let before = s.doc()?.doc.clone();
|
||||
s.edit("Apply Graphic Style", |d, _| {
|
||||
if add {
|
||||
let g = d.graphic_styles[i].clone();
|
||||
@@ -344,12 +345,17 @@ fn style_apply(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
n.extras_mut().graphic_style = None;
|
||||
}
|
||||
}
|
||||
// The graph stores the rows of the kinds the style shows, now on top of each stack.
|
||||
let shows = |kind: bool| g.appearance.items.iter().any(|it| it.is_fill() == kind && it.visible());
|
||||
let kinds = (shows(true), shows(false));
|
||||
super::appearance::capture_series_writes(&before, d, &ids, kinds, kinds, false);
|
||||
} else {
|
||||
d.graphic_style_id(i);
|
||||
let g = d.graphic_styles[i].clone();
|
||||
for id in &ids {
|
||||
apply_style(d, *id, &g, chars);
|
||||
}
|
||||
super::appearance::capture_series_writes(&before, d, &ids, (true, true), (true, true), false);
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
@@ -456,6 +462,7 @@ fn style_redefine(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let look = GraphicStyle::of(String::new(), captured(n), n);
|
||||
let name = d.graphic_styles[i].name.clone();
|
||||
let chars = s.prefs.override_char_color;
|
||||
let before = s.doc()?.doc.clone();
|
||||
s.edit_named("Redefine Graphic Style “{name}”", &name, false, |d, _| {
|
||||
let id = d.graphic_style_id(i);
|
||||
let old = std::mem::replace(&mut d.graphic_styles[i], GraphicStyle { name: name.clone(), id, ..look });
|
||||
@@ -465,9 +472,10 @@ fn style_redefine(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let (follow, edited): (Vec<NodeId>, Vec<NodeId>) = linked.into_iter().partition(|nid| d.node(*nid).is_some_and(|n| in_sync(n, &old)));
|
||||
set_link(d, &edited, None);
|
||||
let g = d.graphic_styles[i].clone();
|
||||
for nid in follow {
|
||||
apply_style(d, nid, &g, chars);
|
||||
for nid in &follow {
|
||||
apply_style(d, *nid, &g, chars);
|
||||
}
|
||||
super::appearance::capture_series_writes(&before, d, &follow, (true, true), (true, true), false);
|
||||
set_link(d, &[src], Some(id));
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
+17
-12
@@ -566,18 +566,23 @@ stroke instead of the whole object: `run_command effect.apply {"effect": "path.o
|
||||
|
||||
On a path directly inside a graph series, `appearance.setItem`, `appearance.addFill`, `appearance.addStroke`,
|
||||
`appearance.removeItem`, `appearance.moveItem`, `appearance.duplicateItem`, `appearance.clear`,
|
||||
`appearance.reduceToBasic`, `appearance.showAllHidden`, `appearance.transfer` and `appearance.copyFrom` store the paint,
|
||||
weight and opacity of the topmost visible fill and stroke on the graph. When the graph is drawn again, columns, bars,
|
||||
wedges, areas, markers and legend swatches take the stored fill and stroke. A thin line takes the stored stroke and has
|
||||
no fill. A Filled Lines band takes the stored fill, or the stored stroke when no fill is stored and that stroke has
|
||||
paint, and has a 0.25 pt black outline, so these commands do not store a stroke paint or weight for a series that
|
||||
draws a band. They still store a stroke opacity set on a band, and the band's outline takes it. Graph
|
||||
designs keep their own paint. A row that had no paint before the command and has none after it does not change what the
|
||||
graph stores. `paint.setFill`, `paint.setStroke`, `paint.none` and the commands that apply one paint kind to the other
|
||||
store the topmost row of the kind even when it is hidden, and store a stroke set on a band.
|
||||
`stroke.set`, `paint.editGradient`, `paint.swap`, `paint.default`, graphic styles and the recolor commands do not store a
|
||||
series' paint. An `appearance.*` command given a series group's id edits the group's own rows, and the graph does not
|
||||
store them.
|
||||
`appearance.reduceToBasic`, `appearance.showAllHidden`, `appearance.transfer`, `appearance.copyFrom`, `paint.default`,
|
||||
`graphicStyle.apply` and `graphicStyle.redefine` (on the marks still linked to the style) store the paint, weight and
|
||||
opacity of the topmost visible fill and stroke on the graph. `graphicStyle.apply` with `add` stores only the kinds of row
|
||||
that are visible in the style. `paint.swap` stores the swapped paints and the stroke weight, keeps the stored opacity,
|
||||
and does not store a hidden topmost row that it swaps. The graph does not store the rest of a graphic style's look (more
|
||||
fill and stroke rows, effects, the object's opacity and blend mode, and the link to the style), and a mark loses it when
|
||||
the graph is drawn again. When the graph is drawn again, columns, bars, wedges, areas, markers and legend swatches take
|
||||
the stored fill and stroke. A thin line takes the stored stroke and has no fill. A Filled Lines band takes the stored
|
||||
fill, or the stored stroke when no fill is stored and that stroke has paint, and has a 0.25 pt black outline, so these
|
||||
commands do not store a stroke paint or weight for a series that draws a band. Except for `paint.swap`, they still store
|
||||
a stroke opacity set on a band, and the band's outline takes it. Graph designs keep their own paint. A row that had no
|
||||
paint before the command and has none after it does not change what the graph stores. `paint.setFill`,
|
||||
`paint.setStroke`, `paint.none` and the commands that apply one paint kind to the other store the topmost row of the kind
|
||||
even when it is hidden, and store a stroke set on a band. `stroke.set`, `paint.editGradient` and the recolor commands do
|
||||
not store a series' paint. An `appearance.*` command or `graphicStyle.apply` given a series group's id, and
|
||||
`graphicStyle.redefine` on a series group linked to the style, edit the group's own rows, and the graph does not store
|
||||
them.
|
||||
|
||||
`effect.apply {"effect": "path.outlineObject"}` (Effect › Path › Outline Object) takes no parameters. Type that carries
|
||||
it is drawn, exported and expanded as its glyph outlines and stays editable type, and the effects after it reshape
|
||||
|
||||
@@ -872,7 +872,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
View › Hide Edges hides the Live Corners widgets, the bounding box and the type widget, and the selection tools then do not answer a press on them or change the cursor over them.
|
||||
- **Blend spine Anchor Point tool (#2544):** on a selected blend, dragging a spine corner makes symmetric Smooth handles; the tool displays and picks existing spine handles when switched to, drags one independently or retracts it on click, and clicking a point with handles retracts both. The spine is edited live without moving its key objects. Windows native canvas checks and screenshots reviewed: symmetric and independent handles, corner/retraction, Shift constraints and perpendicular reshaping, tool switching, key preservation, Undo/Redo, save/reopen, hidden handles and overlapping Path/Blend priority (PR #2643). Behaviour reference: [public path-editing documentation](https://helpx.adobe.com/illustrator/using/adjust-path-segments.html).
|
||||
- **Path editing (#494):** the handles shown are the handles that drag: Direct Selection drags those of a path selected as a whole (as the Pen leaves it), the Anchor Point tool and Pen+Alt no longer grab handles that Show handles when multiple anchors are selected hides, a handle shorter than the tolerance leaves its anchor to be picked, and the anchor tools pick within Selection & Anchor Display › Tolerance. A dragged segment keeps a smooth anchor at either end smooth. With Direct Selection, the Pen, the Curvature and the anchor tools, a path selected as a whole shows all its anchors selected, and the Control bar and the Properties panel offer Convert (corner, smooth) for them too; Remove and Cut wait for anchors direct-selected (#504). A handle drag gathers its snap targets once per drag (it took 21 ms per pointer move on a 20,000-path document, now 1 ms; `vectorcraft-cli perf` measures handle and segment drags on a 5,000-anchor path). Join merges the selected anchor points that lie on one another, as after Average, into one anchor (#1582, fixes #1533); Object › Path › Average & Join… (`path.averageJoin`, Cmd+Alt+Shift+J, the "Create corner or smooth join" shortcut of [Default keyboard shortcuts](https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html)) moves two open path ends to their midpoint and joins them there, a corner or smooth point as its dialog's Points choice asks, in one undo step.
|
||||
- **Graphs:** Column, Stacked Column, Bar, Stacked Bar, Line, Area, Scatter, Pie and Radar graph tools with Graph Data and Graph Type. A series starts in greyscale; Group Selection's fill and stroke, and the paint, weight and opacity of a mark's top visible fill or stroke row after an Appearance command or the Eyedropper, are kept separately on the graph and reapplied when its data, type or size changes, and through save, open and undo. Graph Type's Override Calculated Values (both `axisMin` and `axisMax`) uses exactly that value-axis range, split into the tick count (5 when automatic). Line graphs take Edge-to-Edge Lines (off by default: the points sit at the centres of their categories; graphs saved before the option keep their edge-to-edge lines), and radar graphs follow Mark Data Points and Connect Data Points. Blank cells (an empty CSV cell or `null`) are values left out, not zeros: columns, bars and stacked segments skip them, lines and scatter lines break around them and the value axis ignores them, while area, pie and radar draw them as 0; a number in straight quotes is a label, and the data window writes it back quoted. Graph types combine: Graph Type on series picked with Group Selection (or `series` indexes) draws them as their own type, with column, stacked column, line and area mixing on the vertical value axis and bar and stacked bar on the horizontal one; stacked columns take one slot of the cluster, lines draw in front of columns and areas behind. The value axis goes on the left, the right or both sides (column, stacked column, line, area and scatter graphs; bar graphs keep theirs along the bottom), and Separate Scales gives the series assigned to the right axis a scale of their own with its own tick values. Each value axis takes tick marks of no, short or full (across the plot) length and a count per division, and a prefix and suffix for its numbers; the category axis takes tick marks too (none by default), at or between its labels. Graph designs: Object › Graph › Design lists the designs with a preview of the selected one, and New Design (the selected art), Rename, Delete Design, Paste Design (a copy back to edit) and Select Unused, and Marker draws the data points and legend swatches of line, scatter and radar series with a design, scaled so its backmost object fills the default marker's square. Graph Data transposes rows and columns and switches a scatter graph's x and y, and a `|` in a category or series label starts a new line. Pie graphs take Legends in Wedges, Position (Even, Ratio sized by area, Stacked on one centre) and Sort (None, All, First). Graph Type takes column and cluster widths up to 1000% (overlapping past 100%) with First Row and First Column in Front (First Row orders each series' categories; across series the series order decides, so series stay one group each for Group Selection), draws filled lines, adds drop shadows behind columns, bars, lines and wedges, and puts the legend in rows across the top. Text formatted with the Character or Appearance controls, on the whole graph or on labels picked with Group Selection, is kept per kind of label (value axis numbers, category labels, legend labels) through the graph being drawn again. Object › Graph › Data opens a worksheet: cells picked with a click and edited above the sheet (Tab moves right, Enter down, the arrows between cells), Import Data from a tab-separated text file, Transpose, Switch x/y, Cell Style (decimals shown, column width), Revert and Apply. `graph.setData` returns and takes the data as CSV with each value written exactly, not rounded to two decimals, so applying the returned CSV changes nothing (#1485).
|
||||
- **Graphs:** Column, Stacked Column, Bar, Stacked Bar, Line, Area, Scatter, Pie and Radar graph tools with Graph Data and Graph Type. A series starts in greyscale; Group Selection's fill and stroke, and the paint, weight and opacity of a mark's top visible fill or stroke row after an Appearance command, the Eyedropper, a graphic style, Default Fill and Stroke or Swap Fill and Stroke, are kept separately on the graph and reapplied when its data, type or size changes, and through save, open and undo. Graph Type's Override Calculated Values (both `axisMin` and `axisMax`) uses exactly that value-axis range, split into the tick count (5 when automatic). Line graphs take Edge-to-Edge Lines (off by default: the points sit at the centres of their categories; graphs saved before the option keep their edge-to-edge lines), and radar graphs follow Mark Data Points and Connect Data Points. Blank cells (an empty CSV cell or `null`) are values left out, not zeros: columns, bars and stacked segments skip them, lines and scatter lines break around them and the value axis ignores them, while area, pie and radar draw them as 0; a number in straight quotes is a label, and the data window writes it back quoted. Graph types combine: Graph Type on series picked with Group Selection (or `series` indexes) draws them as their own type, with column, stacked column, line and area mixing on the vertical value axis and bar and stacked bar on the horizontal one; stacked columns take one slot of the cluster, lines draw in front of columns and areas behind. The value axis goes on the left, the right or both sides (column, stacked column, line, area and scatter graphs; bar graphs keep theirs along the bottom), and Separate Scales gives the series assigned to the right axis a scale of their own with its own tick values. Each value axis takes tick marks of no, short or full (across the plot) length and a count per division, and a prefix and suffix for its numbers; the category axis takes tick marks too (none by default), at or between its labels. Graph designs: Object › Graph › Design lists the designs with a preview of the selected one, and New Design (the selected art), Rename, Delete Design, Paste Design (a copy back to edit) and Select Unused, and Marker draws the data points and legend swatches of line, scatter and radar series with a design, scaled so its backmost object fills the default marker's square. Graph Data transposes rows and columns and switches a scatter graph's x and y, and a `|` in a category or series label starts a new line. Pie graphs take Legends in Wedges, Position (Even, Ratio sized by area, Stacked on one centre) and Sort (None, All, First). Graph Type takes column and cluster widths up to 1000% (overlapping past 100%) with First Row and First Column in Front (First Row orders each series' categories; across series the series order decides, so series stay one group each for Group Selection), draws filled lines, adds drop shadows behind columns, bars, lines and wedges, and puts the legend in rows across the top. Text formatted with the Character or Appearance controls, on the whole graph or on labels picked with Group Selection, is kept per kind of label (value axis numbers, category labels, legend labels) through the graph being drawn again. Object › Graph › Data opens a worksheet: cells picked with a click and edited above the sheet (Tab moves right, Enter down, the arrows between cells), Import Data from a tab-separated text file, Transpose, Switch x/y, Cell Style (decimals shown, column width), Revert and Apply. `graph.setData` returns and takes the data as CSV with each value written exactly, not rounded to two decimals, so applying the returned CSV changes nothing (#1485).
|
||||
- **Graph column designs:** Object › Graph › Column (`graph.column`) draws the columns, bars or stacked segments and the legend swatch of the series picked with Group Selection with a graph design: Vertically Scaled stretches it to each column's length, Uniformly Scaled scales it both ways and centers it, Repeating stacks a copy for each Each Design Represents value and chops or scales the last one (For Fractions), and Sliding keeps the part above the design's guide line in proportion and stretches the part below. The dialog lists the designs with a preview; the choice is saved with the graph and follows the design's rename and delete.
|
||||
Type in a column design that holds a `%` followed by two digits shows each column's value in place of the `%` and the digits: the first digit is the least number of places before the decimal point (zeros are added in front; 0 gives as many as the value needs), and the second digit sets the places after it, rounded. Unless it is inside a group that clips or has its own opacity or appearance, or inside a blend, compound shape, envelope, repeat or intertwined objects, the type keeps its size and is drawn once per column in a repeating design. The legend swatch leaves it out.
|
||||
- **Other:** Eyedropper, Gradient annotator, Artboard (Alt-drag duplicates an artboard, and its art with Move/Copy Artwork with Artboard on; Copy, Cut and Paste take the artboard with its art, Paste right of the last artboard, Paste in Place where it was, into this or another document, #446; Object › Artboards › Rearrange All Artboards lays them out in a grid by row or by column, in one row or one column, left to right or right to left, from the menu, the Artboards panel or Properties' Rearrange All, #681; Scale Artwork with Artboard, in the Control bar and Properties beside Move Artwork with Artboard, resizes the artboard proportionally with the art fully inside it and its guides, strokes and corners per the Scale preferences, #602), Measure, Type, Hand, Zoom, Rotate View.
|
||||
|
||||
Reference in New Issue
Block a user