From 84589dc28804dd112c7474a1af6282ff99b2d3ba Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 13 Oct 2026 19:28:22 +0000 Subject: [PATCH] #1284: double-clicking a shape tool adds a comp-sized shape layer Double-click a shape tool in the toolbar to add a shape layer the size of the composition, at its centre, for backgrounds, mattes and overlays, as After Effects does. It uses layer.newShape with the tool's kind, so the undo label and the command are the same as drawing one. Covered by tests::a_comp_sized_shape_layer_sits_on_the_comp_centre; the double-click itself is not covered by an automated test. Claude-Session: https://claude.ai/code/session_0136kZtXrYo5VUWEFxWduwUD --- crates/engine/src/tests.rs | 12 ++++++++++++ crates/ui-egui/src/header.rs | 9 +++++++++ crates/ui-egui/src/panels/viewer.rs | 2 +- 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 587379a85..680cbff22 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -246,6 +246,18 @@ fn text_shape_mask_matte_parent() { assert_eq!(info["layers"].as_array().unwrap().len(), 2); } +/// A shape layer the size of the comp and placed at its default position covers the frame: the +/// Shape Tool's double-click (#1284) makes one this way, for backgrounds, mattes and overlays. +#[test] +fn a_comp_sized_shape_layer_sits_on_the_comp_centre() { + let mut s = Session::default(); + s.execute("comp.new", json!({"width": 400, "height": 300, "duration": 2})).unwrap(); + let sh = s.execute("layer.newShape", json!({"kind": "rect", "size": [400, 300], "fill": "#ff0000"})).unwrap()["layer"].as_u64().unwrap(); + let l = s.active_comp().unwrap().layer(effectcraft_project::LayerId(sh)).unwrap().clone(); + let pos = l.props.prop("transform/position").unwrap().value.clone(); + assert!(matches!(pos, effectcraft_keyframe::Value::Vec3(p) if (p[0] - 200.0).abs() < 1e-9 && (p[1] - 150.0).abs() < 1e-9), "{pos:?}"); +} + /// The toolbar's Fill / Stroke change a selected shape layer's paint (#205): a shape drawn /// without a stroke gets one, later edits change it, and the Contents "Add:" items (#206) include /// an empty Path and a Gradient Stroke that render. diff --git a/crates/ui-egui/src/header.rs b/crates/ui-egui/src/header.rs index 1260c9c9e..0c6a9d169 100644 --- a/crates/ui-egui/src/header.rs +++ b/crates/ui-egui/src/header.rs @@ -82,6 +82,15 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { { app.ui.status = e; } + // Double-click a shape tool: a shape layer filling the composition, centred, for + // backgrounds, mattes and overlays (as After Effects does). + if cur.is_shape() + && resp.double_clicked() + && let Some((w, h)) = app.session.active_comp().map(|c| (c.width, c.height)) + && let Err(e) = crate::menus::invoke(app, ui.ctx(), "layer.newShape", json!({"kind": crate::panels::viewer::shape_kind(cur), "size": [w, h]})) + { + app.ui.status = e; + } if slot.len() > 1 { let held_for = resp.is_pointer_button_down_on().then(|| ui.input(|i| i.pointer.press_start_time().map(|s| i.time - s))).flatten(); if let Some(seconds) = held_for diff --git a/crates/ui-egui/src/panels/viewer.rs b/crates/ui-egui/src/panels/viewer.rs index 5fe7697c9..8410bb78c 100644 --- a/crates/ui-egui/src/panels/viewer.rs +++ b/crates/ui-egui/src/panels/viewer.rs @@ -2138,7 +2138,7 @@ struct ShapeDrag { } /// The kind `tool` draws, as `shape.newShape` / `layer.addMask` name it. -fn shape_kind(tool: Tool) -> &'static str { +pub(crate) fn shape_kind(tool: Tool) -> &'static str { match tool { Tool::Rectangle => "rect", Tool::RoundedRect => "rounded",