diff --git a/crates/ui-egui/tests/ui_workflow_selection.rs b/crates/ui-egui/tests/ui_workflow_selection.rs index 6e264dd3..eae2bd54 100644 --- a/crates/ui-egui/tests/ui_workflow_selection.rs +++ b/crates/ui-egui/tests/ui_workflow_selection.rs @@ -637,3 +637,66 @@ fn timeline_context_menu_creates_shapes_and_masks_from_text() { assert!(h.query_by_label("Create Shapes from Text").is_none(), "no text conversion for a solid"); assert!(h.query_by_label("Create Masks from Text").is_none(), "no text conversion for a solid"); } + +// Regression test for #979 (#958): right-clicking a vector footage layer (SVG) in the Timeline +// or the Composition viewer offered no Create Shapes from Vector Layer; raster layers must not. +#[test] +fn layer_context_menus_offer_create_shapes_from_vector_layer_for_vector_footage() { + use effectcraft_engine::project::LayerSource; + let dir = std::env::temp_dir().join(format!("ec-ui-vector-menu-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let svg = dir.join("logo.svg"); + std::fs::write( + &svg, + r##""##, + ) + .unwrap(); + let mut s = effectcraft_host::session(); + let item = s.execute("file.import", json!({"paths": [svg.to_string_lossy()]})).unwrap()["items"][0].as_u64().unwrap(); + s.execute("comp.new", json!({"name":"Vec","width":320,"height":180,"duration":4})).unwrap(); + let logo = s.execute("layer.addItem", json!({"item": item})).unwrap()["layer"].as_u64().unwrap(); + let plate = s.execute("layer.newSolid", json!({"name":"Plate","width":40,"height":40,"color":"#406080"})).unwrap()["layer"].as_u64().unwrap(); + s.execute("edit.deselectAll", json!({})).unwrap(); + let mut h = Harness::builder().with_size(vec2(1600.0, 1000.0)).build_eframe(|_| EffectcraftApp::new(s)); + h.run_steps(3); + let right_click = |h: &mut Harness<'_, EffectcraftApp>, p: Pos2| { + for pressed in [true, false] { + h.input_mut().events.push(Event::PointerMoved(p)); + h.input_mut().events.push(Event::PointerButton { pos: p, button: PointerButton::Secondary, pressed, modifiers: Modifiers::NONE }); + h.step(); + } + h.run_steps(3); + }; + let row = |h: &Harness<'_, EffectcraftApp>, id: u64| { + let r = rect(h, &format!("timeline.layer.{id}.row")); + pos2(r.min.x + 210.0, r.center().y) + }; + let label = "Create Shapes from Vector Layer"; + let shapes = + |h: &Harness<'_, EffectcraftApp>| h.state().session.active_comp().unwrap().layers.iter().filter(|l| matches!(l.source, LayerSource::Shape)).count(); + // A raster-less solid does not offer the conversion. + h.state_mut().session.execute("layer.select", json!({"layers": [plate]})).unwrap(); + h.run_steps(3); + let at = row(&h, plate); + right_click(&mut h, at); + assert!(h.query_by_label(label).is_none(), "no vector conversion for a solid"); + h.key_press(egui::Key::Escape); + h.run_steps(3); + // The Timeline row of the SVG layer offers it, and it converts the layer. + h.state_mut().session.execute("layer.select", json!({"layers": [logo]})).unwrap(); + h.run_steps(3); + let at = row(&h, logo); + right_click(&mut h, at); + h.get_by_label(label).click(); + h.run_steps(3); + assert_eq!(shapes(&h), 1, "a shape layer was created from the SVG layer"); + // The Composition viewer's menu offers it for the SVG layer under the pointer too. + h.state_mut().session.execute("edit.undo", json!({})).unwrap(); + h.state_mut().session.execute("prop.set", json!({"layer": plate, "path": "transform/position", "value": [40.0, 40.0]})).unwrap(); + h.state_mut().session.execute("layer.select", json!({"layers": [logo]})).unwrap(); + h.run_steps(3); + let comp = rect(&h, "viewer.comp"); + right_click(&mut h, comp.center()); + assert!(h.query_by_label(label).is_some(), "the viewer's context menu offers the vector conversion"); + let _ = std::fs::remove_dir_all(&dir); +}