mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-09 02:34:09 +00:00
Merge pull request #5692 from dotCooCoo/fix/appearance-active-edits-each-target
Appearance: edits to the active row reach the matching row in each selected object
This commit is contained in:
@@ -276,22 +276,24 @@ impl ItemTarget {
|
||||
/// This target for each of `ids` (in their order): an aligned item (a row of the first
|
||||
/// target's stack, or for the active item of the first selected object's, see
|
||||
/// [`ItemTarget::anchor`]) is, in each other target, the item equal to it there (see
|
||||
/// [`SharedStack`]); `None` for a target that has none, which the edit leaves alone. A row the
|
||||
/// targets don't share, and any other target, stands for itself in every target.
|
||||
/// [`SharedStack`]), each target matched against that stack on its own, whatever the other
|
||||
/// targets have. In a target without an equal item, and for any other target, the row stands
|
||||
/// for itself. Every entry is `Some`.
|
||||
pub(crate) fn per_target(self, s: &Session, ids: &[NodeId]) -> Vec<Option<ItemTarget>> {
|
||||
let ItemTarget::Item { index, explicit, aligned: true } = self else { return vec![Some(self); ids.len()] };
|
||||
let Some(st) = s.active().filter(|_| ids.len() >= 2) else { return vec![Some(self); ids.len()] };
|
||||
let k = self.anchor(s, ids);
|
||||
let mut order = ids.to_vec();
|
||||
order.swap(0, k);
|
||||
let Some(shared) = shared_stack(&st.doc, &order) else { return vec![Some(self); ids.len()] };
|
||||
if !shared.has_item(index) {
|
||||
return vec![Some(self); ids.len()];
|
||||
}
|
||||
let mut per: Vec<Option<ItemTarget>> =
|
||||
(0..order.len()).map(|j| shared.item_in(index, j).map(|index| ItemTarget::Item { index, explicit, aligned: true })).collect();
|
||||
per.swap(0, k);
|
||||
per
|
||||
let Some(first) = ids.get(k).and_then(|id| st.doc.node(*id)) else { return vec![Some(self); ids.len()] };
|
||||
ids.iter()
|
||||
.enumerate()
|
||||
.map(|(j, id)| match st.doc.node(*id) {
|
||||
Some(n) if j != k => {
|
||||
let at = SharedStack::of(&[&*first.appearance, &*n.appearance]).item_in(index, 1);
|
||||
Some(at.map_or(self, |index| ItemTarget::Item { index, explicit, aligned: true }))
|
||||
}
|
||||
_ => Some(self),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Where among `ids` (the targets, in paint order) the object whose stack this item is a row of
|
||||
|
||||
@@ -8,11 +8,12 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::appearance::SharedStack;
|
||||
use vectorcraft_doc::{Document, Effect, Node, NodeKind, StrokeLayer};
|
||||
use vectorcraft_geom::{FillRule, PathData};
|
||||
use vectorcraft_render::effects;
|
||||
|
||||
use super::appearance::{ItemTarget, aligned, appearance_targets, index_param, item_indices, item_target, item_target_at, shared_stack};
|
||||
use super::appearance::{ItemTarget, aligned, appearance_targets, index_param, item_indices, item_target, item_target_at};
|
||||
use super::expand::has_seeded;
|
||||
use super::rasterfx;
|
||||
use super::{CommandSpec, EngineError, NodeId, Result, Session, always, bad, cmd, fileio, has_doc, str_param};
|
||||
@@ -105,24 +106,23 @@ fn ids_json(ids: &[NodeId]) -> Value {
|
||||
/// Each target of an effect edit aimed at `item` and effect `index`: the object, the item whose
|
||||
/// effects it edits (`None`: the object's own) and the effect's index there. With `aligned` (see
|
||||
/// [`ItemTarget::per_target`]) `item` and `index` are the first target's, and each other target
|
||||
/// edits its equal item, or its equal object-level effect; a target without them is left out.
|
||||
/// edits its equal item, or its equal object-level effect, each target matched on its own whatever
|
||||
/// the other targets have; in a target without them, `item` and `index` stand for themselves.
|
||||
pub(crate) fn effect_targets(s: &Session, p: &Value, item: ItemTarget, index: usize, cmd: &str) -> Result<Vec<(NodeId, Option<usize>, usize)>> {
|
||||
let roots = appearance_targets(s, p)?;
|
||||
let d = &s.doc()?.doc;
|
||||
let per = item.per_target(s, &roots);
|
||||
let shared = if aligned(p) { shared_stack(d, &roots) } else { None };
|
||||
let first = roots.first().and_then(|f| d.node(*f));
|
||||
// Whether `item` names an item in the stack it is a row of (else the edit is object-level there).
|
||||
let anchor = roots.get(item.anchor(s, &roots)).and_then(|a| d.node(*a));
|
||||
let anchor_level = anchor.map(|a| item.effects_item(&a.appearance, cmd)).transpose()?.flatten();
|
||||
let mut out = Vec::with_capacity(roots.len());
|
||||
for (k, (id, item)) in roots.iter().zip(per).enumerate() {
|
||||
for (id, item) in roots.iter().zip(per) {
|
||||
let (Some(item), Some(n)) = (item, d.node(*id)) else { continue };
|
||||
let at = item.effects_item(&n.appearance, cmd)?;
|
||||
// An object-level effect the targets share is, in each, where it is there.
|
||||
let first = roots.first().and_then(|f| d.node(*f));
|
||||
let first_level = first.map(|f| item.effects_item(&f.appearance, cmd)).transpose()?.flatten();
|
||||
let index = match (&shared, at, first_level) {
|
||||
(Some(sh), None, None) if sh.has_effect(index) => match sh.effect_in(index, k) {
|
||||
Some(i) => i,
|
||||
None => continue,
|
||||
},
|
||||
// An object-level effect this target shares with the first is where it is in this target.
|
||||
let index = match (first.filter(|_| aligned(p)), at, anchor_level) {
|
||||
(Some(f), None, None) => SharedStack::of(&[&*f.appearance, &*n.appearance]).effect_in(index, 1).unwrap_or(index),
|
||||
_ => index,
|
||||
};
|
||||
out.push((*id, at, index));
|
||||
|
||||
@@ -554,6 +554,49 @@ fn add_fill_copies_the_active_row_in_each_object_that_has_it() {
|
||||
}
|
||||
}
|
||||
|
||||
/// A paint edit of the active row changes the equal row of each object that has one, even when
|
||||
/// another selected object has none; that object's row stands for itself, as with only it and the
|
||||
/// first selected object selected.
|
||||
#[test]
|
||||
fn paint_edits_change_the_active_row_in_each_object_that_has_it() {
|
||||
let mut s = session();
|
||||
let [a, b, c] = [rect(&mut s, 50.0), rect(&mut s, 200.0), rect(&mut s, 300.0)];
|
||||
run(&mut s, "select.set", json!({"ids": [a.0, b.0, c.0]}));
|
||||
run(&mut s, "appearance.addFill", json!({}));
|
||||
run(&mut s, "appearance.setItem", json!({"index": 0, "color": "#00ff00"}));
|
||||
run(&mut s, "appearance.setItem", json!({"index": 2, "color": "#ff0000"})); // [Fill green, Stroke, Fill red]
|
||||
// b gets a yellow fill below the others: [Fill yellow, Fill green, Stroke, Fill red].
|
||||
run(&mut s, "appearance.addFill", json!({"ids": [b.0]}));
|
||||
run(&mut s, "appearance.moveItem", json!({"ids": [b.0], "from": 3, "to": 0}));
|
||||
run(&mut s, "appearance.setItem", json!({"ids": [b.0], "index": 0, "color": "#ffff00"}));
|
||||
run(&mut s, "appearance.setItem", json!({"ids": [c.0], "index": 0, "color": "#0000ff"})); // [Fill blue, Stroke, Fill red]
|
||||
run(&mut s, "select.set", json!({"ids": [a.0, b.0, c.0]}));
|
||||
run(&mut s, "appearance.setActiveItem", json!({"index": 0}));
|
||||
run(&mut s, "paint.setFill", json!({"color": "#ff00ff"}));
|
||||
let (na, nb, nc) = (node(&s, a), node(&s, b), node(&s, c));
|
||||
assert_eq!(fill_paint(&na, 0).color().unwrap().to_hex(), "#ff00ff");
|
||||
assert_eq!(fill_paint(&nb, 1).color().unwrap().to_hex(), "#ff00ff", "{:?}", nb.appearance);
|
||||
assert_eq!(fill_paint(&nb, 0).color().unwrap().to_hex(), "#ffff00", "{:?}", nb.appearance);
|
||||
assert_eq!(fill_paint(&nc, 0).color().unwrap().to_hex(), "#ff00ff", "{:?}", nc.appearance);
|
||||
}
|
||||
|
||||
/// An object-level effect is removed from each object that has an equal one, even when another
|
||||
/// selected object has none.
|
||||
#[test]
|
||||
fn removing_an_effect_reaches_each_object_that_has_it() {
|
||||
let mut s = session();
|
||||
let [a, b, c] = [rect(&mut s, 50.0), rect(&mut s, 200.0), rect(&mut s, 300.0)];
|
||||
run(&mut s, "effect.apply", json!({"ids": [a.0], "effect": "stylize.feather", "item": null}));
|
||||
run(&mut s, "effect.apply", json!({"ids": [a.0, b.0], "effect": "distort.twist", "item": null}));
|
||||
// a has [feather, twist], b [twist], c none.
|
||||
run(&mut s, "select.set", json!({"ids": [a.0, b.0, c.0]}));
|
||||
run(&mut s, "effect.remove", json!({"index": 1, "item": null, "aligned": true}));
|
||||
let ids = |id: NodeId| node(&s, id).appearance.effects.iter().map(|e| e.id.clone()).collect::<Vec<_>>();
|
||||
assert_eq!(ids(a), ["stylize.feather"]);
|
||||
assert!(ids(b).is_empty(), "b loses its twist: {:?}", ids(b));
|
||||
assert!(ids(c).is_empty());
|
||||
}
|
||||
|
||||
/// b `[Fill red, Fill white, Stroke]` above a `[Fill white, Stroke]`, selected b first: b's rows 1
|
||||
/// and 2 equal a's rows 0 and 1.
|
||||
fn reversed_pair(s: &mut Session) -> (NodeId, NodeId) {
|
||||
@@ -916,6 +959,16 @@ fn shared_effects_and_new_fills_reach_every_selected_object() {
|
||||
assert!(s.execute("appearance.moveItem", &json!({"from": 1, "to": 0, "aligned": true})).is_err());
|
||||
}
|
||||
|
||||
/// An effect applied to the first object's stroke row goes on the equal stroke of the other
|
||||
/// object, which sits lower in a longer stack there.
|
||||
#[test]
|
||||
fn an_effect_on_a_shared_row_reaches_it_at_another_index() {
|
||||
let mut s = session();
|
||||
let (a, b) = mixed_circles(&mut s, true);
|
||||
run(&mut s, "effect.apply", json!({"effect": "distort.twist", "item": 1, "aligned": true}));
|
||||
assert_eq!((stroke(&node(&s, a), 1).effects.len(), stroke(&node(&s, b), 2).effects.len()), (1, 1));
|
||||
}
|
||||
|
||||
/// Rows deleted together, one shared (the stroke) and one not (a's fill): each object loses its
|
||||
/// own copy of the shared row, and the other object keeps the items at those indices of a's stack.
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user