diff --git a/crates/ui-egui/src/panels/timeline.rs b/crates/ui-egui/src/panels/timeline.rs index 186ebef7..032037b7 100644 --- a/crates/ui-egui/src/panels/timeline.rs +++ b/crates/ui-egui/src/panels/timeline.rs @@ -1909,7 +1909,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { // Switches. let sws: [(Icon, bool, &str, bool); 8] = [ (Icon::Shy, sw.shy, "shy", true), - (Icon::Collapse, sw.collapse, "collapse", matches!(layer.source, LayerSource::Comp { .. } | LayerSource::Shape | LayerSource::Text)), + (Icon::Collapse, sw.collapse, "collapse", collapse_applies(layer, &app.session.project)), (Icon::Quality, sw.quality == effectcraft_engine::project::Quality::Best, "quality", layer.source.is_av()), (Icon::Fx, sw.effects, "fx", layer.effects().is_some_and(|f| !f.children.is_empty())), ( @@ -3505,6 +3505,19 @@ pub(crate) fn vector_footage_layer(project: &effectcraft_engine::project::Projec .is_some_and(|item| matches!(&item.kind, effectcraft_engine::project::ItemKind::Footage(footage) if effectcraft_engine::vector::is_vector(footage))) } +/// Whether the Collapse Transformations / Continuously Rasterize switch applies: precomps, shape and text +/// layers, and vector footage (SVG, PDF, AI, EPS), which the renderer re-rasterizes at the layer's scale. +fn collapse_applies(layer: &Layer, project: &effectcraft_engine::project::Project) -> bool { + match &layer.source { + LayerSource::Comp { .. } | LayerSource::Shape | LayerSource::Text => true, + LayerSource::Footage { item } => { + let kind = project.item(*item).map(|i| &i.kind); + matches!(kind, Some(effectcraft_engine::project::ItemKind::Footage(f)) if effectcraft_engine::render::is_vector_footage(f)) + } + _ => false, + } +} + fn layer_context_menu(resp: &egui::Response, comp: &Comp, layer: &Layer, selected: &[LayerId], vector: bool, actions: &mut Vec<(String, serde_json::Value)>) { let in_selection = selected.contains(&layer.id); if resp.secondary_clicked() && !in_selection && !layer.switches.locked { @@ -3835,6 +3848,24 @@ mod tests { assert!(track_matte_pick(&bottom, &candidates, 1, "-").is_none(), "a separator does nothing"); } + /// #957: vector footage gets the Continuously Rasterize switch; raster footage does not. + #[test] + fn collapse_switch_shows_for_vector_footage_only() { + use effectcraft_engine::color::Label; + use effectcraft_engine::project::{Footage, ItemKind, Project}; + let mut p = Project::default(); + let mut layer_for = |name: &str, codec: &str| { + let footage = Footage { path: name.into(), codec: codec.into(), ..Default::default() }; + let item = p.add_item(name, Label::SeaFoam, None, ItemKind::Footage(footage)); + let comp = Comp::new(64, 36, effectcraft_engine::time::FrameRate::new(30, 1), Tick::from_seconds_f64(1.0)); + effectcraft_engine::project::build::layer(&mut p, &comp, name, LayerSource::Footage { item }, (0, 0), None) + }; + let svg = layer_for("logo.svg", "SVG"); + let png = layer_for("photo.png", "PNG"); + assert!(collapse_applies(&svg, &p)); + assert!(!collapse_applies(&png, &p)); + } + /// A comp with two solids; "Box" has an animated Position and a Gaussian Blur. fn app() -> EffectcraftApp { let mut s = effectcraft_engine::Session::default();