Files
effectcraft/crates/ui-egui/tests/it/ui_preview_align.rs
T

318 lines
14 KiB
Rust
Raw 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.
//! Headless checks for M13.8: the Preview panel's per-shortcut options drive playback (range,
//! play from, resolution, overlays, full screen, move time), the preview shortcuts pick their
//! options, and the Align panel runs `layer.align` / `layer.distribute`.
use effectcraft_engine::Session;
use effectcraft_engine::preview::PreviewShortcut;
use effectcraft_engine::time::Tick;
use effectcraft_ui_egui::EffectcraftApp;
use effectcraft_ui_egui::dock::PanelKind;
use egui::{Event, Pos2, pos2};
use egui_kittest::Harness;
use serde_json::json;
fn harness() -> Harness<'static, EffectcraftApp> {
let mut s = Session::default();
s.execute("comp.new", json!({"name": "Preview", "width": 320, "height": 180, "frameRate": 24, "duration": 10})).unwrap();
s.execute("layer.newSolid", json!({"name": "A", "color": "#406080", "width": 40, "height": 30})).unwrap();
s.execute("layer.newSolid", json!({"name": "B", "color": "#806040", "width": 60, "height": 20})).unwrap();
let mut h = Harness::builder().with_size(egui::vec2(1600.0, 1000.0)).build_eframe(|_| EffectcraftApp::new(s));
h.run_steps(3);
h
}
fn invoke(h: &mut Harness<'_, EffectcraftApp>, id: &str, p: serde_json::Value) -> serde_json::Value {
let ctx = h.ctx.clone();
effectcraft_ui_egui::menus::invoke(h.state_mut(), &ctx, id, p).unwrap()
}
fn click(h: &mut Harness<'_, EffectcraftApp>, id: &str) {
let e = h.state().auto.find(id).unwrap_or_else(|| panic!("no {id}")).clone();
let p: Pos2 = pos2(e.rect[0] + e.rect[2] / 2.0, e.rect[1] + e.rect[3] / 2.0);
h.input_mut().events.push(Event::PointerMoved(p));
h.input_mut().events.push(Event::PointerButton { pos: p, button: egui::PointerButton::Primary, pressed: true, modifiers: Default::default() });
h.step();
h.input_mut().events.push(Event::PointerButton { pos: p, button: egui::PointerButton::Primary, pressed: false, modifiers: Default::default() });
h.run_steps(2);
}
fn set(h: &mut Harness<'_, EffectcraftApp>, p: serde_json::Value) {
h.state_mut().session.execute("playback.settings.set", p).unwrap();
}
#[test]
fn preview_panel_edits_the_shown_shortcut() {
let mut h = harness();
invoke(&mut h, "window.panel", json!({"panel": "preview"}));
// The default workspace keeps the Preview panel short: maximize it.
h.state_mut().toggle_maximize(PanelKind::Preview);
h.run_steps(3);
for id in [
"preview.shortcut",
"preview.includeVideo",
"preview.includeAudio",
"preview.includeOverlays",
"preview.includeLayerControls",
"preview.loop",
"preview.cacheBeforePlayback",
"preview.range",
"preview.playFrom",
"preview.frameRate",
"preview.skip",
"preview.resolution",
"preview.fullScreen",
"preview.playCachedFrames",
"preview.moveTimeToPreviewTime",
] {
assert!(h.state().auto.find(id).is_some(), "{id}");
}
assert!(h.state().session.prefs.preview.spacebar.loop_);
click(&mut h, "preview.loop");
assert!(!h.state().session.prefs.preview.spacebar.loop_);
// Shortcut popup: Shift+Numpad 0; its options are shown and edited from now on.
click(&mut h, "preview.shortcut");
click(&mut h, "preview.shortcut.3");
assert_eq!(h.state().session.prefs.preview.current, PreviewShortcut::ShiftNumpad0);
click(&mut h, "preview.fullScreen");
assert!(h.state().session.prefs.preview.shift_numpad0.full_screen);
assert!(!h.state().session.prefs.preview.spacebar.full_screen);
// Range popup → Play Around Current Time shows the pre/post-roll.
click(&mut h, "preview.range");
click(&mut h, "preview.range.3");
h.run_steps(2);
assert!(h.state().auto.find("preview.preRoll").is_some());
}
#[test]
fn playback_follows_the_shortcut_options() {
let mut h = harness();
// Numpad 0: Work Area from its start; the time returns when the preview stops.
h.state_mut().session.execute("comp.workArea", json!({"start": 2.0, "end": 4.0})).unwrap();
set(&mut h, json!({"shortcut": "numpad0", "moveTimeToPreviewTime": false, "resolution": "quarter", "includeOverlays": false}));
h.state_mut().session.set_time(Tick::from_seconds_f64(7.0));
invoke(&mut h, "playback.ramPreview", json!({}));
let app = h.state();
assert!(app.playback.playing);
assert_eq!(app.playback.shortcut, PreviewShortcut::Numpad0);
let pl = app.playback.plan.unwrap();
assert_eq!((pl.start, pl.end, pl.first), (48, 95, 48));
assert_eq!(app.session.time(), Tick::from_seconds_f64(2.0));
assert_eq!(app.viewer_scale(1.0, 1.0), 0.25);
assert!(!app.overlays_visible());
invoke(&mut h, "playback.stop", json!({}));
assert!(!h.state().playback.playing);
assert_eq!(h.state().session.time(), Tick::from_seconds_f64(7.0));
assert!(h.state().overlays_visible());
// Spacebar with Full Screen: the viewer is maximized while it plays.
set(&mut h, json!({"shortcut": "spacebar", "fullScreen": true, "skip": 2}));
invoke(&mut h, "playback.toggle", json!({}));
assert_eq!(h.state().ui.maximized, Some(PanelKind::Composition));
assert_eq!(h.state().playback.plan.unwrap().step, 3);
invoke(&mut h, "playback.toggle", json!({}));
assert_eq!(h.state().ui.maximized, None);
// Shift+Spacebar from the keyboard plays with its own options.
let ev = Event::Key { key: egui::Key::Space, physical_key: None, pressed: true, repeat: false, modifiers: egui::Modifiers::SHIFT };
h.input_mut().events.push(ev);
h.step();
assert!(h.state().playback.playing);
assert_eq!(h.state().playback.shortcut, PreviewShortcut::ShiftSpacebar);
// Every frame by default (#551): Skip is 0 until the user sets it.
assert_eq!(h.state().playback.plan.unwrap().step, 1);
}
#[test]
fn cache_before_playback_waits_then_plays_cached() {
let mut h = harness();
set(&mut h, json!({"shortcut": "spacebar", "cacheBeforePlayback": true, "range": "entireDuration", "playFrom": "rangeStart"}));
invoke(&mut h, "playback.toggle", json!({}));
assert!(h.state().playback.caching);
// Pressing again while caching ("If caching, play cached frames") stops only when nothing
// is cached yet, otherwise plays the cached frames.
h.run_steps(4);
invoke(&mut h, "playback.toggle", json!({}));
let st = &h.state().playback;
assert!(!st.caching);
if st.playing {
let pl = st.plan.unwrap();
assert!(pl.end < 239);
}
}
#[test]
fn align_panel_runs_align_and_distribute() {
let mut h = harness();
invoke(&mut h, "window.panel", json!({"panel": "align"}));
h.state_mut().toggle_maximize(PanelKind::Align);
h.run_steps(3);
for id in ["align.to", "align.left", "align.vcenter", "distribute.hcenter", "distribute.bottom"] {
assert!(h.state().auto.find(id).is_some(), "{id}");
}
let ids: Vec<u64> = h.state().session.active_comp().unwrap().layers.iter().map(|l| l.id.0).collect();
h.state_mut().session.execute("layer.select", json!({"layers": ids})).unwrap();
h.run_steps(2);
// (x position, width) of A and B (anchors in the middle of the solids).
let pos = |h: &Harness<'_, EffectcraftApp>| -> Vec<(f64, f64)> {
let c = h.state().session.active_comp().unwrap();
["A", "B"]
.iter()
.map(|n| {
let l = c.layers.iter().find(|l| l.name == *n).unwrap();
let x = l.transform().unwrap().get("position").unwrap().value_at(Tick::ZERO).to_json()[0].as_f64().unwrap();
(x, if *n == "A" { 40.0 } else { 60.0 })
})
.collect()
};
let orig = pos(&h);
click(&mut h, "align.left");
assert_eq!(pos(&h), vec![(20.0, 40.0), (30.0, 60.0)]);
// One undo step for the whole alignment.
h.state_mut().session.execute("edit.undo", json!({})).unwrap();
assert_eq!(pos(&h), orig);
// Align Layers to: Selection — right edges meet at the rightmost.
click(&mut h, "align.to");
click(&mut h, "align.to");
h.state_mut().ui.align_to_selection = true;
h.run_steps(2);
assert_eq!(h.state().auto.find("align.to").unwrap().label, "Selection");
click(&mut h, "align.right");
let p = pos(&h);
let right = |(x, w): (f64, f64)| x + w / 2.0;
let goal = orig.iter().map(|v| right(*v)).fold(f64::NEG_INFINITY, f64::max);
assert!((right(p[0]) - goal).abs() < 1e-9 && (right(p[1]) - goal).abs() < 1e-9, "{p:?}");
// Distribute needs three layers: nothing moves.
click(&mut h, "distribute.hcenter");
assert_eq!(pos(&h), p);
}
#[test]
fn cache_before_playback_plays_what_fits_in_ram() {
let mut h = harness();
set(&mut h, json!({"shortcut": "spacebar", "cacheBeforePlayback": true, "range": "entireDuration", "playFrom": "rangeStart"}));
h.run_steps(2);
// Room for about 12 full-size frames: the 240-frame range can't all be cached.
h.state().frames.set_budget(320 * 180 * 4 * 12);
invoke(&mut h, "playback.toggle", json!({}));
assert!(h.state().playback.caching);
for _ in 0..600 {
h.step();
if !h.state().playback.caching {
break;
}
std::thread::sleep(std::time::Duration::from_millis(5));
}
let st = &h.state().playback;
assert!(st.playing && !st.caching, "caching finished and the preview plays");
let pl = st.plan.unwrap();
assert!(pl.start == 0 && pl.end < 239 && pl.end >= 9, "cut to what fits: {}..{}", pl.start, pl.end);
}
#[test]
fn auto_resolution_is_the_same_playing_and_paused() {
let mut h = harness();
let paused = h.state().viewer_scale(0.5, 2.0);
h.state_mut().playback.playing = true;
assert_eq!(h.state().viewer_scale(0.5, 2.0), paused);
}
#[test]
fn achieved_frame_rate_and_real_time() {
let mut p = effectcraft_ui_egui::Playback { playing: true, fps: 24.0, ..Default::default() };
assert_eq!(p.achieved_fps(), None);
for i in 0..25 {
p.frame_shown(i as f64 / 24.0);
}
assert!((p.achieved_fps().unwrap() - 24.0).abs() < 1e-9);
assert_eq!(p.real_time(), Some(true));
let mut slow = effectcraft_ui_egui::Playback { playing: true, fps: 24.0, ..Default::default() };
for i in 0..13 {
slow.frame_shown(i as f64 / 12.0);
}
assert_eq!(slow.real_time(), Some(false));
assert_eq!(slow.shown_at.len(), 13, "only the last second counts");
}
#[test]
fn cache_frames_when_idle_fills_the_work_area() {
let mut h = harness();
let cid = h.state().session.active_comp_id().unwrap();
let cached = |h: &Harness<'_, EffectcraftApp>| h.state().frames.cached_frames(&h.state().shown_series(cid)).len();
// Off: only a few frames ahead of the current time are prefetched (the work area is the
// whole 240 frames).
for _ in 0..120 {
h.step();
std::thread::sleep(std::time::Duration::from_millis(2));
}
let before = cached(&h);
assert!(before < 240, "{before}");
invoke(&mut h, "playback.cacheWhenIdle", json!({"value": true}));
let mut n = 0;
for _ in 0..4000 {
h.step();
n = cached(&h);
if n >= 240 {
break;
}
std::thread::sleep(std::time::Duration::from_millis(2));
}
assert_eq!(n, 240, "the work area is cached while idle");
}
/// A preview that plays into frames it already cached (after a loop) keeps rendering the next
/// uncached ones: the prefetch window used to end within the cached run, and the workers stood
/// idle until the playhead came near its end.
#[test]
fn playback_prefetches_past_the_cached_frames_ahead() {
let mut h = harness();
let cid = h.state().session.active_comp_id().unwrap();
let series = h.state().shown_series(cid);
let scale = series.scale as f64 / 1000.0;
// The first 120 frames (5 s) are cached: more than the widest old window (16 workers × 6).
for f in 0..120 {
h.state().request_frame(cid, f, scale);
}
for _ in 0..4000 {
if h.state().frames.cached_frames(&series).len() >= 120 && h.state().frames.inflight() == 0 {
break;
}
std::thread::sleep(std::time::Duration::from_millis(2));
}
assert_eq!(h.state().frames.cached_frames(&series), (0..120).collect::<Vec<_>>());
// Rendering/instrumentation time must not move the playhead near the end of the
// cached run. Exercise real prefetch workers while holding playback in its first second.
h.state_mut().use_manual_clock();
h.state_mut().session.set_time(Tick::ZERO);
invoke(&mut h, "playback.toggle", json!({}));
h.state_mut().clock_mut().advance(0.25);
h.run_steps(2);
assert!(h.state().playback.playing);
let cur = h.state().session.time();
assert!(cur > Tick::ZERO, "playback advances with the manual clock");
assert!(cur < Tick::from_seconds_f64(1.0), "{cur:?}");
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(60);
while std::time::Instant::now() < deadline {
if h.state().frames.cached_frames(&series).iter().any(|&f| f >= 120) {
break;
}
h.step();
std::thread::sleep(std::time::Duration::from_millis(1));
assert_eq!(h.state().session.time(), cur, "worker waits do not advance playback");
}
assert!(h.state().frames.cached_frames(&series).iter().any(|&f| f >= 120), "frames past the cached run render while it plays the first second");
}
#[test]
fn cache_frames_when_idle_survives_an_inverted_work_area() {
let mut h = harness();
// Start set past the end: an empty work area (it panicked in `clamp` / `rem_euclid`).
invoke(&mut h, "comp.workArea", json!({"end": 2.0}));
invoke(&mut h, "comp.workArea", json!({"start": 5.0}));
invoke(&mut h, "playback.cacheWhenIdle", json!({"value": true}));
for _ in 0..150 {
h.step();
std::thread::sleep(std::time::Duration::from_millis(2));
}
assert!(!h.state().playback.playing);
}