//! Clicks, hovers and marquees with the selection tools leave out the area that raster effects //! (drop shadows, glows, blurs) paint beyond an object: there they reach the object under the //! effect, or nothing. use serde_json::{Value, json}; use vectorcraft_doc::NodeId; use vectorcraft_geom::{Point, Rect}; use vectorcraft_tools::{Cursor, Mods, PointerEvent, PointerKind}; use crate::*; fn session() -> Session { let mut s = Session::new(); s.execute("file.new", &json!({"width": 200, "height": 200})).unwrap(); s } fn run(s: &mut Session, id: &str, p: Value) -> Value { s.execute(id, &p).unwrap_or_else(|e| panic!("{id} {p}: {e}")) } fn id_of(v: &Value) -> NodeId { NodeId(v["id"].as_u64().unwrap()) } /// A rectangle filled with `color`. fn rect(s: &mut Session, x: f64, y: f64, w: f64, h: f64, color: &str) -> NodeId { let id = id_of(&run(s, "shape.rectangle", json!({"x": x, "y": y, "width": w, "height": h}))); run(s, "select.set", json!({"ids": [id.0]})); run(s, "paint.setFill", json!({"color": color})); id } /// A drop shadow on the whole of `id`, `dx` to the right, with no blur. fn shadow(s: &mut Session, id: NodeId, dx: f64) { run(s, "effect.apply", json!({"effect": "stylize.dropShadow", "ids": [id.0], "params": {"x": dx, "y": 0, "blur": 0}, "item": null})); } fn pixel(s: &Session, x: u32, y: u32) -> [u8; 4] { let doc = s.doc().unwrap().doc.clone(); let opts = vectorcraft_render::RenderOptions { background: Some([255, 255, 255, 255]), ..Default::default() }; vectorcraft_render::Renderer::new().render(&doc, 200, 200, vectorcraft_geom::Affine::IDENTITY, &opts).pixel(x, y) } fn press(s: &mut Session, tool: &str, from: (f64, f64), to: (f64, f64), mods: Mods) -> Vec { let v = ViewInfo::default(); s.select_tool(tool, v).unwrap(); for (k, (x, y)) in [(PointerKind::Down, from), (PointerKind::Drag, to), (PointerKind::Up, to)] { if k == PointerKind::Drag && from == to { continue; } s.pointer(&PointerEvent::new(k, x, y).with_mods(mods), v).unwrap(); } s.active().unwrap().selection.objects.clone() } /// Click with the Selection tool at (x, y) on a 100% view: what is selected afterwards. fn click(s: &mut Session, x: f64, y: f64) -> Vec { press(s, "selection", (x, y), (x, y), Mods::default()) } fn bounds(s: &Session, id: NodeId) -> Rect { s.doc().unwrap().doc.node(id).unwrap().geometric_bounds().unwrap() } const WHITE: [u8; 4] = [255; 4]; const BLUE: [u8; 4] = [0, 0, 255, 255]; /// Blue B at x 100 to 160, red A over its left edge's side at x 20 to 80, and A's shadow /// 40 pt to the right, from x 60 to 120: over the canvas from 80 to 100 and over B from 100 to 120. fn shadow_over_b(s: &mut Session) -> (NodeId, NodeId) { let b = rect(s, 100.0, 20.0, 60.0, 60.0, "#0000ff"); let a = rect(s, 20.0, 20.0, 60.0, 60.0, "#ff0000"); shadow(s, a, 40.0); (a, b) } #[test] fn a_click_on_a_drop_shadow_selects_the_object_under_it() { let mut s = session(); let (a, b) = shadow_over_b(&mut s); // The shadow paints over B and over the canvas. assert_ne!(pixel(&s, 110, 50), BLUE); assert_ne!(pixel(&s, 90, 50), WHITE); run(&mut s, "select.none", json!({})); assert_eq!(click(&mut s, 110.0, 50.0), [b]); assert_eq!(click(&mut s, 50.0, 50.0), [a]); } #[test] fn a_click_on_a_drop_shadow_over_the_canvas_selects_nothing_and_clears_the_selection() { let mut s = session(); let (a, _) = shadow_over_b(&mut s); run(&mut s, "select.set", json!({"ids": [a.0]})); assert!(click(&mut s, 90.0, 50.0).is_empty()); } #[test] fn a_click_on_an_outer_glow_or_a_gaussian_blur_outside_the_path_selects_what_is_under_it() { let mut s = session(); let a = rect(&mut s, 20.0, 20.0, 60.0, 60.0, "#ff0000"); run(&mut s, "effect.apply", json!({"effect": "stylize.outerGlow", "ids": [a.0], "params": {"blur": 20, "mode": "normal"}, "item": null})); assert_ne!(pixel(&s, 88, 50), WHITE); run(&mut s, "select.none", json!({})); assert!(click(&mut s, 88.0, 50.0).is_empty()); // A red square whose blur reaches over the canvas and over the left edge of a blue one. let mut s = session(); let b = rect(&mut s, 92.0, 20.0, 60.0, 60.0, "#0000ff"); let a = rect(&mut s, 20.0, 20.0, 60.0, 60.0, "#ff0000"); run(&mut s, "effect.apply", json!({"effect": "blur.gaussian", "ids": [a.0], "params": {"radius": 20}, "item": null})); assert_ne!(pixel(&s, 98, 50), BLUE); assert_ne!(pixel(&s, 87, 50), WHITE); run(&mut s, "select.none", json!({})); assert_eq!(click(&mut s, 98.0, 50.0), [b]); // (With B deselected, so that the press does not take its bounding box handle.) run(&mut s, "select.none", json!({})); assert!(click(&mut s, 87.0, 50.0).is_empty()); } /// The first point of `area` (x0, y0, x1, y1) that the canvas paints, scanning rows. fn painted_in(s: &Session, (x0, y0, x1, y1): (u32, u32, u32, u32)) -> Option<(f64, f64)> { let doc = s.doc().unwrap().doc.clone(); let opts = vectorcraft_render::RenderOptions { background: Some([255, 255, 255, 255]), ..Default::default() }; let img = vectorcraft_render::Renderer::new().render(&doc, 200, 200, vectorcraft_geom::Affine::IDENTITY, &opts); (y0..y1) .flat_map(|y| (x0..x1).map(move |x| (x, y))) .find(|&(x, y)| img.pixel(x, y) != WHITE) .map(|(x, y)| (f64::from(x) + 0.5, f64::from(y) + 0.5)) } #[test] fn a_click_on_the_drop_shadow_of_a_group_or_of_point_type_selects_neither() { let mut s = session(); let a = rect(&mut s, 20.0, 20.0, 30.0, 30.0, "#ff0000"); let b = rect(&mut s, 60.0, 20.0, 30.0, 30.0, "#ff0000"); run(&mut s, "select.set", json!({"ids": [a.0, b.0]})); let g = id_of(&run(&mut s, "object.group", json!({}))); shadow(&mut s, g, 80.0); // The group's shadow lies from x 100 to 170. assert_ne!(pixel(&s, 110, 35), WHITE); run(&mut s, "select.none", json!({})); assert!(click(&mut s, 110.0, 35.0).is_empty()); assert_eq!(click(&mut s, 35.0, 35.0), [g]); // Type 40 pt high at y 150 with its shadow 90 pt to the right. let t = id_of(&run(&mut s, "text.create", json!({"x": 20, "y": 150, "text": "HH", "size": 40}))); shadow(&mut s, t, 90.0); let right = bounds(&s, t).x1; assert!(right < 100.0, "{right}"); let at = painted_in(&s, (right.ceil() as u32 + 10, 110, 199, 160)).expect("the type's shadow paints"); run(&mut s, "select.none", json!({})); assert!(click(&mut s, at.0, at.1).is_empty(), "{at:?}"); } #[test] fn a_click_on_the_drop_shadow_of_a_symbols_art_does_not_select_the_instance() { let mut s = session(); let a = rect(&mut s, 20.0, 20.0, 60.0, 60.0, "#ff0000"); shadow(&mut s, a, 40.0); run(&mut s, "select.set", json!({"ids": [a.0]})); let inst = id_of(&run(&mut s, "symbol.new", json!({"name": "Shadowed"}))); assert_ne!(pixel(&s, 100, 50), WHITE); run(&mut s, "select.none", json!({})); assert!(click(&mut s, 100.0, 50.0).is_empty()); assert_eq!(click(&mut s, 50.0, 50.0), [inst]); } #[test] fn a_click_on_the_drop_shadow_of_an_image_outside_its_frame_does_not_select_it() { let mut s = session(); let mut png = vec![]; image::RgbaImage::from_pixel(30, 30, image::Rgba([255, 0, 0, 255])) .write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png) .unwrap(); let r = run(&mut s, "file.place", json!({"name": "red.png", "dataBase64": vectorcraft_format::base64_encode(&png), "at": [65, 65]})); let im = NodeId(r["ids"][0].as_u64().unwrap()); // The frame lies from x 50 to 80 and the shadow from 90 to 120. shadow(&mut s, im, 40.0); assert_ne!(pixel(&s, 105, 65), WHITE); run(&mut s, "select.none", json!({})); assert!(click(&mut s, 105.0, 65.0).is_empty()); assert_eq!(click(&mut s, 65.0, 65.0), [im]); } #[test] fn shift_click_direct_selection_and_hover_on_a_drop_shadow_pass_through_it() { let mut s = session(); let (a, b) = shadow_over_b(&mut s); // Shift-click on the shadow over B adds B and keeps A. run(&mut s, "select.set", json!({"ids": [a.0]})); let shift = Mods { shift: true, ..Mods::default() }; let mut sel = press(&mut s, "selection", (110.0, 50.0), (110.0, 50.0), shift); sel.sort_by_key(|n| n.0); let mut both = vec![a, b]; both.sort_by_key(|n| n.0); assert_eq!(sel, both); // Direct Selection takes B through the shadow, and nothing over the canvas. run(&mut s, "select.none", json!({})); let sel = press(&mut s, "directSelection", (110.0, 50.0), (110.0, 50.0), Mods::default()); assert!(sel.contains(&b) && !sel.contains(&a), "{sel:?}"); run(&mut s, "select.none", json!({})); assert!(press(&mut s, "directSelection", (90.0, 50.0), (90.0, 50.0), Mods::default()).is_empty()); // With A selected, the pointer over its shadow is the plain arrow, and over A the move pointer. run(&mut s, "select.set", json!({"ids": [a.0]})); let v = ViewInfo::default(); s.select_tool("selection", v).unwrap(); assert_eq!(s.cursor(Point::new(50.0, 50.0), Mods::default(), v), Cursor::Move); assert_eq!(s.cursor(Point::new(110.0, 50.0), Mods::default(), v), Cursor::Arrow); assert_eq!(s.cursor(Point::new(92.0, 50.0), Mods::default(), v), Cursor::Arrow); } #[test] fn a_drag_from_the_drop_shadow_of_a_selected_object_does_not_move_it() { let mut s = session(); let (a, b) = shadow_over_b(&mut s); run(&mut s, "select.set", json!({"ids": [a.0]})); let before = bounds(&s, a); // From the shadow over the canvas: a marquee that touches nothing. assert!(press(&mut s, "selection", (92.0, 50.0), (96.0, 60.0), Mods::default()).is_empty()); assert_eq!(bounds(&s, a), before); // From the shadow over B: B moves, A stays. run(&mut s, "select.set", json!({"ids": [a.0]})); let b_before = bounds(&s, b); assert_eq!(press(&mut s, "selection", (110.0, 50.0), (110.0, 60.0), Mods::default()), [b]); assert_eq!(bounds(&s, a), before); assert!((bounds(&s, b).y0 - b_before.y0 - 10.0).abs() < 1e-6, "{:?}", bounds(&s, b)); } #[test] fn a_marquee_that_touches_only_a_drop_shadow_does_not_select_its_object() { let mut s = session(); let (_, b) = shadow_over_b(&mut s); run(&mut s, "select.none", json!({})); assert!(press(&mut s, "selection", (85.0, 40.0), (95.0, 60.0), Mods::default()).is_empty()); // From above B, over B and the shadow: B only. assert_eq!(press(&mut s, "selection", (105.0, 5.0), (115.0, 30.0), Mods::default()), [b]); }