Outline view: a click on a path's outline selects it through an image or a gradient mesh in front (#6437)

In Outline view an image was still hit anywhere inside its box and a
gradient mesh inside its fill, though Outline draws only the image's
frame and the mesh lines. A path behind them could not be picked there.
Now an image is hit on its frame (unless Show Images in Outline Mode
draws its pixels) and a mesh on its edge and mesh lines. Preview is
unchanged.
This commit is contained in:
HassanKhan
2026-11-11 16:38:59 +05:00
parent d5c75d3699
commit a87d996e1d
4 changed files with 76 additions and 4 deletions
+7 -3
View File
@@ -870,10 +870,10 @@ fn hit_leaf(n: &Node, p: Point, opt: HitOptions, symbols: &SymbolBoxes) -> Optio
on_type_path(n, t, p, opt.tol).then_some(HitKind::Outline)
}
// 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.
// #3291, and in outline mode where the frame is all that is drawn, #6437), and where its
// own fills and strokes paint ([`image_paint`]) except in outline mode.
NodeKind::Image(im) => {
if opt.path_only {
if opt.path_only || (opt.outline && !symbols.document().setup.outline_images) {
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);
@@ -909,6 +909,10 @@ fn hit_leaf(n: &Node, p: Point, opt: HitOptions, symbols: &SymbolBoxes) -> Optio
if stroke_contains(&bp, 0.0, opt.tol, p) {
return Some(HitKind::Outline);
}
// Outline mode draws the mesh lines, not the colours between them (#6437).
if opt.outline {
return stroke_contains(&m.lines().to_bezpath(), 0.0, opt.tol, p).then_some(HitKind::Outline);
}
(!opt.path_only && fill_contains(&bp, vectorcraft_geom::FillRule::NonZero, p)).then_some(HitKind::Fill)
}
_ => None,
+39
View File
@@ -880,6 +880,45 @@ fn path_only_picks_paths_through_images() {
assert_eq!(hit(image_edge, path_only), Some(image), "path only: the image's frame");
}
/// #6437: in outline mode an image is drawn as its frame and a gradient mesh as its mesh lines,
/// so a click inside them, on the outline of a path behind, picks that path, and a click on the
/// frame or a mesh line picks the image or the mesh. In Preview they cover the path as before,
/// and an image whose pixels show in outline mode (Show Images in Outline Mode) still does.
#[test]
fn outline_mode_picks_paths_through_images_and_meshes() {
let mut d = Document::new(200.0, 200.0);
let l = d.layers[0].id;
let (path, image, under, mesh) = (d.alloc_id(), d.alloc_id(), 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();
let xf = vectorcraft_geom::Affine::translate((10.0, 10.0));
let im = crate::ImageObject { key: "k".into(), width: 80, height: 80, xf, link: None, placement: Default::default(), pixels: None };
d.insert(Some(l), 1, Node::new(image, NodeKind::Image(Box::new(im)))).unwrap();
d.insert(Some(l), 2, Node::path(under, shapes::rectangle(Rect::new(120.0, 120.0, 160.0, 160.0)), Appearance::default_art())).unwrap();
let area = shapes::rectangle(Rect::new(110.0, 110.0, 190.0, 190.0));
let m = crate::GradientMesh::for_path_with(&area, 2, 2, &|_| (Color::BLACK, 1.0)).unwrap();
d.insert(Some(l), 3, Node::new(mesh, NodeKind::Mesh(m))).unwrap();
let outline = HitOptions { outline: true, ..Default::default() };
let hit = |d: &Document, p: Point, opt| hit_test(d, p, opt).map(|h| h.leaf);
// The image: the path's edge and the inside, then the frame.
let (path_edge, inside, frame) = (Point::new(20.5, 40.0), Point::new(80.0, 80.0), Point::new(89.5, 50.0));
assert_eq!(hit(&d, path_edge, HitOptions::default()), Some(image), "Preview: the image covers the path");
assert_eq!(hit(&d, inside, HitOptions::default()), Some(image));
assert_eq!(hit(&d, path_edge, outline), Some(path), "outline: the path through the image");
assert_eq!(hit(&d, inside, outline), None, "outline: not inside the image");
assert_eq!(hit(&d, frame, outline), Some(image), "outline: the image's frame");
// The mesh: the path's edge and the inside, then its edge and a mesh line inside it.
let (under_edge, inside, edge, line) = (Point::new(120.5, 130.0), Point::new(130.0, 170.0), Point::new(189.5, 130.0), Point::new(150.0, 170.0));
assert_eq!(hit(&d, under_edge, HitOptions::default()), Some(mesh), "Preview: the mesh covers the path");
assert_eq!(hit(&d, inside, HitOptions::default()), Some(mesh));
assert_eq!(hit(&d, under_edge, outline), Some(under), "outline: the path through the mesh");
assert_eq!(hit(&d, inside, outline), None, "outline: not between the mesh lines");
assert_eq!(hit(&d, edge, outline), Some(mesh), "outline: the mesh's edge");
assert_eq!(hit(&d, line, outline), Some(mesh), "outline: a mesh line");
// Show Images in Outline Mode: the image's pixels are drawn, and it covers the path again.
d.setup.outline_images = true;
assert_eq!(hit(&d, path_edge, outline), Some(image));
}
/// 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]
+27
View File
@@ -2426,3 +2426,30 @@ fn a_ruler_guide_under_the_centre_point_keeps_the_pointer() {
let drag = t.pointer(&c, &ev(PointerKind::Drag, 170.0, 150.0));
assert!(drag.iter().any(|a| matches!(a, Action::Preview(cmd, _) if cmd == "guide.move")), "the guide drags: {down:?} {drag:?}");
}
/// #6437: in Outline view, where an image is drawn as its frame, a click inside it on the edge of
/// a path behind selects that path, with the image selected or not. In Preview the image takes
/// the click.
#[test]
fn outline_view_click_through_an_image_selects_the_path_behind() {
use vectorcraft_doc::{Appearance, Document, ImageObject, Node, NodeKind};
let mut d = Document::new(500.0, 500.0);
let layer = d.layers[0].id;
let (back, image) = (d.alloc_id(), d.alloc_id());
let square = vectorcraft_geom::shapes::rectangle(Rect::new(100.0, 100.0, 200.0, 200.0));
d.insert(Some(layer), 0, Node::path(back, square, Appearance::default_art())).unwrap();
let xf = vectorcraft_geom::Affine::translate((150.0, 150.0));
let im = ImageObject { key: "k".into(), width: 100, height: 100, xf, link: None, placement: Default::default(), pixels: None };
d.insert(Some(layer), 1, Node::new(image, NodeKind::Image(Box::new(im)))).unwrap();
let selects = |a: &[Action], id: NodeId| a.iter().any(|a| matches!(a, Action::Exec(c, v) if c == "select.set" && v["ids"] == json!([id.0])));
let p = paint();
for picked in [vec![], vec![image]] {
let mut s = Selection::default();
s.set(picked.clone());
let outline = ToolContext { outline: true, ..cx(&d, &s, &p) };
let a = SelectionTool::default().pointer(&outline, &ev(PointerKind::Down, 200.0, 180.0));
assert!(selects(&a, back), "outline, {picked:?} selected: {a:?}");
let a = SelectionTool::default().pointer(&cx(&d, &s, &p), &ev(PointerKind::Down, 200.0, 180.0));
assert!(!selects(&a, back), "preview, {picked:?} selected: {a:?}");
}
}
+3 -1
View File
@@ -1,6 +1,6 @@
# UI and interaction parity
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-11 · **Change:** minor (G5: Inventory Icons built-in symbol library (four built-ins); previously minor: files that record no units open in Preferences › Units › General, #5820; previously minor: interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; numeric fields step on wheel hover without taking keyboard focus, #5949; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen; Selection QoL: marquee modes and visible feedback, #5313; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; Drop Shadow and glow dialogs: spinner arrows, #5367, #5368; Stroke Weight Up and Down shortcuts, #5797; Layers: Rename (`ui.rename`) opens the name field of the selected layer, group or object, #5324; Properties in Edit Artboards: Transform section above the Artboards section, #5219; scrubby labels: the Control bar's Stroke: and Opacity: links and the Properties panel's Stroke link scrub their fields, #4088; Properties: Quick Actions has Repeat Options for one selected repeat, #5181; Direct Selection: Shift-click picks several blend spine points, which drag together, #5285; Direct Selection: Cmd/Ctrl held lends the Selection tool and shows its bounding box, #3901; Join tool trims two crossing open paths and joins them at the crossing, #5844; Control bar: Paragraph: link and Align left/center/right while type is selected, #5372; shape tools: a press on the selection's centre point moves it and one in the rotate zone outside a box corner turns it, #5477; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; Units › East Asian Type acts, with Q and Ha units; scores unchanged)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-11 · **Change:** minor (Outline view: images and gradient meshes are picked on what Outline draws, so a click inside them reaches the path behind, #6437; previously minor: G5: Inventory Icons built-in symbol library (four built-ins); previously minor: files that record no units open in Preferences › Units › General, #5820; previously minor: interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; numeric fields step on wheel hover without taking keyboard focus, #5949; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen; Selection QoL: marquee modes and visible feedback, #5313; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; Drop Shadow and glow dialogs: spinner arrows, #5367, #5368; Stroke Weight Up and Down shortcuts, #5797; Layers: Rename (`ui.rename`) opens the name field of the selected layer, group or object, #5324; Properties in Edit Artboards: Transform section above the Artboards section, #5219; scrubby labels: the Control bar's Stroke: and Opacity: links and the Properties panel's Stroke link scrub their fields, #4088; Properties: Quick Actions has Repeat Options for one selected repeat, #5181; Direct Selection: Shift-click picks several blend spine points, which drag together, #5285; Direct Selection: Cmd/Ctrl held lends the Selection tool and shows its bounding box, #3901; Join tool trims two crossing open paths and joins them at the crossing, #5844; Control bar: Paragraph: link and Align left/center/right while type is selected, #5372; shape tools: a press on the selection's centre point moves it and one in the rotate zone outside a box corner turns it, #5477; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; Units › East Asian Type acts, with Q and Ha units; scores unchanged)
>
> Vector Halftone preview update (2026-11-10): temporary dot selection marks are hidden, and the bounding box stays available (#6369).
>
@@ -438,6 +438,7 @@ next unit in the order a right-click on a ruler lists them, from Feet back to Po
| Tabs panel: a stop drags from where the press landed, a stop just added included | done | #1458 |
| Outline view keeps template layers in Preview, dimmed | done | #1486; a picture placed to be traced stays visible under the outlines |
| Outline view draws on a white canvas, the artboards as their edges | done | Outline shows only the paths ([Outline mode](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/preview-artwork-as-outlines.html)). VectorCraft's own rule for the canvas: in every interface brightness and Canvas Color the whole canvas is white, pasteboard included; the artboards draw no paper (no Simulate Colored Paper or artboard color, and no paper with Hide Artboards); the artboard names and the ruler origin's crosshair are dark gray; split views and floating windows follow their own view. Tests `outline_draws_on_a_white_canvas_with_artboards_as_edges`, `outline_draws_no_colored_paper`, `a_pane_draws_its_own_views_outline`, `the_ruler_origin_crosshair_shows_on_the_outline_canvas` |
| Outline view: a click selects by the outlines drawn, so it reaches a path behind an image (picked on its frame, unless Show Images in Outline Mode draws its pixels) or a gradient mesh (picked on its mesh lines) | done | #6437; in Outline mode the path is selected directly ([Object selection methods and preferences](https://helpx.adobe.com/illustrator/desktop/manage-objects/select-objects/object-selection-methods-and-preferences.html), Object Selection by Path Only). Type, symbol instances, blends, envelopes and placed documents are still picked within their bounds. Tests `outline_mode_picks_paths_through_images_and_meshes`, `outline_view_click_through_an_image_selects_the_path_behind` |
| Arrange Tools add-on (VectorCraft addition): Object › Plug-ins › Arrange Tools runs Arrange in Grid… (grid by row, one row or one column; columns, H and V spacing, Center on Artboard; Preview), Swap Objects, Swap Objects and Sizes, Replace with Top Object… (Actual Size, Bounds or Bounds (Fit Proportionally); Keep Original; Scale Strokes & Effects; Preview), Set Size of Each… (Width and Height each ticked on or off, Constrain Width and Height Proportions with one side, the reference point, Scale Strokes & Effects; Preview) and Zoom to Selection, each one undo step | done | `addon.arrange.*`, `crates/engine/src/cmd/addons/arrange.rs`, `crates/ui-egui/src/addons/arrange.rs` and their tests; not a feature of the reference app, so it is not scored |
| Arrange Tools add-on (VectorCraft addition): Object › Plug-ins › Arrange Tools › Group Overlapping Objects groups the selected objects whose visual bounds overlap or touch (chains included, within one layer or group), each group at its frontmost member's stacking place, one undo step | done | #4928; `addon.arrange.groupOverlapping`, `crates/engine/src/cmd/addons/arrange.rs` and its tests; not a feature of the reference app, so it is not scored |
| Arrange Tools add-on (VectorCraft addition): Object › Plug-ins › Arrange Tools › Randomize Opacity… gives each selected object its own opacity at random between Minimum and Maximum (%), with a Seed that repeats it and Preview, one undo step | done | #4925; `addon.arrange.randomizeOpacity`, `crates/engine/src/cmd/addons/arrange.rs`, `crates/ui-egui/src/addons/arrange.rs` and their tests; not a feature of the reference app, so it is not scored |
@@ -507,6 +508,7 @@ separate. Scores and hours unchanged. [Behavior and command details](development
| Date | Change | Summary |
|---|---|---|
| 2026-11-11 | minor | Outline view: a click on a path's outline selects it through an image or a gradient mesh in front, which Outline draws as a frame or mesh lines (#6437). |
| 2026-11-10 | minor | Symbol libraries: Inventory Icons joins Basic Shapes, UI Frames and Map Markers as a fourth built-in library (Sword, Shield, Potion, Key, Gem; no default 9-slice). |
| 2026-11-11 | minor | Files that record no units (PDF, EPS or `.ai` without ruler units, raster images, metafiles, Affinity files, importer plug-ins) open and revert in Preferences › Units › General instead of points; native, SVG and DXF files and `.ai`/EPS ruler units are kept (#5820). Scores and estimates unchanged. |
| 2026-11-10 | minor | Held numeric spinner arrows keep stepping past the click timeout and stop on release (#6307). Scores and estimates unchanged. |