diff --git a/crates/tools/src/direct.rs b/crates/tools/src/direct.rs index cba1c40b9..6fe4819c9 100644 --- a/crates/tools/src/direct.rs +++ b/crates/tools/src/direct.rs @@ -530,6 +530,28 @@ fn anchors_json(v: &[AnchorRef]) -> Value { Value::Array(v.iter().map(|(s, a)| json!([s, a])).collect()) } +/// The segments of `path` that marquee `r` crosses with neither of their anchors inside it, as +/// (subpath, segment) pairs: the reference app selects a segment that a marquee is dragged "over +/// part of" (). +fn crossed_segments(path: &PathData, r: Rect) -> Vec { + let mut out = vec![]; + for (si, sp) in path.subpaths.iter().enumerate() { + let n = sp.anchors.len(); + let count = if sp.closed { n } else { n.saturating_sub(1) }; + for seg in 0..count { + let (Some(a), Some(b)) = (sp.anchors.get(seg), sp.anchors.get((seg + 1) % n)) else { continue }; + if r.contains(a.p) || r.contains(b.p) { + continue; + } + let piece = PathData::single(vectorcraft_geom::SubPath::new(vec![a.clone(), b.clone()], false)); + if vectorcraft_geom::hit::intersects_rect(&piece, r) { + out.push((si, seg)); + } + } + } + out +} + /// A segment of a path under the pointer. struct SegmentHit { si: usize, @@ -1412,10 +1434,29 @@ impl DirectSelectionTool { } } sel.retain(|(id, _)| editable(*id)); + // Direct Selection: a segment the marquee crosses with neither of its anchors inside it is + // selected, as a click on it selects it (#4654). + let mut crossed: Vec<(NodeId, Vec)> = vec![]; + if !self.group { + walk_pickable(cx.doc, cx.isolation, |n| { + if let NodeKind::Path { path, .. } = &n.kind + && editable(n.id) + { + let segs = crossed_segments(path, r); + if !segs.is_empty() { + crossed.push((n.id, segs)); + } + } + }); + } self.frame = marked; let items: Vec = sel.iter().map(|(id, v)| json!({"id": id.0, "anchors": anchors_json(v)})).collect(); let art = Action::Exec("select.anchorsMany".into(), json!({"items": items, "mode": if toggle { "toggle" } else { "set" }})); - let mut out: Vec = std::iter::once(art).chain(crate::rulerguide::marquee_select(cx, r, false)).collect(); + let mode = if toggle { "toggle" } else { "add" }; + let segments = crossed + .iter() + .map(|(id, segs)| Action::Exec("select.segments".into(), json!({"id": id.0, "segments": anchors_json(segs), "mode": mode}))); + let mut out: Vec = std::iter::once(art).chain(segments).chain(crate::rulerguide::marquee_select(cx, r, false)).collect(); if !self.frame.is_empty() { out.push(Action::Notify(FRAME_PICKED.into())); } diff --git a/crates/tools/src/direct/tests.rs b/crates/tools/src/direct/tests.rs index d19108509..d3f394d99 100644 --- a/crates/tools/src/direct/tests.rs +++ b/crates/tools/src/direct/tests.rs @@ -565,6 +565,28 @@ fn marquee_selects_anchors() { assert_eq!(a, vec![Action::Exec("select.anchorsMany".into(), json!({"items": [{"id": id.0, "anchors": [[0, 1]]}], "mode": "set"}))]); } +/// #4654: a marquee over part of a segment, with neither of its anchors inside, selects that +/// segment as a click on it does; with Shift it toggles it. Group Selection selects objects. +#[test] +fn a_marquee_over_part_of_a_segment_selects_it() { + let (d, id) = doc_with_rect(); + let (s, p) = (Selection::default(), paint()); + let cx = cx(&d, &s, &p); + // Across the middle of the square's top edge, (100, 100)–(200, 100). + let drag = |t: &mut DirectSelectionTool, m: Mods| { + t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 140.0, 80.0).with_mods(m)); + t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 160.0, 120.0).with_mods(m)); + t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 160.0, 120.0).with_mods(m)) + }; + let segment = |mode: &str| Action::Exec("select.segments".into(), json!({"id": id.0, "segments": [[0, 0]], "mode": mode})); + let mut t = DirectSelectionTool::new(false); + assert_eq!(drag(&mut t, Mods::default()), vec![Action::Exec("select.anchorsMany".into(), json!({"items": [], "mode": "set"})), segment("add")]); + let shift = Mods { shift: true, ..Mods::default() }; + assert_eq!(drag(&mut t, shift).get(1), Some(&segment("toggle"))); + let mut g = DirectSelectionTool::new(true); + assert!(!drag(&mut g, Mods::default()).iter().any(|a| matches!(a, Action::Exec(c, _) if c == "select.segments"))); +} + #[test] fn marquee_selects_ruler_guides() { let (mut d, id) = doc_with_rect();