Files
vectorcraft/crates/engine/src/tests_effectedit.rs
bflatasticandClaude Opus 5.5 10ea4d5daf M3.42: Appearance editing depth — drag effect rows to reorder them or move them into/out of fills and strokes (Alt copies), click an effect name to edit it in its prefilled dialog, choosing an applied effect asks to edit it or add another, Show All Hidden Attributes, multi-select and Alt-drag duplicate of fills/strokes, drag the thumbnail onto art to copy the appearance, stroke rows note brush/Dashed/profile, Shift-click a chip for the mixer, Properties fx opens the effect menu
Engine: effect.move {from, to, fromItem?, toItem?, copy?}; appearance.showAllHidden;
appearance.duplicateItem takes to? and indices?; appearance.removeItem takes indices?;
Appearance::has_hidden/show_all. UI: effect.dialog takes index? (edit in place via
effect.setParams) and opens the effectExists question for an effect already in the list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 23:01:34 +07:00

122 lines
6.1 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Appearance editing depth: moving and copying effects between the object and its fills and
//! strokes, duplicating and removing several items, Show All Hidden Attributes.
use serde_json::{Value, json};
use vectorcraft_doc::Node;
use super::*;
/// A selected 100×100 rectangle with the default appearance `[Fill white, Stroke black]`.
fn session_with_rect() -> (Session, NodeId) {
let mut s = Session::new();
s.execute("file.new", &json!({"width": 400, "height": 400})).unwrap();
let r = s.execute("shape.rectangle", &json!({"x": 100, "y": 100, "width": 100, "height": 100})).unwrap();
(s, NodeId(r["id"].as_u64().unwrap()))
}
fn node(s: &Session, id: NodeId) -> Node {
s.doc().unwrap().doc.node(id).unwrap().clone()
}
fn run(s: &mut Session, id: &str, p: Value) -> Value {
s.execute(id, &p).unwrap_or_else(|e| panic!("{id} {p}: {e}"))
}
/// Effect ids of the object (`None`) or of item `i`.
fn fx(s: &Session, id: NodeId, item: Option<usize>) -> Vec<String> {
node(s, id).appearance.effects_at(item).unwrap().iter().map(|e| e.id.clone()).collect()
}
#[test]
fn effects_reorder_within_a_list_and_undo_in_one_step() {
let (mut s, id) = session_with_rect();
for e in ["distort.twist", "distort.roughen", "stylize.dropShadow"] {
run(&mut s, "effect.apply", json!({ "effect": e }));
}
let r = run(&mut s, "effect.move", json!({"from": 0, "to": 2}));
assert_eq!((r["index"].clone(), r["item"].clone()), (json!(2), Value::Null));
assert_eq!(fx(&s, id, None), ["distort.roughen", "stylize.dropShadow", "distort.twist"]);
// Past the end lands last.
run(&mut s, "effect.move", json!({"from": 0, "to": 9}));
assert_eq!(fx(&s, id, None), ["stylize.dropShadow", "distort.twist", "distort.roughen"]);
s.execute("edit.undo", &json!({})).unwrap();
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(fx(&s, id, None), ["distort.twist", "distort.roughen", "stylize.dropShadow"]);
assert!(s.execute("effect.move", &json!({"from": 3, "to": 0})).is_err());
}
#[test]
fn effects_move_and_copy_between_the_object_and_its_items() {
let (mut s, id) = session_with_rect();
run(&mut s, "effect.apply", json!({"effect": "distort.twist"}));
run(&mut s, "effect.apply", json!({"effect": "path.offsetPath", "item": 1}));
// Into the stroke, above its own effect.
let r = run(&mut s, "effect.move", json!({"from": 0, "fromItem": null, "to": 1, "toItem": 1}));
assert_eq!((r["index"].clone(), r["item"].clone()), (json!(1), json!(1)));
assert!(fx(&s, id, None).is_empty());
assert_eq!(fx(&s, id, Some(1)), ["path.offsetPath", "distort.twist"]);
// Out of the stroke onto the fill, as a copy (Alt-drag): the stroke keeps it.
run(&mut s, "effect.move", json!({"from": 0, "fromItem": 1, "to": 0, "toItem": 0, "copy": true}));
assert_eq!(fx(&s, id, Some(0)), ["path.offsetPath"]);
assert_eq!(fx(&s, id, Some(1)), ["path.offsetPath", "distort.twist"]);
// Back out to the object, one undo step each.
run(&mut s, "effect.move", json!({"from": 1, "fromItem": 1, "to": 0, "toItem": null}));
assert_eq!(fx(&s, id, None), ["distort.twist"]);
s.execute("edit.undo", &json!({})).unwrap();
assert!(fx(&s, id, None).is_empty());
s.execute("edit.undo", &json!({})).unwrap();
assert!(fx(&s, id, Some(0)).is_empty());
// A missing item is an error, not a silent no-op.
assert!(s.execute("effect.move", &json!({"from": 0, "fromItem": 1, "to": 0, "toItem": 7})).is_err());
}
#[test]
fn duplicate_selected_works_for_effects() {
let (mut s, id) = session_with_rect();
run(&mut s, "effect.apply", json!({"effect": "distort.roughen", "item": 0}));
run(&mut s, "effect.duplicate", json!({"index": 0, "item": 0}));
assert_eq!(fx(&s, id, Some(0)), ["distort.roughen", "distort.roughen"]);
}
#[test]
fn items_duplicate_to_a_position_and_several_remove_at_once() {
let (mut s, id) = session_with_rect();
run(&mut s, "appearance.addFill", json!({})); // [Fill, Stroke, Fill]
run(&mut s, "appearance.setItem", json!({"index": 2, "color": "#ff0000"}));
// Alt-dragging the top fill to the bottom copies it there.
run(&mut s, "appearance.duplicateItem", json!({"index": 2, "to": 0}));
let kinds = |s: &Session| node(s, id).appearance.items.iter().map(|i| i.kind_name()).collect::<Vec<_>>();
assert_eq!(kinds(&s), ["fill", "fill", "stroke", "fill"]);
assert_eq!(node(&s, id).appearance.items[0].paint(), node(&s, id).appearance.items[3].paint());
// Duplicating several copies each above itself.
run(&mut s, "appearance.duplicateItem", json!({"indices": [0, 2]}));
assert_eq!(kinds(&s), ["fill", "fill", "fill", "stroke", "stroke", "fill"]);
assert!(s.execute("appearance.duplicateItem", &json!({"indices": [0, 1], "to": 3})).is_err());
// Removing several in one step; the active item follows.
run(&mut s, "appearance.setActiveItem", json!({"index": 5}));
run(&mut s, "appearance.removeItem", json!({"indices": [4, 0, 1]}));
assert_eq!(kinds(&s), ["fill", "stroke", "fill"]);
assert_eq!(s.appearance_item(), Some(2));
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(node(&s, id).appearance.items.len(), 6);
}
#[test]
fn show_all_hidden_attributes() {
let (mut s, id) = session_with_rect();
run(&mut s, "effect.apply", json!({"effect": "distort.twist"}));
run(&mut s, "effect.apply", json!({"effect": "distort.roughen", "item": 1}));
assert!(s.execute("appearance.showAllHidden", &json!({})).is_err(), "nothing hidden yet");
run(&mut s, "appearance.setItem", json!({"index": 0, "visible": false}));
run(&mut s, "effect.setParams", json!({"index": 0, "visible": false, "item": null}));
run(&mut s, "effect.setParams", json!({"index": 0, "visible": false, "item": 1}));
assert!(node(&s, id).appearance.has_hidden());
let r = run(&mut s, "appearance.showAllHidden", json!({}));
assert_eq!(r["ids"], json!([id.0]));
let n = node(&s, id);
assert!(!n.appearance.has_hidden());
assert!(n.appearance.effects[0].visible && n.appearance.items[1].effects()[0].visible && n.appearance.items[0].visible());
s.execute("edit.undo", &json!({})).unwrap();
assert!(node(&s, id).appearance.has_hidden());
}