diff --git a/crates/ui-egui/tests/ui_viewer.rs b/crates/ui-egui/tests/ui_viewer.rs index 3022cb8a..40e1ac08 100644 --- a/crates/ui-egui/tests/ui_viewer.rs +++ b/crates/ui-egui/tests/ui_viewer.rs @@ -2134,3 +2134,93 @@ fn rotate_tool_counts_revolutions_both_ways() { assert!(seen.windows(2).all(|w| (w[1] - w[0]).abs() <= 20.0), "no jump: {seen:?}"); assert_eq!(h.state().session.history.undo.len(), undo_before + 1, "one drag, one undo step"); } + +/// 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:?}"); +}