mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-15 07:42:42 +00:00
Graphs: an opacity or blend mode set on a series group is kept
transparency.set and object.setProps with an object opacity or blend mode, given a series group picked with Group Selection, set the group's own opacity and blend mode, and the graph drew its series groups again without them. The graph now stores them for the series (groupOpacity and groupBlend) and puts them back on the series group when it is drawn again. Commands that clear the group's own opacity and blend mode (appearance.clear, graphicStyle.apply, appearance.transfer onto the group, graphicStyle.redefine of a linked group) remove what the graph stores.
This commit is contained in:
@@ -363,6 +363,13 @@ pub struct SeriesPaint {
|
||||
/// Object blend mode of each path directly in the series group, as [`Self::opacity`]. Absent: Normal.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub blend: Option<vectorcraft_color::BlendMode>,
|
||||
/// Object opacity (0..1) of the series group itself, taken from the opacity set on the group. Absent: fully opaque.
|
||||
/// Graph labels keep none.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub group_opacity: Option<f32>,
|
||||
/// Object blend mode of the series group itself, as [`Self::group_opacity`]. Absent: Normal.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub group_blend: Option<vectorcraft_color::BlendMode>,
|
||||
}
|
||||
|
||||
/// The object rows one kind of graph label keeps ([`LabelStyles::values_rows`]): the paint, weight, opacity and blend mode
|
||||
|
||||
@@ -477,7 +477,8 @@ pub(crate) fn stood_for(own: &[NodeId], ids: &[NodeId]) -> Vec<NodeId> {
|
||||
}
|
||||
|
||||
/// Clear Appearance on each series group a command gave a whole new appearance through its paths, so the group's own
|
||||
/// opacity, blend mode, isolation, knockout, effects and style link do not stay on top of it.
|
||||
/// opacity, blend mode, isolation, knockout, effects and style link do not stay on top of it. The graph stops keeping the
|
||||
/// groups' opacity and blend mode.
|
||||
pub(crate) fn clear_stood_for(d: &mut Document, groups: &[NodeId]) {
|
||||
for id in groups {
|
||||
if let Some(n) = d.node_mut(*id) {
|
||||
@@ -486,6 +487,7 @@ pub(crate) fn clear_stood_for(d: &mut Document, groups: &[NodeId]) {
|
||||
n.extras_mut().graphic_style = None;
|
||||
}
|
||||
}
|
||||
super::graph::capture_series_object_opacity(d, groups);
|
||||
}
|
||||
|
||||
/// [`whole_targets`] of `cmd`, which needs at least one.
|
||||
|
||||
@@ -6,7 +6,7 @@ use super::labels::{
|
||||
recolour_text, runs, shadow_of,
|
||||
};
|
||||
use super::markers::{MARKER_BUDGET, marker_cost, marker_designs};
|
||||
use super::series::{series_marks, with_series_opacity};
|
||||
use super::series::{series_marks, with_series_group_opacity, with_series_opacity};
|
||||
use super::{
|
||||
Arc, BezPath, Color, Document, GraphKind, GraphSpec, Justify, Node, NodeKind, Paint, PathData, Point, Rect, TickLength, ValueAxisSide, shapes,
|
||||
};
|
||||
@@ -1006,7 +1006,7 @@ impl<'a> Plot<'a> {
|
||||
}
|
||||
}
|
||||
let name = g.series.get(s).cloned().filter(|n| !n.is_empty()).unwrap_or_else(|| format!("Series {}", s + 1));
|
||||
let grp = self.b.series(&name, s, with_series_opacity(g, s, items));
|
||||
let grp = with_series_group_opacity(g, s, self.b.series(&name, s, with_series_opacity(g, s, items)));
|
||||
self.b.out.push(grp);
|
||||
}
|
||||
self.b.out.append(&mut self.on_top);
|
||||
|
||||
@@ -337,11 +337,12 @@ pub(crate) fn capture_series_opacity(doc: &mut Document, edited: &[(NodeId, bool
|
||||
}
|
||||
|
||||
/// Copy the object opacity and blend mode of generated series art just edited (`edited`) into that series' object opacity
|
||||
/// and blend mode. Called inside the same document edit, so both undo together. Returns whether a series' kept object
|
||||
/// opacity or blend mode changed.
|
||||
/// and blend mode: a path directly in a series group sets the paths' values, and the series group itself sets the
|
||||
/// group's. Called inside the same document edit, so both undo together. Returns whether a series' kept object opacity or
|
||||
/// blend mode changed.
|
||||
pub(crate) fn capture_series_object_opacity(doc: &mut Document, edited: &[NodeId]) -> bool {
|
||||
let mut before = SeriesPaintsBefore::new();
|
||||
for (graph, index, opacity, blend) in object_opacity_captures(doc, edited) {
|
||||
for (graph, index, group, (opacity, blend)) in object_opacity_captures(doc, edited) {
|
||||
let Some(spec) = doc.node_mut(graph).and_then(|n| n.extras_if_any().and_then(|e| e.graph.as_deref_mut())) else { continue };
|
||||
// A fully opaque Normal object on a series the graph keeps nothing for leaves the graph as it is.
|
||||
if opacity.is_none() && blend.is_none() && spec.series_paints.len() <= index {
|
||||
@@ -353,7 +354,11 @@ pub(crate) fn capture_series_object_opacity(doc: &mut Document, edited: &[NodeId
|
||||
spec.series_paints.resize(index + 1, vectorcraft_doc::SeriesPaint::default());
|
||||
}
|
||||
let Some(slot) = spec.series_paints.get_mut(index) else { continue };
|
||||
(slot.opacity, slot.blend) = (opacity, blend);
|
||||
if group {
|
||||
(slot.group_opacity, slot.group_blend) = (opacity, blend);
|
||||
} else {
|
||||
(slot.opacity, slot.blend) = (opacity, blend);
|
||||
}
|
||||
}
|
||||
settle_series_paints(doc, before)
|
||||
}
|
||||
@@ -361,20 +366,24 @@ pub(crate) fn capture_series_object_opacity(doc: &mut Document, edited: &[NodeId
|
||||
/// Would [`capture_series_object_opacity`] change what the graph stores for `edited`? Reads the document without copying
|
||||
/// it.
|
||||
pub(crate) fn series_object_opacity_differs(doc: &Document, edited: &[NodeId]) -> bool {
|
||||
let mut after: HashMap<(NodeId, usize), (Option<f32>, Option<BlendMode>)> = HashMap::new();
|
||||
let stored = |graph: NodeId, index: usize| {
|
||||
let mut after: HashMap<(NodeId, usize, bool), ObjectOpacity> = HashMap::new();
|
||||
let stored = |graph: NodeId, index: usize, group: bool| {
|
||||
let spec = doc.node(graph).and_then(|n| n.extras().graph.as_deref());
|
||||
spec.and_then(|g| g.series_paints.get(index)).map_or((None, None), |slot| (slot.opacity, slot.blend))
|
||||
let slot = spec.and_then(|g| g.series_paints.get(index));
|
||||
slot.map_or((None, None), |slot| if group { (slot.group_opacity, slot.group_blend) } else { (slot.opacity, slot.blend) })
|
||||
};
|
||||
for (graph, index, opacity, blend) in object_opacity_captures(doc, edited) {
|
||||
after.insert((graph, index), (opacity, blend));
|
||||
for (graph, index, group, pair) in object_opacity_captures(doc, edited) {
|
||||
after.insert((graph, index, group), pair);
|
||||
}
|
||||
after.into_iter().any(|((graph, index), pair)| stored(graph, index) != pair)
|
||||
after.into_iter().any(|((graph, index, group), pair)| stored(graph, index, group) != pair)
|
||||
}
|
||||
|
||||
/// The series and object opacity and blend mode (None for 100% and Normal) of each of `edited` that is a path directly in a
|
||||
/// series group, in order.
|
||||
fn object_opacity_captures(doc: &Document, edited: &[NodeId]) -> Vec<(NodeId, usize, Option<f32>, Option<BlendMode>)> {
|
||||
/// An object opacity and blend mode, None for 100% and Normal.
|
||||
type ObjectOpacity = (Option<f32>, Option<BlendMode>);
|
||||
|
||||
/// The graph, series, whether it is the series group itself (`true`) or a path directly in it, and the object opacity and
|
||||
/// blend mode of each of `edited` that is one of those, in order.
|
||||
fn object_opacity_captures(doc: &Document, edited: &[NodeId]) -> Vec<(NodeId, usize, bool, ObjectOpacity)> {
|
||||
if edited.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
@@ -385,14 +394,34 @@ fn object_opacity_captures(doc: &Document, edited: &[NodeId]) -> Vec<(NodeId, us
|
||||
edited
|
||||
.iter()
|
||||
.filter_map(|id| {
|
||||
// Only the paths the graph draws again, not the copy of a graph design.
|
||||
let r = members.get(id).filter(|r| doc.parent_of(*id) == Some(r.group))?;
|
||||
let node = doc.node(*id).filter(|n| matches!(n.kind, NodeKind::Path { .. }))?;
|
||||
Some((r.graph, r.index, (node.opacity < 1.0).then_some(node.opacity), (node.blend != BlendMode::Normal).then_some(node.blend)))
|
||||
let r = members.get(id)?;
|
||||
let group = r.group == *id;
|
||||
// Only the series group and the paths the graph draws again, not the copy of a graph design.
|
||||
let node = doc.node(*id).filter(|n| group || (doc.parent_of(*id) == Some(r.group) && matches!(n.kind, NodeKind::Path { .. })))?;
|
||||
let opacity = (node.opacity < 1.0).then_some(node.opacity);
|
||||
Some((r.graph, r.index, group, (opacity, (node.blend != BlendMode::Normal).then_some(node.blend))))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Series `i`'s group (`group`) with the object opacity and blend mode the graph keeps for it. An opacity read from a file
|
||||
/// is held to 0..1, and one that is not a number is left out.
|
||||
pub(super) fn with_series_group_opacity(g: &GraphSpec, i: usize, mut group: Arc<Node>) -> Arc<Node> {
|
||||
let Some(slot) = g.series_paints.get(i) else { return group };
|
||||
let opacity = slot.group_opacity.filter(|o| o.is_finite()).map(|o| o.clamp(0.0, 1.0));
|
||||
if opacity.is_none() && slot.group_blend.is_none() {
|
||||
return group;
|
||||
}
|
||||
let n = Arc::make_mut(&mut group);
|
||||
if let Some(o) = opacity {
|
||||
n.opacity = o;
|
||||
}
|
||||
if let Some(b) = slot.group_blend {
|
||||
n.blend = b;
|
||||
}
|
||||
group
|
||||
}
|
||||
|
||||
/// Graph Data › Transpose row/column: categories become series and series categories.
|
||||
pub(super) fn transpose(g: &mut GraphSpec) {
|
||||
let cells = g.cells();
|
||||
|
||||
@@ -1859,8 +1859,8 @@ fn appearance_commands_given_a_series_group_edit_its_marks() {
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
|
||||
let (rect, blue) = blue_style(&mut s, "Blue", false);
|
||||
let was = spec(&s, id).series_paints;
|
||||
s.execute("object.setProps", &json!({"ids": [group.0], "opacity": 50})).unwrap();
|
||||
let was = spec(&s, id).series_paints;
|
||||
s.execute("appearance.transfer", &json!({"source": rect.0, "target": group.0, "copy": true})).unwrap();
|
||||
assert!(series_0_group(&s, id).1);
|
||||
assert_eq!(s.doc().unwrap().doc.node(group).unwrap().opacity, 1.0, "the appearance replaces the group's own opacity");
|
||||
@@ -2082,10 +2082,10 @@ fn a_graphic_style_applied_to_a_series_group_styles_its_marks() {
|
||||
let (mut s, id, marks) = stored_red_stroke();
|
||||
let (group, _) = series_0_group(&s, id);
|
||||
let (_, blue) = blue_style(&mut s, "Blue", false);
|
||||
let was = spec(&s, id).series_paints;
|
||||
let own = json!({"ids": [group.0], "opacity": 50, "blend": "multiply", "isolate": true, "knockout": "on", "item": null});
|
||||
s.execute("transparency.set", &own).unwrap();
|
||||
s.execute("effect.apply", &json!({"ids": [group.0], "effect": "distort.twist"})).unwrap();
|
||||
let was = spec(&s, id).series_paints;
|
||||
let g = s.doc().unwrap().doc.node(group).unwrap().clone();
|
||||
assert!(g.isolate && g.knockout != Default::default() && g.blend != vectorcraft_color::BlendMode::Normal);
|
||||
s.execute("graphicStyle.apply", &json!({"name": "Blue", "ids": [group.0]})).unwrap();
|
||||
@@ -2162,6 +2162,82 @@ fn redefining_a_graphic_style_keeps_an_edited_mark_of_a_linked_series_group() {
|
||||
assert!(marks[1..].iter().all(|m| d.node(*m).unwrap().extras().graphic_style == Some(style.id)));
|
||||
}
|
||||
|
||||
/// Series `index`'s group: its object opacity and blend mode, and those of each path directly inside it.
|
||||
fn group_transparency(s: &Session, id: NodeId, index: u32) -> ((f32, BlendMode), Vec<(f32, BlendMode)>) {
|
||||
let n = s.doc().unwrap().doc.node(id).unwrap().clone();
|
||||
let g = series(&n, index);
|
||||
((g.opacity, g.blend), g.children().unwrap().iter().map(|m| (m.opacity, m.blend)).collect())
|
||||
}
|
||||
|
||||
/// An object opacity and blend mode set on a selected series group are kept by the graph for the group: the series group
|
||||
/// has them after the graph is drawn again, through save and open, its paths and the other series stay opaque and
|
||||
/// Normal, setting them again records no step, 100% and Normal forget them, and undo takes them back.
|
||||
#[test]
|
||||
fn an_opacity_and_blend_mode_on_a_series_group_survive_regeneration() {
|
||||
let (mut s, id, _, _) = column_marks();
|
||||
let (group, _) = series_0_group(&s, id);
|
||||
s.execute("select.set", &json!({"ids": [group.0]})).unwrap();
|
||||
s.execute("transparency.set", &json!({"opacity": 40, "blend": "multiply"})).unwrap();
|
||||
let kept = spec(&s, id).series_paints[0].clone();
|
||||
assert_eq!((kept.group_opacity, kept.group_blend), (Some(0.4), Some(BlendMode::Multiply)));
|
||||
assert_eq!((kept.opacity, kept.blend), (None, None), "the paths keep their own");
|
||||
let restored: GraphSpec = serde_json::from_value(serde_json::to_value(spec(&s, id)).unwrap()).unwrap();
|
||||
assert_eq!(restored, spec(&s, id));
|
||||
s.execute("graph.setData", &json!({"id": id.0, "rows": [[1, 2], [3, 4]]})).unwrap();
|
||||
let normal = (1.0, BlendMode::Normal);
|
||||
let (own, paths) = group_transparency(&s, id, 0);
|
||||
assert_eq!(own, (0.4, BlendMode::Multiply));
|
||||
assert!(!paths.is_empty() && paths.iter().all(|t| *t == normal), "{paths:?}");
|
||||
let (own, paths) = group_transparency(&s, id, 1);
|
||||
assert_eq!(own, normal);
|
||||
assert!(paths.iter().all(|t| *t == normal), "{paths:?}");
|
||||
|
||||
let (group, _) = series_0_group(&s, id);
|
||||
let undo = s.doc().unwrap().history.undo.len();
|
||||
s.execute("object.setProps", &json!({"id": group.0, "opacity": 40, "blend": "multiply"})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), undo, "the values the group and the graph have already");
|
||||
s.execute("object.setProps", &json!({"id": group.0, "opacity": 100, "blend": "normal"})).unwrap();
|
||||
let kept = spec(&s, id).series_paints[0].clone();
|
||||
assert_eq!((kept.group_opacity, kept.group_blend), (None, None));
|
||||
s.execute("select.set", &json!({"ids": [id.0]})).unwrap();
|
||||
s.execute("graph.setData", &json!({"rows": [[2, 2], [3, 4]]})).unwrap();
|
||||
assert_eq!(group_transparency(&s, id, 0).0, normal);
|
||||
for _ in 0..4 {
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
}
|
||||
assert!(spec(&s, id).series_paints.is_empty(), "undo takes back the edit and what the graph stored");
|
||||
}
|
||||
|
||||
/// Commands that clear a series group's own opacity and blend mode also remove what the graph keeps for it: Clear
|
||||
/// Appearance on the group, a graphic style applied to it and Move Appearance onto it.
|
||||
#[test]
|
||||
fn clearing_a_series_groups_opacity_forgets_what_the_graph_keeps() {
|
||||
let kept = |s: &Session, id: NodeId| {
|
||||
let slot = spec(s, id).series_paints[0].clone();
|
||||
(slot.group_opacity, slot.group_blend)
|
||||
};
|
||||
let faded = (Some(0.5), Some(BlendMode::Screen));
|
||||
let (mut s, id, _, _) = column_marks();
|
||||
let (rect, _) = blue_style(&mut s, "Blue", false);
|
||||
let (group, _) = series_0_group(&s, id);
|
||||
s.execute("object.setProps", &json!({"id": group.0, "opacity": 50, "blend": "screen"})).unwrap();
|
||||
assert_eq!(kept(&s, id), faded);
|
||||
let commands = [
|
||||
("appearance.clear", json!({"ids": [group.0]})),
|
||||
("graphicStyle.apply", json!({"name": "Blue", "ids": [group.0]})),
|
||||
("appearance.transfer", json!({"source": rect.0, "target": group.0, "copy": true})),
|
||||
];
|
||||
for (cmd, params) in commands {
|
||||
s.execute(cmd, ¶ms).unwrap();
|
||||
assert_eq!(kept(&s, id), (None, None), "{cmd}");
|
||||
s.execute("graph.setData", &json!({"id": id.0, "rows": [[1, 2], [3, 4]]})).unwrap();
|
||||
assert_eq!(group_transparency(&s, id, 0).0, (1.0, BlendMode::Normal), "{cmd}");
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(kept(&s, id), faded, "{cmd}");
|
||||
}
|
||||
}
|
||||
|
||||
/// 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() {
|
||||
|
||||
@@ -251,7 +251,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Object Properties",
|
||||
[],
|
||||
None,
|
||||
"{ids?|id?, name?, visible?, locked? (visible: false or locked: true deselects every selected object hidden or locked after the call, the key object included), opacity?: 0..100, blend?: \"Multiply\"…, isolate?, knockout?: \"on\"|\"off\"|\"neutral\"|bool (true = on, false = neutral), knockoutShape?: bool, data?: {key: \"value\" | null (removes it)} (the object's own data, SVG data-* attributes: {pivot: \"100,180\"} is data-pivot; document.node → attrs.data)}. On a path directly in a graph series, the graph also stores the object's opacity and blend mode (exceptions: docs/mcp.md); on a graph label, the graph stores the label's opacity and blend mode for every label of its kind",
|
||||
"{ids?|id?, name?, visible?, locked? (visible: false or locked: true deselects every selected object hidden or locked after the call, the key object included), opacity?: 0..100, blend?: \"Multiply\"…, isolate?, knockout?: \"on\"|\"off\"|\"neutral\"|bool (true = on, false = neutral), knockoutShape?: bool, data?: {key: \"value\" | null (removes it)} (the object's own data, SVG data-* attributes: {pivot: \"100,180\"} is data-pivot; document.node → attrs.data)}. On a path directly in a graph series or on a series group, the graph also stores the object's opacity and blend mode (exceptions: docs/mcp.md); on a graph label, the graph stores the label's opacity and blend mode for every label of its kind",
|
||||
has_doc,
|
||||
set_props
|
||||
),
|
||||
|
||||
@@ -81,7 +81,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Transparency",
|
||||
["Window", "Transparency"],
|
||||
None,
|
||||
"{ids?|id?, opacity?: 0..100, blend?: name, isolate?, knockout?: \"on\"|\"off\"|\"neutral\"|bool (true = on, false = neutral), knockoutShape?: bool, item?: appearance item index|null, target?: \"object\"|\"contents\", aligned?: bool (index and item are rows of the first target's stack: each other target edits its equal one, as the Appearance panel does for a mixed selection)} for `ids`, the selection, or the object whose opacity mask is being edited; opacity and blend go to the targeted fill/stroke item (omitted: the Appearance panel's active item, else the objects; a gradient mesh point last clicked with the Mesh tool or Direct Selection takes the opacity itself). With an object of a dynamic symbol instance selected by Direct Selection and no ids, id or item, opacity and blend change that object for this instance only (isolate, knockout, knockoutShape, aligned and target are refused). On a path directly in a graph series (for a fill or stroke item, a series group's id stands for those paths), the graph also stores the opacity and blend mode of the object and of the topmost visible fill and stroke (exceptions: docs/mcp.md); on a graph label, the graph stores the label's object opacity and blend mode for every label of its kind, and with `item` its topmost visible fill and stroke rows too",
|
||||
"{ids?|id?, opacity?: 0..100, blend?: name, isolate?, knockout?: \"on\"|\"off\"|\"neutral\"|bool (true = on, false = neutral), knockoutShape?: bool, item?: appearance item index|null, target?: \"object\"|\"contents\", aligned?: bool (index and item are rows of the first target's stack: each other target edits its equal one, as the Appearance panel does for a mixed selection)} for `ids`, the selection, or the object whose opacity mask is being edited; opacity and blend go to the targeted fill/stroke item (omitted: the Appearance panel's active item, else the objects; a gradient mesh point last clicked with the Mesh tool or Direct Selection takes the opacity itself). With an object of a dynamic symbol instance selected by Direct Selection and no ids, id or item, opacity and blend change that object for this instance only (isolate, knockout, knockoutShape, aligned and target are refused). On a path directly in a graph series (for a fill or stroke item, a series group's id stands for those paths), the graph also stores the opacity and blend mode of the object and of the topmost visible fill and stroke, and on a series group without an item, the group's own opacity and blend mode (exceptions: docs/mcp.md); on a graph label, the graph stores the label's object opacity and blend mode for every label of its kind, and with `item` its topmost visible fill and stroke rows too",
|
||||
has_doc,
|
||||
transparency
|
||||
),
|
||||
|
||||
+8
-3
@@ -790,10 +790,15 @@ look was edited keep their look. Read as a source, by `graphicStyle.new`, `graph
|
||||
style link of its own gives the look and the style link of the topmost path directly in it, and a move with
|
||||
`appearance.transfer` clears each of those paths; a series group that has one of them is read itself.
|
||||
|
||||
`transparency.set` and `object.setProps` with an opacity or blend mode for the object, given a series group's id or the
|
||||
selected series group, set the group's own opacity and blend mode, and the graph stores them for that series: the
|
||||
series group takes them when the graph is drawn again, over the paths' own. Each of the commands above that clears the
|
||||
group's opacity and blend mode also removes what the graph stores for it.
|
||||
|
||||
The other commands given a series group's id edit the group itself, and the graph does not store what they set on it:
|
||||
`transparency.set` and `object.setProps` with an opacity or blend mode for the object, the `effect.*` commands and
|
||||
`appearance.addEffect`, and the paint, stroke and gradient commands with a fill or stroke `item`, which is a row of the
|
||||
group's own stack. The Appearance panel and `appearance.setActiveItem` read the selected group's own rows.
|
||||
`transparency.set` and `object.setProps` with isolation or knockout, the `effect.*` commands and `appearance.addEffect`,
|
||||
and the paint, stroke and gradient commands with a fill or stroke `item`, which is a row of the group's own stack. The
|
||||
Appearance panel and `appearance.setActiveItem` read the selected group's own rows.
|
||||
|
||||
The graph also stores a paint or stroke weight that one of these commands changes on the topmost visible fill or stroke
|
||||
of a path directly in a series: a weight set with `stroke.set`, `stroke.weightUp` or `stroke.weightDown`; a gradient
|
||||
|
||||
@@ -1097,7 +1097,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- **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).
|
||||
- **Blend steps of brushed strokes:** the steps interpolate the pen pressure, tilt, bearing and rotation recorded with the Paintbrush, so a Calligraphic, Scatter or Art brush that varies with the pen changes gradually from one key to the next. A key without a recording counts as half pressure, half tilt and 0°, as brushes read it, and angles turn the shorter way round. When the blend starts a key's path at another anchor (an open path run from its end, a closed one turned to another start), the key's recordings, width profile and arrowheads turn with the path. Different brushes still switch halfway (VectorCraft's own rule; strokes blend their options, as [Blended objects overview](https://helpx.adobe.com/illustrator/desktop/manage-colors/apply-transparency-and-blending/blended-objects-overview.html) describes; `crates/doc/src/blend.rs`, `pen_pressure_tilt_and_angles_interpolate_for_brushes`, `stroke_profiles_turn_with_a_key_path_started_elsewhere`).
|
||||
- **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), and removes the merged anchors in one pass over the path, so a long run of them no longer moves the anchors after it once per merge (`cmd::path::tests::a_long_run_before_many_other_anchors_merges_in_one_pass`); 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, the paint, weight, opacity and blend mode of a mark's top visible fill or stroke row after an Appearance command or a graphic style (each given a mark or its series picked with Group Selection), the Eyedropper, Default Fill and Stroke or Swap Fill and Stroke, the paint or stroke weight a stroke weight, gradient or recolor command changes there, and a mark's object opacity and blend mode, are kept separately on the graph and reapplied when its data, type or size changes, and through save, open and undo. A series that draws Filled Lines bands keeps no stroke paint or weight: its bands are drawn again with their 0.25 pt black outline. 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, Paragraph, Appearance, Transparency or Graphic Styles controls or the Eyedropper, 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, the paint, weight, opacity and blend mode of a label's topmost visible fill and stroke rows and its object opacity and blend mode included. 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, the paint, weight, opacity and blend mode of a mark's top visible fill or stroke row after an Appearance command or a graphic style (each given a mark or its series picked with Group Selection), the Eyedropper, Default Fill and Stroke or Swap Fill and Stroke, the paint or stroke weight a stroke weight, gradient or recolor command changes there, and the object opacity and blend mode of a mark or of a series group picked with Group Selection, are kept separately on the graph and reapplied when its data, type or size changes, and through save, open and undo. A series that draws Filled Lines bands keeps no stroke paint or weight: its bands are drawn again with their 0.25 pt black outline. 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, Paragraph, Appearance, Transparency or Graphic Styles controls or the Eyedropper, 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, the paint, weight, opacity and blend mode of a label's topmost visible fill and stroke rows and its object opacity and blend mode included. 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).
|
||||
An Eyedropper pick that includes transparency and leaves a mark's top fill or stroke row without paint, or removes that row when the source has no row of that kind, where that row had paint before, also replaces the opacity and blend mode the graph keeps for that kind of row in the series; a row that had no paint before the pick leaves them as they are.
|
||||
- **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.
|
||||
|
||||
Reference in New Issue
Block a user