Shape Builder: erasing near the end of a thin region picks the region

A point 0.1 pt from the end of a 1.5 pt bar is nearest the end of the
bar, and the line from there through the point runs along the bar, so
the region did not count as narrow and an Alt-click there touched the
end edge.

A region now counts as narrow at a point when it is no more than twice
the tolerance across from any outline segment within the tolerance whose
point square to the line to the point lies inside the segment, as well
as from the nearest point of the outline. Segment ends other than the
nearest point are not tried, so a point added on an edge does not change
the result near a corner.
This commit is contained in:
dotCooCoo
2026-10-26 09:09:44 +07:00
committed by bflatastic
parent b92f394c0b
commit 62e562058b
+53 -9
View File
@@ -457,18 +457,37 @@ impl BuilderMap {
}
}
/// Is region `r` no more than `2 * tol` across at `p`, measured from the point of its outline
/// nearest `p`, through `p`, to its outline on the other side? Every point on that line is
/// then within `tol` of the outline. False when `p` is on the outline or more than `tol` from
/// it.
/// Is region `r` no more than `2 * tol` across at `p`, measured from a point of its outline
/// within `tol` of `p`, through `p`, to its outline on the other side? Every point on that line
/// is then within `tol` of the outline. The points tried are the point of the outline nearest
/// `p` and each point inside a segment where the segment is square to the line to `p`, so a
/// point near the end of a thin bar measures across the bar from its sides as well as along it
/// from the end, and splitting a segment in two does not change the answer. False when `p` is
/// on the outline or more than `tol` from it.
fn narrow_at(&self, r: usize, p: Point, tol: f64) -> bool {
let Some((reg, bp, _)) = self.regions.get(r) else { return false };
let Some((.., n, d)) = reg.path.nearest(p) else { return false };
if !(d.is_finite() && d > 1e-9 && d <= tol) {
use kurbo::{ParamCurve as _, ParamCurveNearest as _};
let Some((_, bp, _)) = self.regions.get(r) else { return false };
// Each segment with the parameter, point and distance of its point nearest `p`.
let near: Vec<(kurbo::PathSeg, f64, Point, f64)> = bp
.segments()
.map(|seg| {
let n = seg.nearest(p, 1e-6);
(seg, n.t, seg.eval(n.t), n.distance_sq.sqrt())
})
.collect();
if !near.iter().all(|&(.., d)| d.is_finite() && d > 1e-9) {
return false;
}
let line = kurbo::Line::new(p, p + (p - n) * ((2.0 * tol - d) / d));
bp.segments().any(|seg| seg.intersect_line(line).iter().any(|x| x.line_t > 1e-9 && x.line_t <= 1.0))
let closest = near.iter().map(|&(.., d)| d).fold(f64::INFINITY, f64::min);
// A line from `p` no longer than `2 * tol` only meets segments that close to `p`.
let reach: Vec<kurbo::PathSeg> = near.iter().filter(|&&(.., d)| d <= 2.0 * tol).map(|&(seg, ..)| seg).collect();
// A segment's end is its nearest point when the segment turns away from `p` or stops
// short of the point square to it; only the nearest point of all the outline counts there.
let from = |&&(_, t, _, d): &&(kurbo::PathSeg, f64, Point, f64)| d <= tol && (d == closest || (t > 1e-9 && t < 1.0 - 1e-9));
near.iter().filter(from).any(|&(_, _, n, d)| {
let line = kurbo::Line::new(p, p + (p - n) * ((2.0 * tol - d) / d));
reach.iter().any(|seg| seg.intersect_line(line).iter().any(|x| x.line_t > 1e-9 && x.line_t <= 1.0))
})
}
/// What a gesture along the polyline `pts` touches, in order: [`Self::hit`] at each vertex and
@@ -1996,7 +2015,32 @@ mod tests {
let t = map.builder_hits_along(&[Point::new(20.0, 50.0), Point::new(80.0, 50.0)], Some(4.0));
assert!(t.regions.contains(&inner), "{rs:?}: {t:?}");
assert_eq!(t.regions.len(), rs.len(), "{rs:?}: {t:?}");
// 0.1 pt from an end, the nearest outline is the end, and the bar is still 1.5 pt
// across from its sides: a click there and a drag across the bar there touch it.
assert_eq!(map.builder_hit(Point::new(49.75, 10.1), Some(4.0)), Some(Hit::Region(inner)), "{rs:?}");
assert_eq!(map.builder_hit(Point::new(49.75, 89.9), Some(4.0)), Some(Hit::Region(inner)), "{rs:?}");
let t = map.builder_hits_along(&[Point::new(20.0, 10.1), Point::new(80.0, 10.1)], Some(4.0));
assert!(t.regions.contains(&inner), "{rs:?}: {t:?}");
}
// Near a corner of the square, both of its edges there are close, and the square is wide
// across from each: the edge is touched.
let (d, s) = rects(&[Rect::new(0.0, 0.0, 100.0, 100.0), bar]);
let map = map_of(&d, &s);
assert!(matches!(map.builder_hit(Point::new(1.0, 1.0), Some(4.0)), Some(Hit::Edge(_))));
// So it is when the square's top edge has a point 2 pt from the corner: the end of the
// segment past that point is not where the square is narrow.
let mut d = Document::new(500.0, 500.0);
let l = d.layers[0].id;
let id = d.alloc_id();
let pts = [(0.0, 0.0), (2.0, 0.0), (100.0, 0.0), (100.0, 100.0), (0.0, 100.0)].map(|(x, y)| Point::new(x, y));
let path = PathData::new(vec![SubPath::polyline(&pts, true)]);
d.insert(Some(l), 0, Node::path(id, path, Appearance::default_art())).unwrap();
let mut s = Selection::default();
s.add(id);
let map = map_of(&d, &s);
let square = map.region_at(Point::new(50.0, 50.0)).unwrap();
assert_eq!(map.regions[square].1.segments().count(), 5, "the point on the top edge stays");
assert!(matches!(map.builder_hit(Point::new(1.0, 1.0), Some(4.0)), Some(Hit::Edge(_))));
}
#[test]