mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-02 22:20:50 -06:00
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:
committed by
bflatastic
co-authored by
Claude Opus 5.5
parent
89b749b828
commit
5fd553cc8b
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user