mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-14 07:46:07 +00:00
Merge pull request #7304 from dotCooCoo/fix/graph-series-object-opacity-other-commands
Graphs: Global Edit, Randomize Opacity and Sort Objects keep what they give a series
This commit is contained in:
@@ -20,7 +20,9 @@ use vectorcraft_doc::{Node, NodeId, OrientedBox};
|
||||
use vectorcraft_geom::{Affine, Point, Rect};
|
||||
use vectorcraft_render::effects;
|
||||
|
||||
use super::super::appearance::series_children_in_paint_order;
|
||||
use super::super::edit::selected_roots;
|
||||
use super::super::graph::capture_series_object_opacity;
|
||||
use super::super::menucmds::whole_or;
|
||||
use super::super::object::{choice, object_bounds_transform, scaling, transform_node, transform_patterns};
|
||||
use super::super::typecmd::refresh_bounds_of;
|
||||
@@ -90,7 +92,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Randomize Opacity",
|
||||
["Object", "Plug-ins", "Arrange Tools"],
|
||||
None,
|
||||
"{min?: % 0..100 (30), max?: % 0..100 (100; not less than min), seed?: whole number (0; the same seed, objects and range give the same opacities), ids?: [id…] (default: the selection)} give each selected object (the top-level ones) its own opacity, picked at random and evenly from min to max, as one undo step; it is the object's opacity, as the Transparency panel sets it → {count}",
|
||||
"{min?: % 0..100 (30), max?: % 0..100 (100; not less than min), seed?: whole number (0; the same seed, objects and range give the same opacities), ids?: [id…] (default: the selection)} give each selected object (the top-level ones) its own opacity, picked at random and evenly from min to max, as one undo step; it is the object's opacity, as the Transparency panel sets it. On paths directly in a graph series whose opacity changes, the graph stores the opacity and blend mode of the frontmost of them for its next redraw (docs/mcp.md) → {count}",
|
||||
has_selection,
|
||||
randomize_opacity
|
||||
),
|
||||
@@ -99,7 +101,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Sort Objects",
|
||||
["Object", "Plug-ins", "Arrange Tools"],
|
||||
None,
|
||||
"{by?: \"width\" | \"height\" | \"area\" (width × height) | \"x\" (left edge) | \"y\" (top edge) | \"opacity\" (%) | \"stacking\" (stacking order, from the back) (\"area\"), apply?: one of the same (\"stacking\"), order?: \"ascending\" (small to large) | \"descending\" (large to small) | \"random\" (\"ascending\"), distribute?: false (true: the values step evenly from the smallest to the largest; false: the objects trade the values they have), seed?: whole number (0; the order \"random\" picks), scaleStrokes?: the Scale Strokes & Effects preference (width, height and area), ids?: [id…] (default: the selection)} sort the selected objects (the top-level ones, at most 20000) by the attribute `by` and give them the values of `apply` in that order, as one undo step: the first in order gets the smallest value. Sizes and places are measured and set as object.setBounds measures them (Use Preview Bounds), a width, height or area about each object's centre (an area in proportion); stacking order always trades places, and only among the objects in the same layer or group. A run that changes nothing makes no step → {count}",
|
||||
"{by?: \"width\" | \"height\" | \"area\" (width × height) | \"x\" (left edge) | \"y\" (top edge) | \"opacity\" (%) | \"stacking\" (stacking order, from the back) (\"area\"), apply?: one of the same (\"stacking\"), order?: \"ascending\" (small to large) | \"descending\" (large to small) | \"random\" (\"ascending\"), distribute?: false (true: the values step evenly from the smallest to the largest; false: the objects trade the values they have), seed?: whole number (0; the order \"random\" picks), scaleStrokes?: the Scale Strokes & Effects preference (width, height and area), ids?: [id…] (default: the selection)} sort the selected objects (the top-level ones, at most 20000) by the attribute `by` and give them the values of `apply` in that order, as one undo step: the first in order gets the smallest value. Sizes and places are measured and set as object.setBounds measures them (Use Preview Bounds), a width, height or area about each object's centre (an area in proportion); stacking order always trades places, and only among the objects in the same layer or group. With apply \"opacity\", on paths directly in a graph series whose opacity changes, the graph stores the opacity and blend mode of the frontmost of them for its next redraw (docs/mcp.md). A run that changes nothing makes no step → {count}",
|
||||
has_multi,
|
||||
sort
|
||||
),
|
||||
@@ -396,17 +398,30 @@ fn randomize_opacity(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
let (lo, hi) = ((min / 100.0) as f32, (max / 100.0) as f32);
|
||||
s.edit("Randomize Opacity", |d, _| {
|
||||
let mut changed = vec![];
|
||||
for id in &ids {
|
||||
let o = rng.range(lo, hi);
|
||||
if let Some(n) = d.node_mut(*id) {
|
||||
if let Some(n) = d.node_mut(*id)
|
||||
&& n.opacity != o
|
||||
{
|
||||
n.opacity = o;
|
||||
changed.push(*id);
|
||||
}
|
||||
}
|
||||
keep_series_opacity(d, &changed);
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(json!({ "count": ids.len() }))
|
||||
}
|
||||
|
||||
/// Store on their graphs the opacity just given to the paths among `changed` (objects whose opacity changed) that are
|
||||
/// directly in a graph series, with their blend mode, so that every path of the series has them when the graph is drawn
|
||||
/// again; the frontmost of them in a series sets them.
|
||||
fn keep_series_opacity(d: &mut vectorcraft_doc::Document, changed: &[NodeId]) {
|
||||
let marks = series_children_in_paint_order(d, changed);
|
||||
capture_series_object_opacity(d, &marks);
|
||||
}
|
||||
|
||||
/// An object Sort Objects sorts: its layer or group, its place in it, its box as
|
||||
/// `object.setBounds` measures it and its opacity (%).
|
||||
struct Sorted {
|
||||
@@ -510,6 +525,8 @@ fn sort(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
n.opacity = (v / 100.0) as f32;
|
||||
}
|
||||
}
|
||||
let changed: Vec<NodeId> = wanted.iter().filter(|(o, v)| (o.opacity - v).abs() >= 1e-6).map(|(o, _)| o.id).collect();
|
||||
keep_series_opacity(d, &changed);
|
||||
Ok(())
|
||||
})?;
|
||||
return Ok(count);
|
||||
|
||||
@@ -806,12 +806,18 @@ pub(super) fn capture_series_writes(
|
||||
/// and objects the edit left as they were keep what the graph stores. When the edit changes several marks of one series,
|
||||
/// the frontmost one sets what the graph stores, whatever order `ids` are in. Returns whether the stored paint changed.
|
||||
pub(crate) fn capture_series_changes(before: &vectorcraft_doc::Document, d: &mut vectorcraft_doc::Document, ids: &[NodeId]) -> bool {
|
||||
let marks = series_children_in_paint_order(d, ids);
|
||||
!marks.is_empty() && capture_series_writes(before, d, &marks, (false, false), (false, false), false, false)
|
||||
}
|
||||
|
||||
/// Each of `ids` directly in a series group, from the back to the front: a later mark's capture replaces an earlier
|
||||
/// one's, so the frontmost one sets what the graph stores.
|
||||
pub(crate) fn series_children_in_paint_order(d: &vectorcraft_doc::Document, ids: &[NodeId]) -> Vec<NodeId> {
|
||||
// Only objects directly in a series group can be marks: other edits skip the search of the document for graphs.
|
||||
let in_series = |id: &NodeId| d.parent_of(*id).and_then(|p| d.node(p)).is_some_and(|g| g.extras().series_index.is_some());
|
||||
let mut marks: Vec<NodeId> = ids.iter().copied().filter(in_series).collect();
|
||||
// A later mark's capture replaces an earlier one's: take them in paint order.
|
||||
marks.sort_by_cached_key(|id| d.index_path(*id));
|
||||
!marks.is_empty() && capture_series_writes(before, d, &marks, (false, false), (false, false), false, false)
|
||||
marks
|
||||
}
|
||||
|
||||
/// Store on the graph the paint that an edit of `ids` (from `before` to `d`) leaves on the topmost visible fill and stroke
|
||||
|
||||
@@ -49,6 +49,10 @@
|
||||
//! member with another order, and any other change to the list (items moved, or several added
|
||||
//! or removed at once), takes the model's whole list of fills and strokes.
|
||||
//!
|
||||
//! A path directly in a graph series that takes an edit from another member (a graph's paths are
|
||||
//! members only while the graph is isolated) also has what changed stored on its graph, so that
|
||||
//! every path of the series takes it when the graph is drawn again (docs/mcp.md).
|
||||
//!
|
||||
//! Members that are hidden or locked at the time are left as they are. An edit that would take any
|
||||
//! member past the canvas (beyond [`vectorcraft_doc::MAX_COORD`]) is refused whole, as one that
|
||||
//! takes the selection there is. Whenever the document is put back (Undo, Redo, a cancelled drag
|
||||
@@ -786,9 +790,22 @@ pub(crate) fn propagate(before: &Document, d: &mut Document, ge: &GlobalEdit, se
|
||||
}
|
||||
super::object::keep_compound_aligned(d, *id, Scaling::default());
|
||||
}
|
||||
keep_series_changes(before, d, &edited);
|
||||
edited
|
||||
}
|
||||
|
||||
/// Store on their graphs what the members `edited`, and the objects in them, took on the paths directly in a graph
|
||||
/// series, for the graph's next redraw. The edited member is left to the edit's own command.
|
||||
fn keep_series_changes(before: &Document, d: &mut Document, edited: &[NodeId]) {
|
||||
let mut taken = vec![];
|
||||
for id in edited {
|
||||
if let Some(n) = d.node(*id) {
|
||||
n.walk(&mut |c: &Node| taken.push(c.id));
|
||||
}
|
||||
}
|
||||
super::appearance::capture_series_changes(before, d, &taken);
|
||||
}
|
||||
|
||||
/// The outlines of the members of the active document's Global Edit that are not selected, hold
|
||||
/// no selected object and sit inside no selected object, in their layer's colour: the canvas
|
||||
/// highlights them while Global Edit is on. While it is off, with one supported object selected,
|
||||
|
||||
@@ -572,6 +572,124 @@ fn an_object_opacity_a_series_mark_already_shows_replaces_the_kept_opacity() {
|
||||
assert!(spec(&s, id).series_paints.iter().all(|p| p.opacity.is_none()));
|
||||
}
|
||||
|
||||
/// Randomize Opacity on marks of one series stores the opacity of the frontmost of them on the graph in the same undo
|
||||
/// step, and every mark of the series has it when the graph is drawn again.
|
||||
#[test]
|
||||
fn a_random_opacity_on_series_marks_survives_regeneration() {
|
||||
let (mut s, id, _, marks) = column_marks();
|
||||
s.execute("addon.arrange.randomizeOpacity", &json!({"ids": [marks[1].0, marks[0].0], "min": 30, "max": 60, "seed": 7})).unwrap();
|
||||
let front = s.doc().unwrap().doc.node(marks[1]).unwrap().opacity;
|
||||
assert!(front < 1.0);
|
||||
assert_eq!(spec(&s, id).series_paints[0].opacity, Some(front));
|
||||
s.execute("graph.setData", &json!({"rows": [[1, 2], [3, 4]]})).unwrap();
|
||||
assert!(transparency_of(&s, id, 0).iter().all(|t| t.0 == front), "{:?}", transparency_of(&s, id, 0));
|
||||
assert!(transparency_of(&s, id, 1).iter().all(|t| t.0 == 1.0), "{:?}", transparency_of(&s, id, 1));
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert!(spec(&s, id).series_paints.is_empty(), "undo takes back the opacities and what the graph stored");
|
||||
}
|
||||
|
||||
/// Sort Objects giving series marks new opacities stores the opacity of the frontmost of them on the graph, in place of
|
||||
/// the one another mark left there.
|
||||
#[test]
|
||||
fn an_opacity_sort_objects_gives_series_marks_survives_regeneration() {
|
||||
let (mut s, id, _, marks) = column_marks();
|
||||
s.execute("object.setProps", &json!({"id": marks[2].0, "opacity": 30})).unwrap();
|
||||
assert_eq!(spec(&s, id).series_paints[0].opacity, Some(0.3));
|
||||
// From left to right the three columns get 30%, 65% and 100%: the frontmost, the rightmost, is fully opaque.
|
||||
let ids = [marks[0].0, marks[1].0, marks[2].0];
|
||||
s.execute("addon.arrange.sort", &json!({"ids": ids, "by": "x", "apply": "opacity", "distribute": true})).unwrap();
|
||||
let opacities: Vec<f32> = marks[..3].iter().map(|m| s.doc().unwrap().doc.node(*m).unwrap().opacity).collect();
|
||||
assert_eq!(opacities, [0.3, 0.65, 1.0]);
|
||||
assert_eq!(spec(&s, id).series_paints[0].opacity, None);
|
||||
s.execute("graph.setData", &json!({"rows": [[1, 2], [3, 4]]})).unwrap();
|
||||
assert!(transparency_of(&s, id, 0).iter().all(|t| t.0 == 1.0), "{:?}", transparency_of(&s, id, 0));
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(spec(&s, id).series_paints[0].opacity, Some(0.3), "undo takes back the sort and what the graph stored");
|
||||
}
|
||||
|
||||
/// Sort Objects stores the opacity of the frontmost series mark whose opacity it changes: a mark in front that keeps its
|
||||
/// opacity leaves the stored value to the marks it changed.
|
||||
#[test]
|
||||
fn an_opacity_sort_objects_stores_the_frontmost_mark_it_changes() {
|
||||
let (mut s, id, _, marks) = column_marks();
|
||||
s.execute("object.setProps", &json!({"id": marks[0].0, "opacity": 60})).unwrap();
|
||||
s.execute("object.setProps", &json!({"id": marks[1].0, "opacity": 30})).unwrap();
|
||||
assert_eq!(spec(&s, id).series_paints[0].opacity, Some(0.3));
|
||||
// From left to right the columns trade 60%, 30% and 100% for 30%, 60% and 100%: the frontmost keeps its 100%.
|
||||
let ids = [marks[0].0, marks[1].0, marks[2].0];
|
||||
s.execute("addon.arrange.sort", &json!({"ids": ids, "by": "x", "apply": "opacity"})).unwrap();
|
||||
let opacities: Vec<f32> = marks[..3].iter().map(|m| s.doc().unwrap().doc.node(*m).unwrap().opacity).collect();
|
||||
assert_eq!(opacities, [0.3, 0.6, 1.0]);
|
||||
assert_eq!(spec(&s, id).series_paints[0].opacity, Some(0.6));
|
||||
}
|
||||
|
||||
/// An object opacity, blend mode, fill, stroke and weight that Global Edit gives a mark of another series from the mark
|
||||
/// that was edited are stored on the graph for that series in the same undo step, and every mark of the series has them
|
||||
/// when the graph is drawn again.
|
||||
#[test]
|
||||
fn what_global_edit_gives_series_marks_survives_regeneration() {
|
||||
let (mut s, id, _, _) = column_marks();
|
||||
// Equal values: the first column of each series has the same shape.
|
||||
s.execute("graph.setData", &json!({"rows": [[3, 3], [5, 2]]})).unwrap();
|
||||
let n = s.doc().unwrap().doc.node(id).unwrap().clone();
|
||||
let (first, other) = (series(&n, 0).children().unwrap()[0].id, series(&n, 1).children().unwrap()[0].id);
|
||||
s.execute("object.isolate", &json!({"id": id.0})).unwrap();
|
||||
s.execute("select.set", &json!({"ids": [first.0]})).unwrap();
|
||||
let r = s.execute("select.globalEdit", &json!({})).unwrap();
|
||||
assert!(r["ids"].as_array().unwrap().contains(&json!(other.0)), "{r}");
|
||||
let steps = s.doc().unwrap().history.undo.len();
|
||||
s.execute("transparency.set", &json!({"ids": [first.0], "opacity": 40, "blend": "multiply"})).unwrap();
|
||||
s.execute("paint.setFill", &json!({"ids": [first.0], "color": "#e8573f"})).unwrap();
|
||||
s.execute("paint.setStroke", &json!({"ids": [first.0], "color": "#1f4e9c"})).unwrap();
|
||||
s.execute("stroke.set", &json!({"ids": [first.0], "weight": 5})).unwrap();
|
||||
let mark = s.doc().unwrap().doc.node(other).unwrap().clone();
|
||||
assert_eq!((mark.opacity, mark.blend), (0.4, BlendMode::Multiply), "Global Edit reaches the other series' mark");
|
||||
let kept = spec(&s, id).series_paints[1].clone();
|
||||
assert_eq!((kept.opacity, kept.blend), (Some(0.4), Some(BlendMode::Multiply)));
|
||||
assert_eq!(kept.fill, Some(mark.appearance.fill_paint()));
|
||||
assert_eq!((kept.stroke, kept.stroke_width), (Some(mark.appearance.stroke_paint()), Some(5.0)));
|
||||
s.execute("object.exitIsolation", &json!({})).unwrap();
|
||||
s.execute("graph.setData", &json!({"rows": [[1, 2], [3, 4]]})).unwrap();
|
||||
let normal = BlendMode::Normal;
|
||||
assert!(transparency_of(&s, id, 1).iter().all(|t| *t == (0.4, BlendMode::Multiply, normal, normal)), "{:?}", transparency_of(&s, id, 1));
|
||||
let n = s.doc().unwrap().doc.node(id).unwrap().clone();
|
||||
assert!(series(&n, 1).children().unwrap().iter().all(|m| m.appearance.fill_paint() == mark.appearance.fill_paint()));
|
||||
while s.doc().unwrap().history.undo.len() > steps {
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
}
|
||||
assert!(spec(&s, id).series_paints.iter().all(|p| *p == vectorcraft_doc::SeriesPaint::default()), "{:?}", spec(&s, id).series_paints);
|
||||
}
|
||||
|
||||
/// With a whole series group picked and Global Edit on, the other series groups of the same form take the edit of its
|
||||
/// marks, and the graph stores it for their series too.
|
||||
#[test]
|
||||
fn what_global_edit_gives_the_marks_of_another_series_group_survives_regeneration() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
// Series without names draw no legend, so each series group holds only its columns, laid out like the other's.
|
||||
let csv = ",,\nQ1,3,3\nQ2,5,5";
|
||||
let made = s.execute("graph.create", &json!({"type": "column", "x": 100, "y": 100, "width": 300, "height": 200, "csv": csv})).unwrap();
|
||||
let id = NodeId(made["id"].as_u64().unwrap());
|
||||
let n = s.doc().unwrap().doc.node(id).unwrap().clone();
|
||||
let (first, other) = (series(&n, 0).clone(), series(&n, 1).id);
|
||||
s.execute("object.isolate", &json!({"id": id.0})).unwrap();
|
||||
s.execute("select.set", &json!({"ids": [first.id.0]})).unwrap();
|
||||
let r = s.execute("select.globalEdit", &json!({})).unwrap();
|
||||
assert!(r["ids"].as_array().unwrap().contains(&json!(other.0)), "{r}");
|
||||
let marks: Vec<u64> = first.children().unwrap().iter().map(|m| m.id.0).collect();
|
||||
s.execute("paint.setFill", &json!({"ids": marks, "color": "#e8573f"})).unwrap();
|
||||
let n = s.doc().unwrap().doc.node(id).unwrap().clone();
|
||||
let given = series(&n, 1).children().unwrap()[0].appearance.fill_paint();
|
||||
assert_eq!(given, series(&n, 0).children().unwrap()[0].appearance.fill_paint(), "Global Edit reaches the other group");
|
||||
assert_eq!(spec(&s, id).series_paints[1].fill, Some(given.clone()));
|
||||
s.execute("object.exitIsolation", &json!({})).unwrap();
|
||||
s.execute("graph.setData", &json!({"rows": [[1, 2], [3, 4]]})).unwrap();
|
||||
let n = s.doc().unwrap().doc.node(id).unwrap().clone();
|
||||
assert!(series(&n, 1).children().unwrap().iter().all(|m| m.appearance.fill_paint() == given));
|
||||
}
|
||||
|
||||
/// A blend mode set on the top fill or stroke row of one series mark is kept by the graph and drawn on every mark of
|
||||
/// the series again; Normal forgets it.
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user