mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Shape Builder: a drag picks every region it crosses, however narrow
A drag tested points spaced by a step that grew with the selection (at least 0.25 pt), and the tool's highlight tested points 2 screen pixels apart. A region narrower than the step, such as a thin overlap, fell between samples: it kept its piece of each source, the merge left strips behind, or the drag failed with "no region under the pointer" while the highlight showed the region. Zoomed out, the highlight could miss a region the merge then took. The drag now finds where each segment crosses a region outline and tests the middle of every stretch between crossings, so every region it crosses is picked. The tool's highlight and shapeBuilder.merge use the same picking, and Shaper's scribble picks the faces it crosses the same way. In erase mode the line pieces and edges are still found within the tolerance, and a segment that crosses one also tests the crossing point. Shaper's scribble found the strokes to erase at points at least 0.5 pt apart, so with a tolerance below 1 pt it could pass over a stroke without erasing it. It now also tests where the scribble crosses each stroke and points tolerance / 2 apart.
This commit is contained in:
@@ -392,6 +392,74 @@ impl BuilderMap {
|
||||
Some((i, run))
|
||||
}
|
||||
|
||||
/// What a gesture along the polyline `pts` touches, in order: [`Self::hit`] at each vertex and
|
||||
/// at the points [`Self::probes`] picks between them, so it touches every region it crosses,
|
||||
/// however narrow.
|
||||
pub fn hits_along(&self, pts: &[Point], piece_tol: Option<f64>) -> Touched {
|
||||
let mut touched = Touched::default();
|
||||
let mut last: Option<Point> = None;
|
||||
for &p in pts {
|
||||
if let Some(a) = last {
|
||||
for q in self.probes(a, p, piece_tol) {
|
||||
touched.add(self.hit(q, piece_tol));
|
||||
}
|
||||
}
|
||||
touched.add(self.hit(p, piece_tol));
|
||||
last = Some(p);
|
||||
}
|
||||
touched
|
||||
}
|
||||
|
||||
/// The points strictly between `a` and `b`, in order, that [`Self::hits_along`] tests: the
|
||||
/// middle of each stretch between two places where the segment crosses a region's outline, and
|
||||
/// with `piece_tol` (erasing) those crossings, the places where it crosses a line piece or an
|
||||
/// edge, and points `piece_tol / 2` apart (at most 4,000 per segment), so the line pieces and
|
||||
/// edges the segment crosses or passes close to are touched too.
|
||||
fn probes(&self, a: Point, b: Point, piece_tol: Option<f64>) -> Vec<Point> {
|
||||
let len = a.distance(b);
|
||||
if !(len.is_finite() && len > 0.0) {
|
||||
return vec![];
|
||||
}
|
||||
let line = kurbo::Line::new(a, b);
|
||||
let lb = Rect::from_points(a, b);
|
||||
let mut cross: Vec<f64> = vec![];
|
||||
for (_, bp, rb) in &self.regions {
|
||||
if rb.x0 > lb.x1 || rb.x1 < lb.x0 || rb.y0 > lb.y1 || rb.y1 < lb.y0 {
|
||||
continue;
|
||||
}
|
||||
for seg in bp.segments() {
|
||||
cross.extend(seg.intersect_line(line).iter().map(|x| x.line_t));
|
||||
}
|
||||
}
|
||||
cross.retain(|t| t.is_finite() && *t > 0.0 && *t < 1.0);
|
||||
cross.sort_by(f64::total_cmp);
|
||||
// Crossings closer than this along the segment are one place.
|
||||
let gap = 1e-9 / len;
|
||||
let mut ts: Vec<f64> = vec![];
|
||||
let mut from = 0.0;
|
||||
for &t in cross.iter().chain([1.0].iter()) {
|
||||
if t - from > gap {
|
||||
ts.push((from + t) / 2.0);
|
||||
from = t;
|
||||
}
|
||||
}
|
||||
if let Some(tol) = piece_tol {
|
||||
ts.extend(cross.iter().copied());
|
||||
for (piece, pb) in self.lines.iter().chain(&self.edges) {
|
||||
if pb.x0 > lb.x1 || pb.x1 < lb.x0 || pb.y0 > lb.y1 || pb.y1 < lb.y0 {
|
||||
continue;
|
||||
}
|
||||
for seg in piece.path.to_bezpath().segments() {
|
||||
ts.extend(seg.intersect_line(line).iter().map(|x| x.line_t).filter(|t| t.is_finite() && *t > 0.0 && *t < 1.0));
|
||||
}
|
||||
}
|
||||
let n = ((len / (tol / 2.0).max(1e-3)).ceil() as usize).min(4000);
|
||||
ts.extend((1..n).map(|k| k as f64 / n as f64));
|
||||
ts.sort_by(f64::total_cmp);
|
||||
}
|
||||
ts.into_iter().map(|t| a.lerp(b, t)).collect()
|
||||
}
|
||||
|
||||
/// The regions just either side of the middle of `piece` (None: outside every region).
|
||||
pub fn sides(&self, piece: &po::Shape) -> [Option<usize>; 2] {
|
||||
let Some(sp) = piece.path.subpaths.first() else { return [None; 2] };
|
||||
@@ -446,6 +514,14 @@ impl Touched {
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds what `other` touched, in its order.
|
||||
pub fn extend(&mut self, other: &Touched) {
|
||||
let regions = other.regions.iter().map(|&i| Hit::Region(i));
|
||||
for hit in regions.chain(other.lines.iter().map(|&i| Hit::Line(i))).chain(other.edges.iter().map(|&i| Hit::Edge(i))) {
|
||||
self.add(Some(hit));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.regions.is_empty() && self.lines.is_empty() && self.edges.is_empty()
|
||||
}
|
||||
@@ -741,16 +817,12 @@ impl ShapeBuilderTool {
|
||||
alt.then(|| cx.tol(LINE_TOL_PX))
|
||||
}
|
||||
|
||||
/// Adds what the drag from `from` to `to` touches, picked as `shapeBuilder.merge` picks it.
|
||||
fn touch(&mut self, cx: &ToolContext, from: Option<Point>, to: Point) {
|
||||
let pts = match from {
|
||||
Some(a) => sample_polyline(&[a, to], cx.tol(2.0)),
|
||||
None => vec![to],
|
||||
};
|
||||
let pts: Vec<Point> = from.into_iter().chain([to]).collect();
|
||||
let tol = Self::line_tol(cx, self.alt);
|
||||
let map = self.cache.get(cx, self.alt, self.opts.open_filled_as_closed);
|
||||
for p in pts {
|
||||
self.touched.add(map.hit(p, tol));
|
||||
}
|
||||
let touched = self.cache.get(cx, self.alt, self.opts.open_filled_as_closed).hits_along(&pts, tol);
|
||||
self.touched.extend(&touched);
|
||||
}
|
||||
|
||||
/// Selection Straight Line: the drag touches what the line from its first point to `p` crosses.
|
||||
|
||||
Reference in New Issue
Block a user