Pick placed images on their frame with Selection by Path Only (#3291)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
HEOJUNFO
2026-10-21 16:55:17 +07:00
committed by bflatastic
co-authored by Claude Opus 5.5
parent 3cd2ae2f12
commit 764d04926e
+34 -4
View File
@@ -503,10 +503,18 @@ fn hit_leaf(n: &Node, p: Point, opt: HitOptions, symbols: &SymbolBoxes) -> Optio
n.geometric_bounds().filter(|b| b.inflate(opt.tol, opt.tol).contains(p))?;
on_type_path(n, t, p, opt.tol).then_some(HitKind::Outline)
}
// An image is hit within its frame, and where its own fills and strokes paint
// ([`image_paint`]) except in outline mode.
NodeKind::Image(_) => {
if n.geometric_bounds_with(symbols).is_some_and(|b| b.inflate(opt.tol, opt.tol).contains(p)) {
// An image is hit within its frame (on the frame's edge only with Selection by Path Only,
// #3291), and where its own fills and strokes paint ([`image_paint`]) except in outline
// mode.
NodeKind::Image(im) => {
if opt.path_only {
use vectorcraft_geom::kurbo::Shape;
let mut frame = Rect::new(0.0, 0.0, f64::from(im.width), f64::from(im.height)).to_path(0.1);
frame.apply_affine(im.xf);
if stroke_contains(&frame, 0.0, opt.tol, p) {
return Some(HitKind::Outline);
}
} else if n.geometric_bounds_with(symbols).is_some_and(|b| b.inflate(opt.tol, opt.tol).contains(p)) {
return Some(HitKind::Bounds);
}
if opt.outline {
@@ -1134,6 +1142,28 @@ mod tests {
}
}
/// #3291: with Object Selection by Path Only, a placed image is picked on its frame's edge, so
/// a path under it can be picked through it.
#[test]
fn path_only_picks_paths_through_images() {
let mut d = Document::new(200.0, 200.0);
let l = d.layers[0].id;
let (path, image) = (d.alloc_id(), d.alloc_id());
d.insert(Some(l), 0, Node::path(path, shapes::rectangle(Rect::new(20.0, 20.0, 60.0, 60.0)), Appearance::default_art())).unwrap();
// 100×50 pixels, scaled to 100×100 and moved to (10, 10): over the whole path.
let xf = vectorcraft_geom::Affine::translate((10.0, 10.0)) * vectorcraft_geom::Affine::scale_non_uniform(1.0, 2.0);
let im = crate::ImageObject { key: "k".into(), width: 100, height: 50, xf, link: None, placement: Default::default() };
d.insert(Some(l), 1, Node::new(image, NodeKind::Image(Box::new(im)))).unwrap();
let path_only = HitOptions { path_only: true, ..Default::default() };
let hit = |p: Point, opt| hit_test(&d, p, opt).map(|h| h.leaf);
let (path_edge, inside, image_edge) = (Point::new(20.5, 40.0), Point::new(80.0, 80.0), Point::new(80.0, 109.5));
assert_eq!(hit(path_edge, HitOptions::default()), Some(image), "the image covers the path");
assert_eq!(hit(inside, HitOptions::default()), Some(image));
assert_eq!(hit(path_edge, path_only), Some(path), "path only: the path through the image");
assert_eq!(hit(inside, path_only), None, "path only: not inside the image");
assert_eq!(hit(image_edge, path_only), Some(image), "path only: the image's frame");
}
/// Type Object Selection by Path Only: point type is picked on its baseline, area type on its
/// frame and type on a path on its path, no longer anywhere in its bounds.
#[test]