From d33e82a571f25db43e0473a0605146064485391a Mon Sep 17 00:00:00 2001 From: Vikram Bhaskaran Date: Tue, 13 Oct 2026 01:01:11 +1100 Subject: [PATCH] Cover mask vertex multi-select: Shift-click toggle and marquee (#1004) Shift-click adds a vertex to the selection and removes it again, a plain click collapses to one, and a marquee from empty viewer area selects the enclosed vertices (additive with Shift) without moving the layer. These behaviors regressed or were never covered; the tests drive them through the Composition viewer like a user. --- crates/ui-egui/tests/ui_viewer.rs | 90 +++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/crates/ui-egui/tests/ui_viewer.rs b/crates/ui-egui/tests/ui_viewer.rs index 07036ca8..e71bbec8 100644 --- a/crates/ui-egui/tests/ui_viewer.rs +++ b/crates/ui-egui/tests/ui_viewer.rs @@ -2088,3 +2088,93 @@ fn fit_up_to_100_follows_the_maximized_panel() { h.run_steps(3); assert!((rect(&h, "viewer.comp").width() - 640.0 / ppp).abs() < 0.01); } + +/// Mask vertices multi-select with the Selection tool: Shift-click adds a vertex to the +/// selection and Shift-clicking it again removes it; a plain click collapses to one (#1004). +#[test] +fn mask_shift_click_toggles_vertices() { + use egui::Modifiers; + let mut h = harness(); + let plate = layer_id(&h, "Plate"); + h.state_mut().session.execute("layer.select", json!({"layers": [plate]})).unwrap(); + h.state_mut() + .session + .execute("mask.new", json!({"layer": plate, "vertices": [[100.0, 60.0], [500.0, 60.0], [500.0, 300.0], [100.0, 300.0]], "closed": true})) + .unwrap(); + h.state_mut().ui.tool = Tool::Selection; + h.run_steps(2); + let (p0, p1) = (screen(&h, [100.0, 60.0]), screen(&h, [500.0, 60.0])); + let shift_click = |h: &mut Harness<'_, EffectcraftApp>, p: Pos2| { + h.input_mut().events.push(Event::ModifiersChanged(Modifiers::SHIFT)); + h.step(); + h.input_mut().events.push(Event::PointerMoved(p)); + h.input_mut().events.push(Event::PointerButton { pos: p, button: egui::PointerButton::Primary, pressed: true, modifiers: Modifiers::SHIFT }); + h.step(); + h.input_mut().events.push(Event::PointerButton { pos: p, button: egui::PointerButton::Primary, pressed: false, modifiers: Modifiers::SHIFT }); + h.step(); + h.input_mut().events.push(Event::ModifiersChanged(Modifiers::NONE)); + h.run_steps(2); + }; + click(&mut h, p0); + assert_eq!(h.state().session.state.selected_vertices.len(), 1, "plain click selects one vertex"); + shift_click(&mut h, p1); + assert_eq!(h.state().session.state.selected_vertices.len(), 2, "shift-click adds a second vertex"); + shift_click(&mut h, p1); + assert_eq!(h.state().session.state.selected_vertices.len(), 1, "shift-click again removes it"); +} + +/// Dragging a marquee from empty viewer area selects the mask vertices inside it, additive +/// with Shift, without moving the layer (#1004). +#[test] +fn mask_marquee_selects_vertices() { + let mut h = harness(); + let plate = layer_id(&h, "Plate"); + let bx = layer_id(&h, "Box"); + // A full-frame layer behind leaves no empty area to start a marquee from: remove it. + h.state_mut().session.execute("layer.select", json!({"layers": [plate]})).unwrap(); + h.state_mut().session.execute("edit.clear", json!({"layers": [plate]})).unwrap(); + assert_eq!(h.state().session.active_comp().unwrap().layers.len(), 1); + h.state_mut().session.execute("layer.select", json!({"layers": [bx]})).unwrap(); + h.state_mut() + .session + .execute("mask.new", json!({"layer": bx, "vertices": [[10.0, 10.0], [70.0, 10.0], [70.0, 70.0], [10.0, 70.0]], "closed": true})) + .unwrap(); + h.state_mut().ui.tool = Tool::Selection; + h.state_mut().session.execute("mask.selectVertices", json!({"vertices": []})).unwrap(); + h.run_steps(2); + // Mask vertices are layer-local; map vertex 0 to comp space (no rotation, uniform scale). + let (pp, aa) = { + let l = h.state().session.active_comp().unwrap().layer(LayerId(bx)).unwrap().clone(); + (l.props.prop("transform/position").unwrap().value.as_vec3(), l.props.prop("transform/anchor").unwrap().value.as_vec3()) + }; + let v0 = [10.0 - aa[0] + pp[0], 10.0 - aa[1] + pp[1]]; + let pos = |h: &Harness<'_, EffectcraftApp>| { + h.state().session.active_comp().unwrap().layer(LayerId(bx)).unwrap().props.prop("transform/position").unwrap().value.as_vec3() + }; + let before = pos(&h); + // Drag a box around the top-left vertex only, starting/ending on empty area. + let (from, to) = (screen(&h, [(v0[0] - 90.0) as f32, (v0[1] - 50.0) as f32]), screen(&h, [(v0[0] + 10.0) as f32, (v0[1] + 10.0) as f32])); + drag(&mut h, from, to); + let sel = &h.state().session.state.selected_vertices; + assert_eq!(sel.len(), 1, "marquee selects the enclosed vertex: {sel:?}"); + assert_eq!(sel[0].index, 0); + assert_eq!(pos(&h), before, "the layer does not move under a vertex marquee"); + // Shift-marquee adds the vertices it encloses. + let v1 = [70.0 - aa[0] + pp[0], 10.0 - aa[1] + pp[1]]; + let (from, to) = (screen(&h, [(v1[0] - 10.0) as f32, (v1[1] - 50.0) as f32]), screen(&h, [(v1[0] + 60.0) as f32, (v1[1] + 50.0) as f32])); + h.input_mut().events.push(Event::ModifiersChanged(egui::Modifiers::SHIFT)); + h.step(); + h.input_mut().events.push(Event::PointerMoved(from)); + h.input_mut().events.push(Event::PointerButton { pos: from, button: egui::PointerButton::Primary, pressed: true, modifiers: egui::Modifiers::SHIFT }); + h.step(); + for i in 1..=8 { + h.input_mut().events.push(Event::PointerMoved(from + (to - from) * (i as f32 / 8.0))); + h.step(); + } + h.input_mut().events.push(Event::PointerButton { pos: to, button: egui::PointerButton::Primary, pressed: false, modifiers: egui::Modifiers::SHIFT }); + h.step(); + h.input_mut().events.push(Event::ModifiersChanged(egui::Modifiers::NONE)); + h.run_steps(2); + let sel = &h.state().session.state.selected_vertices; + assert_eq!(sel.len(), 2, "shift-marquee adds: {sel:?}"); +}