Merge fix/713

This commit is contained in:
bflatastic
2026-10-17 15:20:35 +07:00
2 changed files with 52 additions and 2 deletions
+8 -2
View File
@@ -51,6 +51,10 @@ pub fn is_closed(n: &Node) -> bool {
}
}
/// How much further than `tol` a stroke with nothing filled reaches: a thin line or stroke-only path
/// is hard to land on with the plain tolerance, so it gets a wider band (5 screen px for the usual 2 px tolerance).
const STROKE_REACH: f64 = 2.5;
/// Whether `pt` hits `n` itself; in `outline` mode (nothing filled on the canvas) a shape only where
/// its outline is.
fn hits_node_itself(doc: &Document, id: NodeId, n: &Node, pt: Point, tol: f64, outline: bool) -> bool {
@@ -62,7 +66,7 @@ fn hits_node_itself(doc: &Document, id: NodeId, n: &Node, pt: Point, tol: f64, o
NodeKind::Group | NodeKind::BooleanOp { .. } | NodeKind::Page { .. } => false,
NodeKind::Text(_) | NodeKind::Slice => n.local_rect().inflate(tol_l, tol_l).contains(lp),
NodeKind::Line => {
let half = (n.stroke.weight / 2.0).max(tol_l);
let half = (n.stroke.weight / 2.0).max(tol_l * STROKE_REACH);
lp.x >= -half && lp.x <= n.width + half && lp.y.abs() <= half
}
NodeKind::Frame | NodeKind::Component { .. } | NodeKind::ComponentSet { .. } | NodeKind::Instance { .. } | NodeKind::Section { .. } => {
@@ -78,7 +82,9 @@ fn hits_path(n: &Node, path: &BezPath, lp: Point, tol: f64, inside: bool) -> boo
if inside && (n.fills.iter().any(|f| f.visible) || !n.has_visible_stroke()) {
return true;
}
let reach = (n.stroke.max_weight() / 2.0).max(tol);
// Only a stroke with nothing filled gets the wider band: a filled shape's edge keeps the plain one.
let stroke_only = n.has_visible_stroke() && !n.fills.iter().any(|f| f.visible);
let reach = (n.stroke.max_weight() / 2.0).max(if stroke_only { tol * STROKE_REACH } else { tol });
if !path.bounding_box().inflate(reach, reach).contains(lp) {
return false;
}
@@ -133,6 +133,50 @@ fn entering_and_leaving_vector_edit_mode() {
assert!(matches!(s.doc().unwrap().node(id).unwrap().kind, NodeKind::Rectangle), "converting is one undo step");
}
#[test]
fn thin_strokes_are_easy_to_enter_and_bend() {
use crate::tools::ToolId;
// A diagonal line from (100, 100) to (200, 50); `off` steps away from its middle, square to it.
let at = |off: f64| (150.0 + off * 50.0 / 125f64.sqrt() / 10.0, 75.0 + off * 100.0 / 125f64.sqrt() / 10.0);
// (layer, screen px off the stroke, in edit mode after a double-click, derivation)
let rows: Vec<(&str, f64, bool, &str)> = vec![
("line", 0.0, true, "double-clicking a selected line edits it as a vector"),
("line", 4.0, true, "a 1 px line reaches a few pixels, so a double-click near it still lands (#646)"),
("line", 8.0, false, "a double-click well away from it does not"),
("path", 4.0, true, "a stroke-only path reaches as far as a line does"),
("path", 8.0, false, "and no further"),
];
for (layer, off, after, why) in rows {
let mut s = Session::new();
let id = if layer == "line" {
s.set_tool(ToolId::Line);
drag(&mut s, (100.0, 100.0), (200.0, 50.0), NONE);
*s.selection().first().unwrap()
} else {
let id = id_of(&ex(&mut s, "vector.create", json!({"points": [[100, 100], [200, 50]], "closed": false}))["id"]).unwrap();
// Styled like a pen stroke: 1 px, no fill.
ex(&mut s, "node.set", json!({"ids": [id], "props": {"fills": [], "strokes": ["#000000"], "strokeWeight": 1}}));
s.set_selection(vec![id]);
id
};
assert!(!s.doc().unwrap().node(id).unwrap().fills.iter().any(|f| f.visible), "{why}: the {layer} only has a stroke");
let p = at(off);
click(&mut s, p, NONE);
s.pointer(&ev(PointerKind::Down, p, NONE)).unwrap();
s.pointer(&ev(PointerKind::DoubleClick, p, NONE)).unwrap();
s.pointer(&ev(PointerKind::Up, p, NONE)).unwrap();
assert_eq!(editing(&s).is_some(), after, "{why}");
if after {
// Cmd/Ctrl then bends it through the pointer.
drag(&mut s, at(0.0), (170.0, 115.0), Mods { cmd: true, ..NONE });
let w = s.doc().unwrap().world_transform(id);
let n = net(&s, id);
assert!(!n.segments[0].is_straight(), "{why}: Cmd/Ctrl bends it");
assert!((w * n.cubic(0).unwrap().eval(0.5) - Point::new(170.0, 115.0)).hypot() < 1.0, "{why}: through the pointer");
}
}
}
#[test]
fn the_pen_follows_the_documented_rules() {
let mut s = Session::new();