mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Live Paint: the Live Paint tools skip hidden edges
An edge hidden on its own (for example with Object > Hide) while its Live Paint group stays visible could still be highlighted, selected or stroked, and a hidden edge nearer the pointer kept a visible one from being picked. lp_part_at now takes the nearest visible edge (visible_edge_near), inside and outside the faces, for the hover, the Live Paint Selection click and the Bucket's Shift-click. The Bucket's Shift-click sends nothing where no edge is highlighted, and livePaint.strokeEdge also skips hidden edges, so the Bucket strokes the edge it highlights.
This commit is contained in:
@@ -766,7 +766,7 @@ fn lp_stroke_edge(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let tol = f64_or(p, "tolerance", 4.0).clamp(0.0, 1000.0);
|
||||
let edge = s.edit("Live Paint Bucket", |d, _| {
|
||||
let n = d.node(g).filter(|n| is_live_paint(n)).ok_or_else(|| bad(C, format!("{g} is not a Live Paint group")))?;
|
||||
let eid = edge_near(n, pt, tol).map(|e| e.id).ok_or_else(|| EngineError::Other("no Live Paint edge near that point".into()))?;
|
||||
let eid = b::visible_edge_near(n, pt, tol).map(|e| e.id).ok_or_else(|| EngineError::Other("no Live Paint edge near that point".into()))?;
|
||||
let e = d.node_mut(eid).ok_or(EngineError::NoNode(eid))?;
|
||||
e.appearance.set_stroke(paint.clone());
|
||||
if let Some(st) = e.appearance.stroke_mut() {
|
||||
|
||||
@@ -531,6 +531,28 @@ fn live_paint_stroke_edge() {
|
||||
assert!(s.execute("livePaint.strokeEdge", &json!({"group": g.0, "point": [300, 300]})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn live_paint_stroke_edge_skips_hidden_edges() {
|
||||
let mut s = session();
|
||||
let g = live(&mut s);
|
||||
// The vertical edge at x=100 is hidden on its own; the group stays visible.
|
||||
let vertical_at = |s: &Session, x: f64| -> Vec<u64> {
|
||||
vectorcraft_tools::builder::edges(&node(s, g))
|
||||
.iter()
|
||||
.filter(|e| e.geometric_bounds().is_some_and(|bb| (bb.x0 - x).abs() < 1e-6 && (bb.x1 - x).abs() < 1e-6))
|
||||
.map(|e| e.id.0)
|
||||
.collect()
|
||||
};
|
||||
let hidden = vertical_at(&s, 100.0);
|
||||
assert!(!hidden.is_empty());
|
||||
s.execute("object.setProps", &json!({"ids": hidden, "visible": false})).unwrap();
|
||||
// 1 pt from the hidden edge, the nearest visible edge within the tolerance is the one at x=50.
|
||||
let r = s.execute("livePaint.strokeEdge", &json!({"group": g.0, "point": [99, 50], "tolerance": 60})).unwrap();
|
||||
assert!(vertical_at(&s, 50.0).contains(&r["edge"].as_u64().unwrap()), "{r}");
|
||||
// With only the hidden edge within the tolerance, no edge is stroked.
|
||||
assert!(s.execute("livePaint.strokeEdge", &json!({"group": g.0, "point": [99, 50]})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn live_paint_release_and_expand() {
|
||||
let mut s = session();
|
||||
|
||||
@@ -76,8 +76,18 @@ pub fn face_at(group: &Node, p: Point) -> Option<&Arc<Node>> {
|
||||
|
||||
/// The edge of `group` nearest to `p` within `tol`.
|
||||
pub fn edge_near(group: &Node, p: Point, tol: f64) -> Option<&Arc<Node>> {
|
||||
nearest_edge(edges(group), p, tol)
|
||||
}
|
||||
|
||||
/// The edge of `group` nearest to `p` within `tol` among its edges that are not hidden.
|
||||
pub fn visible_edge_near(group: &Node, p: Point, tol: f64) -> Option<&Arc<Node>> {
|
||||
nearest_edge(edges(group).into_iter().filter(|e| e.visible), p, tol)
|
||||
}
|
||||
|
||||
/// The edge of `edges` nearest to `p` within `tol`.
|
||||
fn nearest_edge<'a>(edges: impl IntoIterator<Item = &'a Arc<Node>>, p: Point, tol: f64) -> Option<&'a Arc<Node>> {
|
||||
let mut best: Option<(f64, &Arc<Node>)> = None;
|
||||
for e in edges(group) {
|
||||
for e in edges {
|
||||
if let Some((.., d)) = e.path_data().and_then(|pd| pd.nearest(p))
|
||||
&& d <= tol
|
||||
&& best.is_none_or(|b| d < b.0)
|
||||
@@ -604,12 +614,12 @@ enum LpHover {
|
||||
|
||||
/// The Live Paint group and part the Live Paint tools hover and click at `p`. Inside a face of a
|
||||
/// visible Live Paint group ([`live_paint_at`]) it is that group with its face under `p`, or with
|
||||
/// `edge` its edge nearest `p` within `tol` (None when there is none). Outside every face, with
|
||||
/// `edge`, it is the edge nearest `p` within `tol` among the visible Live Paint groups (the
|
||||
/// front-most group's on a tie). None when no group is found.
|
||||
/// `edge` its visible edge nearest `p` within `tol` (None when there is none). Outside every face,
|
||||
/// with `edge`, it is the visible edge nearest `p` within `tol` among the visible Live Paint groups
|
||||
/// (the front-most group's on a tie). None when no group is found.
|
||||
fn lp_part_at(doc: &Document, p: Point, tol: f64, edge: bool) -> Option<(NodeId, Option<&Arc<Node>>)> {
|
||||
if let Some(g) = live_paint_at(doc, p).and_then(|id| doc.node(id)) {
|
||||
return Some((g.id, if edge { edge_near(g, p, tol) } else { face_at(g, p) }));
|
||||
return Some((g.id, if edge { visible_edge_near(g, p, tol) } else { face_at(g, p) }));
|
||||
}
|
||||
if !edge {
|
||||
return None;
|
||||
@@ -617,7 +627,7 @@ fn lp_part_at(doc: &Document, p: Point, tol: f64, edge: bool) -> Option<(NodeId,
|
||||
let mut best: Option<(f64, NodeId, &Arc<Node>)> = None;
|
||||
doc.walk(|n| {
|
||||
if is_live_paint(n)
|
||||
&& let Some(e) = edge_near(n, p, tol)
|
||||
&& let Some(e) = visible_edge_near(n, p, tol)
|
||||
&& let Some((.., d)) = e.path_data().and_then(|pd| pd.nearest(p))
|
||||
&& best.is_none_or(|b| d <= b.0)
|
||||
&& doc.is_visible(n.id)
|
||||
@@ -681,10 +691,10 @@ impl Tool for LivePaintBucketTool {
|
||||
let point = json!([p.x, p.y]);
|
||||
if ev.mods.shift {
|
||||
return match lp_part_at(cx.doc, p, cx.tol(4.0), true) {
|
||||
Some((g, _)) => {
|
||||
Some((g, Some(_))) => {
|
||||
vec![Action::Exec("livePaint.strokeEdge".into(), json!({ "group": g.0, "point": point, "tolerance": cx.tol(4.0) }))]
|
||||
}
|
||||
None => vec![],
|
||||
_ => vec![],
|
||||
};
|
||||
}
|
||||
if let Some(g) = live_paint_at(cx.doc, p) {
|
||||
@@ -1201,6 +1211,44 @@ mod tests {
|
||||
check(&d, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn live_paint_tools_skip_hidden_edges() {
|
||||
let mut d = Document::new(500.0, 500.0);
|
||||
let (_, [_, _, back_right, _]) = live_square(&mut d, Rect::new(0.0, 0.0, 100.0, 100.0));
|
||||
let (front, [front_left, ..]) = live_square(&mut d, Rect::new(104.0, 0.0, 204.0, 100.0));
|
||||
// The back group's right side is hidden on its own; both groups stay visible.
|
||||
d.node_mut(back_right).unwrap().visible = false;
|
||||
let s = Selection::default();
|
||||
let p = paint();
|
||||
let cx = cx(&d, &s, &p);
|
||||
let alt = Mods { alt: true, ..Default::default() };
|
||||
let shift = Mods { shift: true, ..Default::default() };
|
||||
let edge_x = |ov: &[Overlay]| match ov {
|
||||
[] => None,
|
||||
[Overlay::Path { path, .. }] => Some(path.bounding_box().x0),
|
||||
_ => panic!("{ov:?}"),
|
||||
};
|
||||
// Outside the faces, 1.5 pt from the hidden side and 2.5 pt from the front group's left
|
||||
// side, both tools highlight and pick the visible side. Inside the back face, 1.5 pt from
|
||||
// the hidden side, no edge is in reach, so neither tool highlights or picks one.
|
||||
for (x, want) in [(101.5, Some((front, front_left, 104.0))), (98.5, None)] {
|
||||
let mut sel = LivePaintSelectionTool::default();
|
||||
sel.pointer(&cx, &ev(PointerKind::Move, x, 50.0).with_mods(alt));
|
||||
assert_eq!(edge_x(&sel.overlays(&cx)), want.map(|w| w.2), "x {x}");
|
||||
let picked = match want {
|
||||
Some((_, e, _)) => Action::Exec("select.set".into(), json!({ "ids": [e.0] })),
|
||||
None => Action::Exec("select.none".into(), json!({})),
|
||||
};
|
||||
assert_eq!(sel.pointer(&cx, &ev(PointerKind::Down, x, 50.0).with_mods(alt)), vec![picked], "x {x}");
|
||||
let mut bucket = LivePaintBucketTool::default();
|
||||
bucket.pointer(&cx, &ev(PointerKind::Move, x, 50.0).with_mods(shift));
|
||||
assert_eq!(edge_x(&bucket.overlays(&cx)), want.map(|w| w.2), "x {x}");
|
||||
let stroked =
|
||||
want.map(|(g, ..)| Action::Exec("livePaint.strokeEdge".into(), json!({ "group": g.0, "point": [x, 50.0], "tolerance": 4.0 })));
|
||||
assert_eq!(bucket.pointer(&cx, &ev(PointerKind::Down, x, 50.0).with_mods(shift)), stroked.into_iter().collect::<Vec<_>>(), "x {x}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn live_paint_selection_options_set_the_highlight() {
|
||||
let (d, ..) = live_group_doc();
|
||||
|
||||
Reference in New Issue
Block a user