mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-25 10:36:56 -06:00
Guide layers render only in their own comp's viewer: a nested renderer (precomp, collapsed precomp in 2D, Classic 3D and Advanced 3D, comps read by effects, aux passes, the GPU walk's resident precomps) now runs with guides off (Renderer::nested_opts), so a precomp's guide layers no longer show in the parent comp's viewer or in a "Current Settings" render. The Timeline layer and Composition viewer right-click menus (shared layer_menu) gain a Guide Layer item (a plain toggle, no check mark) acting on the selection, and guide layers show a guide mark before their name in the Timeline. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
130 lines
5.8 KiB
Rust
130 lines
5.8 KiB
Rust
//! Guide Layer in the layer context menus (Timeline rows and the Composition viewer), a plain item
|
||
//! (no check mark) that toggles the switch on the selection, and the guide mark by a guide layer's
|
||
//! name.
|
||
use effectcraft_engine::Session;
|
||
use effectcraft_engine::project::LayerId;
|
||
use effectcraft_ui_egui::EffectcraftApp;
|
||
use egui::{Event, Modifiers, PointerButton, Pos2, Rect, pos2, vec2};
|
||
use egui_kittest::Harness;
|
||
use egui_kittest::kittest::{NodeT as _, Queryable as _};
|
||
use serde_json::json;
|
||
|
||
/// A 640×360 comp with two 200×200 solids, "Left" at (160, 180) and "Right" at (480, 180).
|
||
fn harness() -> (Harness<'static, EffectcraftApp>, LayerId, LayerId) {
|
||
let mut session = Session::default();
|
||
session.execute("comp.new", json!({"name": "Guides", "width": 640, "height": 360, "duration": 4})).unwrap();
|
||
let mut solid = |name: &str, x: f64| {
|
||
let id = session.execute("layer.newSolid", json!({"name": name, "color": "#406080", "width": 200, "height": 200})).unwrap()["layer"].as_u64().unwrap();
|
||
session.execute("prop.set", json!({"layer": id, "path": "transform/position", "value": [x, 180.0]})).unwrap();
|
||
LayerId(id)
|
||
};
|
||
let left = solid("Left", 160.0);
|
||
let right = solid("Right", 480.0);
|
||
let mut h = Harness::builder().with_size(vec2(1600.0, 1000.0)).build_eframe(|_| EffectcraftApp::new(session));
|
||
h.run_steps(3);
|
||
(h, left, right)
|
||
}
|
||
|
||
fn rect(h: &Harness<'_, EffectcraftApp>, id: &str) -> Rect {
|
||
let e = h.state().auto.find(id).unwrap_or_else(|| panic!("missing {id}"));
|
||
Rect::from_min_size(pos2(e.rect[0], e.rect[1]), vec2(e.rect[2], e.rect[3]))
|
||
}
|
||
|
||
fn click(h: &mut Harness<'_, EffectcraftApp>, p: Pos2, button: PointerButton) {
|
||
for pressed in [true, false] {
|
||
h.input_mut().events.push(Event::PointerMoved(p));
|
||
h.input_mut().events.push(Event::PointerButton { pos: p, button, pressed, modifiers: Modifiers::NONE });
|
||
h.step();
|
||
}
|
||
h.run_steps(3);
|
||
}
|
||
|
||
fn guide(h: &Harness<'_, EffectcraftApp>, id: LayerId) -> bool {
|
||
h.state().session.active_comp().unwrap().layer(id).unwrap().switches.guide
|
||
}
|
||
|
||
fn snapshot(h: &mut Harness<'_, EffectcraftApp>, name: &str) {
|
||
if let Ok(dir) = std::env::var("GUIDE_LAYER_SNAPSHOTS") {
|
||
h.render().unwrap().save(format!("{dir}/{name}.png")).unwrap();
|
||
}
|
||
}
|
||
|
||
/// The open menu's Guide Layer item carries no check mark (a plain toggle).
|
||
fn plain(h: &Harness<'_, EffectcraftApp>) -> bool {
|
||
!h.get_by_label_contains("Guide Layer").accesskit_node().label().unwrap_or_default().contains('✔')
|
||
}
|
||
|
||
fn row(h: &Harness<'_, EffectcraftApp>, id: LayerId) -> Pos2 {
|
||
let r = rect(h, &format!("timeline.layer.{}.row", id.0));
|
||
pos2(r.min.x + 210.0, r.center().y)
|
||
}
|
||
|
||
/// The Guide Layer item of the open menu `prefix`.
|
||
fn guide_item(h: &Harness<'_, EffectcraftApp>, prefix: &str) -> Rect {
|
||
rect(h, &format!("{prefix}.switch.guide"))
|
||
}
|
||
|
||
#[test]
|
||
fn timeline_layer_menu_toggles_guide_layer() {
|
||
let (mut h, left, right) = harness();
|
||
assert!(h.state().auto.find(&format!("timeline.layer.{}.guide", left.0)).is_none(), "no guide mark yet");
|
||
let p = row(&h, left);
|
||
click(&mut h, p, PointerButton::Secondary);
|
||
assert_eq!(h.state().session.state.selected_layers, vec![left], "right-click selects the row");
|
||
assert!(plain(&h), "no check mark");
|
||
let item = guide_item(&h, "timeline.layerMenu");
|
||
click(&mut h, item.center(), PointerButton::Primary);
|
||
assert!(guide(&h, left), "Guide Layer turned on");
|
||
assert!(!guide(&h, right), "only the target layer");
|
||
// The guide mark shows by the name.
|
||
h.run_steps(2);
|
||
assert!(h.state().auto.find(&format!("timeline.layer.{}.guide", left.0)).is_some(), "guide mark by the name");
|
||
// Again: off. Undo brings it back.
|
||
let p = row(&h, left);
|
||
click(&mut h, p, PointerButton::Secondary);
|
||
assert!(plain(&h), "no check mark on a guide layer either");
|
||
snapshot(&mut h, "timeline-menu");
|
||
let item = guide_item(&h, "timeline.layerMenu");
|
||
click(&mut h, item.center(), PointerButton::Primary);
|
||
assert!(!guide(&h, left), "Guide Layer turned off");
|
||
h.state_mut().session.execute("edit.undo", json!({})).unwrap();
|
||
assert!(guide(&h, left));
|
||
}
|
||
|
||
#[test]
|
||
fn timeline_layer_menu_guide_layer_acts_on_the_selection() {
|
||
let (mut h, left, right) = harness();
|
||
h.state_mut().session.execute("layer.select", json!({"layers": [left.0, right.0]})).unwrap();
|
||
h.run_steps(3);
|
||
let p = row(&h, right);
|
||
click(&mut h, p, PointerButton::Secondary);
|
||
let item = guide_item(&h, "timeline.layerMenu");
|
||
click(&mut h, item.center(), PointerButton::Primary);
|
||
assert!(guide(&h, left) && guide(&h, right), "both selected layers become guide layers");
|
||
let p = row(&h, left);
|
||
click(&mut h, p, PointerButton::Secondary);
|
||
let item = guide_item(&h, "timeline.layerMenu");
|
||
click(&mut h, item.center(), PointerButton::Primary);
|
||
assert!(!guide(&h, left) && !guide(&h, right), "and back together");
|
||
}
|
||
|
||
#[test]
|
||
fn viewer_layer_menu_toggles_guide_layer() {
|
||
let (mut h, left, right) = harness();
|
||
let comp = rect(&h, "viewer.comp");
|
||
let k = comp.width() / 640.0;
|
||
let at = |x: f32| comp.min + vec2(x, 180.0) * k;
|
||
click(&mut h, at(480.0), PointerButton::Secondary);
|
||
assert_eq!(h.state().session.state.selected_layers, vec![right], "right-click picks the layer under the pointer");
|
||
let item = guide_item(&h, "viewer.layerMenu");
|
||
click(&mut h, item.center(), PointerButton::Primary);
|
||
assert!(guide(&h, right) && !guide(&h, left));
|
||
// The Timeline and viewer menus share the item: the viewer menu turns it off again.
|
||
click(&mut h, at(480.0), PointerButton::Secondary);
|
||
assert!(plain(&h), "no check mark");
|
||
snapshot(&mut h, "viewer-menu");
|
||
let item = guide_item(&h, "viewer.layerMenu");
|
||
click(&mut h, item.center(), PointerButton::Primary);
|
||
assert!(!guide(&h, right));
|
||
}
|