mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-01 05:37:23 -06:00
Merge pull request #4914 from dotCooCoo/fix/appearance-per-target-order
Appearance: paint, stroke and effect edits change the active row of the first selected object
This commit is contained in:
@@ -27,7 +27,7 @@ use vectorcraft_doc::appearance::SharedStack;
|
||||
use vectorcraft_doc::{Appearance, AppearanceItem, CharStyle, FillLayer, Node, NodeKind, ParaStyle, StrokeLayer};
|
||||
use vectorcraft_geom::Rect;
|
||||
|
||||
use super::edit::{outermost, selected_roots};
|
||||
use super::edit::{outermost, roots_of, selected_roots};
|
||||
use super::opacitymask::percent;
|
||||
use super::paint::paint_from;
|
||||
use super::{AppearanceAttrs, EyedropperAttrs, FillAttrs, StrokeAttrs};
|
||||
@@ -255,17 +255,36 @@ impl ItemTarget {
|
||||
if fits == Some(true) { self } else { ItemTarget::Top }
|
||||
}
|
||||
|
||||
/// This target for each of `ids` in `d` (in their order): an aligned item (a row of the first
|
||||
/// target's stack) is, in each other target, the item equal to it there (see
|
||||
/// 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.
|
||||
pub(crate) fn per_target(self, d: &vectorcraft_doc::Document, ids: &[NodeId]) -> Vec<Option<ItemTarget>> {
|
||||
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(shared) = shared_stack(d, ids) 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()];
|
||||
}
|
||||
(0..ids.len()).map(|k| shared.item_in(index, k).map(|index| ItemTarget::Item { index, explicit, aligned: true })).collect()
|
||||
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
|
||||
}
|
||||
|
||||
/// Where among `ids` (the targets, in paint order) the object whose stack this item is a row of
|
||||
/// is: the first selected object (or the compound path holding it) for the active item, which
|
||||
/// need not paint first; the first target otherwise, or when that object is not among them.
|
||||
pub(crate) fn anchor(self, s: &Session, ids: &[NodeId]) -> usize {
|
||||
let ItemTarget::Item { explicit: false, .. } = self else { return 0 };
|
||||
let Some(st) = s.active() else { return 0 };
|
||||
let Some(first) = st.selection.subjects().first() else { return 0 };
|
||||
let root = roots_of(&st.doc, vec![*first]).first().copied().unwrap_or(*first);
|
||||
ids.iter().position(|id| *id == root).unwrap_or(0)
|
||||
}
|
||||
|
||||
/// The item of `ap` a fill (`fill`) or stroke edit changes; `None` = the topmost one.
|
||||
@@ -407,7 +426,7 @@ pub(crate) fn edit_items_then(
|
||||
return Ok(());
|
||||
}
|
||||
let before = s.doc()?.doc.clone();
|
||||
let per = item.per_target(&before, ids);
|
||||
let per = item.per_target(s, ids);
|
||||
super::overprint::edit_counted(s, label, |d| {
|
||||
let mut edited = Vec::new();
|
||||
for (id, item) in ids.iter().zip(&per) {
|
||||
@@ -429,7 +448,7 @@ pub(crate) fn count_changed(d: &vectorcraft_doc::Document, before: &vectorcraft_
|
||||
}
|
||||
|
||||
/// Whether an edit aimed at `item` changes a stroke: the `stroke` param, else the kind of the
|
||||
/// targeted item (on the first target object), else `default`. A `stroke` other than true, false or
|
||||
/// targeted item (on the object whose row it is, see [`ItemTarget::anchor`]), else `default`. A `stroke` other than true, false or
|
||||
/// null is an error of command `cmd`.
|
||||
pub(crate) fn edits_stroke(s: &Session, p: &Value, item: ItemTarget, default: bool, cmd: &str) -> Result<bool> {
|
||||
check_switches(p, &["stroke"], cmd)?;
|
||||
@@ -439,7 +458,7 @@ pub(crate) fn edits_stroke(s: &Session, p: &Value, item: ItemTarget, default: bo
|
||||
let ItemTarget::Item { index, .. } = item else { return Ok(default) };
|
||||
let ids = appearance_targets(s, p)?;
|
||||
let d = &s.doc()?.doc;
|
||||
Ok(ids.first().and_then(|id| d.node(*id)?.appearance.items.get(index)).map_or(default, |it| !it.is_fill()))
|
||||
Ok(ids.get(item.anchor(s, &ids)).and_then(|id| d.node(*id)?.appearance.items.get(index)).map_or(default, |it| !it.is_fill()))
|
||||
}
|
||||
|
||||
/// The whole number of 0 or more that `key` holds, read with [`index`].
|
||||
|
||||
@@ -108,7 +108,7 @@ fn ids_json(ids: &[NodeId]) -> Value {
|
||||
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(d, &roots);
|
||||
let per = item.per_target(s, &roots);
|
||||
let shared = if aligned(p) { shared_stack(d, &roots) } else { None };
|
||||
let mut out = Vec::with_capacity(roots.len());
|
||||
for (k, (id, item)) in roots.iter().zip(per).enumerate() {
|
||||
@@ -219,8 +219,9 @@ pub(crate) fn apply(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
index = fx.len() - 1;
|
||||
true
|
||||
})?;
|
||||
// The item the effect landed on in the first object (the active item applies where it fits).
|
||||
let first = ids.first().and_then(|id| s.doc().ok()?.doc.node(*id));
|
||||
// The item the effect landed on in the object whose row the item is (the first selected object
|
||||
// for the active item, which applies where it fits).
|
||||
let first = ids.get(item.anchor(s, &ids)).and_then(|id| s.doc().ok()?.doc.node(*id));
|
||||
let landed = first.and_then(|n| item.effects_item(&n.appearance, C).ok().flatten());
|
||||
Ok(json!({ "ids": ids_json(&ids), "index": index, "item": landed }))
|
||||
}
|
||||
|
||||
@@ -463,6 +463,56 @@ fn reduce_to_basic_keeps_the_active_row_of_the_first_selected_object() {
|
||||
assert_eq!(fill_paint(&node(&s, a), 0).color().unwrap().to_hex(), "#0000ff");
|
||||
}
|
||||
|
||||
/// 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) {
|
||||
let a = rect(s, 50.0);
|
||||
let b = rect(s, 200.0);
|
||||
run(s, "appearance.addFill", json!({}));
|
||||
run(s, "appearance.moveItem", json!({"from": 2, "to": 0}));
|
||||
run(s, "appearance.setItem", json!({"index": 0, "color": "#ff0000"}));
|
||||
run(s, "select.set", json!({"ids": [b.0, a.0]}));
|
||||
(a, b)
|
||||
}
|
||||
|
||||
/// The active row is a row of the first selected object also for paint, stroke and effect edits
|
||||
/// when that object paints above the others.
|
||||
#[test]
|
||||
fn edits_change_the_active_row_of_the_first_selected_object() {
|
||||
let mut s = session();
|
||||
let (a, b) = reversed_pair(&mut s);
|
||||
run(&mut s, "appearance.setActiveItem", json!({"index": 0})); // b's red fill
|
||||
run(&mut s, "paint.setFill", json!({"color": "#00ff00"}));
|
||||
let (na, nb) = (node(&s, a), node(&s, b));
|
||||
assert_eq!(fill_paint(&nb, 0).color().unwrap().to_hex(), "#00ff00");
|
||||
assert_eq!(*fill_paint(&nb, 1), Paint::solid(Color::WHITE));
|
||||
// a shares no red row, so its fill row 0 changes, as for an active row the objects don't share.
|
||||
assert_eq!(fill_paint(&na, 0).color().unwrap().to_hex(), "#00ff00");
|
||||
run(&mut s, "effect.apply", json!({"effect": "distort.twist"}));
|
||||
let nb = node(&s, b);
|
||||
assert_eq!((nb.appearance.items[0].effects().len(), nb.appearance.items[1].effects().len()), (1, 0));
|
||||
|
||||
let mut s = session();
|
||||
let (a, b) = reversed_pair(&mut s);
|
||||
run(&mut s, "appearance.setActiveItem", json!({"index": 1})); // b's white fill, a's row 0
|
||||
run(&mut s, "paint.editGradient", json!({"kind": "radial"}));
|
||||
let (na, nb) = (node(&s, a), node(&s, b));
|
||||
assert!(matches!(fill_paint(&nb, 1), Paint::Gradient(_)), "b's white fill takes the gradient");
|
||||
assert!(matches!(fill_paint(&na, 0), Paint::Gradient(_)), "so does a's equal fill");
|
||||
assert_eq!(fill_paint(&nb, 0).color().unwrap().to_hex(), "#ff0000");
|
||||
assert!(!matches!(stroke(&nb, 2).paint, Paint::Gradient(_)) && !matches!(stroke(&na, 1).paint, Paint::Gradient(_)));
|
||||
// The effect goes on the equal rows, and the reply names the active row of b.
|
||||
run(&mut s, "appearance.setActiveItem", json!({"index": 2})); // b's stroke, a's row 1
|
||||
let r = run(&mut s, "effect.apply", json!({"effect": "distort.twist"}));
|
||||
assert_eq!(r["item"], 2);
|
||||
let (na, nb) = (node(&s, a), node(&s, b));
|
||||
assert_eq!((stroke(&nb, 2).effects.len(), stroke(&na, 1).effects.len()), (1, 1));
|
||||
// With nothing selected an aligned item is an error, not a crash.
|
||||
run(&mut s, "select.set", json!({"ids": []}));
|
||||
let e = s.execute("effect.remove", &json!({"index": 0, "item": 0, "aligned": true})).unwrap_err().to_string();
|
||||
assert!(!e.contains("internal error"), "{e}");
|
||||
}
|
||||
|
||||
/// How many steps there are to undo and to redo, and whether the document has unsaved changes.
|
||||
fn history(s: &Session) -> (usize, usize, bool) {
|
||||
let st = s.doc().unwrap();
|
||||
|
||||
Reference in New Issue
Block a user