diff --git a/crates/ui-egui/src/panels/viewer.rs b/crates/ui-egui/src/panels/viewer.rs index 795ac0650..bec2440d6 100644 --- a/crates/ui-egui/src/panels/viewer.rs +++ b/crates/ui-egui/src/panels/viewer.rs @@ -8,8 +8,8 @@ use effectcraft_engine::commands::shape_tool::PaintKind; use effectcraft_engine::effects::puppet::PinKind; use effectcraft_engine::geom::{Mat3, vec2 as gv2}; use effectcraft_engine::project::{Comp, Layer, LayerId, LayerSource}; -use effectcraft_engine::render::EvalCtx; use effectcraft_engine::render::three_d::{self, CameraState, View3D}; +use effectcraft_engine::render::{EvalCtx, Renderer}; use effectcraft_engine::time::Tick; use egui::{Align2, Color32, Pos2, Rect, Sense, Stroke, StrokeKind, pos2, vec2}; use serde_json::json; @@ -1194,16 +1194,21 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { } // Footage (or the proxy in use for footage or a precomp) that can't be decoded (no decoder - // for its codec / profile): say why on the layer instead of leaving a black picture. - let mut notices = 0; - for layer in comp.layers.iter().filter(|l| l.is_active_at(time) && l.switches.video) { - let (effectcraft_engine::project::LayerSource::Footage { item } | effectcraft_engine::project::LayerSource::Comp { item }) = &layer.source else { - continue; - }; - let Some(why) = app.session.project.item(*item).and_then(|i| app.session.footage.item_problem(i)) else { continue }; - if notices >= 3 { - break; - } + // for its codec / profile): say why on the layer instead of leaving a black picture. A + // collapsed precomp draws its own layers, never the comp's proxy. + let problems: Vec<(&Layer, String)> = { + let r = Renderer::new(&snap_project, &*app.session.footage, Default::default()); + comp.layers + .iter() + .filter(|l| l.is_active_at(time) && l.switches.video && r.collapsed(&ectx, l).is_none()) + .filter_map(|layer| { + let (LayerSource::Footage { item } | LayerSource::Comp { item }) = &layer.source else { return None }; + Some((layer, snap_project.item(*item).and_then(|i| app.session.footage.item_problem(i))?)) + }) + .take(3) + .collect() + }; + for (notices, (layer, why)) in problems.into_iter().enumerate() { let (m, _) = l2c(&ectx, layer); let (w, h) = effectcraft_engine::render::source_size(&app.session.project, layer); let c = m.apply(effectcraft_engine::geom::vec2(w as f64 / 2.0, h as f64 / 2.0)); @@ -1215,7 +1220,6 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { painter.rect_stroke(r, 3.0, Stroke::new(1.0, t.warning), StrokeKind::Inside); painter.galley(pos2(r.min.x + 12.0, r.center().y - galley.size().y / 2.0), galley, Color32::WHITE); app.auto.add(&format!("viewer.footageProblem.{}", layer.id.0), r, &text); - notices += 1; } // Free transform box of a path (double-click a path with the Selection tool). diff --git a/crates/ui-egui/tests/ui_unreadable_footage.rs b/crates/ui-egui/tests/ui_unreadable_footage.rs index b1b12d6d2..63e3a75c2 100644 --- a/crates/ui-egui/tests/ui_unreadable_footage.rs +++ b/crates/ui-egui/tests/ui_unreadable_footage.rs @@ -45,10 +45,14 @@ fn harness(broken: fn(&Footage) -> bool, proxy: Option) -> (Harness<'st item.proxy = Some(Box::new(Proxy { footage, enabled: true })); } s.execute("layer.addItem", json!({"item": clip.0})).unwrap(); + (open(s), clip) +} + +fn open(mut s: Session) -> Harness<'static, EffectcraftApp> { s.execute("project.select", json!({"items": []})).unwrap(); let mut h = Harness::builder().with_size(egui::vec2(1600.0, 1000.0)).build_eframe(|_| EffectcraftApp::new(s)); h.run_steps(4); - (h, clip) + h } #[test] @@ -78,3 +82,23 @@ fn an_undecodable_proxy_in_use_is_marked_as_the_proxy() { let notice = h.state().auto.query("viewer.footageProblem.").first().copied().expect("a notice in the viewer").clone(); assert!(notice.label.contains(&format!("Proxy: {WHY}")), "{notice:?}"); } + +/// A precomp whose proxy can't be decoded: the layer says so while it renders through the +/// proxy, but not with Collapse Transformations on, which draws the precomp's own layers. +#[test] +fn a_collapsed_precomp_has_no_notice_for_its_proxy() { + for collapse in [false, true] { + let mut s = Session { footage: Arc::new(Stub(|f| f.path.ends_with(".avi"))), ..Default::default() }; + s.execute("comp.new", json!({"name": "Inner", "width": 64, "height": 36, "duration": 4})).unwrap(); + s.execute("comp.new", json!({"name": "Main", "width": 320, "height": 180, "duration": 4})).unwrap(); + let p = Arc::make_mut(&mut s.project); + let inner = p.items.values_mut().find(|i| i.name == "Inner").unwrap(); + let proxy = Footage { kind: FootageKind::Video, path: "inner_proxy.avi".into(), width: 64, height: 36, has_video: true, ..Default::default() }; + inner.proxy = Some(Box::new(Proxy { footage: proxy, enabled: true })); + let inner = inner.id; + s.execute("layer.addItem", json!({"item": inner.0})).unwrap(); + s.execute("layer.setSwitch", json!({"layers": ["#1"], "switch": "collapse", "value": collapse})).unwrap(); + let h = open(s); + assert_eq!(h.state().auto.query("viewer.footageProblem.").is_empty(), collapse, "collapse {collapse}"); + } +}