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.
This commit is contained in:
Vikram Bhaskaran committed 2026-10-13 01:01:11 +11:00
1 parent 9efe75330c
commit d33e82a571
1 file changed
+90
+90
View File
@@ -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:?}");
}