mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-31 05:27:08 -06:00
* Dev profile: line tables only for workspace crates, no debug info for dependencies * xtask ci: dev-profile gate, nextest, tests without examples, wasm --keep-going * One integration-test binary per crate * xtask ci: run effectcraft-gpu's shared-device tests in one cargo test process * xtask ci: run every stage and report all failures * xtask ci: name the stage type (clippy type_complexity) * xtask ci: run every test pass and report all failures * cargo fmt
159 lines
7.8 KiB
Rust
159 lines
7.8 KiB
Rust
//! #612: a shape layer's Gradient Fill start and end points are dragged in the Composition viewer,
|
||
//! as in After Effects. With the gradient (or a property in it) selected, `viewer.gradientPoint.*`
|
||
//! handles sit on the points, in the space of the group holding the gradient; a drag sets the
|
||
//! point in one undo step and doesn't move the layer. Uses only APIs that existed before the
|
||
//! handles, so it runs (and fails) on the code without them.
|
||
|
||
use effectcraft_engine::Session;
|
||
use effectcraft_engine::project::{LayerId, PropGroup};
|
||
use effectcraft_ui_egui::EffectcraftApp;
|
||
use effectcraft_ui_egui::panels::viewer::comp_to_screen;
|
||
use egui::{Event, Modifiers, PointerButton, Pos2, pos2, vec2};
|
||
use egui_kittest::Harness;
|
||
use serde_json::json;
|
||
|
||
/// The Gradient Fill group in shape contents `g`, through nested groups.
|
||
fn gfill(g: &PropGroup) -> Option<&PropGroup> {
|
||
for x in g.groups() {
|
||
if x.match_id == "gfill" {
|
||
return Some(x);
|
||
}
|
||
if let Some(found) = gfill(x) {
|
||
return Some(found);
|
||
}
|
||
}
|
||
None
|
||
}
|
||
|
||
struct Ids {
|
||
layer: u64,
|
||
gfill: u64,
|
||
start: u64,
|
||
end: u64,
|
||
}
|
||
|
||
/// A 640×360 comp with a rectangle shape layer, a Gradient Fill added inside the rectangle's group
|
||
/// (`in_group`) or at the top of its contents, after `prep` runs on the session.
|
||
fn setup(in_group: bool, prep: impl FnOnce(&mut Session, u64, u64)) -> (Session, Ids) {
|
||
let mut s = Session::default();
|
||
s.execute("comp.new", json!({"name": "Main", "width": 640, "height": 360, "duration": 4})).unwrap();
|
||
let l = s.execute("layer.newShape", json!({"kind": "rect"})).unwrap()["layer"].as_u64().unwrap();
|
||
let group = s.active_comp().unwrap().layer(LayerId(l)).unwrap().props.sub("contents").unwrap().groups().next().unwrap().uid;
|
||
let mut p = json!({"layer": l, "kind": "gfill"});
|
||
if in_group {
|
||
p["group"] = json!(group);
|
||
}
|
||
s.execute("layer.addShapeItem", p).unwrap();
|
||
prep(&mut s, l, group);
|
||
let layer = s.active_comp().unwrap().layer(LayerId(l)).unwrap();
|
||
let g = gfill(&layer.props).expect("a Gradient Fill");
|
||
let ids = Ids { layer: l, gfill: g.uid, start: g.get("start").unwrap().uid, end: g.get("end").unwrap().uid };
|
||
s.state.selected_layers = vec![LayerId(l)];
|
||
(s, ids)
|
||
}
|
||
|
||
fn harness(s: Session) -> Harness<'static, EffectcraftApp> {
|
||
let mut h = Harness::builder().with_size(vec2(1600.0, 1000.0)).build_eframe(|_| EffectcraftApp::new(s));
|
||
h.run_steps(4);
|
||
h
|
||
}
|
||
|
||
fn handle(h: &Harness<'_, EffectcraftApp>, uid: u64) -> Option<Pos2> {
|
||
h.state().auto.find(&format!("viewer.gradientPoint.{uid}")).map(|e| pos2(e.rect[0] + e.rect[2] / 2.0, e.rect[1] + e.rect[3] / 2.0))
|
||
}
|
||
|
||
fn point(h: &Harness<'_, EffectcraftApp>, ids: &Ids, uid: u64) -> [f64; 2] {
|
||
h.state().session.active_comp().unwrap().layer(LayerId(ids.layer)).unwrap().props.find(uid).unwrap().value.as_vec2()
|
||
}
|
||
|
||
/// Screen pixels per comp pixel.
|
||
fn zoom(h: &Harness<'_, EffectcraftApp>) -> f32 {
|
||
let a = comp_to_screen(&h.ctx, [0.0, 0.0]).unwrap();
|
||
let b = comp_to_screen(&h.ctx, [100.0, 0.0]).unwrap();
|
||
(b.x - a.x) / 100.0
|
||
}
|
||
|
||
fn drag(h: &mut Harness<'_, EffectcraftApp>, from: Pos2, to: Pos2) {
|
||
h.event(Event::PointerMoved(from));
|
||
h.step();
|
||
h.event(Event::PointerButton { pos: from, button: PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE });
|
||
h.step();
|
||
for k in 1..=8 {
|
||
h.event(Event::PointerMoved(from + (to - from) * (k as f32 / 8.0)));
|
||
h.step();
|
||
}
|
||
h.event(Event::PointerButton { pos: to, button: PointerButton::Primary, pressed: false, modifiers: Modifiers::NONE });
|
||
h.run_steps(3);
|
||
}
|
||
|
||
fn near(a: [f64; 2], b: [f64; 2], tol: f64) -> bool {
|
||
(a[0] - b[0]).abs() <= tol && (a[1] - b[1]).abs() <= tol
|
||
}
|
||
|
||
/// With the Gradient Fill selected, its End Point is a handle on the point; dragging it moves the
|
||
/// point by the drag (in comp pixels, the layer and group being unscaled), as one undo step,
|
||
/// without moving the layer.
|
||
#[test]
|
||
fn dragging_the_end_point_moves_it_in_one_undo_step() {
|
||
let (mut s, ids) = setup(false, |_, _, _| {});
|
||
s.state.selected_props = vec![(LayerId(ids.layer), ids.gfill)];
|
||
let mut h = harness(s);
|
||
let at = handle(&h, ids.end).unwrap_or_else(|| panic!("no viewer.gradientPoint.{}: the Gradient Fill's End Point isn't draggable in the viewer", ids.end));
|
||
assert!(handle(&h, ids.start).is_some(), "and its Start Point");
|
||
let (end, start) = (point(&h, &ids, ids.end), point(&h, &ids, ids.start));
|
||
let position = h.state().session.active_comp().unwrap().layer(LayerId(ids.layer)).unwrap().props.prop("transform/position").unwrap().value.clone();
|
||
let undo = h.state().session.history.undo.len();
|
||
let z = zoom(&h);
|
||
drag(&mut h, at, at + vec2(60.0, 30.0) * z);
|
||
let moved = point(&h, &ids, ids.end);
|
||
assert!(near(moved, [end[0] + 60.0, end[1] + 30.0], 1.5), "End Point {end:?} moved by the drag: {moved:?}");
|
||
assert_eq!(point(&h, &ids, ids.start), start, "the Start Point stays");
|
||
let layer = h.state().session.active_comp().unwrap().layer(LayerId(ids.layer)).unwrap().clone();
|
||
assert_eq!(layer.props.prop("transform/position").unwrap().value, position, "the layer didn't move");
|
||
assert_eq!(h.state().session.history.undo.len(), undo + 1, "the drag is one undo step");
|
||
h.state_mut().session.execute("edit.undo", json!({})).unwrap();
|
||
assert_eq!(point(&h, &ids, ids.end), end, "one undo puts it back");
|
||
}
|
||
|
||
/// A gradient inside a moved and scaled group: its handles sit where the group draws the points,
|
||
/// and a drag converts back into the group's space (half as far at 200%).
|
||
#[test]
|
||
fn points_follow_the_group_transform() {
|
||
let (mut s, ids) = setup(true, |s, l, group| {
|
||
let tr = s.active_comp().unwrap().layer(LayerId(l)).unwrap().props.find_group(group).unwrap().sub("transform").unwrap().clone();
|
||
s.execute("prop.set", json!({"layer": l, "prop": tr.get("scale").unwrap().uid, "value": [200.0, 200.0]})).unwrap();
|
||
s.execute("prop.set", json!({"layer": l, "prop": tr.get("position").unwrap().uid, "value": [40.0, -20.0]})).unwrap();
|
||
});
|
||
s.state.selected_props = vec![(LayerId(ids.layer), ids.start)];
|
||
let mut h = harness(s);
|
||
// Layer at the comp's centre (320, 180); group: position (40, -20), scale 200%.
|
||
let start = point(&h, &ids, ids.start);
|
||
let want = comp_to_screen(&h.ctx, [(320.0 + 40.0 + 2.0 * start[0]) as f32, (180.0 - 20.0 + 2.0 * start[1]) as f32]).unwrap();
|
||
let at = handle(&h, ids.start).expect("a Start Point handle (selecting a property in the gradient shows it)");
|
||
assert!((at - want).length() < 1.5, "the handle is where the group draws the point: {at:?} vs {want:?}");
|
||
let z = zoom(&h);
|
||
drag(&mut h, at, at + vec2(40.0, 20.0) * z);
|
||
let moved = point(&h, &ids, ids.start);
|
||
assert!(near(moved, [start[0] + 20.0, start[1] + 10.0], 1.0), "in group space, half the comp drag: {start:?} → {moved:?}");
|
||
}
|
||
|
||
/// No handles unless the gradient (or a property in it) is selected, or with layer controls off.
|
||
#[test]
|
||
fn handles_show_only_for_a_selected_gradient() {
|
||
let (s, ids) = setup(false, |_, _, _| {});
|
||
let h = harness(s);
|
||
assert!(handle(&h, ids.end).is_none(), "only the layer is selected");
|
||
let (mut s, ids) = setup(false, |_, _, _| {});
|
||
let position = s.active_comp().unwrap().layer(LayerId(ids.layer)).unwrap().props.prop("transform/position").unwrap().uid;
|
||
s.state.selected_props = vec![(LayerId(ids.layer), position)];
|
||
let h = harness(s);
|
||
assert!(handle(&h, ids.end).is_none(), "another property is selected");
|
||
let (mut s, ids) = setup(false, |_, _, _| {});
|
||
s.state.selected_props = vec![(LayerId(ids.layer), ids.gfill)];
|
||
let mut h = harness(s);
|
||
assert!(handle(&h, ids.end).is_some());
|
||
h.state_mut().ui.viewer.show_layer_controls = false;
|
||
h.run_steps(3);
|
||
assert!(handle(&h, ids.end).is_none(), "View ▸ Show Layer Controls off hides them");
|
||
}
|