G1: loaded text, double-clicks and guide release take what is drawn

The selection tools' clipping-path outline hit was also used where the
Selection and Direct Selection tools act on what is drawn rather than
select. Near a layer's clipping mask edge, with art drawn over it:
loaded text threaded into the unpainted mask instead of the art, a
double-click on type entered isolation on the mask instead of editing
the type (Selection) or did nothing (Direct Selection), and Cmd/Ctrl+
Shift double-click could not release a guide path there. These now
take the ordinary hit (ToolContext::hit_options); a press or hover
that selects keeps the outline hit.

Tests (each fails with the selection hit options):
select::tests::loaded_text_threads_into_the_art_on_a_layer_masks_edge,
a_double_click_on_type_at_a_layer_masks_edge_edits_it (Selection and
Direct Selection) and a_guide_on_a_layer_masks_edge_is_released, over
the new testutil helpers add_masked_layer and
doc_with_type_on_a_layer_masks_edge. Recorded in
docs/target-app-parity.md.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Drew Dibble
2026-10-21 15:44:27 +07:00
committed by bflatastic
co-authored by Claude Opus 5.5
parent 89b749b828
commit 5fd553cc8b
5 changed files with 105 additions and 10 deletions
+12 -4
View File
@@ -486,10 +486,8 @@ impl Tool for DirectSelectionTool {
if crate::rulerguide::guide_at(cx, p).is_some() {
return vec![];
}
if hit_test(cx.doc, p, cx.selection_hit_options())
.and_then(|h| cx.doc.node(h.leaf))
.is_some_and(|n| matches!(n.kind, NodeKind::Text(_)))
{
// A double-click edits what is drawn: not a clipping path's outline.
if hit_test(cx.doc, p, cx.hit_options()).and_then(|h| cx.doc.node(h.leaf)).is_some_and(|n| matches!(n.kind, NodeKind::Text(_))) {
return vec![Action::SwitchTool("type".into())];
}
vec![]
@@ -2042,6 +2040,16 @@ mod tests {
}
}
/// Direct Selection's double-click on type where a layer's clipping mask edge runs through it
/// edits the type, though a press there takes the mask's segment.
#[test]
fn a_double_click_on_type_at_a_layer_masks_edge_edits_it() {
let (d, _, at) = doc_with_type_on_a_layer_masks_edge();
let (s, p) = (Selection::default(), paint());
let acts = DirectSelectionTool::new(false).pointer(&cx(&d, &s, &p), &PointerEvent::new(PointerKind::DoubleClick, at.x, at.y));
assert_eq!(acts, vec![Action::SwitchTool("type".into())]);
}
#[test]
fn double_clicking_type_switches_to_type_tool() {
use std::sync::Arc;
+29
View File
@@ -419,6 +419,8 @@ impl ToolContext<'_> {
/// [`Self::hit_options`] for the selection tools (Selection, Direct Selection, Group
/// Selection): a clipping path is also hit on its outline ([`HitOptions::clip_outlines`]).
/// Only for what a press or a hover selects: a double-click, loaded text or a guide's release
/// acts on what is drawn and takes [`Self::hit_options`].
///
/// [`HitOptions::clip_outlines`]: vectorcraft_doc::hit::HitOptions::clip_outlines
pub fn selection_hit_options(&self) -> vectorcraft_doc::hit::HitOptions {
@@ -887,6 +889,33 @@ pub(crate) mod testutil {
(d, g, clip, art)
}
/// A new layer of `d`, above the others, with the unpainted rectangle `mask` as its clipping
/// mask and `art` clipped by it: the layer and the clipping mask.
pub fn add_masked_layer(d: &mut Document, mask: Rect, art: Node) -> (NodeId, NodeId) {
let l = d.add_layer(Some("Masked"));
let clip = d.alloc_id();
let mut path = Node::path(clip, shapes::rectangle(mask), Default::default());
if let vectorcraft_doc::NodeKind::Path { clipping, .. } = &mut path.kind {
*clipping = true;
}
d.insert(Some(l), 0, path).unwrap();
d.insert(Some(l), 1, art).unwrap();
d.node_mut(l).unwrap().set_clips(true);
(l, clip)
}
/// A layer clipped to (250, 250)–(360, 400), its mask's right edge through 120 × 40 area type
/// at (300, 300): the type, and a point on it just inside the mask's edge, within the
/// selection tolerance, where the selection tools take the mask.
pub fn doc_with_type_on_a_layer_masks_edge() -> (Document, NodeId, Point) {
let (mut d, _) = doc_with_rect();
let id = d.alloc_id();
let mut t = vectorcraft_doc::TextObject::point(Point::new(300.0, 300.0), "Some words", Default::default());
t.kind = vectorcraft_doc::TextKind::Area { frame: shapes::rectangle(Rect::new(0.0, 0.0, 120.0, 40.0)) };
add_masked_layer(&mut d, Rect::new(250.0, 250.0, 360.0, 400.0), Node::new(id, vectorcraft_doc::NodeKind::Text(Box::new(t))));
(d, id, Point::new(358.5, 320.0))
}
/// [`doc_with_rect`] plus 120 × 40 area type at (300, 300).
pub fn doc_with_area_type() -> (Document, NodeId) {
let (mut d, _) = doc_with_rect();
+62 -5
View File
@@ -372,9 +372,9 @@ fn thread_params(cx: &ToolContext, from: NodeId, start: Point, end: Point) -> Va
let frame = |n: &vectorcraft_doc::Node| {
is_area_type(n) || matches!(&n.kind, vectorcraft_doc::NodeKind::Path { path, .. } if path.subpaths.first().is_some_and(|s| s.closed))
};
let to = hit_test(cx.doc, start, cx.selection_hit_options())
.map(|h| h.top_object(cx.isolation))
.filter(|id| *id != from && cx.doc.node(*id).is_some_and(frame));
// The art the text flows into: what is drawn, not a clipping path's outline.
let to =
hit_test(cx.doc, start, cx.hit_options()).map(|h| h.top_object(cx.isolation)).filter(|id| *id != from && cx.doc.node(*id).is_some_and(frame));
match to {
Some(to) => json!({ "from": from.0, "to": to.0 }),
None => json!({ "from": from.0, "at": [start.x, start.y] }),
@@ -489,7 +489,8 @@ impl Tool for SelectionTool {
if crate::rulerguide::guide_at(cx, p).is_some() {
return vec![];
}
if let Some(h) = hit_test(cx.doc, p, cx.selection_hit_options()) {
// A double-click edits what is drawn: not a clipping path's outline.
if let Some(h) = hit_test(cx.doc, p, cx.hit_options()) {
let top = h.top_object(cx.isolation);
if cx.doc.node(top).is_some_and(|n| matches!(n.kind, vectorcraft_doc::NodeKind::Text(_))) {
return vec![Action::SwitchTool("type".into())];
@@ -845,7 +846,7 @@ pub(crate) fn release_guide_at(cx: &ToolContext, p: Point, m: Mods) -> Option<Ac
if let Some(i) = crate::rulerguide::guide_at(cx, p) {
return Some(Action::Exec("view.guides.release".into(), json!({ "index": i })));
}
let leaf = hit_test(cx.doc, p, cx.selection_hit_options())?.leaf;
let leaf = hit_test(cx.doc, p, cx.hit_options())?.leaf;
let guide = cx.doc.node(leaf).is_some_and(|n| matches!(n.kind, vectorcraft_doc::NodeKind::Path { guide: true, .. }));
guide.then(|| Action::Exec("view.guides.release".into(), json!({ "ids": [leaf.0] })))
}
@@ -2088,6 +2089,62 @@ mod tests {
assert_eq!(t.pointer(&cx, &ev(PointerKind::Down, corner.x, corner.y)), vec![Action::Begin("Resize Type Area".into())]);
}
/// Loaded text clicked just inside a layer's clipping mask, where art is drawn over the mask's
/// edge, threads into that art, not into the unpainted mask: placing text acts on what is drawn,
/// so it takes the ordinary hit, not the selection tools' clipping-path outline.
#[test]
fn loaded_text_threads_into_the_art_on_a_layer_masks_edge() {
let (mut d, id, _, port) = overflowing_area_type();
let art = d.alloc_id();
let picture = vectorcraft_doc::Node::path(
art,
vectorcraft_geom::shapes::rectangle(Rect::new(350.0, 350.0, 550.0, 550.0)),
vectorcraft_doc::Appearance::default_art(),
);
add_masked_layer(&mut d, Rect::new(400.0, 400.0, 500.0, 500.0), picture);
let mut s = Selection::default();
s.set([id]);
let p = paint();
let cx = cx(&d, &s, &p);
assert!(cx.selection_hit_options().clip_outlines, "the selection tools would take the mask here");
let mut t = SelectionTool::default();
t.pointer(&cx, &ev(PointerKind::Down, port.x, port.y));
t.pointer(&cx, &ev(PointerKind::Up, port.x, port.y));
t.pointer(&cx, &ev(PointerKind::Down, 498.5, 450.0));
assert_eq!(
t.pointer(&cx, &ev(PointerKind::Up, 498.5, 450.0)),
vec![Action::Exec("text.thread.add".into(), json!({"from": id.0, "to": art.0}))]
);
}
/// A double-click on type where a layer's clipping mask edge runs through it edits the type,
/// though a press there selects the mask: what a double-click edits is what is drawn.
#[test]
fn a_double_click_on_type_at_a_layer_masks_edge_edits_it() {
let (d, _, at) = doc_with_type_on_a_layer_masks_edge();
let (s, p) = (Selection::default(), paint());
let a = SelectionTool::default().pointer(&cx(&d, &s, &p), &ev(PointerKind::DoubleClick, at.x, at.y));
assert_eq!(a, vec![Action::SwitchTool("type".into())]);
}
/// A Cmd/Ctrl+Shift double-click releases a guide path where it crosses a layer's clipping
/// mask edge (its left side, x = 450, just inside the mask's bottom): the mask, unpainted and
/// never a guide, doesn't stand in front of it.
#[test]
fn a_guide_on_a_layer_masks_edge_is_released() {
let (mut d, _) = doc_with_rect();
let id = d.alloc_id();
let mut guide_path =
vectorcraft_doc::Node::path(id, vectorcraft_geom::shapes::rectangle(Rect::new(450.0, 350.0, 550.0, 550.0)), Default::default());
let vectorcraft_doc::NodeKind::Path { guide, .. } = &mut guide_path.kind else { panic!("a path") };
*guide = true;
add_masked_layer(&mut d, Rect::new(400.0, 400.0, 500.0, 500.0), guide_path);
let (s, p) = (Selection::default(), paint());
let both = Mods { cmd: true, shift: true, ..Mods::default() };
let a = SelectionTool::default().pointer(&cx(&d, &s, &p), &ev(PointerKind::DoubleClick, 450.0, 498.5).with_mods(both));
assert_eq!(a, vec![Action::Exec("view.guides.release".into(), json!({"ids": [id.0]}))]);
}
/// Escape drops the loaded text, and the next click is an ordinary one; type that fits its
/// frame shows no out port to click.
#[test]
+1
View File
@@ -2022,6 +2022,7 @@ fn hit_with<T>(
type_path_only: prefs.type_selection_by_path_only,
scope: app.session.active().and_then(|st| st.isolation),
skip_guides: !app.ui.view.guides,
..Default::default()
};
test(&app.session.active()?.doc, p, opt)
}
+1 -1
View File
@@ -404,7 +404,7 @@ since 2026-10-01.
- **Selected Mirror Repeat handles:** a headless canvas regression creates the default mirror repeat and drags its Top bounding-box handle where the axis crosses it; the repeat grows vertically while its width stays fixed. Existing mirror-axis checks cover center movement and end-handle hit priority. The focused UI mirror tests pass. Scores and hours unchanged.
- **Warp Options (#1760):** Effect > Warp and existing Appearance effect links share the fifteen existing styles, original schematic previews, orientation choices and percent Bend/Distortion controls. Focused tests passed for real numeric entry and slider input, Preview/Cancel, immediate Enter/OK, mixed-style edits, target/slot/visibility preservation and Undo/Redo. Each applied style stays unchanged until an explicit Style choice. Original style-menu and live-preview captures were reviewed; their numeric values were supplied programmatically. Public references: [Reshaping with effects](https://helpx.adobe.com/illustrator/using/reshaping-objects-effects.html) and [Using effects](https://helpx.adobe.com/illustrator/using/effects.html). Warp geometry, optional Properties integration and remembered defaults are unchanged; no broader parity claim.
- **Clipping paths on their outline (G1):** the selection tools (Selection, Direct Selection, Group Selection), Object Highlighting and a right-click take a clipping path on its outline before the art it clips; Select Behind lists the same; the eyedropper and the tools that act on what is drawn take the art. Doc and tool regression tests; score unchanged.
- **Clipping paths on their outline (G1):** the selection tools (Selection, Direct Selection, Group Selection), Object Highlighting and a right-click take a clipping path on its outline before the art it clips; Select Behind lists the same; the eyedropper, the tools that act on what is drawn and the selection tools' double-clicks, loaded text and guide release take the art. Doc and tool regression tests; score unchanged.
- **Existing-guide Escape cancellation (G1):** Escape clears a pending guide press or drag through Selection and Direct/Group Selection, cancelling a begun preview. Eight app-input regressions cover both axes, move/copy, temporary selection-tool borrowing from idle Pen, preselected-guide document/interaction-start selection and Undo/Redo/saved-state preservation, later on-canvas motion/release, Shift-press cancellation, and ordinary-release/Enter/idle controls. Historical focused checks on `8ab574b7` recorded five expected RED failures with three passing controls, then all eight GREEN tests and the Direct Selection Escape control passed. All nine also passed in current full CI. A full CI retry on the five-unit combined source at `a0671777` passed all seven stages with 6,613 test outcomes on 2026-10-14. An earlier full-CI run on the identical source panicked in the randomized `ai_mutated_save_never_panics` test; the cause remains unresolved. This five-fix batch does not claim to repair that failure. The original 1280×960 headless app capture from `b076025a` was reviewed for one vertical Selection guide after Escape; source review found its scene unchanged on `a0671777`. Scores and hours unchanged.
- **TIFF in screen and asset export:** both format menus expose the existing TIFF encoder and Format Settings. Headless UI regressions choose TIFF and check scaled dimensions, artboard selection, filenames, encoding options, saved settings and collected-asset output. Public reference: [Export for Screens](https://helpx.adobe.com/illustrator/desktop/save-and-export/export-files-to-different-formats/export-for-screens.html), updated 2026-09-28, lists TIFF for both artboards and assets. Adobe staff [confirmed support in 30.2.1](https://illustrator.uservoice.com/forums/333657-illustrator-desktop-feature-requests/suggestions/49081034-add-tiff-format-into-export-for-screens-assets-d) on 2026-02-19; the older 2025 Collect assets guide omits TIFF.
- **Export for Screens of the selected artboards:** while the Artboard tool is in use, File › Export › Export for Screens… (`file.exportForScreens`) opens with the artboards the tool has selected checked on its Artboards tab, and Export Artboard writes only those. Shift-clicks on the canvas, or Shift- or Ctrl/Cmd-clicks on the Artboards panel's rows, select several. The other fields show the settings the document last exported with. With another tool in use, the dialog takes its artboard choice from those settings too. Public reference: [Export selected artboards](https://helpx.adobe.com/illustrator/desktop/create-manage-artboards/organize-manage-artboards/export-artboards.html).