mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-12 05:12:39 +00:00
Engine: Erase and Smooth search each distinct drag sample once (#2272)
* engine: Erase/Smooth look at each distinct drag sample once, thinned past 4096 * Apply review fixes - crates/engine/src/cmd/anchors.rs * address review: decide whether a drag touched the path on every distinct sample, then thin --------- Co-authored-by: autocraft-bot[bot] <354215646+autocraft-bot[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
bcbc3ed424
commit
589f5ff8a3
1 file changed
+63
-10
@@ -291,33 +291,61 @@ fn convert_points(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Most samples of a drag that Erase / Smooth look at.
|
||||
const MAX_DRAG_SAMPLES: usize = 4096;
|
||||
|
||||
/// Erase / Smooth: the stretch of one subpath touched by the drag.
|
||||
fn erase_or_smooth(s: &mut Session, p: &Value, smooth: bool) -> Result<Value> {
|
||||
let cmd = if smooth { "path.smooth" } else { "path.erase" };
|
||||
let id = super::id_param(p, "id").ok_or_else(|| bad(cmd, "missing id"))?;
|
||||
let tol = p.get("tolerance").and_then(Value::as_f64).unwrap_or(4.0);
|
||||
let pts: Vec<Point> = p
|
||||
let mut pts: Vec<Point> = p
|
||||
.get("points")
|
||||
.and_then(Value::as_array)
|
||||
.map(|a| a.iter().filter_map(|v| Some(Point::new(v.get(0)?.as_f64()?, v.get(1)?.as_f64()?))).collect())
|
||||
.unwrap_or_default();
|
||||
// Each sample is a search of every segment: repeated samples add nothing, and a drag has no use
|
||||
// for more than MAX_DRAG_SAMPLES (evenly thinned, the first and last kept). Whether the drag
|
||||
// touched the path is decided on every distinct sample, though: see below.
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
pts.retain(|q| seen.insert((q.x.to_bits(), q.y.to_bits())));
|
||||
let thinned: Vec<Point> = if pts.len() > MAX_DRAG_SAMPLES {
|
||||
let (n, last) = (pts.len(), pts.last().copied());
|
||||
(0..MAX_DRAG_SAMPLES).filter_map(|i| pts.get((i as u64 * n as u64 / MAX_DRAG_SAMPLES as u64) as usize).copied()).chain(last).collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
s.edit(|d, sel| {
|
||||
path_editable(d, id, cmd)?;
|
||||
let inv = to_inner(d, id).ok_or(designcraft_doc::DocError::NoItem(id))?;
|
||||
let scale = inv.determinant().abs().sqrt().max(1e-9);
|
||||
let it = d.item_mut(id).ok_or(designcraft_doc::DocError::NoItem(id))?;
|
||||
// Where the drag touched the path, per subpath.
|
||||
let mut hits: Vec<(usize, usize, f64)> = Vec::new();
|
||||
for q in &pts {
|
||||
if let Some((si, seg, t, _, dist)) = it.path.nearest(inv * *q)
|
||||
&& dist <= tol * scale
|
||||
{
|
||||
hits.push((si, seg, t));
|
||||
let hits_of = |set: &[Point]| -> Vec<(usize, usize, f64)> {
|
||||
let mut hits = Vec::new();
|
||||
for q in set {
|
||||
if let Some((si, seg, t, _, dist)) = it.path.nearest(inv * *q)
|
||||
&& dist <= tol * scale
|
||||
{
|
||||
hits.push((si, seg, t));
|
||||
}
|
||||
}
|
||||
hits
|
||||
};
|
||||
// A long drag is searched thinned; only when that finds nothing are all its samples, so a
|
||||
// touch the thinning skipped over still counts and a long drag is never refused for it.
|
||||
let mut hits = hits_of(if thinned.is_empty() { &pts } else { &thinned });
|
||||
if hits.is_empty() && !thinned.is_empty() {
|
||||
hits = hits_of(&pts);
|
||||
}
|
||||
// The subpath with the most hits (the last of equals), counted in one pass.
|
||||
let mut counts = vec![0usize; it.path.subpaths.len()];
|
||||
for h in &hits {
|
||||
if let Some(n) = counts.get_mut(h.0) {
|
||||
*n += 1;
|
||||
}
|
||||
}
|
||||
let Some(si) =
|
||||
(0..it.path.subpaths.len()).max_by_key(|i| hits.iter().filter(|h| h.0 == *i).count()).filter(|i| hits.iter().any(|h| h.0 == *i))
|
||||
else {
|
||||
let Some((si, _)) = counts.iter().copied().enumerate().filter(|(_, n)| *n > 0).max_by_key(|(_, n)| *n) else {
|
||||
return Err(bad(cmd, "the drag didn't touch the path"));
|
||||
};
|
||||
let on: Vec<(usize, f64)> = hits.iter().filter(|h| h.0 == si).map(|h| (h.1, h.2)).collect();
|
||||
@@ -663,6 +691,31 @@ mod tests {
|
||||
assert!(s.execute("path.erase", &json!({"id": id, "points": [[100, 600]]})).is_err());
|
||||
}
|
||||
|
||||
/// A touch only thinning would skip still counts: a long drag is not refused as missing the path.
|
||||
#[test]
|
||||
fn a_touch_the_thinning_skips_still_counts() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
let pts: Vec<Value> = (0..=20).map(|i| json!({"p": [100.0 + i as f64 * 20.0, 300.0]})).collect();
|
||||
let id = s.execute("path.create", &json!({"anchors": pts})).unwrap()["id"].as_u64().unwrap();
|
||||
// 6,000 distinct samples far from the path but one, at an index thinning to 4,096 drops.
|
||||
let drag: Vec<Value> = (0..6_000).map(|i| if i == 3 { json!([200.0, 300.0]) } else { json!([200.0 + f64::from(i) * 0.01, 900.0]) }).collect();
|
||||
assert!(s.execute("path.erase", &json!({"id": id, "points": drag})).is_ok());
|
||||
}
|
||||
|
||||
/// A drag of many repeated and many distinct samples is thinned and still erases where it went.
|
||||
#[test]
|
||||
fn a_drag_with_many_samples_still_erases() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
let pts: Vec<Value> = (0..=20).map(|i| json!({"p": [100.0 + i as f64 * 20.0, if i % 2 == 0 { 300.0 } else { 306.0 }]})).collect();
|
||||
let id = s.execute("path.create", &json!({"anchors": pts})).unwrap()["id"].as_u64().unwrap();
|
||||
// 60,000 samples along the middle of the path, each point repeated.
|
||||
let drag: Vec<Value> = (0..60_000).map(|i| json!([200.0 + f64::from(i % 6000) * 0.025, 303.0])).collect();
|
||||
let r = s.execute("path.erase", &json!({"id": id, "points": drag})).unwrap();
|
||||
assert_eq!(r["subpaths"], 2, "a gap in the middle");
|
||||
}
|
||||
|
||||
/// The Direct Selection's selected points stay the same points when the path changes under
|
||||
/// them: a point added before them, one of them deleted, the path reversed.
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user